chronaxis.core.numeric_numpy
源码基线:8904c2f744951857659abd40aaa3535d57faf53c
1"""对同长度数值列执行算术和比较,并逐位置记录有效性与质量。23输入NumericArray保存一维values、bool类型的valid和uint16类型的quality。4算术先合并质量原因,再用布尔掩码选择能计算的位置,输出独立的只读列。5整数先检查符号、幅度和目标定宽范围,再执行NumPy逐元素函数;6需要float64时先检查整数的精确表示,失败位置不执行正常运算。78所有列的形状都是(N,),N为行数;枚举存为int64编码并携带固定labels。9无效位置的零或False仅是存储占位,不能脱离valid当作业务值使用。10本模块依赖NumPy及核心类型规则,不读写文件或加载旧解码路径。11仅共享静态类型与模型检查,不调用标量参考数值函数,也不以object数组12或逐样本Python回调代替列运算;输出复制存储,不修改调用者数组。13"""1415# 冻结模型及静态白名单;数组内核使用实际ufunc和掩码。16from dataclasses import dataclass17# 只读静态映射或显式质量/type枚举用于结构协议。18from types import MappingProxyType19# NumPy显式加载,承担真实typed列/ufunc与掩码计算。20import numpy as np2122# 仅共享类型契约,不导入参考运算。23from .numeric import COMPARISONS, result_dtype24# 只读静态映射或显式质量/type枚举用于结构协议。25from .types import QualityFlag, SignalDType26# 共享原生/文本结构校验,不共享数值参考。27from .validation import require_text2829# 每个输出声明有固定物理布局;enum是固定域的int64编码。30DTYPES = MappingProxyType({SignalDType.INT64: np.dtype('int64'), SignalDType.UINT64: np.dtype('uint64'),31 SignalDType.FLOAT64: np.dtype('float64'), SignalDType.BOOL: np.dtype('bool'),32 SignalDType.ENUM: np.dtype('int64')})33# ufunc只来自静态注册,用户不能传自由函数。34COMPARE_UFUNCS = MappingProxyType({'eq': np.equal, 'ne': np.not_equal, 'lt': np.less,35 'le': np.less_equal, 'gt': np.greater, 'ge': np.greater_equal})363738def _readonly(array: np.ndarray) -> np.ndarray:39 """复制列并设置普通只读所有权约定。4041 参数: array为已校验ndarray。42 返回: 拥有独立存储的只读列。43 异常: 内存分配错误传播;不声称防恶意篡改。44 """45 # 不借用调用侧可变缓冲。46 result = array.copy()47 # 输出不能普通原位修改。48 result.flags.writeable = False49 # 独立所有权允许调用侧继续使用其原数组。50 return result515253@dataclass(frozen=True)54class NumericArray:55 """保存没有时间轴的一组数值,以及每个位置的有效性和质量原因。5657 values、valid和quality均为形状(N,)的普通ndarray,长度必须一致。58 values的物理类型由dtype指定,valid为bool,quality为uint16;59 valid严格等于quality == 0,无效位置只能作为存储占位使用。60 枚举以int64编码表示,labels给出固定标签顺序,只检查有效位置的编码。61 构造时复制三个数组并设置只读标志,保留独立存储;这是一项普通使用62 约定,不声称能抵御主动解除写保护。实例不保存时间、单位或跨批次状态。63 """6465 dtype: SignalDType # 逻辑类型不靠NumPy推断。66 values: np.ndarray # int64/uint64/float64/bool列;enum为int64 code。67 valid: np.ndarray # bool有效位置,禁止maskless运算。68 quality: np.ndarray # uint16足够容纳九种质量位。69 labels: tuple[str, ...] = () # enum顺序域,其他dtype为空。70 code: 'NumericArray | None' = None # 仅请求 enum_code 时附带独立整数列。7172 def take(self, indices):73 """按活动行切出标签及所需原码,保持同一坐标系。7475 参数: indices 为行号或布尔掩码。返回: 对应行的独立 NumericArray。76 异常: 索引或布局不合法时传播数组错误。77 """78 return NumericArray(self.dtype, self.values[indices], self.valid[indices],79 self.quality[indices], self.labels,80 None if self.code is None else self.code.take(indices))8182 def __post_init__(self) -> None:83 """验证布局、基础质量、有限性及enum域后冻结独立副本。8485 参数: 无;读取数据类字段。86 返回: None;不隐式cast或清除质量。87 异常: TypeError为布局;ValueError为shape/质量/非法正常值。88 """89 # dtype必须是当前协议声明。90 if not isinstance(self.dtype, SignalDType) or not isinstance(self.labels, tuple):91 # 无类型字段不进入数组执行。92 raise TypeError('NumericArray需要typed dtype与tuple labels')93 # 严格ndarray不接隐藏掩码或自定义子类行为。94 for array, dtype in ((self.values, DTYPES[self.dtype]), (self.valid, np.dtype('bool')), (self.quality, np.dtype('uint16'))):95 # shape和物理类型不能靠自动astype补救。96 if type(array) is not np.ndarray or array.ndim != 1 or array.dtype != dtype:97 # 输出接口与内存布局必须明确。98 raise TypeError('NumericArray需要1维精确dtype ndarray')99 # 禁止广播掩盖行错位。100 if not (len(self.values) == len(self.valid) == len(self.quality)):101 # 不截断或重复最后一行。102 raise ValueError('数值/valid/quality长度不同')103 if self.code is not None and (self.dtype is not SignalDType.ENUM or not isinstance(self.code, NumericArray)104 or self.code.dtype not in (SignalDType.INT64, SignalDType.UINT64) or len(self.code.values) != len(self.values)):105 raise ValueError('只有枚举可附带等长整数原码列')106 # 仅非负已知原因位;valid严格等于基础无质量。107 if np.any(self.quality > 511) or not np.array_equal(self.valid, self.quality == 0):108 # invalid不能无原因,ESTIMATED也不能默认当正常。109 raise ValueError('质量与valid矛盾或未知质量位')110 # 正常float不能携带NaN/Inf。111 if self.dtype is SignalDType.FLOAT64 and np.any(~np.isfinite(self.values[self.valid])):112 # invalid诊断只存储,不通过finite单一检查判断整数质量。113 raise ValueError('正常float必须有限')114 # enum域由显式labels提供,不能隐式猜顺序。115 if self.dtype is SignalDType.ENUM:116 # 非空且唯一;映射归一化已处理多码同标签。117 if not self.labels or len(set(self.labels)) != len(self.labels):118 # 空域不能解释任何枚举编码。119 raise ValueError('enum需要非空唯一域')120 # 大小写敏感的业务文本原样保留。121 for label in self.labels:122 # labels不是表达式名称。123 require_text(label, 'enum.label')124 # 只校验正常编码,invalid占位不参与业务比较。125 if np.any((self.values[self.valid] < 0) | (self.values[self.valid] >= len(self.labels))):126 # 不采用Python负索引解释未知枚举。127 raise ValueError('正常enum code超出域')128 # 非枚举字段不能隐藏带入业务标签域。129 elif self.labels:130 # 普通数值不附带隐藏枚举语义。131 raise ValueError('非enum不能携带labels')132 # 数据类只读与数组只读是两层约定,均有独立所有权。133 for field in ('values', 'valid', 'quality'):134 # 三个输入列各自复制,不改调用侧写保护。135 object.__setattr__(self, field, _readonly(getattr(self, field)))136137138def _output(dtype, values, quality, labels=()) -> NumericArray:139 """用已有质量列建立统一有效性结果。140141 参数: dtype/values/quality为局部结果;labels为enum域。142 返回: 独立只读NumericArray。143 异常: 模型校验/分配错误传播,不改变数值政策。144 """145 # 只有质量NONE的位置可以正常使用。146 return NumericArray(dtype, values, quality == 0, quality, labels)147148149def _magnitude(values: np.ndarray) -> np.ndarray:150 """在uint64中取得整数列的精确绝对幅度。151152 参数:153 values: 形状(N,)的int64或uint64数组,由调用者保证布局。154 返回:155 新的uint64数组,形状保持(N,);int64最小值得到2**63。156 异常:157 内存分配或底层数组操作异常直接传播。158 流程:159 先复制位表示,再对有符号负数按补码取反加一,不调用会溢出的abs(min)。160 """161 # unsigned输入本身就是幅度,复制防止原位别名。162 result = values.view(np.uint64).copy()163 # signed补码只用于定义良好的位解释,不先算有符号abs(min)。164 if values.dtype == np.dtype('int64'):165 # 负数的按位取反加1最大是2**63,全部在uint64范围内。166 negative = values < 0167 # 显式补码幅度算法;~位结果不超过uint64 max-1。168 result[negative] = np.bitwise_not(result[negative]) + np.uint64(1)169 # 后续乘法/精度检查均在此精确整数幅度域。170 return result171172173def _from_magnitude(magnitude: np.ndarray, negative: np.ndarray) -> np.ndarray:174 """把已证明范围合法的整数幅度和符号恢复成int64列。175176 参数:177 magnitude: 形状(N,)的uint64幅度。正数不超过2**63-1,负数不超过2**63。178 negative: 同形状bool掩码,True表示该位置应为负数。179 返回:180 独立存储的int64数组;零和最小负数均通过位表示恢复。181 异常:182 调用者负责证明范围与布局;内存分配或数组操作异常直接传播。183 边界:184 先复制幅度再构建负数补码,最后改变位视图,不先执行危险的有符号负号。185 """186 # 正数位模式与signed相同,负数逐位置构建补码。187 bits = magnitude.copy()188 # 零幅度无需形成uint64 max+1;非零负数补码不溢出。189 selected = negative & (magnitude != 0)190 # ~幅度最大uint64 max-1,加1安全。191 bits[selected] = np.bitwise_not(magnitude[selected]) + np.uint64(1)192 # 已证明符号范围后进行位视图,不隐式算术wrap。193 return bits.view(np.int64)194195196def to_float_numpy(value: NumericArray) -> NumericArray:197 """用固定11个尾位掩码独立检查整数到float64是否精确。198199 参数: value为数值NumericArray;invalid输入不转换。200 返回: float64列及保留/新增PRECISION_LOSS质量;shape保持。201 异常: TypeError为结构/逻辑类型;分配错误传播。202 """203 # bool/enum不进入数值提升。204 if not isinstance(value, NumericArray) or value.dtype in (SignalDType.BOOL, SignalDType.ENUM):205 # 不隐式把False当0或编码当数值。206 raise TypeError('float转换需要数值列')207 # 复制原因和独立输出占位,无效0不可使用。208 quality = value.quality.copy()209 # 输出固定float64且一维N。210 result = np.zeros(len(value.values), dtype=np.float64)211 # float已通过模型有限检查,不需要整数精度判据。212 if value.dtype is SignalDType.FLOAT64:213 # 仅拷贝正常位置,invalid占位不参与转换。214 result[value.valid] = value.values[value.valid]215 else:216 # 幅度精确包括signed min与unsigned max。217 magnitude = _magnitude(value.values)218 # 丢精度掩码按指数要求尾位为0,不做危险float→int64回转。219 loss = np.zeros(len(result), dtype=bool)220 # 64位幅度只需检查11种尾位间距;每轮检查整列,循环次数不随样本数增长。221 for bits in range(1, 12):222 # 指数区间最低有效位间距为2**bits;更大指数继续检查更多尾位。223 loss |= (magnitude >= np.uint64(2 ** (52 + bits))) & ((magnitude & np.uint64(2 ** bits - 1)) != 0)224 # 仅正常输入因精度变无效,已有原因不被计算占位污染。225 failed = value.valid & loss226 # 质量掩码至少uint16,不截断256等高位。227 quality[failed] |= np.uint16(QualityFlag.PRECISION_LOSS)228 # 真正转换只发生在已经证明精确且有效的位置。229 selected = value.valid & ~loss230 # 64位整数范围内不会超float64有限范围。231 result[selected] = value.values[selected].astype(np.float64)232 # 模型共享只校验输出,不共享参考数值。233 return _output(SignalDType.FLOAT64, result, quality)234235236def _integer_compare(operator, left, right, selected) -> np.ndarray:237 """在整数域精确比较signed/unsigned两列。238239 参数: operator静态比较名,left/right整数列,selected有效bool掩码。240 返回: bool结果列,invalid占位False。241 异常: 调用者保证等长/类型,分配错误传播。242 """243 # 默认为不可使用的False,占位必须随质量掩码传递。244 result = np.zeros(len(left), dtype=bool)245 # 同dtype直接精确ufunc,不经过float。246 if left.dtype == right.dtype:247 # where不触碰invalid数值。248 COMPARE_UFUNCS[operator](left, right, out=result, where=selected)249 # 同型比较已经完成。250 return result251 # 哪一侧signed明示,不依赖NumPy混合promotion。252 signed_left = left.dtype == np.dtype('int64')253 # 负signed与任何unsigned之间的序固定。254 signed = left if signed_left else right255 # signed非负位置才能安全转uint64。256 negative = selected & (signed < 0)257 # 负signed永小于unsigned;换侧序相反。258 negative_result = operator in ('ne', 'lt', 'le') if signed_left else operator in ('ne', 'gt', 'ge')259 # 一次填满确定比较的负数子集。260 result[negative] = negative_result261 # 其余正常位置在无符号域精确比较。262 remaining = selected & ~negative263 # 子集cast证明非负,不从uint64 max转signed。264 result[remaining] = COMPARE_UFUNCS[operator](left[remaining].astype(np.uint64), right[remaining].astype(np.uint64))265 # 完整结果保持原行布局。266 return result267268269def _integer_arithmetic(operator, left, right, selected):270 """先检查整数结果是否超界,再计算安全位置的加减乘。271272 参数:273 operator: add、sub或mul;left、right为同形状(N,)且同类型的整数数组。274 selected: 同形状bool掩码,标出两侧输入均有效的位置。275 返回:276 同类型结果数组和bool溢出掩码,均为形状(N,)。277 未选中或溢出位置保持零占位,调用者必须同时传递有效性与质量。278 异常:279 调用者保证输入布局和注册运算;内存分配或数组操作异常直接传播。280 流程:281 加减按符号检查安全门槛;乘法在uint64幅度域检查limit // b。282 检查后才调用逐元素函数,避免先发生整数回绕再判断溢出。283 """284 # 局部结果占位与失败掩码均为一维N。285 result = np.zeros_like(left)286 # 只考虑有效位置,invalid任意位模式不触发附加错误。287 overflow = np.zeros(len(left), dtype=bool)288 # 同型signed/unsigned路径分开,避免隐式promotion。289 signed = left.dtype == np.dtype('int64')290 # 先用减/加安全门槛检查add/sub。291 if operator in ('add', 'sub'):292 # unsigned add max-b安全;sub直接比较a<b。293 if not signed:294 # 全列门槛只是合法定宽值,无危险算术。295 overflow = selected & ((left > np.uint64(2 ** 64 - 1) - right) if operator == 'add' else (left < right))296 else:297 # 分符号取子集,min/max与相应b之差保证在范围内。298 positive = selected & (right > 0)299 # 0右值永不产生add/sub溢出。300 negative = selected & (right < 0)301 # add的正b只检查上界,负b只检查下界。302 if operator == 'add':303 # max-正数与min-负数均安全。304 overflow[positive] = left[positive] > np.int64(2 ** 63 - 1) - right[positive]305 # 包含b=min,min-min=0合法。306 overflow[negative] = left[negative] < np.int64(-(2 ** 63)) - right[negative]307 else:308 # sub正b检查min+b,负b检查max+b,门槛安全。309 overflow[positive] = left[positive] < np.int64(-(2 ** 63)) + right[positive]310 # 包含a=max,b=min,明确失败不执行subtract。311 overflow[negative] = left[negative] > np.int64(2 ** 63 - 1) + right[negative]312 # 只对已证明结果范围的元素调用ufunc。313 ufunc = np.add if operator == 'add' else np.subtract314 # where阻止无效/失败位置参与计算。315 ufunc(left, right, out=result, where=selected & ~overflow)316 else:317 # 精确幅度计算避免abs(min),乘法不先wrap再检查。318 a, b = _magnitude(left), _magnitude(right)319 # signed负结果允许幅度2**63,正结果只允许max。320 negative = (left < 0) ^ (right < 0) if signed else np.zeros(len(left), dtype=bool)321 # 所有幅度限值都使用uint64,避免浮点近似改变溢出边界。322 limit = np.where(negative, np.uint64(2 ** 63), np.uint64(2 ** 63 - 1)) if signed else np.full(len(left), 2 ** 64 - 1, dtype=np.uint64)323 # b=0不做正常除法,乘积确定0合法。324 nonzero = selected & (b != 0)325 # 除法门槛在精确unsigned整数域。326 overflow[nonzero] = a[nonzero] > limit[nonzero] // b[nonzero]327 # 正常乘法完全不接触越界位置。328 np.multiply(left, right, out=result, where=selected & ~overflow)329 # 数值内核返回质量原因,由外层统一组合。330 return result, overflow331332333def binary_numpy(operator: str, left: NumericArray, right: NumericArray, *, allow_float_literal: bool = False) -> NumericArray:334 """对两列执行注册算术或比较,只计算质量允许的位置。335336 参数:337 operator: 已注册的二元运算名称;left、right为同长度NumericArray。338 allow_float_literal: 编译器证明浮点字面量可用于混合类型时才为True。339 返回:340 独立只读NumericArray,形状保持(N,)。质量按位合并,计算失败追加原因。341 比较返回BOOL,算术遵守静态结果类型;无效占位不表示正常零或False。342 异常:343 TypeError、ValueError: 模型、类型、长度、注册运算或枚举域不合法。344 内存分配异常直接传播;除零、溢出或丢精度作为质量原因返回。345 流程:346 先检查类型与等长布局,再合并有效掩码;整数路径保留精确比较和定宽347 检查,浮点路径先做精度转换。各路径独立于标量参考实现,不修改输入。348 """349 # 完整typed输入必须存在,空列也做静态配置检查。350 if not isinstance(left, NumericArray) or not isinstance(right, NumericArray):351 # 不接受自由字典或裸数组。352 raise TypeError('binary需要两数值列')353 # 静态dtype协议允许共享,不调用参考运算。354 dtype = result_dtype(operator, left.dtype, right.dtype, allow_float_literal)355 # 同长行布局,无隐式broadcast。356 if len(left.values) != len(right.values):357 # 调用側必须显式对齐。358 raise ValueError('binary两列长度不同')359 # enum编码等值仅在同一标签域内成立。360 if left.dtype is SignalDType.ENUM and left.labels != right.labels:361 # 不能以整数code相同当不同逻辑标签相同。362 raise ValueError('enum比较需要相同标签域')363 # 基础质量合并不受存储占位影响。364 quality = left.quality | right.quality365 # 正常运算只允许双侧有效。366 selected = left.valid & right.valid367 # float算术/比较需要独立精度转换先验。368 float_path = dtype is SignalDType.FLOAT64 or (operator in COMPARISONS and SignalDType.FLOAT64 in (left.dtype, right.dtype))369 # 同型整数和逻辑比较保持精确原布局。370 if operator in COMPARISONS and not float_path:371 # 两种整数使用专门符号门槛,不隐式浮点promotion。372 if left.dtype in (SignalDType.INT64, SignalDType.UINT64):373 # 此函数单次批量精确执行。374 values = _integer_compare(operator, left.values, right.values, selected)375 else:376 # bool与同域enum等值ufunc。377 values = np.zeros(len(quality), dtype=bool)378 # 无效编码不参与比较。379 COMPARE_UFUNCS[operator](left.values, right.values, out=values, where=selected)380 # 质量并集正确保留invalid占位。381 return _output(dtype, values, quality)382 # float路径先检查两操作数,各自独立转换。383 if float_path:384 # 仅共同有效输入触发新质量,避免无效另一侧诱发多余精度错误。385 a = to_float_numpy(_output(left.dtype, left.values, quality, left.labels))386 # 同样quality使失败位置不在第二侧执行正常转换。387 b = to_float_numpy(_output(right.dtype, right.values, quality, right.labels))388 # 共同有效位置可能因整数精度丢失变无效。389 quality = a.quality | b.quality390 # 数值执行仅在全部转换成功处。391 selected = quality == 0392 # 比较结果逻辑型,float算术结果数值型。393 values = np.zeros(len(quality), dtype=DTYPES[dtype])394 # 除零先剔除,不能先产生Inf后当正常。395 if operator == 'div':396 # 包含正负0分母,0/0亦同。397 zero = selected & (b.values == 0)398 # 该位置明示域错误,不触发正常除法。399 quality[zero] |= np.uint16(QualityFlag.DOMAIN_ERROR)400 # 更新执行掩码后调用真实ufunc。401 selected &= ~zero402 # 浮点overflow/invalid转质量,不向外伪报正常数值。403 with np.errstate(over='ignore', invalid='ignore', divide='raise'):404 # 静态分派既包括比较又包括算术。405 ufunc = COMPARE_UFUNCS[operator] if operator in COMPARISONS else {'add': np.add, 'sub': np.subtract, 'mul': np.multiply, 'div': np.divide}[operator]406 # where保证invalid及除零位置未执行。407 ufunc(a.values, b.values, out=values, where=selected)408 # 正常float结果非有限需要新增诊断,bool结果不会非有限。409 if dtype is SignalDType.FLOAT64:410 # 只标记实际执行位置,不污染invalid占位。411 quality[selected & ~np.isfinite(values)] |= np.uint16(QualityFlag.NONFINITE)412 # 构造输出前模型保证所有valid浮点有限。413 return _output(dtype, values, quality)414 # 剩余路径都是同dtype整数+/-/*。415 values, overflow = _integer_arithmetic(operator, left.values, right.values, selected)416 # 精确门槛失败原因与上游质量共存。417 quality[overflow] |= np.uint16(QualityFlag.OVERFLOW)418 # 溢出0只是无效存储占位,不能当成功值。419 return _output(dtype, values, quality)420421422def unary_numpy(operator: str, value: NumericArray) -> NumericArray:423 """独立数组abs/neg,signed min和unsigned负号先拒绝。424425 参数: operator为abs/neg;value为数值列。426 返回: 同dtype列及保留/新增质量,shape保持。427 异常: TypeError/ValueError为结构/未知运算,分配错误传播。428 """429 # 不接受逻辑数值化或任意函数。430 if not isinstance(value, NumericArray) or value.dtype in (SignalDType.BOOL, SignalDType.ENUM) or operator not in ('abs', 'neg'):431 # 与参考输入拒绝语义一致。432 raise ValueError('unary仅支持数值abs/neg')433 # 输入原因先复制,输出独立存储。434 quality = value.quality.copy()435 # invalid占位从不作为正常结果使用。436 result = np.zeros_like(value.values)437 # 只有正常输入需检查目标定宽。438 overflow = np.zeros(len(result), dtype=bool)439 # signed min abs/neg均超上界。440 if value.dtype is SignalDType.INT64:441 # 不先执行危险abs(min)或neg(min)。442 overflow = value.valid & (value.values == np.int64(-(2 ** 63)))443 # 无符号abs恒合法,只有neg需要下溢检查。444 elif value.dtype is SignalDType.UINT64 and operator == 'neg':445 # 非零unsigned负号必然下溢;0合法。446 overflow = value.valid & (value.values != 0)447 # 静态ufunc仅对已证明安全的位置执行。448 ufunc = np.absolute if operator == 'abs' else np.negative449 # unsigned neg允许位置仅0,不wrap。450 ufunc(value.values, out=result, where=value.valid & ~overflow)451 # 失败原因独立可追溯。452 quality[overflow] |= np.uint16(QualityFlag.OVERFLOW)453 # 不改变输入布局或质量。454 return _output(value.dtype, result, quality)455