chronaxis.planning.models
源码基线:8904c2f744951857659abd40aaa3535d57faf53c
1"""保存配置解析得到的来源、信号、计算节点和执行选项声明。23配置解析器先检查字段与引用,再构造本模块的冻结数据类;调用者的自由4元数据在解析边界复制为只读快照。输出记录分析对象、会话范围、来源定位、5显式信号转换、条件规则、窗口、统计指标、当前消息计算及预算和输出格式。6当前消息计算明确新policy,不表示旧完整触发/cache兼容。本层不读取数据、7不调用旧reader或DBC,也不运行表达式。8规则、窗口与预算是配置声明;实际可用能力由计划报告另行说明。9"""1011# 冻结数据类及明确元数据映射接口。12from dataclasses import dataclass, field13from collections.abc import Mapping14# 核心模型提供信号契约、静态表达式、时钟和内存来源声明。15from ..core.binding import SignalDefinition16from ..core.expression import DerivedDefinition, ExprNode, StaticExpression17from ..core.message_binding import ExpressionParameter, MessageDeclarations18from ..core.models import Interval, SourceSpec, TimebaseSpec19from ..core.types import SignalDType20from ..core.validation import require_identifier, require_text21from ..core.window_delta import WindowDeltaSpec22from ..core.stability import StabilitySpec23from ..core.start_end import StartEndSpec24# CSV/CAN仅导入静态配置,不实例化适配器、旧worker或打开文件。25from ..sources.csv_spec import SignalCSVSpec26from ..sources.can_spec import LegacyCANSpec27from ..sources.text_can_spec import CANTextSpec28from ..legacy.records import PROFILE_ID, normalize_frame_id29from ..legacy.disjunction import LegacyDisjunction30from .export_options import SignalCSVOutput,CANFramesOutput313233@dataclass(frozen=True)34class DatasetPlan:35 """保存单个分析对象、会话及请求时间范围。3637 dataset_id与entity命名数据集和对象,session_id确定会话。38 domain是用户请求的左闭右开区间,不证明实际输入已覆盖该范围。39 domain_explicit区分用户时间筛选与domain=None的整文件声明;后者不能40 当作观测时间或向运行结果报告为已采集范围。41 timebase保存来源声明确定的分析时钟,metadata保存只读附加信息。42 实例字段冻结,不维持运行状态。43 """4445 dataset_id: str # 单计划数据集ID。46 entity: str # 分析对象ID,不内置车辆含义。47 session_id: str # 当前单会话ID。48 domain: Interval | None # 用户请求半开范围;None为整文件无筛选,不能当作实际空范围。49 timebase: TimebaseSpec # 同步分析基准。50 metadata: Mapping # 深冻结自由JSON元数据。51 domain_explicit: bool = True # 旧位置构造保留显式请求;False仅供合格整文件消息任务。525354@dataclass(frozen=True)55class SourcePlan:56 """保存一个已支持来源种类的配置和定位声明。5758 name是唯一来源名,source_type区分memory、signal_csv、can_blf、can_asc和raw_can_csv。59 config保存对应静态来源规格,文本CAN使用CANTextSpec,供实际适配器构造。60 time_unit与timezone_text记录时间表示;input_identity规范记录路径顺序、61 格式和时间映射声明,不能证明文件内容。metadata是只读自由元数据。62 CSV/BLF/DBC尚未打开,实际列、单位与范围留待读取核实;不创建适配器或持有句柄。63 """6465 name: str # 当前唯一来源名。66 source_type: str # 来源类型,由配置解析器检查允许的格式。67 config: SourceSpec | SignalCSVSpec | LegacyCANSpec | CANTextSpec # 对应memory/信号表/旧BLF/新文本CAN。68 time_unit: str # 原时间表示单位。69 timezone_text: str | None # 明确声明原时区文本。70 input_identity: str # 规范来源配置,包含路径顺序/格式/时间映射。71 metadata: Mapping # 深冻结自由元数据。727374@dataclass(frozen=True)75class SignalPlan:76 """保存一个原字段到简单表达式别名的显式信号定义。7778 definition包含原字段、类型、单位、转换和valid_for_ns有效期等契约。79 有效期是有效样本最多保持的时长;None表示等待运行启动读取DBC周期,80 不能用于计算状态。实际过期判断由时间驱动层按真实更新时间执行。81 metadata独立冻结,不改变转换或基础质量规则。本类不保存信号值。82 """8384 definition: SignalDefinition # 已校验归一化/保持契约。85 metadata: Mapping # 自由键不会修改基础质量政策。868788@dataclass(frozen=True)89class DerivedPlan:90 """保存由已有信号计算的新信号声明及可选语义信息。9192 definition保存名称、表达式和显式单位,类型由表达式静态检查推断。93 semantic_id和semantic_contract_version声明语义及版本,sign_convention94 与measurement_location声明方向及位置;这些字段不证明角色已核实。95 metadata为只读附加信息。派生值不能另行声明更长有效期或覆盖推断类型,96 实际有效性仍取决于输入依赖。本类不执行计算。97 """9899 definition: DerivedDefinition # 名称/表达式/显式单位。100 semantic_id: str | None # 语义文本不是属性路径。101 semantic_contract_version: str | None # 声明版本不代表角色已验。102 sign_convention: str | None # 可选调用者方向声明。103 measurement_location: str | None # 可选调用者位置声明。104 metadata: Mapping # 深冻结自由JSON。105106107@dataclass(frozen=True)108class RulePlan:109 """保存持续、边沿、窗口变化量、迟滞和开始/结束规则的静态参数。110111 rule_id与rule_type确定规则,condition通常是布尔表达式;window_delta时保存数值signal树。112 minimum_duration_ns与selection_origin仅用于持续条件,表示确认时长和选择起点。113 emit_initial说明初始触发策略;window_duration_ns是start_only的固定抑制时长。114 stability保存防抖时长;exit_condition保存迟滞退出或start_end结束条件。115 start_end保存专用时限/首次策略,不借普通emit_initial启动condition_duration检测器。116 实例不保存状态;迟滞复用普通条件时间线,窗口变化量使用有界历史回读。117 """118119 rule_id: str # 唯一规则ID。120 rule_type: str # 明确首版规则种类。121 condition: StaticExpression # 普通BOOL树或window_delta数值树,不能直接执行。122 minimum_duration_ns: int | None # 仅condition_duration,非负纳秒。123 selection_origin: str | None # 仅condition_duration的onset/confirmed。124 emit_initial: bool # 边沿/start_only初始化策略。125 window_duration_ns: int | None # 仅start_only正抑制窗口。126 window_delta: WindowDeltaSpec | None = None # window_delta规则的完整回看契约。127 retrigger: str = 'edge' # edge等待新False→True;once在本会话只触发一次。128 stability: StabilitySpec | None = None # debounce/hysteresis的独立进入/退出防抖时长。129 exit_condition: StaticExpression | None = None # hysteresis或start_end的独立退出条件。130 start_end: StartEndSpec | None = None # 开始/结束的时限与首次启动,追加字段保持旧位置兼容。131 duration_comparison: str = '>=' # 持续条件达到/>门槛,其他规则只能保持默认。132133 def __post_init__(self):134 """检查窗口、迟滞和开始/结束规则,保持原字段位置参数兼容。135136 参数: 无。返回: None。异常: 新规则类型、配置或普通规则混入专用参数。137 """138 if self.duration_comparison not in ('>=', '>') or (self.rule_type != 'condition_duration' and self.duration_comparison != '>='):139 raise ValueError('duration_comparison仅持续条件支持>=或>')140 if self.rule_type == 'start_end':141 if (not isinstance(self.start_end, StartEndSpec)142 or not isinstance(self.condition, StaticExpression)143 or self.condition.root.dtype is not SignalDType.BOOL144 or not isinstance(self.exit_condition, StaticExpression)145 or self.exit_condition.root.dtype is not SignalDType.BOOL):146 raise TypeError('start_end需要两个BOOL条件及StartEndSpec')147 if (self.minimum_duration_ns is not None or self.selection_origin is not None148 or self.emit_initial is not False or self.window_duration_ns is not None149 or self.window_delta is not None or self.stability is not None or self.retrigger != 'edge'):150 raise ValueError('start_end不能混用其他规则参数')151 return152 if self.start_end is not None:153 raise ValueError('其他规则不接受StartEndSpec')154 if self.rule_type in ('debounce', 'hysteresis'):155 if (not isinstance(self.stability, StabilitySpec)156 or not isinstance(self.condition, StaticExpression)157 or self.condition.root.dtype is not SignalDType.BOOL):158 raise TypeError('迟滞/防抖需要BOOL条件及StabilitySpec')159 if self.rule_type == 'hysteresis':160 if not isinstance(self.exit_condition, StaticExpression) or self.exit_condition.root.dtype is not SignalDType.BOOL:161 raise TypeError('hysteresis需要BOOL退出条件')162 elif self.exit_condition is not None:163 raise ValueError('debounce退出使用not condition,不接受exit_condition')164 if (self.minimum_duration_ns is not None or self.selection_origin is not None165 or self.emit_initial is not False or self.window_duration_ns is not None166 or self.window_delta is not None or self.retrigger != 'edge'):167 raise ValueError('迟滞/防抖不能混用其他规则参数')168 elif self.stability is not None or self.exit_condition is not None:169 raise ValueError('普通规则不接受迟滞/防抖参数')170 elif self.rule_type == 'window_delta':171 if (not isinstance(self.window_delta, WindowDeltaSpec)172 or not isinstance(self.condition, StaticExpression)173 or self.condition.root.dtype not in (SignalDType.INT64, SignalDType.UINT64, SignalDType.FLOAT64)):174 raise TypeError('window_delta需要数值signal与WindowDeltaSpec')175 if (self.minimum_duration_ns is not None or self.selection_origin is not None176 or self.window_duration_ns is not None or type(self.emit_initial) is not bool177 or self.retrigger not in ('edge', 'once')):178 raise ValueError('window_delta参数冲突或重触发策略非法')179 elif self.window_delta is not None or self.retrigger != 'edge':180 raise ValueError('普通规则不接受window_delta或retrigger参数')181182 @property183 def temporal_rule_type(self):184 """读取交给事件检测器的类型。参数: 无。返回: str。异常: 无。185186 window_delta使用rising_edge;迟滞/防抖确认后的状态使用condition_duration=0活动。187 完整窗口、条件、防抖和开始/结束时限保留在原计划;start_end也输出活动。188 """189 return ('rising_edge' if self.rule_type == 'window_delta' else190 'condition_duration' if self.rule_type in ('debounce', 'hysteresis', 'start_end') else self.rule_type)191192 @property193 def expressions(self):194 """列出规则全部静态条件。参数: 无。返回: tuple。异常: 无。195196 迟滞退出条件同样参与实际绑定、投影、同步与预算,不遗漏第二个来源。197 """198 return (self.condition,) if self.exit_condition is None else (self.condition, self.exit_condition)199200 @property201 def dependencies(self):202 """取得进出条件的去重依赖。参数: 无。返回: 名称tuple。异常: 无。"""203 return tuple(dict.fromkeys(name for expression in self.expressions for name in expression.dependencies))204205206@dataclass(frozen=True)207class WindowPlan:208 """保存事件的上下文窗口及片段合并声明。209210 window_id标识窗口,event引用规则,anchor区分时间点和持续活动。211 pre_ns与post_ns声明前后扩展纳秒范围,merge决定窗口的合并方式。212 exit_delay_ns只用于退出延迟合并,该模式要求活动锚点且post_ns为0。213 声明不会改写原事件边界;实际运行还须保存合并片段的全部父事件身份。214 """215216 window_id: str # 唯一窗口ID。217 event: str # 当前计划规则ID。218 anchor: str # point或activity,与规则kind一致。219 pre_ns: int # 非负请求前扩范围。220 post_ns: int # 非负请求后扩范围。221 merge: str # 显式none/overlap/overlap_and_adjacent/exit_delay。222 exit_delay_ns: int | None # 仅exit_delay模式有效。223224225@dataclass(frozen=True)226class CalculationPlan:227 """保存要统计的信号、选择条件、范围和结果政策。228229 calculation_id标识指标,scope区分全会话、原事件和合并片段的统计范围。230 operation确定选择、积分、时长或统计口径,signal与condition是静态表达式结构。231 unit保存已检查的输出单位,partial_policy规定未知部分如何报告,232 minimum_coverage是覆盖率质量门槛;method仅用于积分,当前为左侧保持'zoh'。233 本类只保存声明,不生成数值或选择行;片段合并不改变原事件的统计范围。234 """235236 calculation_id: str # 唯一指标ID。237 scope: str # session/event:ID/segment:ID。238 operation: str # 首版明确算子,不接受模糊mean/min/max。239 signal: StaticExpression | None # 数值/BOOL信号或派生别名树。240 condition: StaticExpression | None # 可选BOOL选择或condition_duration条件。241 unit: str # 已推断/校验输出单位。242 partial_policy: str # report或strict。243 minimum_coverage: int | float # 0—1质量判断阈值,不补未知。244 method: str | None # 仅integral为zoh。245 count_target: str | None = None # v4对象计数显式引用的规则或窗口名。246 ddof: int | None = field(default=None, kw_only=True) # 样本/时间方差和标准差的分母选择。247 q: int | float | None = field(default=None, kw_only=True) # 仅sample_quantile显式指定概率。248249250@dataclass(frozen=True)251class LegacyMessageSelector:252 """明确选择当前消息计算的CAN帧,不猜旧profile的求值选择器默认。253254 通道与ID必须提供,两个帧类型可省略。ID支持整数或带进制文本,不接受bool或255 float;实际记录保留remote/error等标志,消息计算只处理数据帧。256 本声明不读DBC,也不证明该消息在输入中出现。257 """258259 channel: int # 显式声明的原通道。260 frame_id: int # 原报文ID,不以零代表缺项。261 is_extended: bool | None = None # 省略时由实际帧确定;不能默认标准帧。262 is_fd: bool | None = None # 省略时由实际帧确定经典CAN/FD。263264 def __post_init__(self) -> None:265 """核原生类型和帧ID范围,不进行输入过滤或DBC查找。266267 参数: 无,读取冻结字段。返回: None,ID统一为整数,省略类型保留None。268 异常: TypeError为非法类型;ValueError为ID格式、负通道或ID超相应帧范围。269 """270 if type(self.channel) is not int:271 raise TypeError('消息选择器channel需要原生int')272 object.__setattr__(self, 'frame_id', normalize_frame_id(self.frame_id))273 if any(value is not None and type(value) is not bool for value in (self.is_extended, self.is_fd)):274 raise TypeError('消息选择器is_extended/is_fd需要bool或省略')275 # 不根据数值大小猜帧类型,也不复制旧ChrgInfo对ID0的缺失判断。276 maximum = 0x7FF if self.is_extended is False else 0x1FFFFFFF277 if self.channel < 0 or not 0 <= self.frame_id <= maximum:278 raise ValueError('消息选择器通道或帧ID范围非法')279280281@dataclass(frozen=True)282class MessageCalculationPlan:283 """保存一个会话内按当前匹配消息逐次求值的明确计算声明。284285 expr来自本来源MessageDeclarations,parameters为同一冻结参数tuple。286 operation仅legacy_sum,message_policy必须显式current_message-v1;这里不设置287 旧触发器、跨消息缓存、普通TTL、连续时间覆盖率或事件/片段范围。288 profile标识原解码路径,与新计算policy分别保存,不冒充旧完整兼容。289 可选disjunction只把本条贡献路由到正/负桶;原方向定位关系由计划交叉校验。290 """291292 calculation_id: str # 与rules/windows/普通calculations共用计划ID空间。293 source: str # 当前单一CAN来源。294 profile: str # 原解码profile,须与实际来源声明对应。295 operation: str # 固定legacy_sum,不放入普通CalculationDefinition。296 update_message: LegacyMessageSelector # 通道/ID必填,帧类型可省略的当前帧选择器。297 expr: StaticExpression # 逐消息表达式的静态语法树;构建它不会读取或执行分析。298 unit: str # 一次贡献及累计结果的单位,必须等于表达式结果单位。299 parameters: tuple[ExpressionParameter, ...] # 原单位、规范FLOAT64参数。300 message_policy: str # 固定current_message-v1,不能与旧完整cache等义。301 disjunction: LegacyDisjunction | None = None # 可选原解码方向选择,不进入表达式或TTL。302303 def __post_init__(self) -> None:304 """检查固定模式及静态消息表达式的一致性,不打开任何来源。305306 参数: 无,读取冻结声明。返回: None,不重编译AST或重建原信号。307 异常: TypeError为模型/容器错误;ValueError为模式、来源、单位或参数冲突。308 """309 require_identifier(self.calculation_id, 'message_calculation.name')310 require_identifier(self.source, 'message_calculation.source')311 require_text(self.unit, 'message_calculation.unit')312 # 末尾缺省保既有位置构造;只有明确冻结选择器才启用分桶。313 if self.disjunction is not None and type(self.disjunction) is not LegacyDisjunction:314 raise TypeError('逐消息disjunction需要LegacyDisjunction或None')315 if (type(self.profile) is not str or self.profile not in (PROFILE_ID, 'can-text-v1')316 or type(self.operation) is not str or self.operation != 'legacy_sum'317 or type(self.message_policy) is not str or self.message_policy != 'current_message-v1'):318 raise ValueError('当前逐消息计算仅支持固定解码profile、legacy_sum及current_message-v1')319 if (type(self.update_message) is not LegacyMessageSelector320 or not isinstance(self.expr, StaticExpression)321 or type(self.expr.bindings) is not MessageDeclarations322 or not isinstance(self.expr.root, ExprNode)323 or type(self.parameters) is not tuple324 or any(type(item) is not ExpressionParameter for item in self.parameters)):325 raise TypeError('逐消息计划需要选择器、消息域StaticExpression及参数tuple')326 # 声明不是原始字符串;保原单位和参数,不能借包装重写编译结果。327 declaration = self.expr.bindings328 if (declaration.source != self.source or declaration.message_policy != self.message_policy329 or declaration.parameters != self.parameters or declaration.derived_names):330 raise ValueError('逐消息计划来源、policy或参数与消息表达式声明不符')331 if self.expr.root.dtype not in (SignalDType.INT64, SignalDType.UINT64, SignalDType.FLOAT64):332 raise ValueError('legacy_sum表达式必须为数值,不接受bool或enum')333 if self.expr.root.unit != self.unit:334 raise ValueError('legacy_sum单位必须与表达式结果一致')335336337@dataclass(frozen=True)338class ExecutionPlan:339 """保存后端和资源限额,实际执行器负责落实这些声明。340341 backend选择'numpy'、'reference'或按静态类别选择的'auto',batch_rows限制来源批次总样本数。342 memory_budget_bytes声明新分析层内存预算,result_buffer_rows限制结果缓冲,343 spool_quota_bytes声明磁盘暂存配额,None表示没有显式配额。344 partial_policy是全局默认结果政策;on_budget_exceeded当前只允许'error'。345 构造计划不测量内存或磁盘,也不启动运行、暂存或发布。346 """347348 backend: str # numpy、reference或auto;运行前解析为具体后端。349 batch_rows: int # 来源批次样本上限。350 memory_budget_bytes: int # 新分析层RAM预算。351 result_buffer_rows: int # 结果流缓冲上限。352 partial_policy: str # 全局指标默认部分政策。353 spool_quota_bytes: int | None # 磁盘配额,None表示未显式限额。354 on_budget_exceeded: str = 'error' # 首版不丢数据或增加旧扫描。355 process_count: int = 1 # 本次分析读取/解码工作进程上限;不包含主分析进程。356357358359360@dataclass(frozen=True)361class OutputPlan:362 """保存新结果表和清单的输出格式声明。363364 tables接受普通使用默认'readable_csv',或供程序使用的'csv'/'jsonl';manifest使用'json'。365 contributions仅控制逐消息贡献明细,none仍保留统计汇总和其他请求结果;省略为none。366 本类不创建目录或写文件;run生成本次实际原始片段,write只复制已封口输出。367 """368369 tables: str # readable_csv、csv或jsonl;只生成所选的一套表。370 manifest: str # json。371 contributions: str = 'none' # 只有明确请求full时生成逐消息贡献明细。372 signal_csv: SignalCSVOutput | None = None # 显式请求的信号长表,可单独设置线性网格。373 can_frames: CANFramesOutput | None = None # 显式原帧/重建帧输出,默认不保额外原始数据。374