chronaxis.core.numeric
源码基线:8904c2f744951857659abd40aaa3535d57faf53c
1"""把已解码的单个值转换成声明类型,并提供独立的标量运算参考实现。23normalize_reference接收SignalDefinition、原生标量和上游质量原因,4只应用额外声明的scale、offset、枚举映射与有效范围,输出ScalarValue。5算术和比较接收ScalarValue,先检查类型与质量,再计算并检查结果范围。6value保存数值或原诊断;dtype说明类型;valid表示能否使用;quality记录原因。78整数先在Python精确整数域中运算,再检查int64或uint64的定宽边界。9需要float64时,先检查整数是否能被精确表示;无效输入不参与正常计算。10memory_series_reference从已有内存行生成信号历史,保留来源和纳秒时间。11本模块只依赖标准库与核心模型,不读取文件、加载DBC或调用NumPy数值内核。12数组实现可共享静态类型规则和模型检查,数值计算仍由各自实现承担。13"""1415# 有限性/配置端点辅助只使用标准库,不替代数组数值内核。16from math import isfinite17# 静态内建比较仅用于参考白名单,不接自由函数。18from operator import eq, ne, lt, le, gt, ge19# 只读静态映射或显式质量/type枚举用于结构协议。20from types import MappingProxyType2122# 使用真实已校验定义与来源模型,memory适配只处理已暴露原行。23from .binding import SignalBindings, SignalDefinition24# 保留原样本来源与身份,不能重写原行。25from .models import SignalSample26# 接入已校验保持模型,不改变原来源顺序。27from .state import SignalSeries28# 只读静态映射或显式质量/type枚举用于结构协议。29from .types import QualityFlag, SignalDType30# 共享原生结构校验,不共享数值参考。31from .validation import require_scalar32# 时间offset使用已有精确整数加法协议。33from ..timebase.conversion import checked_add_ns3435# 重导出共用标量类,保持公开导入和进程序列化路径不变。36from .scalar import INTEGER_BOUNDS, ScalarValue3738# 首版算术和比较白名单,DSL语法检查由EXPR承担。39ARITHMETIC = frozenset({"add", "sub", "mul", "div"})40# 精确比较白名单与算术分开,不放行任意函数。41COMPARISONS = frozenset({"eq", "ne", "lt", "le", "gt", "ge"})42# 静态内建比较映射不接受用户函数,也无需创建无说明内部函数。43COMPARE_FUNCTIONS = MappingProxyType({"eq": eq, "ne": ne, "lt": lt, "le": le, "gt": gt, "ge": ge})444546def result_dtype(operator: str, left: SignalDType, right: SignalDType, allow_float_literal: bool = False) -> SignalDType:47 """根据运算和两侧声明类型,确定允许的结果类型。4849 参数:50 operator: 已注册的算术或比较名称。51 left、right: 两侧SignalDType声明。52 allow_float_literal: 编译器已证明混合类型中的浮点侧是字面量时才为True。53 返回:54 SignalDType。比较返回BOOL;除法或允许的浮点算术返回FLOAT64。55 异常:56 TypeError: 类型声明或字面量证明标志的类型不合法。57 ValueError: 运算未注册、逻辑类型不支持该运算,或混合类型未显式归一化。58 边界:59 本函数只检查静态类型,不计算信号值。INT64与UINT64可精确比较;60 两种不同整数类型的信号不能直接混合算术。61 """62 # 编译语义只接受完整枚举和明确布尔字面量证明。63 if not isinstance(left, SignalDType) or not isinstance(right, SignalDType) or type(allow_float_literal) is not bool:64 # 不以字符串类型名绕过静态门槛。65 raise TypeError("运算dtype必须显式声明")66 # 未注册函数/运算不能进入数值执行。67 if operator not in ARITHMETIC | COMPARISONS:68 # 幂等未支持项由上层直接拒绝。69 raise ValueError(f"未支持数值运算:{operator}")70 # bool和enum仅支持同类型等值比较。71 if left in (SignalDType.BOOL, SignalDType.ENUM) or right in (SignalDType.BOOL, SignalDType.ENUM):72 # 不将逻辑值转0/1参加一般数值运算。73 if left is not right or operator not in ("eq", "ne"):74 # 枚举排序/算术不是首版契约。75 raise ValueError("bool/enum仅支持同类型eq/ne")76 # 逻辑等值比较仍返回显式bool掩码。77 return SignalDType.BOOL78 # 两种定宽整数比较在整数域精确,不能隐藏转float。79 if operator in COMPARISONS:80 # 仅整数/float信号混合比较需先显式归一化。81 if left is not right and SignalDType.FLOAT64 in (left, right) and not allow_float_literal:82 # 常量路径须由编译器证明,信号路径配置拒绝。83 raise ValueError("整数/float信号比较需显式归一化")84 # 同型或signed/unsigned比较输出bool。85 return SignalDType.BOOL86 # 不同整数dtype信号也不能隐式promotion算术。87 if left is not right and not (allow_float_literal and SignalDType.FLOAT64 in (left, right)):88 # 配置错误不伪装成样本缺失。89 raise ValueError("不同dtype信号算术需显式归一化")90 # 除法或明确浮点字面量算术使用float64,先检查整数精度。91 return SignalDType.FLOAT64 if operator == "div" or SignalDType.FLOAT64 in (left, right) else left929394def _exact_float(value: int | float) -> tuple[float | None, QualityFlag]:95 """参考路径用整数往返校验显式浮点转换。9697 参数: value为原生整数或float;输入类型由调用者明确校验。98 返回: float值及NONE,或None及PRECISION_LOSS/NONFINITE。99 异常: 无主动异常;数值溢出转换为质量,不吞配置类型错误。100 """101 # 超大Python整数转换可能抛OverflowError,表示非有限浮点输出。102 try:103 # 显式转换,不能先丢精度后比较浮点近似。104 result = float(value)105 except OverflowError:106 # 只将明确转换溢出变为NONFINITE。107 return None, QualityFlag.NONFINITE108 # 已有float的非有限同样禁止。109 if not isfinite(result):110 # 不能保留Inf为正常数值。111 return None, QualityFlag.NONFINITE112 # 整数与转回整数精确比较,不先把原整数也变float。113 if type(value) is int and int(result) != value:114 # 2**53+1不能默默舍入为2**53。115 return None, QualityFlag.PRECISION_LOSS116 # 合法显式float转换完成,不恢复来源已有float精度。117 return result, QualityFlag.NONE118119120def to_float_reference(value: ScalarValue) -> ScalarValue:121 """显式将数值结果转换float64,保留invalid和精度失败。122123 参数: value为int64/uint64/float64 ScalarValue。124 返回: ScalarValue float64,无法精确表示时invalid/PRECISION_LOSS。125 异常: TypeError/ValueError为结构或逻辑dtype。126 """127 # 逻辑bool/enum不能进入数值转换。128 if not isinstance(value, ScalarValue) or value.dtype in (SignalDType.BOOL, SignalDType.ENUM):129 # 不用truthiness或字符串解析实现转换。130 raise TypeError("float转换需要数值ScalarValue")131 # 无效输入不执行正常转换,原因传递。132 if not value.valid:133 # 输出占位不参与正常算术。134 return ScalarValue(None, SignalDType.FLOAT64, False, value.quality)135 # 参考使用独立整数往返,不调用数组精度掩码。136 number, quality = _exact_float(value.value)137 # 转换失败是样本质量,不是正常舍入结果。138 return ScalarValue(number, SignalDType.FLOAT64, quality == QualityFlag.NONE, quality)139140141def binary_reference(operator: str, left: ScalarValue, right: ScalarValue, *, allow_float_literal: bool = False) -> ScalarValue:142 """对两个已检查的标量执行算术或比较,并保留质量原因。143144 参数:145 operator: 已注册的二元运算名称;left、right为ScalarValue。146 allow_float_literal: 由编译器证明浮点字面量可以参与混合类型运算。147 返回:148 ScalarValue。输入无效时传播两侧原因;整数超界、除零、非有限结果149 或浮点转换丢精度时返回无效结果,不以零或舍入值替代。150 异常:151 TypeError、ValueError: 输入模型、运算类型或枚举比较域不符合要求。152 流程:153 先检查静态类型,即使输入无效也不跳过配置错误。正常整数在Python154 精确整数域中计算,再检查目标定宽范围;浮点路径先检查转换精度。155 """156 # 运行输入须已经通过逻辑结果构造门槛。157 if not isinstance(left, ScalarValue) or not isinstance(right, ScalarValue):158 # 不把无类型对象直接算作正常数据。159 raise TypeError("二元运算需要ScalarValue")160 # 即使数据invalid也先检查静态配置错误。161 dtype = result_dtype(operator, left.dtype, right.dtype, allow_float_literal)162 # enum等值比较须同一公开逻辑域。163 if left.dtype is SignalDType.ENUM and left.labels != right.labels:164 # 不同域的相同标签不证明相同业务含义。165 raise ValueError("enum比较域不一致")166 # 原原因全部保留,invalid不进入正常值计算。167 quality = left.quality | right.quality168 # 基础质量禁全部非零位。169 if not left.valid or not right.valid:170 # 结果无正常值,不能用0假装成功。171 return ScalarValue(None, dtype, False, quality)172 # 原生值用于独立参考,整数不会先经float。173 a, b = left.value, right.value174 # float数值路径先核查全部整数可精确表示。175 if dtype is SignalDType.FLOAT64 or (operator in COMPARISONS and SignalDType.FLOAT64 in (left.dtype, right.dtype)):176 # 参考两个操作数分别转换/检查。177 a, qa = _exact_float(a)178 # 保留两侧同时发生的精度/非有限诊断。179 b, qb = _exact_float(b)180 # 转换失败不继续算术或比较。181 quality |= qa | qb182 # 返回明确invalid,不能比较已舍入整数。183 if quality != QualityFlag.NONE:184 # 同样错误政策由NumPy独立精度掩码实现。185 return ScalarValue(None, dtype, False, quality)186 # 比较在精确原生域中按白名单执行。187 if operator in COMPARISONS:188 # 此处允许signed/unsigned Python整数精确比较。189 result = COMPARE_FUNCTIONS[operator](a, b)190 # 正常比较结果是bool,不转Truth或吞UNKNOWN。191 return ScalarValue(result, SignalDType.BOOL)192 # 0分母在真正除法之前拒绝。193 if operator == "div" and b == 0:194 # 包含0/0,不能生成正常Inf/NaN或0。195 return ScalarValue(None, dtype, False, QualityFlag.DOMAIN_ERROR)196 # Python整数中间值无界,之后仍检查定宽规范。197 result = a + b if operator == "add" else a - b if operator == "sub" else a * b if operator == "mul" else a / b198 # 定宽输出不能以Python大整数成功为由放行。199 if dtype in INTEGER_BOUNDS:200 # signed/unsigned各自端点明确。201 lower, upper = INTEGER_BOUNDS[dtype]202 # 下溢同样属于OVERFLOW,不返回wrap后的值。203 if not lower <= result <= upper:204 # 质量失败不会参与后续事件/统计。205 return ScalarValue(None, dtype, False, QualityFlag.OVERFLOW)206 # 浮点溢出产生非有限结果,不能沿用整数OVERFLOW原因。207 elif not isfinite(result):208 # float输出非有限是NONFINITE,不正常归约。209 return ScalarValue(None, dtype, False, QualityFlag.NONFINITE)210 # 正常结果保持输出dtype和NONE质量。211 return ScalarValue(result, dtype)212213214def unary_reference(operator: str, value: ScalarValue) -> ScalarValue:215 """独立参考abs/negate,在整数域拒绝min翻转及unsigned下溢。216217 参数: operator为abs或neg;value为数值ScalarValue。218 返回: 同dtype ScalarValue,超界为OVERFLOW,invalid原因保留。219 异常: TypeError/ValueError为输入或未支持逻辑/运算。220 """221 # 不接受逻辑值或自由运算名。222 if not isinstance(value, ScalarValue) or value.dtype in (SignalDType.BOOL, SignalDType.ENUM) or operator not in ("abs", "neg"):223 # unary DSL白名单由EXPR静态映射。224 raise ValueError("unary仅支持数值abs/neg")225 # 无效输入不能通过abs恢复正常。226 if not value.valid:227 # 原原因作为结果质量传播。228 return ScalarValue(None, value.dtype, False, value.quality)229 # Python精确整数参考,不发生int64 min的先wrap。230 result = abs(value.value) if operator == "abs" else -value.value231 # 正常结果仍服从同一定宽。232 if value.dtype in INTEGER_BOUNDS:233 # unsigned neg非零下溢与signed min翻转均在范围外。234 lower, upper = INTEGER_BOUNDS[value.dtype]235 # 先验证再构造,禁止饱和值假装正常。236 if not lower <= result <= upper:237 # 明示原因,无正常值可计算。238 return ScalarValue(None, value.dtype, False, QualityFlag.OVERFLOW)239 # 有限float取abs/neg仍有限,逻辑构造再次校验。240 return ScalarValue(result, value.dtype)241242243def normalize_reference(definition: SignalDefinition, value, quality: QualityFlag = QualityFlag.NONE) -> ScalarValue:244 """按显式信号声明转换一个已解码值,并检查业务有效范围。245246 参数:247 definition: SignalDefinition,提供目标类型、scale、offset、choices及范围。248 value: 已解码的原生标量;不解析数值字符串。249 quality: 上游已有质量原因,默认NONE。250 返回:251 ScalarValue。正常值使用目标类型;无效时保留原值及缺失、非有限、252 超范围、解码失败、精度损失或溢出的原因。枚举同时保留固定标签域。253 异常:254 TypeError: 声明、质量或原生标量结构不合法。255 数值转换失败记入quality,配置错误仍由底层检查直接抛出。256 流程:257 先处理已有质量、缺失值和显式无效值;逻辑类型按choices映射,258 数值类型应用额外的value * scale + offset,再检查定宽与valid_range。259 输入已完成上游解码,本函数不加载DBC,也不重复上游的工程单位转换。260 """261 # 不接无类型转换字典或自由质量值。262 if not isinstance(definition, SignalDefinition) or not isinstance(quality, QualityFlag):263 # 配置校验先于数值操作。264 raise TypeError("normalize需要SignalDefinition及QualityFlag")265 # 输入原生类型检查无副作用,不解析数值字符串。266 require_scalar(value)267 # enum结果域来自已经冻结的编码映射。268 labels = tuple(dict.fromkeys(label for _, label in definition.choices)) if definition.dtype is SignalDType.ENUM else ()269 # 已有质量不足以正常使用,不再执行转换。270 if quality != QualityFlag.NONE:271 # 保留上游原值及原因。272 return ScalarValue(value, definition.dtype, False, quality, labels)273 # 原缺失先明确标记,不补0。274 if value is None:275 # 缺失没有正常业务类型。276 return ScalarValue(value, definition.dtype, False, QualityFlag.MISSING, labels)277 # 输入NaN/Inf先拒绝,不能借scale0把非有限输入恢复正常。278 if type(value) is float and not isfinite(value):279 # 来源已有浮点精度不做伪修复。280 return ScalarValue(value, definition.dtype, False, QualityFlag.NONFINITE, labels)281 # 所有原整数(包含映射编码)须属于原64位数值协议。282 if type(value) is int and not -(2 ** 63) <= value <= 2 ** 64 - 1:283 # 原值仍保存,超宽Python整数不是新输入dtype。284 return ScalarValue(value, definition.dtype, False, QualityFlag.OVERFLOW, labels)285 # 无效值规则使用原类型与值,bool不隐式等于int编码。286 if any(type(item) is type(value) and item == value for item in definition.invalid_values):287 # 明示业务无效值没有正常计算意义。288 return ScalarValue(value, definition.dtype, False, QualityFlag.OUT_OF_RANGE, labels)289 # 逻辑映射与数值转换分开,不能通过truthiness猜标签。290 if definition.dtype in (SignalDType.BOOL, SignalDType.ENUM):291 # 原生bool在没有额外映射时可以直接使用。292 if definition.dtype is SignalDType.BOOL and not definition.choices and type(value) is bool:293 # 真正False是合法值,不代表UNKNOWN。294 return ScalarValue(value, definition.dtype)295 # 明示编码精确匹配,未声明码不能自动取第一个。296 matches = [label for code, label in definition.choices if type(code) is type(value) and code == value]297 # 编码域之外或没有bool声明映射均失败。298 if not matches:299 # 保留错误原码诊断,不能转换任意数值为bool。300 return ScalarValue(value, definition.dtype, False, QualityFlag.OUT_OF_RANGE, labels)301 # 定义已保证编码唯一,不猜目标标签。302 return ScalarValue(matches[0], definition.dtype, labels=labels)303 # 普通数值原值只允许int/float,bool与str不能隐藏转换。304 if type(value) not in (int, float):305 # I/O层解析失败需明确原因,不自行float字符串。306 return ScalarValue(value, definition.dtype, False, QualityFlag.DECODE_ERROR)307 # 整数输入与整数系数可在独立Python整数域精确变换。308 integer_path = definition.dtype in INTEGER_BOUNDS and all(type(item) is int for item in (value, definition.scale, definition.offset))309 # 完整线性结果在精确域计算,避免中间机器wrap。310 if integer_path:311 # 转换是额外已声明线性关系,不重复上游缩放。312 result = value * definition.scale + definition.offset313 else:314 # 非整数系数或float输出须先检查输入和整数系数可精确表示。315 number, qvalue = _exact_float(value)316 # 大整数系数同样不能悄悄舍入。317 scale, qscale = _exact_float(definition.scale)318 # 偏移涉及整数时亦遵守精度契约。319 offset, qoffset = _exact_float(definition.offset)320 # 任一精度/非有限问题先停止正常变换。321 failure = qvalue | qscale | qoffset322 # 保留原诊断,不执行失败位置乘加。323 if failure != QualityFlag.NONE:324 # 两后端以独立掩码验证同样政策。325 return ScalarValue(value, definition.dtype, False, failure)326 # 已核实输入后再计算额外变换。327 result = number * scale + offset328 # 浮点非有限输出明确无效。329 if not isfinite(result):330 # 不能用范围检查忽略NaN/Inf。331 return ScalarValue(value, definition.dtype, False, QualityFlag.NONFINITE)332 # 整型目标要求整数精确结果,不能截断float小数。333 if definition.dtype in INTEGER_BOUNDS:334 # 整型结果若为float必须恰为整数。335 if type(result) is float and not result.is_integer():336 # 目标dtype丢失小数是明确精度失败。337 return ScalarValue(value, definition.dtype, False, QualityFlag.PRECISION_LOSS)338 # Python精确比较定宽上下限,不把max先转float。339 lower, upper = INTEGER_BOUNDS[definition.dtype]340 # 目标范围在真正cast前校验。341 if not lower <= result <= upper:342 # signed/unsigned下溢/溢出均无效。343 return ScalarValue(value, definition.dtype, False, QualityFlag.OVERFLOW)344 # 已证明精确整型结果才转换原生int。345 result = int(result)346 # 业务范围针对转换后的值,与DBC min/max分开。347 if definition.valid_range is not None and not definition.valid_range[0] <= result <= definition.valid_range[1]:348 # 不裁剪错误工程值冒充正常。349 return ScalarValue(value, definition.dtype, False, QualityFlag.OUT_OF_RANGE)350 # 正常输出保持显式目标dtype。351 return ScalarValue(result, definition.dtype)352353354def memory_series_reference(bindings: SignalBindings, name: str, *, capture_code=False) -> SignalSeries:355 """从当前内存来源提取一个字段,转换后生成信号历史。356357 参数:358 bindings: 当前真实SignalBindings;name为其中已定义的简单别名。359 原行时间使用整数纳秒,来源offset_ns只作为显式时间偏移应用一次。360 返回:361 SignalSeries。保存正常转换值或无效原值,以及质量、时间、序号和来源。362 异常:363 绑定、归属、时间溢出、顺序或冲突检查失败时传播底层异常。364 边界:365 只处理原行中实际出现的字段;其他字段更新不生成该信号的新观测。366 创建新的样本集合,不修改原行、不排序,也不读取日志或加载DBC。367 """368 # 当前定义与来源必须明确,不能继承其他计划。369 definition = bindings.resolve(name)370 # 已绑定来源在会话内唯一。371 source = next(item for item in bindings.session.sources if item.name == definition.source)372 # 新结果局部保存,不改变原memory行。373 samples = []374 # 原行顺序、参数和身份不被调整。375 for row in source.rows:376 # 其他字段更新不伪造当前列的新观测。377 if definition.field not in row.values:378 # 缺列不是该列明确invalid更新。379 continue380 # 原值已是解码/表格输出,参考只应用额外声明变换。381 original = row.values[definition.field]382 # 独立参考归一化,不调用被测NumPy核。383 value = normalize_reference(definition, original)384 from .enum_codes import original_code385 # 输出样本保留原诊断、稳定身份与显式时间offset。386 samples.append(SignalSample(name, checked_add_ns(row.time_ns, source.offset_ns),387 value.value if value.valid else original, value.valid, value.quality,388 bindings.source_ref(name), row.sequence,389 original_code(definition, original) if capture_code else None))390 # 交给SignalSeries检查同时间更新、原行身份和声明类型,不调整输入顺序。391 return SignalSeries(bindings, name, tuple(samples))392