Chronaxis用户手册English
本页目录

chronaxis.core.integral_sum_numpy

源码基线:8904c2f744951857659abd40aaa3535d57faf53c

1"""用独立 NumPy 误差数组归约原序积分面积,最后一次换算输出单位。23输入是有限块 NumpyIntegralTerms,只消费 valid 为 True 的 float64 面积;4面积已经带有 input_unit 乘秒的量纲,本核不重复乘区间时长、不累计覆盖事实。5跨块保存按绝对值递增、互不重叠的误差项,默认最多 4096 项;不保留历史面积。6原序面积推进属于 state_driver;每项内部用真实 accumulate、ufunc 和掩码处理7误差数组,不调用参考模块或 math.fsum,也不把整窗宣称为无状态数组求和。8状态与临时数组均受 partial_limit 限制,输入索引受当前有限块长度限制;9本纯核不管理预算或关闭资源,调用方须先预约相应空间并负责异常后的生命周期。10finish 是不破坏状态的快照,单位换算故障只影响返回结果;原量纲累计溢出永久保留。11"""1213# 仅共享结果模型、参数门槛和输入模型;数值误差推进由本模块独立实现。14import numpy as np15from .integral_sum import (IntegralStateLimitError, IntegralSumResult,16                           require_partial_limit, require_sum_finish)17from .integral_terms_numpy import NumpyIntegralTerms18from .types import QualityFlag as Q192021class NumpyIntegralSum:22    """保存有限 float64 误差项和有效项计数,保持跨块原序及持久溢出状态。2324    partials 只在完整单项候选满足容量后安装,不按输入项数增长;中途溢出25    清空数值状态并永久标失败,但后续有效面积仍计数。输入和已返回结果不被修改。26    """2728    def __init__(self, partial_limit: int = 4096):29        """建立独立的空数组误差状态。3031        参数: partial_limit 为正原生 int,表示误差状态的最大项数。32        返回: 无;初始化有效项数 0、累计故障 False 及空 float64 数组。33        异常: TypeError/ValueError 为限额不合法;内存分配错误直接传播。34        """35        # 状态容量受显式限额约束;预约责任属于调用方,纯核不另开预算。36        self._partial_limit = require_partial_limit(partial_limit)37        self._partials = np.empty(0, dtype=np.float64)38        self._valid_term_count = 039        self._calculation_failed = False4041    @property42    def valid_term_count(self) -> int:43        """读取已消费的有效面积数,包含导致中途溢出的项及其后续项。4445        参数: 无。返回: 非负原生 int。异常: 无。46        """47        # 计数不等同于有效时间或真实更新样本数,覆盖信息由外层独立累计。48        return self._valid_term_count4950    @property51    def calculation_failed(self) -> bool:52        """读取原量纲累计中途非有限的持久标志。5354        参数: 无。返回: bool;finish 的单位换算故障不修改本属性。异常: 无。55        """56        # 失败不能通过后来抵消或再调用 finish 恢复为成功。57        return self._calculation_failed5859    @property60    def partial_count(self) -> int:61        """读取当前持有的非零误差项数,不暴露可修改的数组。6263        参数: 无。返回: 不超过 partial_limit 的原生 int。异常: 无。64        """65        # 返回标量规模,调用方不能通过属性改写数值不变量。66        return len(self._partials)6768    def push(self, terms: NumpyIntegralTerms) -> None:69        """按输入顺序消费当前有限块的有效面积,不先对块求和。7071        参数: terms 为已校验的只读 float64 面积、bool 掩码和 IntegralFacts。72        返回: None;安装成功的单项更新状态和有效计数,输入数组保持原样。73        异常: TypeError 为输入类型错误;IntegralStateLimitError 为候选容量超限;74        分配或取消错误传播。超限项不安装,其前面已成功消费的项保留。75        """76        try:77            # typed 输入已保证同长有限面积;UNKNOWN 占位不能作为有效零参与归约。78            if not isinstance(terms, NumpyIntegralTerms):79                raise TypeError('数组积分归约需要 NumpyIntegralTerms')80            # 保留原积分typed门槛,同后端通用入口承接完全相同的数值推进。81            self.push_values(terms.areas, terms.valid)82        finally:83            # 原异常及TB仍交调用方;只断本层借入参数、视图和临时,不清外部帧。84            self = terms = None8586    def push_values(self, values: np.ndarray, valid: np.ndarray) -> None:87        """用本数组后端累计有限浮点项,不构造虚假的积分事实。8889        参数: values/valid 为同长只读普通一维float64/bool数组;未知位置只能存0。90        返回: None;原序推进、容量、中间溢出及finish行为与积分入口相同。91        异常: TypeError/ValueError 为结构或写保护错误;容量、分配和取消传播。92        内存: 检查掩码与索引均为当前有限(N,)临时列,调用方必须先预约预算。93        """94        try:95            # 拒绝自动cast、广播或可写别名;不把未知占位当正常数值。96            if (type(values) is not np.ndarray or type(valid) is not np.ndarray97                    or values.ndim != 1 or valid.ndim != 198                    or values.dtype != np.dtype('float64') or valid.dtype != np.dtype('bool')99                    or len(values) != len(valid) or values.flags.writeable or valid.flags.writeable):100                raise TypeError('数组原序归约需要同长只读float64/bool一维列')101            if np.any(~np.isfinite(values)) or np.any(values[~valid] != 0):102                raise ValueError('数组原序项需有限,未知位置只能存0')103            # 当前块的索引空间为 O(N),属于调用方有限块预约;不保存索引到跨块状态。104            indices = np.flatnonzero(valid)105            if self._calculation_failed:106                self._valid_term_count += len(indices)107                return108            # 原序外层属于 state_driver;每项内的误差推进是独立的真实数组运算。109            for index in indices:110                if self._calculation_failed:111                    self._valid_term_count += 1112                    continue113                candidate = self._advance(values[index])114                # 容量检查已完成才安装;数值溢出单项仍计数,清空不再使用的误差状态。115                if candidate is None:116                    self._partials = np.empty(0, dtype=np.float64)117                    self._calculation_failed = True118                else:119                    self._partials = candidate120                self._valid_term_count += 1121        finally:122            # 原异常及TB仍交调用方;只断本层借入参数、视图和临时,不清外部帧。123            self = values = valid = indices = candidate = None124125    def _advance(self, area: np.float64) -> np.ndarray | None:126        """生成一项的独立误差候选,不提前改写当前状态。127128        参数: area 为有限 float64 面积,量纲仍为 input_unit 乘秒。129        返回: float64 形状 (K,) 的新候选,K 不超过限额;中途非有限返回 None。130        异常: IntegralStateLimitError 为候选超限;分配和取消错误传播。131        """132        try:133            # (K+1,) accumulate 保留每一步 hi,递推顺序与原序 FastTwoSum 相同。134            chain = np.concatenate((np.array([area], dtype=np.float64), self._partials))135            with np.errstate(over='ignore', under='ignore', invalid='ignore'):136                highs = np.add.accumulate(chain, dtype=np.float64)137            if not np.all(np.isfinite(highs)):138                return None139            # 每一步的旧 x 与 partial 并列为 (K,);交换只用于找出绝对值较大的加数。140            incoming, old = highs[:-1], self._partials141            swap = np.abs(incoming) < np.abs(old)142            large = np.where(swap, old, incoming)143            small = np.where(swap, incoming, old)144            with np.errstate(over='ignore', under='ignore', invalid='ignore'):145                lows = np.subtract(small, np.subtract(highs[1:], large))146            # 非零 lo 按原误差顺序压紧;末 hi 为零时不追加,保留全零的空状态。147            kept = lows[lows != 0]148            last = highs[-1]149            count = len(kept) + int(last != 0)150            if count > self._partial_limit:151                raise IntegralStateLimitError('数组积分误差状态超过 partial_limit')152            # 新数组不借用 chain 的底层空间,跨块状态容量只包含实际保留的 K 项。153            if last != 0:154                return np.concatenate((kept, np.array([last], dtype=np.float64)))155            return kept.copy()156        finally:157            # 原异常及TB仍交调用方;只断本层借入参数、视图和临时,不清外部帧。158            self = area = chain = highs = incoming = old = None159            swap = large = small = lows = kept = last = None160161    def finish(self, unit: str, factor: float = 1.0) -> IntegralSumResult:162        """正确舍入当前误差状态,再做一次输出单位换算,返回独立快照。163164        参数: unit 为非空输出单位;factor 为正有限原生 float,仅在最后相乘。165        返回: IntegralSumResult;没有有效项为 None,有效全零为 +0.0;累计或166        换算非有限为 NONFINITE,非零换算下溢为 PRECISION_LOSS,均标计算失败。167        异常: TypeError/ValueError 为参数错误;分配或取消错误传播。168        """169        try:170            # 参数先校验;快照不消费或清空状态,外层 owner 决定是否锁定后续输入。171            unit, factor = require_sum_finish(unit, factor)172            if self._calculation_failed:173                return IntegralSumResult(None, unit, True, Q.NONFINITE, self._valid_term_count)174            if self._valid_term_count == 0:175                return IntegralSumResult(None, unit, False, Q.NONE, 0)176            # 空误差状态表示有效面积精确抵消或全零,不等同于没有有效面积。177            raw = self._rounded() if len(self._partials) else np.float64(0.0)178            with np.errstate(over='ignore', under='ignore', invalid='ignore'):179                converted = np.multiply(raw, np.float64(factor))180            if not np.isfinite(raw) or not np.isfinite(converted):181                return IntegralSumResult(None, unit, True, Q.NONFINITE, self._valid_term_count)182            if raw != 0 and converted == 0:183                return IntegralSumResult(None, unit, True, Q.PRECISION_LOSS, self._valid_term_count)184            # 转为 Python float 仅用于冻结结果,不把数值推进交给参考或标量求和核。185            return IntegralSumResult(float(converted), unit, False, Q.NONE, self._valid_term_count)186        finally:187            # 原异常及TB仍交调用方;只断本层借入参数、视图和临时,不清外部帧。188            self = unit = factor = raw = converted = None189190    def _rounded(self) -> np.float64:191        """从最大误差项向小项求和,在首个舍入误差处完成半偶修正。192193        参数: 无;当前状态必须至少包含一个非零有限 partial。194        返回: 当前原量纲的正确舍入 float64;不修改误差数组。195        异常: 分配或取消错误传播;数值非有限由 finish 分类。196        """197        try:198            # 逆序是只读视图;后续向量计算全部局限于 (K,) 或 (K-1,) 的固定容量。199            reverse = self._partials[::-1]200            with np.errstate(over='ignore', under='ignore', invalid='ignore'):201                highs = np.add.accumulate(reverse, dtype=np.float64)202                lows = np.subtract(reverse[1:], np.subtract(highs[1:], highs[:-1]))203            inexact = np.flatnonzero(lows != 0)204            if len(inexact) == 0:205                return highs[-1]206            # 首个非零 lo 已界定舍入位置,不能用后来多算的 hi 或故障污染这个结果。207            position = int(inexact[0])208            hi, lo = highs[position + 1], lows[position]209            next_position = position + 2210            if next_position < len(reverse):211                smaller = reverse[next_position]212                if (lo < 0 and smaller < 0) or (lo > 0 and smaller > 0):213                    # 与下一更小 partial 同号时探测半偶修正;比较差值确保只跨正确的 ULP。214                    with np.errstate(over='ignore', under='ignore', invalid='ignore'):215                        doubled = np.multiply(lo, np.float64(2.0))216                        corrected = np.add(hi, doubled)217                        difference = np.subtract(corrected, hi)218                    if doubled == difference:219                        hi = corrected220            return hi221        finally:222            # 原异常及TB仍交调用方;只断本层借入参数、视图和临时,不清外部帧。223            self = reverse = highs = lows = inexact = None224            hi = lo = smaller = doubled = corrected = difference = None225
Chronaxis 用户手册 · 离线阅读