chronaxis.core.start_end
源码基线:8904c2f744951857659abd40aaa3535d57faf53c
1"""从开始条件到结束条件记录活动,并区分超时与缺观测。23输入是同一会话中按时间严格递增的两组ConditionTick。开始采用已知上升沿,4可选择首次已知TRUE也启动;活动期间只等待结束条件或时限,不排队重复开始。5输出沿用正长度Event,status=timeout表示在可判断的结束条件下等到时限。6UNKNOWN/真实失效与EOF继续区分interrupted/truncated,不能凭缺数据判超时。7这是有界有序状态转换,不读取来源、不保留全量历史,也不属于数值数组核。8"""910from dataclasses import dataclass11from .temporal import make_event12from .temporal_reference import require_batch, require_detector13from .types import QualityFlag as Q, Truth as T14from ..timebase.conversion import require_int64_ns151617@dataclass(frozen=True)18class StartEndSpec:19 """保存最长等待时长与首次启动策略;None明确表示不限时。"""2021 timeout_ns: int | None = None # 从开始时刻算起;正纳秒或显式不限时。22 emit_initial: bool = True # 首次已知开始条件为TRUE时允许启动。2324 def __post_init__(self):25 """拒绝零/负时限及假布尔。参数: 无。返回: None。异常: 类型/范围错误。"""26 if self.timeout_ns is not None:27 value = require_int64_ns(self.timeout_ns, 'timeout_ns')28 if value <= 0:29 raise ValueError('start_end时限必须为正或None')30 object.__setattr__(self, 'timeout_ns', value)31 if type(self.emit_initial) is not bool:32 raise TypeError('emit_initial必须为bool')333435class StartEndDetector:36 """为单个规则保存至多一段活动和开始条件前态;批次边界不重置。3738 活动中忽略重复开始;首次真实结束优先同刻时限。结束条件未知会终止已知39 活动,恢复后需要新的开始边沿;当前开始和结束同时成立不制造零长度活动。40 source_refs在构造时固定,输出可drain移交,未移交事件才留在实例中。41 """4243 def __init__(self, rule, start_ns, end_ns, spec, source_refs=(), *, backend='reference', record_clock=False):44 """建立独立的活动状态,规则沿用condition_duration=0输出模型。4546 参数: rule为TemporalRule;start_ns/end_ns为会话域;spec为StartEndSpec;47 source_refs为真实依赖引用;backend验证reference/numpy入口。48 返回: None;初始化实例状态。异常: 规则/范围/参数错误。状态算法没有数值后端分叉。49 """50 require_detector(rule, start_ns, end_ns, source_refs)51 if not isinstance(spec, StartEndSpec) or rule.rule_type != 'condition_duration' or rule.minimum_duration_ns != 0:52 raise ValueError('start_end需要StartEndSpec和duration=0输出规则')53 if backend not in ('reference', 'numpy'):54 raise ValueError('start_end后端需reference或numpy')55 if type(record_clock) is not bool:56 raise TypeError('record_clock需要bool')57 self.record_clock, self.remaining = record_clock, None58 self.rule, self.spec, self.source_refs = rule, spec, source_refs59 self.start_ns, self.end_ns = int(start_ns), int(end_ns)60 self.active_start, self.deadline = None, None61 self.previous_start, self.seen_start = T.UNKNOWN, False62 self.diagnostic = Q.NONE63 self.last_time, self.ended, self.finished = None, False, False64 self.events = []6566 @property67 def next_deadline_ns(self):68 """提供域内时限供时间线推进。参数: 无。返回: int或None。异常: 无。"""69 return self.deadline if not self.record_clock and self.deadline is not None and self.deadline <= self.end_ns else None7071 def _close(self, time_ns, status, quality=Q.NONE):72 """保存有正长度的活动后清理计时,绝不延长零长度点。7374 参数: time_ns结束纳秒;status/quality表示实际结束原因。返回: None。75 异常: Event结构或分配错误;失败后调用方丢弃实例。76 """77 if self.active_start is not None and time_ns > self.active_start:78 self.events.append(make_event(self.rule, self.active_start, self.active_start,79 time_ns, status, quality, self.diagnostic, self.source_refs))80 self.active_start, self.deadline = None, None81 self.remaining = None82 self.diagnostic = Q.NONE8384 @staticmethod85 def _unknown_status(end_condition):86 """按真实失效与观测结束区分原因。参数: end_condition证据。返回: str。异常: 无。"""87 return ('truncated' if (end_condition.terminal or end_condition.observation_end) and not end_condition.actual_unknown88 else 'interrupted')8990 def _advance(self, start_condition, end_condition, advance=None):91 """推进一个完整时间组,先补精确时限,再处理实际结束和新开始。9293 参数: 同刻start_condition/end_condition条件证据。返回: None。异常: 输出构造错误。94 边界: 跨过时限前未出现其它组,表示此前连续结束条件已知不成立;95 同刻未知则不能证明截止点没有end_condition,保留未知/EOF原因。96 """97 time_ns = int(start_condition.time_ns)98 if self.record_clock and self.remaining is not None:99 self.remaining = max(0, self.remaining - advance)100 if self.deadline is not None and self.deadline < time_ns:101 self._close(self.deadline, 'timeout')102 diagnostic = (start_condition.diagnostic_quality | end_condition.diagnostic_quality | start_condition.quality103 | end_condition.quality | start_condition.point_quality | end_condition.point_quality)104 was_active = self.active_start is not None105 if was_active:106 self.diagnostic |= diagnostic107 # 只有结束条件决定已启动活动;开始信号后来失效不抹去已确认的开始。108 if end_condition.point_state is T.TRUE:109 self._close(time_ns, 'closed')110 elif end_condition.point_state is T.UNKNOWN:111 self._close(time_ns, self._unknown_status(end_condition), end_condition.point_quality)112 elif end_condition.state is T.TRUE and not end_condition.terminal:113 # 右侧连续条件已经成立时,结束边界就在当前时刻,不等下条采样。114 self._close(time_ns, 'closed')115 elif (self.remaining == 0 if self.record_clock else self.deadline is not None and self.deadline == time_ns):116 self._close(time_ns, 'timeout')117 elif end_condition.terminal or end_condition.observation_end:118 self._close(time_ns, 'truncated', end_condition.quality | Q.MISSING)119 elif end_condition.state is T.UNKNOWN:120 self._close(time_ns, 'interrupted', end_condition.quality)121 # 不在结束同刻抢占新的开始,也不把持续TRUE或UNKNOWN恢复当新上升沿。122 # 首次策略也适用于常量TRUE:有实际观测域即可,不要求常量收到信号更新。123 initial = not self.seen_start and self.spec.emit_initial124 edge = self.previous_start is T.FALSE and start_condition.actual_update125 begins = start_condition.point_state is T.TRUE and (initial or edge)126 if (not was_active and self.active_start is None and begins and not start_condition.terminal127 and end_condition.point_state is T.FALSE and end_condition.state is T.FALSE and not end_condition.observation_end):128 self.active_start = time_ns129 # Python整数避免跨int64上界回绕;域外时限不会交给时间推进器。130 self.deadline = None if self.record_clock or self.spec.timeout_ns is None else time_ns + self.spec.timeout_ns131 self.remaining = self.spec.timeout_ns132 self.diagnostic = diagnostic133 if start_condition.point_state is not T.UNKNOWN:134 self.seen_start = True135 self.previous_start = start_condition.state136137 def push(self, ticks, end_ticks, *, advances=None):138 """消费同域、同刻、完整的开始/结束条件批。139140 参数: ticks/end_ticks为ConditionTick tuple,可为空;advances仅record_clock模式141 提供本组周期增量,不改变开始/结束的实际时间。返回: 本批新增Event tuple;142 同时保留到drain。异常: 协议/排序/结束后推进错误;输入失败不排序或补齐。143 """144 if self.finished:145 raise ValueError('start_end检测器已finish')146 require_batch(ticks, self.start_ns, self.end_ns, self.last_time, self.ended)147 from .temporal_clock import require_clock_advances148 require_clock_advances(ticks, advances, self.record_clock, self.last_time)149 require_batch(end_ticks, self.start_ns, self.end_ns, self.last_time, self.ended)150 if len(ticks) != len(end_ticks) or any(a.time_ns != b.time_ns for a, b in zip(ticks, end_ticks)):151 raise ValueError('start_end两个条件必须在相同完整时间组求值')152 first = len(self.events)153 for index, (start, end) in enumerate(zip(ticks, end_ticks)):154 self._advance(start, end, None if advances is None else advances[index])155 self.last_time, self.ended = int(start.time_ns), start.terminal156 return tuple(self.events[first:])157158 def drain_events(self):159 """转交当前输出,不清活动状态。参数: 无。返回: Event tuple。异常: 分配错误。"""160 result = tuple(self.events)161 self.events.clear()162 return result163164 def finish(self):165 """仅在已收到真实末组后封闭。参数: 无。返回: 未移交Event tuple。异常: 协议错误。"""166 if self.finished or not self.ended:167 raise ValueError('start_end需要唯一finish且已有真实terminal')168 self.finished = True169 return tuple(self.events)170