chronaxis.core.expression_numpy
源码基线:8904c2f744951857659abd40aaa3535d57faf53c
1"""在同长度信号列上批量求值表达式,并逐位置保留质量诊断。23evaluate_numpy接收当前编译绑定和context中的NumericArray,输出4ArrayEvaluation:数值结果列与独立的uint16依赖诊断列均为形状(N,)。5布尔逻辑用掩码区分TRUE、FALSE和UNKNOWN(未知),不把无效False占位6当作条件不成立;已知值决定结果时,原依赖异常仍留在诊断列。78解释器按表达式树遍历节点,每个节点用NumPy逐元素函数计算整列。9isclose使用显式atol与rtol计算abs(a-b) <= atol + rtol*abs(b)。10evaluate_memory_numpy从已有内存行归一化信号,用searchsorted找到每个11查询时间的最近观测,再按有效期保持并求值,不额外读取或解码。1213依赖NumPy及核心编译、数值和对齐模块;共享检查协议,不调用参考数值14求值器。结果为独立只读存储,当前批次缓存不跨调用保留,不修改输入。15"""1617# 冻结输出及明确映射接口。18from dataclasses import dataclass19# 数组依赖显式,不污染标准库core导入。20import numpy as np21# 当前静态树是共享已校验协议,不共享样本数值内核。22from .enum_codes import memory_histories, aligned_column23from .expression import CompiledExpression24# 真实绑定参与执行身份检查。25from .binding import SignalBindings26# 解码运行证据与实际memory绑定共用检查协议,不共享参考数值计算。27from .decoded_binding import DecodedBindings28# 消息绑定只扩显式typed context,不改变普通数值核或memory时间保持入口。29from .message_binding import MessageBindings30# 只共享dtype常量,精确整数比较literal不转float。31from .numeric import INTEGER_BOUNDS32# 数组真实数值入口与布局模型。33from .numeric_numpy import (COMPARE_UFUNCS, DTYPES, NumericArray, _output, _readonly,34 binary_numpy, to_float_numpy, unary_numpy)35# 批量单位转换独立于标量数值核。36from .affine_numpy import affine_numpy37from .expression_math_v4 import MATH_KERNELS38from .expression_math_numpy import math_numpy39from .expression_integer_v4 import INTEGER_KERNELS40from .expression_integer_numpy import integer_numpy41from .lookup_numpy import lookup_numpy42from .enum_codes import enum_code_numpy43# 此函数只校验身份与列映射,没有参考数值运算。44from .expression_reference import require_context45# 原行批量适配独立于参考转换。46from .normalize_numpy import memory_series_numpy47# 独立NumPy保持,不调用参考对齐。48from .state_numpy import align_numpy, build_signal_array, _require_axis49# 基础质量及bool有效mask实施三值,不对数组bool求值。50from .types import QualityFlag as Q, SignalDType as D515253@dataclass(frozen=True)54class ArrayEvaluation:55 """保存同长度的计算结果列与依赖质量诊断列。5657 value为NumericArray,dependency_quality是形状(N,)的uint16数组。58 布尔结果可能因已知TRUE或FALSE而有效,诊断仍可包含非零的原依赖原因;59 不能因此把UNKNOWN填为FALSE。构造时检查诊断布局并复制为只读存储。60 实例不可重新赋值,不持有跨批次的求值状态。61 """6263 value: NumericArray # 真实逻辑结果列。64 dependency_quality: np.ndarray # 同长uint16原因并集。6566 def __post_init__(self):67 """验证诊断布局和已知位后复制只读列。6869 参数: 无,读取value与dependency_quality。70 返回: None,不修改调用侧数组。71 异常: TypeError/ValueError为结构/长度/未知位。72 """73 # 真实typed结果需与诊断同一布局。74 if not isinstance(self.value, NumericArray) or type(self.dependency_quality) is not np.ndarray or self.dependency_quality.ndim != 1 or self.dependency_quality.dtype != np.dtype('uint16'):75 # 不用广播/隐式cast修正错误接口。76 raise TypeError('ArrayEvaluation诊断需1维uint16列')77 # 未知位不能以诊断为名通过。78 if len(self.dependency_quality) != len(self.value.values) or np.any(self.dependency_quality > 511):79 # 正常BOOL可带诊断,但结构仍严格。80 raise ValueError('ArrayEvaluation诊断长度/原因不合法')81 # 结果与诊断都具独立所有权。82 object.__setattr__(self, 'dependency_quality', _readonly(self.dependency_quality))838485def _logic(operator, values):86 """独立以整列掩码实现and/or/not三值真值表。8788 参数: operator固定逻辑名;values为同长BOOL列tuple。89 返回: NumericArray,known决定时质量NONE,否则UNKNOWN原因并集。90 异常: 调用者保证static BOOL及shape,分配错误传播。91 """92 # not不将invalid False占位当作真False。93 if operator == 'not':94 # 正常位置取反,valid/原因不改变。95 result = np.zeros(len(values[0].values), dtype=bool)96 # where避免对invalid占位作正常逻辑。97 np.logical_not(values[0].values, out=result, where=values[0].valid)98 # 原原因保持,不清UNKNOWN。99 return _output(D.BOOL, result, values[0].quality.copy())100 # 行数明确,不对数组做Python truthiness。101 size = len(values[0].values)102 # 默认没有known决断/没有原因。103 decided = np.zeros(size, dtype=bool)104 # uint16保留高位诊断。105 quality = np.zeros(size, dtype=np.uint16)106 # 逻辑节点的静态操作数循环,每次都真实整列mask。107 for value in values:108 # unknown原因先并集,decision后只清结果质量。109 quality |= value.quality110 # and由knownFalse决定,or由knownTrue决定。111 decided |= value.valid & (~value.values if operator == 'and' else value.values)112 # 已知决断给正常结果,原诊断由ArrayEvaluation另保存。113 quality[decided] = 0114 # and没有knownFalse且没有unknown时TRUE;or仅decided位置TRUE。115 result = ~decided if operator == 'and' else decided116 # unknown存储值不解释为正常bool,valid由quality决定。117 return _output(D.BOOL, result, quality)118119120def _constant(node, size):121 """将已校验literal装箱同长typed列,不猜dtype或enum域。122123 参数: node为const,size为非负明确行数。124 返回: 正常NumericArray;integer边界此前静态检查。125 异常: 模型/分配错误传播,不自动promotion。126 """127 # enum固定标签域,正常code由当前域唯一确定。128 value = node.labels.index(node.value) if node.labels else node.value129 # 常量复制为整列,不靠广播误配输入。130 values = np.full(size, value, dtype=DTYPES[node.dtype])131 # literal没有上游质量异常。132 return _output(node.dtype, values, np.zeros(size, dtype=np.uint16), node.labels)133134135def _literal_compare(node, value):136 """在整数域比较原列与任意精确Python整数常量边界。137138 参数: node含固定比较名/原整数边界;value为定宽整数列。139 返回: BOOL列,invalid保持原原因,不转float或cast越界常量。140 异常: 编译器保证类型与注册比较,分配错误传播。141 """142 # 边界上下限是精确Python静态配置,不是逐样本计算。143 lower, upper = INTEGER_BOUNDS[value.dtype]144 # 运算名来自白名单编译节点。145 operator = node.operator[12:]146 # 输出占位不正常使用。147 result = np.zeros(len(value.values), dtype=bool)148 # 太小边界:所有正常原值都大于它。149 if node.value < lower:150 # 不把超界常量cast成机器整数。151 result[value.valid] = operator in ('ne', 'gt', 'ge')152 # 太大边界:所有正常原值都小于它。153 elif node.value > upper:154 # 即使2**100也精确判定,不经过float。155 result[value.valid] = operator in ('ne', 'lt', 'le')156 else:157 # 已证明可表示,才按相同dtype精确ufunc比较。158 COMPARE_UFUNCS[operator](value.values, DTYPES[value.dtype].type(node.value), out=result, where=value.valid)159 # known比较正常,unknown原因原样保持。160 return _output(D.BOOL, result, value.quality.copy())161162163def _where(node, context, size, cache):164 """按有效条件拆成真、假两个活动子批,再按原行序还原输出。165166 参数: node为已统一分支类型/单位的where,context各列均为(size,);167 cache仅对应当前批次的坐标系。168 返回: ArrayEvaluation;条件未知的行保留条件原因,其他行仅并入所选分支诊断。169 异常: 模型、数组布局或分配异常原样传播;不以逐行解释器代替掩码计算。170 """171 # 未知条件不进入任一子批;初值保留其实际质量,数值占位不作为观测。172 condition = _evaluate(node.args[0], context, size, cache)173 values = np.zeros(size, dtype=DTYPES[node.dtype])174 quality = condition.value.quality.copy()175 diagnostic = condition.dependency_quality.copy()176 for truth, branch in ((True, node.args[1]), (False, node.args[2])):177 # 即使分支包含除零或定宽溢出,也只运算确实选到该分支的行。178 keep = condition.value.valid & (condition.value.values if truth else ~condition.value.values)179 count = int(np.count_nonzero(keep))180 if count == 0:181 continue182 if count == size:183 # 全批选择一致时保持同一坐标系,公共子树仍使用原缓存。184 selected = _evaluate(branch, context, size, cache)185 else:186 # 子批只复制此分支静态所需的列;嵌套where会在子批内再次按条件选行。187 # 新缓存防止原坐标系的整列结果误用在不同长度/行序的子批上。188 subset = {}189 for name in branch.aliases:190 column = context[name]191 subset[name] = column.take(keep)192 selected = _evaluate(branch, subset, count, {})193 # 按分支位置写回结果及该路径诊断;未选独有依赖不产生原因。194 values[keep] = selected.value.values195 quality[keep] = selected.value.quality196 diagnostic[keep] |= selected.dependency_quality197 return ArrayEvaluation(_output(node.dtype, values, quality), diagnostic)198199200def _evaluate(node, context, size, cache):201 """按静态节点结构调用真实批量数值/逻辑内核。202203 参数: node冻结树;context当前同长列;size行数;cache本次batch局部缓存。204 返回: ArrayEvaluation,完整诊断不因known逻辑结果丢失。205 异常: 静态/输入模型问题传播,不调用reference/eval/逐样本Python。206 """207 # 当前batch/真实绑定局部公共子表达式复用。208 if node in cache:209 # 不能跨batch/会话继承数值。210 return cache[node]211 # 裸literal仅类型装箱,没有逐样本计算。212 if node.operator == 'const':213 # shape与所有信号相同。214 value = _constant(node, size)215 # 常量没有原依赖异常。216 result = ArrayEvaluation(value, value.quality)217 # where按活动行递归,不能使用普通节点先计算全部操作数的策略。218 elif node.operator == 'where':219 result = _where(node, context, size, cache)220 elif node.operator == 'enum_code':221 # 读取与标签独立的原码质量;未映射标签不能屏蔽有效的整数代码。222 operand = _evaluate(node.args[0], context, size, cache).value223 value = enum_code_numpy(node, operand)224 result = ArrayEvaluation(value, value.quality)225 # Name只取当前显式context已解码列。226 elif node.operator == 'name':227 # 外层已校验dtype/labels/shape及真实绑定身份。228 value = context[node.name]229 # 原质量同步保存在诊断列。230 result = ArrayEvaluation(value, value.quality)231 else:232 # 对静态子树递归,每个节点执行整列,不逐样本解释。233 children = tuple(_evaluate(arg, context, size, cache) for arg in node.args)234 # 真正数值输入不是diagnostic数组。235 values = tuple(child.value for child in children)236 # 诊断逐节点整列并集,不借normal bool清原因。237 diagnostic = np.zeros(size, dtype=np.uint16)238 # 此循环维度是表达式静态操作数个数。239 for child in children:240 # 所有求值子树的原/计算质量纳入诊断。241 diagnostic |= child.dependency_quality242 # 三值布尔核独立于标量参考真值表代码。243 if node.operator in ('and', 'or', 'not'):244 # 已知决断可normal;diagnostic仍保留UNKNOWN。245 value = _logic(node.operator, values)246 # 数值单目先检查定宽再ufunc。247 elif node.operator in ('abs', 'neg'):248 # 调用真实数组数值核,而非np.vectorize。249 value = unary_numpy(node.operator, values[0])250 # 显式常量float上下文仍用独立尾位精度核。251 elif node.operator == 'to_float':252 # 大整数不默默变近似float。253 value = to_float_numpy(values[0])254 elif node.operator == 'unit_convert':255 # 整批应用已编译系数,数组核检查定宽和质量,不逐行构造单位对象。256 value = affine_numpy(values[0], *node.value, node.dtype)257 elif node.operator == 'lookup':258 value = lookup_numpy(node.value, values[0])259 elif node.operator == 'host_function':260 from ..extension_runtime import point_call261 value = point_call(node.host_function,'numpy',values)262 elif node.operator in MATH_KERNELS:263 # 同一时间行跨列求值,逐行无效原因与dtype保留。264 value = math_numpy(node.operator, values)265 elif node.operator in INTEGER_KERNELS:266 # 位、定宽取余和精确类型转换都按整列有效位置计算。267 value = integer_numpy(node, values)268 # 常量大边界不能塞进定宽列后wrap。269 elif node.operator.startswith('int_literal_'):270 # 独立静态端点判定及正常整数ufunc。271 value = _literal_compare(node, values[0])272 # 显式isclose判据由安全批量数值原语实施。273 elif node.operator == 'isclose':274 # 四个实参各自先检查整数精度,不调用np.isclose默认策略。275 a, b, atol, rtol = (to_float_numpy(item) for item in values)276 # 左边abs(a-b),非有限中间结果UNKNOWN。277 distance = unary_numpy('abs', binary_numpy('sub', a, b))278 # 右边rtol*abs(b),保持判据非对称。279 relative = binary_numpy('mul', rtol, unary_numpy('abs', b))280 # atol+relative真实float64列。281 tolerance = binary_numpy('add', atol, relative)282 # 不把非有限容差当作可正常无限阈值。283 value = binary_numpy('le', distance, tolerance)284 # 计算中间诊断同步纳入整列统计。285 diagnostic |= a.quality | b.quality | atol.quality | rtol.quality | distance.quality | tolerance.quality286 else:287 # 只对静态白名单operator执行真实批量数值。288 value = binary_numpy(node.operator, values[0], values[1], allow_float_literal=node.allow_float_literal)289 # known逻辑输出可以有效,诊断是独立原因列。290 result = ArrayEvaluation(value, diagnostic | value.quality)291 # 局部cache只存当前批列结果。292 cache[node] = result293 # 不持有跨调用输入绑定或信号状态。294 return result295296297def evaluate_numpy(compiled: CompiledExpression, context, bindings: SignalBindings | DecodedBindings | MessageBindings, *, size: int | None = None) -> ArrayEvaluation:298 """检查绑定、依赖列与行数后,批量计算一个表达式。299300 参数:301 compiled: 当前CompiledExpression;bindings为对应普通或MessageBindings运行绑定。302 context: 以精确依赖别名为键的NumericArray映射;所有列须同长同布局。303 size: 可选的非负行数N。可从第一列推断,纯常量无依赖时必须显式提供。304 返回:305 ArrayEvaluation,结果和诊断均为形状(N,)的独立只读数组,允许N为零。306 异常:307 TypeError、ValueError: 模型、绑定身份、依赖键、类型、枚举域或形状不合法。308 数值失败追加质量原因;内存分配异常直接传播。309 边界:310 不广播或补齐错位列。每次调用创建新缓存,只复用当前批次的公共子树。311 """312 # 共享协议校验不包含参考数据计算。313 require_context(compiled, context, bindings)314 # 显式行数不得用bool或任意隐式整数对象替代。315 if size is not None and (type(size) is not int or size < 0):316 # 不按int truthiness解读shape。317 raise TypeError('size需要非负原生int')318 # 每个实际依赖列都需相同当前dtype和enum域。319 for name, value in context.items():320 # 对照当前真实定义,不从全局查找。321 definition = bindings.resolve(name)322 # 不接裸数组或错误类型并偷偷astype。323 if not isinstance(value, NumericArray) or value.dtype is not definition.dtype:324 # 类型错误不能补成UNKNOWN。325 raise TypeError('context列dtype与当前绑定不同')326 # 当前声明固定标签域。327 labels = tuple(dict.fromkeys(label for _, label in definition.choices)) if definition.dtype is D.ENUM else ()328 # 相同代码但不同域不允许。329 if value.labels != labels:330 # 来源语义必须明确隔离。331 raise ValueError('context enum域不同')332 # 没有显式行数时取第一合法列,不broadcast。333 if size is None:334 # 只有真正数据列决定N。335 size = len(value.values)336 # 错行数必须执行前拒绝。337 if len(value.values) != size:338 # 不补齐或截断以凑shape。339 raise ValueError('context各列shape不同')340 # 常量无依赖也不能猜输出行数。341 if size is None:342 # 此时context为空,没有合法可推断shape。343 raise ValueError('纯常量批量执行须明确size')344 # 每次批量求值创建新缓存;编译树可复用,上一批的数值结果不继承。345 return _evaluate(compiled.root, context, size, {})346347348def evaluate_memory_numpy(compiled: CompiledExpression, times: np.ndarray, *, point: bool = False) -> ArrayEvaluation:349 """从已有内存信号批量对齐到查询时间,再计算表达式。350351 参数:352 compiled: 关联真实输入的CompiledExpression。353 times: 非递减的一维int64纳秒轴,形状(N,);point控制真实末点查询视图。354 返回:355 同形状ArrayEvaluation,保留有效掩码、质量原因和绑定身份。356 异常:357 TypeError: 非memory绑定,包括DecodedBindings;其他时间/归属/形状异常原样传播。358 流程:359 只建立实际依赖的历史列,独立归一化并按有效期对齐,再整列求值。360 纯常量也检查时间轴;保留完整历史,结果复制存储,不额外读取或解码。361 """362 # 纯常量也不能绕过memory来源门槛,显式错误避免解码绑定缺session属性的偶发异常。363 if not isinstance(compiled, CompiledExpression) or not isinstance(compiled.bindings, SignalBindings):364 raise TypeError('memory快捷求值需要真实SignalBindings的CompiledExpression')365 # 常量表达式也校验真正时间轴,拒绝MaskArray/回退/浮点时间。366 _require_axis(times)367 # 末点控制必须明确bool,不猜truthiness。368 if type(point) is not bool:369 # 保持协议与reference一致。370 raise TypeError('point需要bool')371 # 局部context只材料化实际依赖列。372 context = {}373 # 循环维度是依赖列,列内对齐/计算实际数组化。374 for name, history in memory_histories(compiled, compiled.dependencies, memory_series_numpy).items():375 # 用searchsorted定位最近观测,并结合有效期与质量掩码决定哪些保持值可用。376 aligned = align_numpy(build_signal_array(history), times, point=point)377 # 原valid/质量保留,包括最新invalid覆盖,enum代码域来自历史。378 context[name] = aligned_column(aligned)379 # 精确行数还覆盖无依赖/空查询情况。380 return evaluate_numpy(compiled, context, compiled.bindings, size=len(times))381