chronaxis.configuration.explanation
源码基线:8904c2f744951857659abd40aaa3535d57faf53c
1"""把已验证的分析声明讲成人可阅读的步骤;不加载来源、不展开运行时数据。23文本与JSON报告使用同一计划和能力报告。这里仅翻译已知术语、格式化精确时间,4并从原始声明区分显式设置与默认设置,不建立另一套配置默认值或执行规则。5"""6from collections.abc import Mapping789OPERATIONS={10 'integral':'积分(按前值保持累计)',11 'sample_mean':'样本均值(每个有效样本权重相同)',12 'time_weighted_mean':'时间加权均值(按有效持续时间加权)',13 'sample_minimum':'有效样本最小值','sample_maximum':'有效样本最大值',14 'min_time':'有效时间区间内最小值','max_time':'有效时间区间内最大值',15 'duration':'条件成立的累计时长','select':'选择符合条件的记录',16}17RULES={'condition_duration':'条件持续成立','rising_edge':'由假变真的上升沿',18 'falling_edge':'由真变假的下降沿','start_only':'触发后保留固定窗口',19 'window_delta':'回看窗口内变化量','debounce':'条件去抖','hysteresis':'进入与退出使用不同条件',20 'start_end':'开始条件启动、结束条件停止'}21SOURCES={'memory':'配置中的内存数据','signal_csv':'已解码信号CSV',22 'can_blf':'CAN二进制BLF','can_asc':'CAN文本ASC','asc_or_blf':'CAN连续ASC/BLF','raw_can_csv':'原始CAN帧CSV'}232425def time_text(nanoseconds):26 """无损显示纳秒时长。参数: 整数或None。返回: 带单位文本。异常: 无。"""27 if nanoseconds is None:28 return '未设置'29 for divisor,unit in ((10**9,'s'),(10**6,'ms'),(1000,'us')):30 if nanoseconds and nanoseconds%divisor==0:31 return str(nanoseconds//divisor)+unit32 return str(nanoseconds)+'ns'333435def _declared(plan,*parts):36 """检查原声明是否包含字段。参数: 计划/路径。返回: bool。异常: 无;不取文件默认值。"""37 current=plan.raw_config38 for part in parts:39 if isinstance(current,Mapping) and part in current:40 current=current[part]41 elif type(current) is tuple and type(part) is int and 0<=part<len(current):42 current=current[part]43 else:44 return False45 return True464748def _setting(plan,label,value,*parts):49 """显示实际生效值及其来源。参数: 计划/标签/值/路径。返回: 文本。异常: 无。"""50 if type(value) is bool:51 value='开启' if value else '关闭'52 source='配置指定' if _declared(plan,*parts) else '默认'53 return f'{label}={value}({source})'545556def _scope(value):57 """解释计算/输出范围。参数: 已校验范围文本。返回: 中文及具体ID。异常: 无。"""58 if value=='session':59 return '整个会话'60 prefix,_,identifier=value.partition(':')61 return ('每个事件 ' if prefix=='event' else '每个窗口片段 ')+identifier626364def _validation_lines(validation):65 """保留报告中全部错误与限制。参数: ValidationReport。返回: 文本行。异常: 无。"""66 lines=['静态检查通过:当前声明有可用执行路径。' if validation.valid and validation.execution_ready67 else '静态检查未通过:请修正以下问题。']68 lines.extend('错误:'+message for message in validation.errors)69 lines.append('此检查尚未读取日志、信号表或DBC;未验证实际字段、数据覆盖和内存峰值。')70 lines.extend('提示:'+message for message in validation.warnings)71 return lines727374def validation_text(plan,validation):75 """生成简短校验说明。参数: 已加载计划/同次报告。返回: 多行文本。异常: 无。"""76 return '\n'.join([f'分析 {plan.dataset.dataset_id}:{len(plan.sources)}个来源,'77 f'{len(plan.signals)}个信号,{len(plan.rules)}个事件规则,'78 f'{len(plan.calculations)+len(plan.message_calculations)}个计算项。',79 *_validation_lines(validation)])808182def _rule_lines(plan):83 """说明事件、窗口和请求响应的有效声明。参数: 计划。返回: 文本行。异常: 内部模型不符。"""84 lines=[]85 rule_indices={item['name']:index for index,item in enumerate(plan.raw_config.get('rules',()))}86 for rule in plan.rules:87 index=rule_indices.get(rule.rule_id)88 line=f' {rule.rule_id}:{RULES[rule.rule_type]};条件 {rule.condition.text}'89 if rule.minimum_duration_ns is not None:90 line+=(';持续严格超过 ' if rule.duration_comparison == '>' else ';至少持续 ')+time_text(rule.minimum_duration_ns)91 line+=';统计从'+('首次成立' if rule.selection_origin=='onset' else '持续确认')+'时刻开始'92 if rule.window_duration_ns is not None:93 line+=';固定窗口 '+time_text(rule.window_duration_ns)94 if rule.exit_condition is not None:95 line+=';退出条件 '+rule.exit_condition.text96 if rule.stability is not None:97 line+=';进入稳定 '+time_text(rule.stability.enter_ns)+',退出稳定 '+time_text(rule.stability.exit_ns)98 if rule.window_delta is not None:99 spec=rule.window_delta100 line+=f';回看 {time_text(spec.window_ns)},门槛 {spec.threshold},方向 {spec.direction},单调约束 {spec.monotonic}'101 if rule.start_end is not None:102 line+=';等待上限 '+('不限制' if rule.start_end.timeout_ns is None else time_text(rule.start_end.timeout_ns))103 # 开始/结束规则的首次策略存于专用规格;活动和稳定性规则没有该配置项。104 if rule.rule_type not in ('condition_duration','debounce','hysteresis'):105 emit=rule.start_end.emit_initial if rule.start_end is not None else rule.emit_initial106 setting=_setting(plan,'首次已知条件触发',emit,'rules',index,'emit_initial')107 line+=';'+(setting if index is not None else setting.replace('(默认)','(由预设展开)'))108 lines.append(line)109 for window in plan.windows:110 merge={'none':'分别保留','overlap':'合并重叠片段','overlap_and_adjacent':'合并重叠或相邻片段',111 'exit_delay':'按退出延迟合并'}[window.merge]112 lines.append(f' 窗口 {window.window_id}:来自事件 {window.event},向前 {time_text(window.pre_ns)},'113 f'向后 {time_text(window.post_ns)},{merge},退出延迟 {time_text(window.exit_delay_ns)}')114 for sequence in plan.sequences:115 lines.append(f' 序列 {sequence.sequence_id}:请求 {sequence.start_rule} → 响应 {sequence.response_rule};'116 f'允许延迟 [{time_text(sequence.min_delay_ns)}, {time_text(sequence.max_delay_ns)}](包含两端);'117 f'最多等待 {sequence.max_pending} 个请求')118 return lines or [' 未声明事件、窗口或请求响应序列。']119120121def explanation_text(plan,report,*,backend_override=None):122 """按输入→信号→事件→计算→输出说明同一有效计划。参数: 计划/报告/CLI后端。返回: 多行文本。123124 异常: 内部接口不符传播,不用默认文案掩盖未实现类型;无输入I/O。125 """126 dataset=plan.dataset127 domain=dataset.domain128 declared_domain=plan.raw_config['dataset'].get('domain',{})129 start=declared_domain.get('start',time_text(domain.start_ns) if domain is not None else '')130 end=declared_domain.get('end',time_text(domain.end_ns) if domain is not None else '')131 lines=[f'分析 {dataset.dataset_id}:对象 {dataset.entity},会话 {dataset.session_id}',132 '请求范围:'+('整文件(没有时间筛选)' if domain is None else133 f'[{start}, {end}),包含起点,不包含终点'),134 '输入来源:']135 for source in plan.sources:136 config=source.config137 if source.source_type=='memory':138 paths='数据已在配置内声明'139 elif source.source_type=='signal_csv':140 paths=','.join(map(str,config.paths))141 elif source.source_type=='can_blf':142 paths=','.join(map(str,config.paths or (config.path,)))143 else:144 paths=','.join(str(item.path) for item in config.inputs)145 lines.append(f' {source.name}:{SOURCES[source.source_type]};{paths};时间单位 {source.time_unit},偏移 {time_text(config.offset_ns)}')146 if source.source_type in ('can_blf','can_asc','asc_or_blf','raw_can_csv'):147 lines.append(f' 通道 {config.channel + 1},DBC {config.dbc_path}(缺省信号声明按需读取元数据,未读取报文)')148 execution=next((item for item in report.source_execution if item['source']==source.name),None)149 if execution is not None:150 merge='按文件顺序接续' if execution['file_merge']=='concatenate' else '按记录时间归并,需要磁盘暂存'151 lines.append(f" {execution['file_count']}个文件;{merge};读取子进程上限{execution['read_process_limit']};保持一个连续会话(未实测输入时间)")152 lines.append('输入信号:')153 signal_paths={}154 for index,raw in enumerate(plan.raw_config.get('signals',())):155 if 'signals' in raw:156 for child_index,child in enumerate(raw['signals']):157 signal_paths[child['name']]=('signals',index,'signals',child_index)158 else:159 signal_paths[raw['name']]=('signals',index)160 source_types={source.name:source.source_type for source in plan.sources}161 for index,item in enumerate(plan.signals):162 signal=item.definition163 # 未读DBC时直说等待周期,不把None展示为没有有效期或假定无限保持。164 validity = ('DBC周期的3倍(运行时确定)' if signal.valid_for_ns is None165 else time_text(signal.valid_for_ns))166 location=f'{signal.source}.{signal.field}'167 if source_types[signal.source] in ('can_blf','can_asc','asc_or_blf','raw_can_csv'):168 # 人读说明沿用配置通道;内部规范键和机器报告仍保持0基模型合同。169 from ..sources.can_spec import can_field_from_key170 field=can_field_from_key(signal.field)171 location=(f'{signal.source} 通道 {field.channel+1},报文0x{field.frame_id:X}'172 f'({field.frame_id}),信号 {field.signal}')173 lines.append(f' {signal.name} ← {location};单位 {signal.unit};'174 f'有效期 valid_for={validity};'175 +_setting(plan,'样本策略','在有效期内沿用最近有效值(backward_hold)',176 *signal_paths.get(signal.name,('signals',index)),'sample_policy'))177 lines.append(' 有效期表示一个有效样本最多沿用多久;到期即未知,无效更新覆盖前值,缺口不补零。')178 if plan.derived:179 lines.append('派生信号:')180 lines.extend(f' {item.definition.name} = {item.definition.expr};单位 {expression.root.unit}'181 for item,expression in zip(plan.derived,plan.derived_expressions))182 lines.append('事件与时间窗口:')183 lines.extend(_rule_lines(plan))184 lines.append('计算项:')185 for index,calculation in enumerate(plan.calculations):186 signal='无单独信号' if calculation.signal is None else calculation.signal.text187 condition='' if calculation.condition is None else ';筛选 '+calculation.condition.text188 lines.append(f' {calculation.calculation_id}:{OPERATIONS[calculation.operation]};'189 f'{_scope(calculation.scope)};信号 {signal};结果单位 {calculation.unit}{condition};'190 +_setting(plan,'有效覆盖门槛',calculation.minimum_coverage,'calculations',index,'minimum_coverage')191 +f',部分结果策略 {calculation.partial_policy}')192 for calculation in plan.message_calculations:193 lines.append(f' {calculation.calculation_id}:逐消息累计;来源 {calculation.source};'194 f'表达式 {calculation.expr.text};单位 {calculation.unit};按原消息参数求值,不额外乘时间间隔')195 if not plan.calculations and not plan.message_calculations:196 lines.append(' 未声明计算项。')197 lines.append('计算间隔:')198 choices=report.intervals.resolved if report.intervals is not None else ()199 for name,choice in choices:200 description={'actual':'采用实际记录时间差','fixed':'采用指定间隔 '+time_text(choice.step_ns),201 'dbc':'运行时从DBC读取周期'}[choice.mode]202 lines.append(f' {name}:{description}'+(';驱动信号 '+choice.driver if choice.driver else ''))203 if not choices:204 lines.append(' 没有使用普通计算间隔的消费者。')205 lines.append(' 固定间隔或DBC周期不延长信号有效期;导出网格独立于计算间隔。')206 lines.append('输出与默认设置:')207 lines.append(' '+_setting(plan,'结果表格式',plan.outputs.tables,'outputs','tables')+';清单格式 JSON')208 contributions={'none':'关闭','full':'逐条保存'}[plan.outputs.contributions]209 lines.append(' '+_setting(plan,'逐消息贡献明细',contributions,'outputs','contributions'))210 for name,label,output in (('signal_csv','信号CSV导出',plan.outputs.signal_csv),211 ('can_frames','CAN帧导出',plan.outputs.can_frames)):212 if output is None:213 lines.append(' '+label+':关闭(默认)')214 continue215 description='开启,范围 '+output.scope+(',窗口 '+output.window if output.window else '')216 grid=output.spec.resampling if name=='signal_csv' else output.resampling217 if name=='signal_csv':218 description+=';信号 '+', '.join(output.spec.signals)219 else:220 description+=';来源 '+output.source+',格式 '+output.spec.format221 if grid is None:222 description+=';保留原始采样,不重采样'223 else:224 description+=';独立导出网格 '+('运行时读取DBC周期' if grid.period_ns is None else time_text(grid.period_ns))225 lines.append(' '+label+':'+description)226 backend=plan.execution.backend if backend_override is None else backend_override227 lines.append(' 执行后端='+backend+('(命令行覆盖)' if backend_override is not None else228 '(配置指定)' if _declared(plan,'execution','backend') else '(默认)'))229 if report.requested_backend == 'auto':230 lines.append(' 本次选择='+report.selected_backend+';'+report.backend_reason)231 for field,label in (('process_count','读取解码工作进程上限'),('batch_rows','每批记录上限'),('memory_budget_bytes','分析层内存预算/字节'),232 ('result_buffer_rows','结果缓冲行数'),('spool_quota_bytes','暂存磁盘预算/字节')):233 value=report.budget[field] if field=='process_count' else getattr(plan.execution,field)234 if field=='process_count' and value!=plan.execution.process_count:235 lines.append(' '+label+'='+str(value)+'(本次调用覆盖)')236 continue237 lines.append(' '+_setting(plan,label,'未设置' if value is None else value,'execution',field))238 for field,label in (('trace','详细追溯'),('integrity','完整性诊断'),('retain_work','保留现场')):239 lines.append(' '+_setting(plan,label,getattr(plan.diagnostics,field),'diagnostics',field))240 lines.append('检查结论:')241 lines.extend(_validation_lines(report.validation))242 return '\n'.join(lines)243