chronaxis.core.scalar
源码基线:8904c2f744951857659abd40aaa3535d57faf53c
1"""保存数值及枚举的声明类型、有效性和质量原因。23ScalarValue 是模型和数值计算共同使用的不可变数据对象。它只依赖基础类型和4原生标量校验,不导入信号模型、绑定或计算模块,供这些模块安全地共同引用。5公开入口仍由 chronaxis.core.numeric 和 chronaxis.core 重导出同一个类。6"""78from dataclasses import dataclass, field9from math import isfinite10from types import MappingProxyType1112from .types import QualityFlag, SignalDType13from .validation import require_scalar, require_text141516# 定宽端点由协议明确给出,不依赖 NumPy 提升类型或整数回绕。17INTEGER_BOUNDS = MappingProxyType({SignalDType.INT64: (-(2 ** 63), 2 ** 63 - 1), SignalDType.UINT64: (0, 2 ** 64 - 1)})181920@dataclass(frozen=True)21class ScalarValue:22 """保存一个值的声明类型、有效性和质量原因。2324 valid只能是bool,且当且仅当quality为NONE时为True。有质量问题的value25 可保留原始诊断,例如None或NaN,但不能作为正常数值参加后续计算。26 有效整数必须落在dtype对应的定宽范围内,有效float64必须有限。27 枚举使用labels中的文本值;labels是不可变的固定标签域,比较时必须同域。28 实例不可重新赋值,不持有跨时间或跨分析任务的状态。29 """3031 value: int | float | bool | str | None # 正常值或无效原诊断。32 dtype: SignalDType # 输出声明类型,不靠值推断。33 valid: bool = True # 无效不代表正常0。34 quality: QualityFlag = QualityFlag.NONE # 输入原因和计算失败原因。35 labels: tuple[str, ...] = () # 仅enum有固定公开逻辑域。36 code: 'ScalarValue | None' = None # 仅 enum_code 需要的原整数,可与标签有不同有效性。37 partial: bool = field(default=False,kw_only=True,repr=False) # 有限变换窗口仅有部分输入时仍可估算的数值。3839 def __post_init__(self) -> None:40 """检查值与声明类型是否一致,并核对有效性和质量原因。4142 参数:43 无显式参数;读取value、dtype、valid、quality和labels。44 返回:45 None。保留输入声明,不自动转换类型、补零或清除质量原因。46 异常:47 TypeError: 字段类型或枚举标签域结构不合法。48 ValueError: 有效性与质量矛盾,浮点值非有限或枚举值不在声明域内。49 OverflowError: 有效整数超出声明类型的定宽范围。50 """51 # 原诊断也需不可变原生标量。52 require_scalar(self.value)53 if type(self.partial) is not bool or self.partial and not self.valid:54 raise ValueError('部分结果标志仅用于实际可用的数值')55 # dtype和质量都必须是真实已定义枚举。56 if not isinstance(self.dtype, SignalDType) or not isinstance(self.quality, QualityFlag) or type(self.valid) is not bool:57 # 不接受字符串类型名,也不把任意数值按Python真假规则当作valid。58 raise TypeError("ScalarValue需要typed dtype/quality及bool valid")59 # 逻辑域只能由明确冻结标签构成。60 if self.code is not None and (self.dtype is not SignalDType.ENUM or not isinstance(self.code, ScalarValue)61 or self.code.dtype not in INTEGER_BOUNDS):62 raise TypeError('只有枚举可附带整数原码')63 if not isinstance(self.labels, tuple) or (self.dtype is not SignalDType.ENUM and self.labels):64 # 数值不携带隐式业务枚举含义。65 raise TypeError("labels仅支持enum tuple域")66 # enum即使invalid也需有合法声明域。67 if self.dtype is SignalDType.ENUM:68 # 空域或重复标签不能证明逻辑值身份。69 if not self.labels or len(set(self.labels)) != len(self.labels):70 # 不自动猜枚举域。71 raise ValueError("enum需要非空唯一labels")72 # 标签保留完整大小写业务文本。73 for label in self.labels:74 # 不将标签当作表达式名称。75 require_text(label, "enum.label")76 # 有效性不能与禁止质量矛盾。77 if self.valid != (self.quality == QualityFlag.NONE):78 # 无效结果必须携带原因,不能悄悄恢复正常。79 raise ValueError("valid与基础质量策略矛盾")80 # invalid诊断无需满足正常输出类型,不参加正常计算。81 if not self.valid:82 # 保留原NaN/None/错误编码及原因。83 return84 # 严格原生类型,bool不视作整数。85 expected = {SignalDType.INT64: int, SignalDType.UINT64: int, SignalDType.FLOAT64: float,86 SignalDType.BOOL: bool, SignalDType.ENUM: str}[self.dtype]87 # 不隐式promotion或解析字符串数值。88 if type(self.value) is not expected:89 # 类型冲突是结构错误,归一化层须明确转换。90 raise TypeError("ScalarValue有效值与dtype不一致")91 # 有效定宽整数不能沿用Python无界结果。92 if self.dtype in INTEGER_BOUNDS:93 # signed/unsigned端点分别校验。94 lower, upper = INTEGER_BOUNDS[self.dtype]95 # 边界值本身合法,只有超界拒绝。96 if not lower <= self.value <= upper:97 # 运算内核应先转OVERFLOW无效,不能直接构造正常越界值。98 raise OverflowError("ScalarValue有效整数超界")99 # 浮点正常值单独检查有限,不能借整数范围掩盖Inf。100 elif self.dtype is SignalDType.FLOAT64 and not isfinite(self.value):101 # float正常路径不接受NaN/Inf。102 raise ValueError("ScalarValue有效float必须有限")103 # 枚举正常标签必须确属当前声明域。104 elif self.dtype is SignalDType.ENUM and self.value not in self.labels:105 # 未声明字符串不是正常枚举。106 raise ValueError("ScalarValue枚举标签不在域内")107108109def _scalar_from_validated_fields(value, dtype, valid=True, quality=QualityFlag.NONE,110 labels=(), code=None, *, partial=False):111 """从同次运行已核的数值列或已有标量构造共享值,不重复检查声明。112113 参数: 与ScalarValue相同;调用方必须已经证明全部字段不变量,不能传入114 外部未检查数据或刚计算而未检查范围/有限性的结果。115 返回: 字段完全相同的冻结ScalarValue,不保存检查标志或额外副本。116 异常: 分配错误原样传播。仅限内部调用,公开构造仍完整验证。117 """118 result = object.__new__(ScalarValue)119 # 数值和标签都已不可变;只安装现有字段,不建立其他可变状态。120 object.__setattr__(result, 'value', value)121 object.__setattr__(result, 'dtype', dtype)122 object.__setattr__(result, 'valid', valid)123 object.__setattr__(result, 'quality', quality)124 object.__setattr__(result, 'labels', labels)125 object.__setattr__(result, 'code', code)126 object.__setattr__(result, 'partial', partial)127 return result128129130# 进程间传输与既有pickle使用这一公开路径;生成数据类后设置,避免循环导入。131ScalarValue.__module__ = "chronaxis.core.numeric"132