chronaxis.core.binding
源码基线:8904c2f744951857659abd40aaa3535d57faf53c
1"""将内存来源的原字段与当前计划的信号别名明确关联,并保存可追溯身份。23输入为SessionSpec、SignalDefinition、派生名称和可选同步证据;输出不可变的4SignalBindings及SHA-256身份。先检查信号转换声明,再检查名称冲突、实际字段5存在性和来源归属;联合计算另按实际依赖检查公共时间基准。身份包含实际内存6内容、时间映射、转换、有效期和策略,避免同名不同输入混用。7本模块只依赖标准库及核心模型,不读取文件,不加载DBC,不持有全局缓存;8样本转换、保持和表达式计算由相应执行模块完成。9"""1011# 冻结数据类存元数据,哈希只处理明确的不可变原生输入。12from dataclasses import dataclass, field as dataclass_field13from hashlib import sha2561415# 共享已校验模型和精确时间协议,不依赖被保护的旧链。16from .models import SessionSpec, SourceRef, SourceSpec, TimebaseSpec17from .types import BASE_FORBIDDEN_QUALITY, SignalDType, TimeType18from .units import require_number19from .unit_conversion import binding_version, converted_dtype, require_config_version, validate_conversion_v420from .unit_registry import REGISTRY_VERSION21from .lookup import LookupTablePlan, validate_lookups22from .validation import require_identifier, require_scalar, require_text23from ..timebase.conversion import require_int64_ns242526def _identity(value) -> str:27 """把已知不可变元数据编码为带类型的确定身份文本。2829 参数: value为本模块内部的原生标量或tuple,可含无效原NaN/Inf。30 返回: str;保留bool/int区别及浮点位值,tuple用长度前缀防歧义。31 异常: TypeError为内部未声明结构;不读取文件或执行任意对象repr。32 """33 # tuple逐项编码并保留结构,避免分隔符碰撞。34 if isinstance(value, tuple):35 # 局部输出只用于计划身份,不是产品JSON序列化。36 parts = tuple(_identity(item) for item in value)37 # 每段字符长度明确,字段中的点号/冒号不会改变层级。38 return "t" + "".join(f"{len(item)}:{item}" for item in parts)39 # None与缺失观测保留明确类型。40 if value is None:41 # 不将None等价为0或空文本。42 return "n"43 # 原生类型分别编码,不让True与1共享身份。44 if type(value) is bool:45 # 布尔身份只存在两个值。46 return "b1" if value else "b0"47 # Python整数保留全部位值,不经float。48 if type(value) is int:49 # 负号和十进制值构成精确身份。50 return "i" + str(value)51 # float.hex保留-0及有限浮点身份,诊断NaN/Inf可稳定表示。52 if type(value) is float:53 # NaN不参与普通有效计算,但可存在原始行的输入身份。54 return "f" + value.hex()55 # 已校验文本原样保留,大小写及semantic点号不归一化。56 if type(value) is str:57 # 字符串类型标记避免与整数文本同一身份。58 return "s" + value59 # 只允许本模块明确构造的身份值。60 raise TypeError("绑定身份含未声明结构")616263def _source_identity(source: SourceSpec) -> str:64 """保存内存来源定位、时间映射、观测域和实际行内容的身份。6566 参数: source为已校验SourceSpec;原时间单位ns,原sequence不变。67 返回: SHA256文本;不同原内容不会只因同名来源而复用绑定。68 异常: TypeError为内部结构错误;无文件或旧reader副作用。69 """70 # 字典字段按名称规范排列,仅用于身份,不改变更新顺序。71 rows = tuple((row.time_ns, row.sequence, tuple(sorted(row.values.items()))) for row in source.rows)72 # 保留原始坐标/offset/范围,不把局部从0开始视为公共起点。73 payload = (source.name, source.source_type.value, source.timebase.timebase_id, source.timebase.time_type.value,74 source.timebase.origin, source.offset_ns, source.observation_start_ns,75 source.observation_end_ns, rows)76 # SHA输入完全来源于冻结模型,不调用任意外部序列化器。77 return sha256(_identity(payload).encode("utf-8")).hexdigest()787980@dataclass(frozen=True)81class SignalDefinition:82 """声明原字段如何映射为分析信号,以及转换后值的类型、单位和有效期。8384 name是表达式使用的简单别名,field保留原字段文本;valid_for_ns为正的有限85 有效期,None仅表示声明阶段等待DBC周期,不能交给实际状态计算。86 choices保存不可变的原编码与目标标签或bool配对。基础质量策略87 拒绝全部非零质量位,不接受任意质量策略。方向、位置及可选语义信息只保存88 调用者声明,不推断业务含义;本对象不保存随时间变化的值。89 input_sign_convention等方向证据属于核心模型元数据,不能据此认为公开JSON90 配置已经支持同名扩展字段。91 """9293 name: str # 当前计划简单别名。94 source: str # 会话内显式来源名称。95 field: str # memory原字段,不替换点号。96 dtype: SignalDType # 转换后逻辑类型。97 unit: str # 转换后单位,未知标签保留未校验状态。98 valid_for_ns: int | None # 有限有效期,单位纳秒;None为尚待DBC周期的声明。99 input_unit: str | None = None # 缺省使用unit,不推测原DBC单位。100 scale: int | float = 1 # 已解码值之后额外转换,包含方向因子。101 offset: int | float = 0 # 已解码值之后额外偏移。102 choices: tuple[tuple[int | bool | str, str | bool], ...] = () # 明示编码映射。103 valid_range: tuple[int | float, int | float] | None = None # 转换后业务范围。104 invalid_values: tuple[int | float | bool | str | None, ...] = () # 明示原输入无效值。105 sample_policy: str = "backward_hold" # 首版只实施有限向后保持。106 same_time_policy: str = "error" # 可显式选择last_by_sequence,由状态模块按来源序号取最后值。107 semantic_id: str | None = None # 可选领域字符串,不是DSL路径。108 semantic_contract_version: str | None = None # 声明身份,不证明契约通过。109 mapping_version: str | None = None # 当前映射版本身份。110 sign_convention: str | None = None # 转换后方向,由调用者声明。111 measurement_location: str | None = None # 可选测量位置,不从名称猜测。112 input_sign_convention: str | None = None # 转换前方向证据,core typed元数据。113 direction_factor: int = 1 # 方向不变1,显式反转-1。114 config_version: int = dataclass_field(default=4, kw_only=True) # 规则版本;低层系数已确定,不按省略推导。115 input_dtype: SignalDType | None = dataclass_field(default=None, kw_only=True) # 配置推导前类型;不新增用户字段。116 code_dtype: SignalDType | None = dataclass_field(default=None, kw_only=True) # 仅 v4 枚举的原始整数类型。117 capture_code: bool = dataclass_field(default=False, kw_only=True, compare=False, repr=False) # 当前执行投影要求。118 unit_status: str = dataclass_field(init=False) # 构造校验所得单位状态。119120 def __post_init__(self) -> None:121 """校验完整声明并冻结默认输入单位及精确有效期。122123 参数: 无额外参数;valid_for_ns为正int64纳秒,或尚待DBC周期的None。124 返回: None;unit_status为verified/unverified,未修改输入值。125 异常: TypeError为字段结构;ValueError为缺项/策略/映射/转换冲突;126 TimeOverflowError为有效期超界。数值样本失败由数值层生成质量原因。127 """128 # 配置版本决定单位规则,不能因新增目录改变旧声明。129 require_config_version(self.config_version)130 if self.code_dtype is not None:131 from .enum_codes import validate_code_dtype132 if self.config_version != 4 or self.dtype is not SignalDType.ENUM:133 raise ValueError('code_dtype 仅适用于 v4 枚举信号')134 validate_code_dtype(self.code_dtype, self.choices)135 if type(self.capture_code) is not bool or self.capture_code and self.code_dtype is None:136 raise ValueError('保留原始枚举码需声明 code_dtype')137 # 别名和来源都服从简单名称,不接受属性路径。138 require_identifier(self.name, "signal.name")139 # 当前会话显式定位来源,不能继承上次任务。140 require_identifier(self.source, "signal.source")141 # 原字段独立保留文本,合法同名绑定由名称空间校验。142 require_text(self.field, "signal.field")143 # dtype不是字符串占位,也不从值推断。144 if not isinstance(self.dtype, SignalDType):145 # 未解析的配置不能冒充typed定义。146 raise TypeError("signal.dtype必须为SignalDType")147 # 原始声明类型仅用于证明单位换算引起的提升,低层调用不猜缺省来源类型。148 if self.input_dtype is not None:149 if not isinstance(self.input_dtype, SignalDType):150 raise TypeError("input_dtype必须为SignalDType或None")151 if self.config_version != 4 or converted_dtype(self.input_dtype, self.scale, self.offset) is not self.dtype:152 raise ValueError("input_dtype与已确定的单位转换类型不一致")153 # 目标单位必填,未知标签可以显式未校验。154 require_text(self.unit, "signal.unit")155 # 默认表示没有额外单位变化,不补DBC信息。156 input_unit = self.unit if self.input_unit is None else require_text(self.input_unit, "signal.input_unit")157 # 声明可以等待DBC;已有数值仍须精确且有限,0不代表无限保持。158 valid_for_ns = (None if self.valid_for_ns is None else159 require_int64_ns(self.valid_for_ns, "valid_for_ns"))160 if valid_for_ns is not None and valid_for_ns <= 0:161 # 错误工程声明执行前失败。162 raise ValueError("valid_for_ns必须大于0")163 # 转换系数拒绝bool、非有限和任意对象。164 require_number(self.scale, "signal.scale")165 # 偏移同样在配置入口校验。166 require_number(self.offset, "signal.offset")167 # 方向因子只支持明确不变或反转。168 if type(self.direction_factor) is not int or self.direction_factor not in (-1, 1):169 # 不接受truthiness或任意工程倍率作为方向。170 raise ValueError("direction_factor必须为1或-1")171 # 可选领域/版本/位置只保存非空原字符串,不建立全局目录。172 for key in ("semantic_id", "semantic_contract_version", "mapping_version", "sign_convention",173 "measurement_location", "input_sign_convention"):174 # 只读取已声明dataclass字段,不用于表达式对象访问。175 value = getattr(self, key)176 # None表示未声明,不伪造缺省契约。177 if value is not None:178 # 空声明不等于有效可选元数据。179 require_text(value, f"signal.{key}")180 # 契约版本没有语义声明时不能独立伪装为绑定证明。181 if self.semantic_contract_version is not None and self.semantic_id is None:182 # 版本须关联可选语义身份。183 raise ValueError("semantic_contract_version需要semantic_id")184 # 明示反转同时保留原/目标方向,不从负scale猜测业务。185 if self.direction_factor == -1 and (self.input_sign_convention is None or self.sign_convention is None):186 # 缺少方向证据的反转不能用于契约校验。187 raise ValueError("方向反转必须声明转换前和转换后方向")188 # 只接受向后保持及明确的同时间策略,行为由状态模块实现。189 if self.sample_policy != "backward_hold" or self.same_time_policy not in ("error", "last_by_sequence"):190 # 不接受插值/未实现策略作为已支持声明。191 raise ValueError("首版仅支持backward_hold及error/last_by_sequence")192 # 固定不可变容器,避免后续外部修改映射或无效值。193 if not isinstance(self.choices, tuple) or not isinstance(self.invalid_values, tuple):194 # 不隐式接受可变dict/list。195 raise TypeError("choices/invalid_values必须为tuple")196 # 输入无效值保留类型,不让bool自动等同整数编码。197 for value in self.invalid_values:198 # 无效原标量可含NaN,数值层另行标记NONFINITE。199 require_scalar(value)200 # 编码身份类型化,避免Python的True==1掩盖冲突。201 keys = set()202 # 每条映射都需明确原编码和目标标签。203 for choice in self.choices:204 # tuple配对之外的结构不可定位。205 if not isinstance(choice, tuple) or len(choice) != 2:206 # 配置层不能传入半个声明。207 raise TypeError("choices各项必须为二元tuple")208 # 明示映射两端,不按顺序自动编号。209 code, label = choice210 # 原编码只接可精确比较的固定类型。211 if type(code) not in (int, bool, str):212 # 浮点/容器不能作为首版枚举编码。213 raise TypeError("choices原编码仅支持int/bool/str")214 # 类型加值构成明确原编码身份。215 key = (type(code), code)216 # 重复编码无法唯一定位业务标签。217 if key in keys:218 # 不取第一条或最后一条隐式覆盖。219 raise ValueError("choices原编码重复")220 # 当前声明中的唯一编码在局部集合登记。221 keys.add(key)222 # enum目标是公开逻辑标签,不允许数值别名。223 if self.dtype is SignalDType.ENUM:224 # 标签保留大小写和领域文本。225 require_text(label, "choices.label")226 elif self.dtype is SignalDType.BOOL:227 # 任意数值truthiness不属于声明布尔映射。228 if type(label) is not bool:229 # 1不是合法目标True。230 raise TypeError("bool choices目标必须为bool")231 else:232 # 数值信号的编码可作为数值处理,不套业务标签映射。233 raise ValueError("choices仅用于enum或bool")234 # enum必须有明确编码/标签域,不接任意字符串。235 if self.dtype is SignalDType.ENUM and not self.choices:236 # 空域不是可执行枚举声明。237 raise ValueError("enum需要非空choices")238 # 标签可由多个编码指向同一含义,域按第一次出现顺序稳定保留。239 if self.dtype in (SignalDType.BOOL, SignalDType.ENUM) and (self.scale != 1 or self.offset != 0):240 # 不对逻辑值施加数值缩放。241 raise ValueError("bool/enum不能声明数值转换")242 # 范围只支持转换后数值,两个端点都要明确。243 if self.valid_range is not None:244 # 不接列表或不完整端点。245 if not isinstance(self.valid_range, tuple) or len(self.valid_range) != 2:246 # 配置结构必须完整。247 raise TypeError("valid_range必须为二元tuple")248 # 逻辑bool/enum不得以数值区间验证业务含义。249 if self.dtype in (SignalDType.BOOL, SignalDType.ENUM):250 # 使用choices声明逻辑域。251 raise ValueError("bool/enum不能使用数值valid_range")252 # 范围端点原生有限,保留整数精度。253 lower = require_number(self.valid_range[0], "valid_range.lower")254 # 上限与下限采用相同入口。255 upper = require_number(self.valid_range[1], "valid_range.upper")256 # 不自动交换反向范围。257 if upper < lower:258 # 声明冲突须执行前拒绝。259 raise ValueError("valid_range上限必须不小于下限")260 # 已知单位校验关系,未知单位明确保留状态。261 status = validate_conversion_v4(input_unit, self.unit, self.scale, self.offset, self.direction_factor)262 # v4整数转换中的1.0等整数系数规范为int,保留安全定宽整数计算路径。263 if self.dtype in (SignalDType.INT64, SignalDType.UINT64):264 for coefficient in ('scale', 'offset'):265 value = getattr(self, coefficient)266 if type(value) is float and value.is_integer():267 object.__setattr__(self, coefficient, int(value))268 # 冻结缺省输入单位,后续身份与转换都读取同一事实。269 object.__setattr__(self, "input_unit", input_unit)270 # 规范有效期为Python整数纳秒,不改变数值。271 object.__setattr__(self, "valid_for_ns", valid_for_ns)272 # 状态来自真实校验,不允许调用者填verified。273 object.__setattr__(self, "unit_status", status)274275 def identity(self) -> tuple:276 """返回声明字段完整身份,供执行计划缓存和追溯使用。277278 参数: 无;读取当前冻结声明。279 返回: tuple;含实际定位、转换、有效期、质量及语义/版本元数据。280 异常: 无主动异常;不使用全局缓存或当前样本值。281 """282 # 全部影响绑定/质量/单位的字段进入身份,基础质量固定九位全禁。283 payload = (self.name, self.source, self.field, self.dtype.value, self.unit, self.valid_for_ns,284 self.input_unit, self.scale, self.offset, self.choices, self.valid_range,285 self.invalid_values, self.sample_policy, self.same_time_policy, self.semantic_id,286 self.semantic_contract_version, self.mapping_version, self.sign_convention,287 self.measurement_location, self.input_sign_convention, self.direction_factor,288 self.unit_status)289 # 身份包含固定单位规则,避免不同规则的结果复用。290 if self.code_dtype is not None:291 payload += ('code_dtype', self.code_dtype.value)292 # 预设核对真实身份的末项质量策略;版本和原码类型不能占据该位置。293 return (*payload, self.config_version, REGISTRY_VERSION,294 None if self.input_dtype is None else self.input_dtype.value, BASE_FORBIDDEN_QUALITY)295296 @property297 def unit_conversion_promoted(self) -> bool:298 """说明浮点类型是否由已知整数输入的单位换算引起。299300 参数: 无。301 返回: 仅有整数输入证据且实际提升时为True。302 异常: 无;不依据样本数值猜类型。303 """304 # 显式FLOAT64但没有原输入声明时不能冒称整数提升。305 return (self.dtype is SignalDType.FLOAT64306 and self.input_dtype in (SignalDType.INT64, SignalDType.UINT64)307 and converted_dtype(self.input_dtype, self.scale, self.offset) is SignalDType.FLOAT64)308309310@dataclass(frozen=True)311class SynchronizationEvidence:312 """保存来源使用固定时间偏移进入公共分析时基的核实说明。313314 source指定会话内来源,analysis_timebase指定目标时基,evidence保留调用者315 提供的依据。本模型检查声明结构,不检查物理设备、不估计漂移或改动偏移。316 来源缺少同步依据时仍可独立分析;参与联合计算时须满足同步检查。317 """318319 source: str # 明确会话内来源。320 analysis_timebase: TimebaseSpec # offset映射后的公共分析基准。321 evidence: str # 调用者声明的核实依据,不自动生成业务事实。322323 def __post_init__(self) -> None:324 """校验显式来源和时基证据结构。325326 参数: 无;读取source、analysis_timebase及evidence。327 返回: None;不更改来源或推断时间映射。328 异常: TypeError/ValueError为无效声明。329 """330 # 来源必须能在当前会话唯一定位。331 require_identifier(self.source, "synchronization.source")332 # 空依据不能当作已核实同步。333 require_text(self.evidence, "synchronization.evidence")334 # 目标分析时基也必须通过模型构造门槛。335 if not isinstance(self.analysis_timebase, TimebaseSpec):336 # 字典或字符串不提供完整公共起点身份。337 raise TypeError("analysis_timebase必须为TimebaseSpec")338339340@dataclass(frozen=True)341class SignalBindings:342 """检查并冻结一个会话的信号定义、实际来源和同步证据。343344 signals与derived_names共用简单名称空间,每个原字段必须出现在本会话内存行中。345 identity包含实际内容、时基、转换和策略,来源同名也不能替代完整身份。346 本对象没有全局注册表,不继承其他任务绑定;值更新由状态执行器读取冻结输入,347 不会修改本对象。348 """349350 session: SessionSpec # 当前计划单会话来源上下文。351 signals: tuple[SignalDefinition, ...] # 已声明原字段到别名的关联。352 derived_names: tuple[str, ...] = () # 实际派生定义由表达式层提供,此处只检查名称冲突。353 synchronization: tuple[SynchronizationEvidence, ...] = () # 明示固定offset依据。354 config_version: int | None = dataclass_field(default=None, kw_only=True) # 缺省继承定义;空绑定默认4。355 extensions: object = dataclass_field(default=None, kw_only=True, repr=False) # 仅接收宿主传入的不可变注册。356 lookups: tuple[LookupTablePlan, ...] = dataclass_field(default=(), kw_only=True) # 数值标定表声明。357 identity: str = dataclass_field(init=False) # 实际校验所得完整绑定SHA。358359 def __post_init__(self) -> None:360 """检查统一名称空间、来源定位和同步证据,再构建身份。361362 参数: 无;session/signals及可选derived_names/synchronization均为已校验且不可变的模型输入。363 返回: None;通过后所有字段保持不可变。364 异常: TypeError为结构;ValueError为名称、来源字段或证据冲突。365 """366 # 顶层结构不能以可变列表或未校验对象进入计划。367 if not isinstance(self.session, SessionSpec) or not all(isinstance(item, tuple) for item in368 (self.signals, self.derived_names, self.synchronization)):369 # 不隐式解析自由配置或无限迭代器。370 raise TypeError("绑定需要SessionSpec及tuple声明")371 # 来源名唯一性已由SessionSpec构造证明。372 sources = {source.name: source for source in self.session.sources}373 # 名称集合局部于当前计划,signals/derived不能各自重复。374 names = set()375 # 保留信号声明顺序,不取某个冲突项覆盖。376 for definition in self.signals:377 # 每项必须通过完整SignalDefinition校验。378 if not isinstance(definition, SignalDefinition):379 # 类名或dict占位不属于真实定义。380 raise TypeError("signals必须包含SignalDefinition")381 # 实际内存绑定没有待查询DBC,不允许未确定的有效期进入对齐内核。382 if definition.valid_for_ns is None:383 raise ValueError(f"{definition.name}有效期尚未确定;请提供period或valid_for")384 # 重复别名不能解析为唯一原字段。385 if definition.name in names:386 # 不自动加后缀或取第一项。387 raise ValueError(f"signal别名重复:{definition.name}")388 # 登记唯一别名供派生名称冲突检查。389 names.add(definition.name)390 # 来源必须在本会话存在,不能靠语义名称补齐。391 if definition.source not in sources:392 # 错误定位在编译前拒绝。393 raise ValueError(f"{definition.name}来源不存在:{definition.source}")394 # memory字段必须已在实际行中暴露,不猜列或读旧DBC。395 if not any(definition.field in row.values for row in sources[definition.source].rows):396 # 空来源没有已声明字段schema,不能声称字段可解析。397 raise ValueError(f"{definition.name}原字段不存在:{definition.field}")398 # 所有定义属于同一不可变版本;空绑定也可显式选择4。399 object.__setattr__(self, "config_version", binding_version(self.signals, self.config_version))400 validate_lookups(self.lookups, self.config_version)401 if self.extensions is not None:402 from ..extensions import ExtensionRegistry403 if type(self.extensions) is not ExtensionRegistry or self.config_version != 4:404 raise ValueError('extensions需要版本4及宿主ExtensionRegistry')405 # 派生声明也遵守同一简单别名规则。406 for name in self.derived_names:407 # 包括关键字及首版函数名冲突。408 require_identifier(name, "derived.name")409 # signals和derived统一校验,未生成表达式也不能隐藏同名。410 if name in names:411 # 不修改用户声明名称。412 raise ValueError(f"signals/derived名称冲突:{name}")413 # 保留派生名称登记事实,供后续表达式层接线。414 names.add(name)415 # 同步证据也只能一个来源一个明确目标。416 evidence_names = set()417 # 所有显式证据须关联实际来源和当前分析时基。418 for evidence in self.synchronization:419 # 保留typed证据结构,不接自由字典。420 if not isinstance(evidence, SynchronizationEvidence):421 # 不能以名称声明冒充完整同步依据。422 raise TypeError("synchronization需要SynchronizationEvidence")423 # 不存在/重复来源证据都是配置错误。424 if evidence.source not in sources or evidence.source in evidence_names:425 # 不默认选第一条同步说明。426 raise ValueError("同步证据来源不存在或重复")427 # 所有校准声明必须进入当前会话分析基准。428 if evidence.analysis_timebase != self.session.timebase:429 # 不接受指向其他会话时基的旧映射。430 raise ValueError("同步证据目标与会话分析时基不一致")431 # 记录该来源已有明确证据。432 evidence_names.add(evidence.source)433 # 完整实际输入身份避免跨任务同名不同数据误复用执行缓存。434 payload = (self.session.session_id, self.session.timebase.timebase_id, self.session.timebase.time_type.value,435 self.session.timebase.origin, self.session.domain_start_ns, self.session.domain_end_ns,436 tuple(_source_identity(source) for source in self.session.sources),437 tuple(definition.identity() for definition in self.signals), self.derived_names,438 tuple((item.source, item.analysis_timebase.timebase_id, item.analysis_timebase.time_type.value,439 item.analysis_timebase.origin, item.evidence) for item in self.synchronization))440 # 即使没有信号,v4绑定也不能与旧空绑定共享身份。441 payload += (self.config_version, REGISTRY_VERSION)442 if self.extensions is not None:443 payload += ('host-extensions-v1', self.extensions.identity)444 if self.lookups:445 payload += ('lookup-table-v1', tuple(t.identity for t in self.lookups))446 # 身份由全部校验事实生成,不允许调用者提供假SHA。447 object.__setattr__(self, "identity", sha256(_identity(payload).encode("utf-8")).hexdigest())448449 def resolve(self, name: str) -> SignalDefinition:450 """解析当前计划真实信号别名,不自动寻找semantic候选。451452 参数: name为简单别名;派生名称由表达式层另行处理。453 返回: 对应冻结SignalDefinition。454 异常: ValueError为非法或未知别名;不访问Python对象属性。455 """456 # 与声明使用相同名称规则,不转换点号。457 require_identifier(name, "signal alias")458 # 当前会话逐项查找明确唯一引用。459 for definition in self.signals:460 # 比较原大小写完整别名。461 if definition.name == name:462 # 不返回其他任务中同名定义。463 return definition464 # 未注册名字不能推断为字段或领域语义。465 raise ValueError(f"未知信号别名:{name};可用:{', '.join(item.name for item in self.signals)}")466467 def source_ref(self, name: str) -> SourceRef:468 """给出当前会话下解析后的完整memory字段引用。469470 参数: name为已定义信号别名。471 返回: SourceRef;含当前session_id、source_name和原field。472 异常: ValueError为未定义或非法别名。473 """474 # 先证明引用属于当前定义,不从全局映射继承。475 definition = self.resolve(name)476 # 完整会话身份使跨会话同名输入可以明确区别。477 return SourceRef(self.session.session_id, definition.source, definition.field)478479 def require_synchronized(self, names: tuple[str, ...]) -> None:480 """在联合依赖执行前检查实际来源的公共分析基准。481482 参数: names为参与同一表达式/指标的原信号别名tuple;单来源允许独立分析。483 返回: None;公共起点或显式固定offset依据齐全时通过。484 异常: TypeError为容器;ValueError为未知别名或同步缺项。485 """486 # 不接会被调用者更改的依赖容器。487 if not isinstance(names, tuple):488 # 编译器需显式给出当前节点依赖。489 raise TypeError("联合依赖names必须为tuple")490 # 同一来源多个信号不会制造额外同步要求。491 used = {self.resolve(name).source for name in names}492 # 单来源可在自己坐标上独立分析。493 if len(used) <= 1:494 # 未使用来源不参与当前绑定的证据检查。495 return496 # 只检查实际参与联合计算的来源。497 sources = {source.name: source for source in self.session.sources if source.name in used}498 # 显式证据已在构造时证明指向当前分析基准。499 evidenced = {item.source for item in self.synchronization}500 # 没有校准声明的来源需直接拥有相同公共起点。501 for source in sources.values():502 # 明示证据提供固定offset映射依据,不估计漂移。503 if source.name in evidenced:504 # offset在SourceSpec已检查范围,执行器按原映射应用。505 continue506 # absolute统一UTC;relative需timebase_id/origin共同一致。507 same = source.timebase.time_type is self.session.timebase.time_type and (508 source.timebase.time_type is TimeType.ABSOLUTE or source.timebase == self.session.timebase)509 # 仅offset数值或各自首点0不证明公共起点。510 if not same:511 # 错误列出缺项来源,调用者可补核实声明或独立分析。512 raise ValueError(f"联合依赖缺少同步依据:{source.name};需要公共起点或固定offset核实证据")513