chronaxis.core.windows_numpy
源码基线:8904c2f744951857659abd40aaa3535d57faf53c
1"""独立用NumPy扩展、分组和裁剪事件窗口,再恢复片段模型。23输入已排序Event tuple、WindowSpec和实际观测Interval,输出Segment tuple。4时间端点为(N,) int64纳秒列,先检查扩展溢出,再执行UFUNC;按真实活动间隔5或请求窗口重叠建立组边界,reduceat归约每组范围,数组裁剪并计算覆盖。6输出循环只打包模型/元数据,不逐事件执行参考数值;不调用参考窗口算法。7输入和数组完整物化,不读旧链、不写文件、不承诺整个AnalysisRunner的预算。8"""910# 真正数组端点、掩码与分组归约,禁止object/vectorize替代。11import numpy as np12from ..timebase.conversion import INT64_MIN, INT64_MAX, TimeOverflowError13from .models import Interval14# 只共享已校验协议与ID/诊断包装。15from .windows import require_windows, make_segment, require_resolved_windows161718def build_segments_numpy(events, spec, observation, *, bounds=None, compact=False):19 """以数组计算请求与实际范围,输出保留原事件关联的片段。2021 参数: events已排序真实事件;spec为WindowSpec;observation为真实Interval。22 返回: Segment或compact的CompactSegment tuple;bounds为已解析的实际边界。旧覆盖保持独立float64比值。23 异常: 配置/模型错误传播;端点或跨度溢出明确拒绝;分配失败不吞。24 流程: 扩展前校验int64上下界,批量建立连接掩码/组索引、归约、裁剪及覆盖。25 恢复Interval时校验请求跨度,再做安全整数差;不改变原事件。26 """27 # 共享的门槛没有窗口数值或归约。28 require_windows(events, spec, observation)29 require_resolved_windows(events,bounds,compact)30 if not events:31 return ()32 # 每个时间列为(N,)纳秒,point的结束锚点等于真实触发时刻。33 onsets = np.array([event.start_ns for event in events], dtype=np.int64)34 endings = np.array([event.start_ns if event.end_ns is None else event.end_ns for event in events], dtype=np.int64)35 extension = spec.exit_delay_ns if spec.merge == 'exit_delay' else spec.post_ns36 if bounds is None:37 if np.any(onsets < np.int64(INT64_MIN + spec.pre_ns)) or np.any(endings > np.int64(INT64_MAX - extension)):38 raise TimeOverflowError('窗口请求扩展超出int64纳秒')39 starts = np.subtract(onsets, np.int64(spec.pre_ns))40 ends = np.add(endings, np.int64(extension))41 left_clip=right_clip=np.zeros(len(events),dtype=bool)42 else:43 starts=np.array([item[0] for item in bounds],dtype=np.int64)44 ends=np.array([item[1] for item in bounds],dtype=np.int64)45 left_clip=np.array([item[2] for item in bounds],dtype=bool)46 right_clip=np.array([item[3] for item in bounds],dtype=bool)47 # 连接掩码(N-1,);single比较真实onset与前活动退出deadline,不使用pre。48 if spec.merge == 'exit_delay':49 connected = (onsets[1:] <= ends[:-1]) | right_clip[:-1]50 elif spec.merge == 'overlap':51 prior=np.maximum.accumulate(ends)[:-1]52 connected = (starts[1:] < prior) | ((starts[1:] == prior) & (np.maximum.accumulate(right_clip)[:-1] | left_clip[1:]))53 elif spec.merge == 'overlap_and_adjacent':54 connected = starts[1:] <= np.maximum.accumulate(ends)[:-1]55 else:56 connected = np.zeros(len(events)-1, dtype=bool)57 # 组首索引和结束索引只依赖本次真实事件,未重新排序旧输入。58 first = np.concatenate((np.array([0], dtype=np.int64), np.flatnonzero(~connected)+1))59 stop = np.concatenate((first[1:], np.array([len(events)], dtype=np.int64)))60 group_starts = np.minimum.reduceat(starts, first)61 group_ends = np.maximum.reduceat(ends, first)62 # 模型恢复先验证合并请求跨度,之后整数求差才保证不wrap。63 requests = tuple(Interval(int(start), int(end)) for start, end in zip(group_starts, group_ends))64 left = np.maximum(group_starts, np.int64(observation.start_ns))65 right = np.minimum(group_ends, np.int64(observation.end_ns))66 observed = right >= left67 actuals = tuple(Interval(int(start), int(end)) if flag else None for start, end, flag in zip(left, right, observed))68 if compact:69 coverages=[None]*len(first)70 else:71 requested_duration = np.subtract(group_ends, group_starts)72 actual_duration = np.zeros(len(first), dtype=np.int64)73 np.subtract(right, left, out=actual_duration, where=observed)74 coverages = np.divide(actual_duration.astype(np.float64), requested_duration.astype(np.float64))75 clipped=np.logical_or.reduceat(left_clip|right_clip,first)76 # 输出循环只关联父事件和包装typed模型,全部边界/分组/覆盖已在数组完成。77 return tuple(make_segment(spec, requests[index], actuals[index], None if compact else float(coverages[index]), events[int(start):int(end)],compact=compact,clipped=bool(clipped[index]))78 for index, (start, end) in enumerate(zip(first, stop)))79