chronaxis.planning.preset_models
源码基线:8904c2f744951857659abd40aaa3535d57faf53c
1"""保存预设v1的公开不可变声明、角色契约及可序列化计划片段。23输入JSON参数及类型化信号引用,输出冻结spec、角色追溯和公共节点声明。4先校验spec结构并深复制参数,再由外部构建器检查工程契约;本模块不运行I/O或分析。5声明模型独立于构建器/AnalysisPlan,两个入口共用真实类型,避免循环导入及不可解析注解。6候选语义名仅是领域预设v1本地协议;不建立核心目录,不伪造时钟/来源/工程结论。7序列化产生独立JSON副本,节点仍须由正常公共编译器检查依赖与单位。8"""910# 不可变模型及结构校验不依赖预设算法或公开计划。11from collections.abc import Mapping12from dataclasses import dataclass13from ..core.binding import SignalDefinition14from ..core.expression import DerivedDefinition15from ..core.types import SignalDType as D16from ..core.windows import WindowSpec17from .checks import (PlanValidationError, choice_at, json_value, metadata_at,18 object_at, require_json_value, text_at)19from .sequences import SequencePlan202122@dataclass(frozen=True)23class RoleContract:24 """公开本预设版本的角色类型、单位、语义候选、方向与位置要求。2526 有效期必须由SignalDefinition明确提供且正;基础质量使用其固定九位禁用策略。27 semantic_id/version仅用于自动查找,显式绑定仍逐字段检查,不能只凭版本宣称符合。28 """2930 role: str # mode/current/voltage/alarm;不注册全量领域名称。31 dtypes: tuple[D, ...] # 转换后的可接受逻辑类型。32 unit: str # 精确目标单位,关系须verified。33 semantic_id: str # 本模块v1候选标识,大小写敏感。34 sign_convention: str | None = None # 需要时为明确转换后方向。35 measurement_location: str | None = None # 需要时为明确测量位置。36 version: str = '1' # 本公开候选契约版本。37 forbidden_quality: int = 511 # 核心已定义九位基础质量全部禁止。383940# 每个角色仅声明预设实际需要的内容;无通用汽车/电池本体。41CHARGE_ROLES = (42 RoleContract('mode', (D.INT64, D.UINT64), '1', 'battery.mode'),43 RoleContract('current', (D.FLOAT64,), 'A', 'battery.current', 'positive_is_charge', 'pack_terminal'),44 RoleContract('voltage', (D.FLOAT64,), 'V', 'battery.voltage', measurement_location='pack_terminal'),45)46FAULT_ROLES = (RoleContract('alarm', (D.BOOL,), '1', 'fault.alarm'),)47COMMAND_ROLES = (RoleContract('request', (D.BOOL,), '1', 'command.request'),48 RoleContract('response', (D.BOOL,), '1', 'command.response'))495051@dataclass(frozen=True)52class PresetSpec:53 """冻结预设ID、preset_type/version、角色别名及工程参数的独立JSON快照。5455 id只用简单标识;role_bindings值只引用当前计划简单别名,不能写领域属性路径。56 parameters不保留调用者可变对象,实际preset_type参数在build_preset中统一校验。57 """5859 id: str # 生成节点的明确前缀,不自动加后缀解冲突。60 preset_type: str # charge_session、fault_context、command_response或threshold_excursion。61 version: str # 当前仅'1'。62 role_bindings: Mapping # 只读角色→简单别名。63 parameters: Mapping # 深冻结JSON工程声明。6465 def __post_init__(self):66 """检查公开spec结构并复制冻结,不运行数据计算。6768 参数: 无;读取构造字段。69 返回: None,字段保存独立只读副本。70 异常: PlanValidationError为名称、版本、JSON结构或角色别名错误。71 """72 # 公共入口与JSON路径使用同一严格名称/版本检查。73 text_at(self.id, '$.preset.name', identifier=True)74 choice_at(self.preset_type, ('charge_session', 'fault_context', 'command_response', 'threshold_excursion'), '$.preset.preset_type')75 choice_at(self.version, ('1',), '$.preset.version')76 # Mapping只用于冻结后再构建;不接受数组或任意对象作声明。77 if not isinstance(self.role_bindings, Mapping) or not isinstance(self.parameters, Mapping):78 raise PlanValidationError('$.preset', 'role_bindings/parameters必须为对象')79 roles = dict(self.role_bindings)80 parameters = dict(self.parameters) if type(self.parameters) is dict else json_value(self.parameters)81 require_json_value(roles, '$.preset.role_bindings')82 require_json_value(parameters, '$.preset.parameters')83 for role, alias in roles.items():84 text_at(role, '$.preset.role_bindings', identifier=True)85 text_at(alias, '$.preset.role_bindings.' + role, identifier=True)86 object.__setattr__(self, 'role_bindings', metadata_at(roles, '$.preset.role_bindings'))87 object.__setattr__(self, 'parameters', metadata_at(parameters, '$.preset.parameters'))8889 @classmethod90 def from_dict(cls, value):91 """从当前配置的预设对象构造公开不可变声明。9293 参数: value为原生JSON对象,必填name/preset_type/parameters,可选version/role_bindings。94 返回: PresetSpec,role_bindings缺省为空。95 异常: PlanValidationError为未知键、缺项或构造协议错误。96 """97 # 自由元数据不能借预设参数绕过契约。98 raw = object_at(value, '$.preset', ('name', 'preset_type', 'parameters'), ('version', 'role_bindings'))99 return cls(raw['name'], raw['preset_type'], raw.get('version', '1'), raw.get('role_bindings', {}), raw['parameters'])100101102@dataclass(frozen=True)103class RoleBinding:104 """保存已通过字段契约的角色及其原信号定义,供调用者追溯转换与来源。105106 自动/显式查找方式不同,但definition保留完整来源、单位、方向转换和质量身份。107 不保存样本值,也不伪称元数据的物理事实已由系统测量核实。108 """109110 contract: RoleContract # 本次实际校验的v1角色。111 definition: SignalDefinition # 原不可变完整绑定,包含转换证据。112 resolution: str # explicit或semantic_unique。113114115@dataclass(frozen=True)116class PresetRule:117 """保存生成的持续、rising_edge或迟滞声明,尚未绑定来源时钟或执行状态。118119 condition是由合法简单别名/原生有限常量生成的受限DSL;正常计划编译仍检查类型和依赖。120 selection_origin仅持续规则为onset,point没有持续选择;实例不产生事件。121 """122123 id: str # 生成节点ID。124 rule_type: str # condition_duration、rising_edge或hysteresis。125 condition: str | None # 公共DSL;迟滞改用两个明确条件。126 minimum_duration_ns: int | None = None # condition_duration门槛,单位ns。127 selection_origin: str | None = None # condition_duration为onset。128 emit_initial: bool = False # rising_edge首次初始化政策。129 enter_condition: str | None = None # 迟滞进入条件,普通规则不使用。130 exit_condition: str | None = None # 迟滞退出条件。131 enter_delay_ns: int = 0 # 进入确认等待,纳秒。132 exit_delay_ns: int = 0 # 退出确认等待,纳秒。133134135@dataclass(frozen=True)136class PresetCalculation:137 """保存按原活动范围计算的积分或时长声明及独立覆盖/部分结果政策。138139 积分signal是公共信号或派生别名,unit为Wh/Ah;condition_duration用明确BOOL条件和秒。140 partial_policy=None表示继承execution,不是默认强制strict;覆盖质量由既有CalculationDefinition判断。141 """142143 id: str # 生成指标ID。144 scope: str # event:生成活动ID。145 signal: str | None # 简单别名;condition-only duration不提供。146 unit: str # 明确积分输出单位。147 minimum_coverage: int | float = 0 # 0—1,只影响覆盖质量判断。148 partial_policy: str | None = None # 未提供则由完整计划执行选项继承。149 operation: str = 'integral' # integral或按实际事件区间计算duration。150 condition: str | None = None # duration可用恒真条件计整个已知活动区间。151152153@dataclass(frozen=True)154class PlanFragment:155 """保存预设生成的公共节点,不含来源、执行器或真实样本。156157 derived/rules/windows/calculations/sequences为类型化不可变tuple;序列化只是声明副本。158 ID不隐式改变,合入完整计划时继续检查用户节点及其他预设冲突。159 """160161 derived: tuple[DerivedDefinition, ...] = ()162 rules: tuple[PresetRule, ...] = ()163 windows: tuple[WindowSpec, ...] = ()164 calculations: tuple[PresetCalculation, ...] = ()165 sequences: tuple[SequencePlan, ...] = () # 可选点事件配对声明;空值不输出sequences键。166167 def to_dict(self):168 """将类型化片段序列化为当前配置节点副本;没有配对时省略空sequences键。169170 参数: 无;读取生成的不可变声明。171 返回: dict,可合入AnalysisPlan.from_dict;不含伪造来源或执行结果。172 异常: 无主动异常;实例由build_preset构造。173 """174 # 派生元数据不伪造领域意义,公共名称与表达式保持。175 derived = {item.name: {'expr': item.expr, 'unit': item.unit} for item in self.derived}176 rules, windows, calculations = [], [], []177 for item in self.rules:178 row = {'name': item.id, 'rule_type': item.rule_type}179 if item.rule_type == 'hysteresis':180 row.update(enter_condition=item.enter_condition, exit_condition=item.exit_condition,181 enter_delay=f'{item.enter_delay_ns}ns', exit_delay=f'{item.exit_delay_ns}ns')182 rules.append(row)183 continue184 row['condition'] = item.condition185 if item.rule_type == 'condition_duration':186 row.update(minimum_duration=f'{item.minimum_duration_ns}ns', duration_comparison='>=',187 selection_origin=item.selection_origin)188 else:189 row['emit_initial'] = item.emit_initial190 rules.append(row)191 for item in self.windows:192 windows.append({'name': item.window_id, 'rule': item.event_rule_id, 'anchor': item.anchor,193 'pre': f'{item.pre_ns}ns', 'post': f'{item.post_ns}ns', 'merge': item.merge})194 for item in self.calculations:195 row = {'name': item.id, 'scope': item.scope, 'operation': item.operation, 'unit': item.unit,196 'minimum_coverage': item.minimum_coverage}197 if item.signal is not None:198 row['expr'] = item.signal199 if item.condition is not None:200 row['condition'] = item.condition201 if item.operation == 'integral':202 row['method'] = 'zoh'203 if item.partial_policy is not None:204 row['partial_policy'] = item.partial_policy205 calculations.append(row)206 result = {'derived': derived, 'rules': rules, 'windows': windows, 'calculations': calculations}207 if self.sequences:208 result['sequences'] = [dict(name=item.sequence_id, start=item.start_rule, response=item.response_rule,209 min_delay=f'{item.min_delay_ns}ns', max_delay=f'{item.max_delay_ns}ns', max_pending=item.max_pending)210 for item in self.sequences]211 from .config_groups import public_configuration212 return public_configuration(result)213