chronaxis.core.calculations
源码基线:8904c2f744951857659abd40aaa3535d57faf53c
1"""声明积分、样本/时间统计及选择行流的协议,并统一结果状态。23CalculationDefinition说明算子、整数纳秒窗口、输出单位及部分结果政策。4CalculationSeries保存完整时间轴、typed值、可选三值条件、真实更新标志和来源。5CalculationResult保存数值、状态、覆盖率、未知区间和质量原因,使正常零、6无法计算的None及部分结果能明确区分。78输入按零阶保持处理:每个左端值用于直到下一时间的区间,窗口左闭右开,9末点只提供边界,不贡献积分面积。值有效性与条件可判断性分别记录,10UNKNOWN(未知)不能填成FALSE,也不能以零补齐未知区间。1112BOOL仅用于duration与select,ENUM仅用于保留标签的select行流。minimum_coverage13单独判断值、条件和请求三种覆盖是否达到门槛,不重写report/strict数值政策。14本模块只检查模型、推导单位并分类结果;面积、时间/样本归约和覆盖时长由15独立的参考与NumPy实现计算。仅依赖标准库和核心模型,不读写文件,16不修改调用者输入,也不保存可变全局状态。17"""1819# 冻结协议及默认元数据;不会持有可变任务全局状态。20from dataclasses import dataclass, field21# 输出数值必须有限,配置单位转换也不猜测。22from math import isfinite23# 精确静态单位因子来自已验证转换表。24# 已实现半开区间/来源引用结构。25from .models import Interval, SourceRef26from .moments import MOMENTS, moment_ddof, moment_source_unit27from .quantiles import SAMPLE_QUANTILES, quantile_probability28# 标量结果仅用于typed输入协议,不共享参考计算。29from .numeric import ScalarValue30# 类型/质量严格区分,BOOL只允许TRUE时长或原值行流。31from .types import SignalDType as D, QualityFlag as Q32# 明确ID、文本与int64时间协议。33from .validation import require_identifier, require_text34# 精确时间坐标校验,不经float转换。35from ..timebase.conversion import require_int64_ns363738@dataclass(frozen=True)39class CalculationDefinition:40 """声明一个指标如何计算,以及对不完整覆盖采用什么政策。4142 operation区分integral、明确samples/time极值与均值、BOOL duration及独立select行流。43 window使用左闭右开的整数纳秒范围,允许零长度,但此时不能计算面积。44 partial_policy为report时报告已核实部分,为strict时覆盖不足不返回正常值。45 scope说明窗口属于会话、活动或片段;本类不负责寻找或绑定对应活动。46 实例不可重新赋值,不持有计算进度。47 minimum_coverage为0—1有限比例;只有三种覆盖均达到门槛时覆盖质量充分。48 select输出SelectionRow迭代器,不形成CalculationResult标量;49 同一量种目标单位同步转换行值、类型与质量。50 """5152 calculation_id: str # 当前任务指标唯一简单名称。53 operation: str # 明确的数值/时长口径或select,select不能当作标量归约。54 window: Interval # 请求半开ns范围,零长度允许但没有面积。55 partial_policy: str = 'report' # report部分值,strict覆盖不足为null。56 output_unit: str | None = None # 明确单位转换,省略时按输入推导。57 scope: str = 'session' # 区间含义,不将上下文指标写成event指标。58 minimum_coverage: float = 0.0 # 0—1质量门槛,不删除report可核实诊断值。59 config_version: int = field(default=4, kw_only=True) # 单位规则版本,不改变旧位置参数。60 ddof: int | None = field(default=None, kw_only=True) # 只对方差/标准差有含义。61 q: int | float | None = field(default=None, kw_only=True) # 仅sample_quantile显式指定概率。6263 def __post_init__(self):64 """验证指标类型/政策/范围,不隐式选择mean口径。6566 参数: 无;读取当前冻结声明字段。67 返回: None,保持原请求窗口与scope。68 异常: TypeError/ValueError为ID/窗口/未知算子或政策。69 """70 # 指标身份遵循当前简单名称规则。71 require_identifier(self.calculation_id, 'calculation_id')72 # 版本是计划身份,不从单位文字猜测或修改全局默认。73 if type(self.config_version) is not int or self.config_version != 4:74 raise ValueError('CalculationDefinition仅支持config_version=4')75 # 未实现算子不能仅声明名就称支持。76 supported = ('integral', 'sample_minimum', 'sample_maximum', 'sample_mean', 'min_time', 'max_time',77 'time_weighted_mean', 'duration', 'select')78 supported += ('sample_count', *MOMENTS, *SAMPLE_QUANTILES)79 if self.operation in SAMPLE_QUANTILES:80 quantile_probability(self.operation,self.q)81 elif self.q is not None:82 raise ValueError('q仅适用于sample_quantile')83 object.__setattr__(self, 'ddof', moment_ddof(self.operation, self.ddof))84 if self.operation not in supported or not isinstance(self.window, Interval):85 # 只有已接线口径才接受,不开放模糊mean或线性模型。86 raise ValueError('指标需明确已实现算子及Interval')87 # 部分政策必须显式受支持,不默认猜UNKNOWN处理。88 if self.partial_policy not in ('report', 'strict') or self.scope not in ('session', 'event', 'segment'):89 # 原窗口scope不会随片段合并改变。90 raise ValueError('未知partial_policy或scope')91 # 非空单位文本保留大小写。92 if self.output_unit is not None:93 # 没有静默常量映射或工程方向推断。94 require_text(self.output_unit, 'output_unit')95 # 覆盖门槛仅判断质量,bool不能借整数继承关系进入比例。96 if type(self.minimum_coverage) not in (int, float) or not 0 <= self.minimum_coverage <= 1 or not isfinite(self.minimum_coverage):97 raise ValueError('minimum_coverage需0—1有限数值')9899100@dataclass(frozen=True)101class CalculationSeries:102 """保存一段明确观测范围内的保持值,以及真实样本的证据。103104 times_ns严格递增并包含观测首末端;values与各标志均与时间轴等长。105 每个values位置描述从当前时间到下一时间的左闭右开区间,末值仅是点,106 不能贡献面积。零长度观测域只需一个边界点,不外推文件结束后的周期。107 actual_value_updates区分真实数值依赖更新与窗口边界、有效期到期等合成边界;108 条件单独更新不能成为数值样本。conditions保留TRUE、FALSE和UNKNOWN三值,109 diagnostics另存依赖原因;所有值使用同一声明类型与已经归一化的unit。110 字段均为不可变模型或元组,不修改来源,也不保存流式批次状态。111 """112113 observation: Interval # 实际连续分析域,不外推EOF。114 times_ns: tuple[int, ...] # 严格递增,完整首/末端;零域一点。115 values: tuple[ScalarValue, ...] # 同一dtype;BOOL用于duration/select,ENUM只用于select。116 actual_value_updates: tuple[bool, ...] # 同长真实数值依赖更新标志。117 unit: str # 当前分析值单位,已经归一化,不能重复DBC factor。118 conditions: tuple[ScalarValue, ...] | None = None # 可选BOOL三值选择。119 diagnostics: tuple[Q, ...] = () # 原依赖质量,与逻辑结果质量分开。120 binding_identity: str = 'explicit-synthetic' # 当前输入执行身份/外部合成声明。121 source_refs: tuple[SourceRef, ...] = () # 所有实际依赖原字段关联。122123 def __post_init__(self):124 """验证完整域、同长typed列/三值、来源与实际更新协议。125126 参数: 无;读取全部冻结输入。127 返回: None;省略diagnostics时仅按输入原因生成元数据。128 异常: TypeError/ValueError/OverflowError为结构/回退/范围/dtype。129 """130 # 输入是已明确分析域和冻结列,不排序或补观测。131 if not isinstance(self.observation, Interval) or not isinstance(self.times_ns, tuple) or not self.times_ns:132 # 没有观测也必须明确零域和边界。133 raise TypeError('CalculationSeries需Interval及非空tuple时间轴')134 # 同长typed输入不能broadcast/truncate。135 if not isinstance(self.values, tuple) or not isinstance(self.actual_value_updates, tuple) or len(self.values) != len(self.times_ns) or len(self.actual_value_updates) != len(self.times_ns):136 # 存储错不降为UNKNOWN。137 raise ValueError('指标时间/值/actual列必须同长tuple')138 # 类型由算子门槛检查,BOOL/ENUM不会隐式形成面积。139 if any(not isinstance(value, ScalarValue) for value in self.values):140 # 无类型对象不能冒充含完整质量的逻辑值。141 raise TypeError('指标输入需typed ScalarValue')142 # 整列不隐式把整数提升为float。143 if any(value.dtype is not self.values[0].dtype for value in self.values) or any(type(flag) is not bool for flag in self.actual_value_updates):144 # 原定义dtype与实际更新布尔协议严格一致。145 raise TypeError('指标dtype需相同且actual需bool')146 # ENUM行流需同一标签域,不混合不同字典的相同编码。147 if any(value.labels != self.values[0].labels for value in self.values):148 raise ValueError('指标枚举标签域必须相同')149 # 当前连续域首末完整,不猜缺失外部边界。150 if self.times_ns[0] != self.observation.start_ns or self.times_ns[-1] != self.observation.end_ns:151 # 请求范围另裁剪,不修改观测域。152 raise ValueError('指标轴首末必须完整覆盖声明观测域')153 # 静态时间协议逐组校验,不承担数值归约。154 for index, time in enumerate(self.times_ns):155 # 相邻差可由已验证Interval跨度保证安全。156 require_int64_ns(time, 'calculation.time_ns')157 # 同时刻原子组只提交一次,不能按顺序采样两次。158 if index and time <= self.times_ns[index-1]:159 # 无默默排序或同刻去重。160 raise ValueError('指标完整时间组必须严格递增')161 # 条件已知FALSE和未知分别typed保存。162 if self.conditions is not None:163 # 未知选择不能以空数组代替。164 if not isinstance(self.conditions, tuple) or len(self.conditions) != len(self.times_ns) or any(not isinstance(value, ScalarValue) or value.dtype is not D.BOOL for value in self.conditions):165 # 错结构属于配置错误,不是全FALSE。166 raise TypeError('条件需同长tuple BOOL ScalarValue')167 # 省略诊断只用原质量元数据,不检测或推算面积。168 if not self.diagnostics:169 # 可决定known逻辑仍需来源质量时由调用者传完整diagnostics。170 object.__setattr__(self, 'diagnostics', tuple(value.quality | (self.conditions[index].quality if self.conditions else Q.NONE) for index, value in enumerate(self.values)))171 # 每组原因必须是合法typed组合。172 if not isinstance(self.diagnostics, tuple) or len(self.diagnostics) != len(self.times_ns) or any(not isinstance(q, Q) for q in self.diagnostics):173 # 不丢掉未识别质量位。174 raise TypeError('诊断需同长tuple QualityFlag')175 # 单位与绑定身份是元数据,不是通用领域目录。176 require_text(self.unit, 'calculation.unit')177 # 当前计划可追溯,不用语法文本代替绑定。178 require_text(self.binding_identity, 'binding_identity')179 # 来源定位必须typed且完整保留。180 if not isinstance(self.source_refs, tuple) or any(not isinstance(ref, SourceRef) for ref in self.source_refs):181 # 无类型路径不可冒充实际信号来源。182 raise TypeError('source_refs需tuple SourceRef')183184185@dataclass(frozen=True)186class CalculationResult:187 """保存指标值和解释该值所需的窗口、覆盖与质量事实。188189 spec保留原请求、政策和统计范围;actual_window给出与真实观测的交集。190 所有duration字段使用整数纳秒,coverage为0—1比例,分母为零时用None。191 value=None表示没有可核实的正常结果,与正常零值不同;status和reason192 共同说明完整、部分或无法判断的结果。值未知和条件未知分别保存区间。193 模型不可重新赋值,只检查事实的一致性,不修补结果或重新计算覆盖时长。194 """195196 spec: CalculationDefinition # 原请求/政策/scope不被EOF重写。197 value: int | float | None # 正常有限结果、合法零或无法核实null。198 unit: str # 结果单位,转换已经应用一次。199 status: str # complete/partial/indeterminate。200 reason: str # 明确empty_selection/no_observation等原因。201 actual_window: Interval | None # 与真实域交集,无交集为None。202 window_duration_ns: int # 实际观测窗口时长。203 value_valid_duration_ns: int # 可用于本算子的正常数值时长。204 condition_known_duration_ns: int # 条件可判断时长,无条件等于实际窗口。205 selected_duration_ns: int # 有效值与TRUE选择交集;duration额外要求主BOOL为TRUE。206 value_coverage: float | None # 值正常时长/实际窗口,零分母null。207 condition_coverage: float | None # 条件已知时长/实际窗口。208 requested_coverage: float | None # 实际窗口/请求窗口。209 valid_sample_count: int # 真实数值更新且有效并选中的样本数。210 value_unknown_ranges: tuple[Interval, ...] # 值不可核实范围。211 condition_unknown_ranges: tuple[Interval, ...] # 条件未知范围,FALSE不在内。212 quality: Q # 原依赖及计算诊断组合,不以零掩盖未知。213 binding_identity: str # 当前完整执行输入身份。214 source_refs: tuple[SourceRef, ...] # 当前实际原字段关联。215 coverage_status: str = 'sufficient' # sufficient或indeterminate,区别于诊断数值和partial状态。216217 def __post_init__(self):218 """校验结果事实一致性,不能构造正常非有限或不可能覆盖。219220 参数: 无;读取结果字段,全部duration精确ns。221 返回: None,不更正或补齐错误结果。222 异常: TypeError/ValueError为模型/数值/状态/覆盖/范围矛盾。223 """224 # 结果必须保留真实请求模型,scope与政策不会被重写。225 if not isinstance(self.spec, CalculationDefinition) or not isinstance(self.quality, Q):226 # 必须使用规定的指标定义和质量标记类型。227 raise TypeError('指标结果需CalculationDefinition及QualityFlag')228 # null与正常有限数值分开,bool不当统计值。229 if self.value is not None and (type(self.value) not in (int, float) or type(self.value) is float and not isfinite(self.value)):230 # 不接受NaN/Inf作为可用结果。231 raise ValueError('指标结果需有限原生数值或None')232 # 结果分类只使用已实现政策状态。233 if self.status not in ('complete', 'partial', 'indeterminate'):234 # 不以自由字符串掩盖未知完成状态。235 raise ValueError('未知指标status')236 # 最低覆盖率只给质量判断,不强制删除可核实report结果。237 if self.coverage_status not in ('sufficient', 'indeterminate'):238 raise ValueError('未知coverage_status')239 # 文本元数据不能空或任意对象化。240 require_text(self.reason, 'calculation.reason')241 # 输出单位保留明确数学含义。242 require_text(self.unit, 'calculation.unit')243 # 当前真实执行身份可追溯。244 require_text(self.binding_identity, 'binding_identity')245 # 无观测交集为None,零域不能补周期。246 if self.actual_window is not None and not isinstance(self.actual_window, Interval):247 # 原请求与实际观测必须有typed范围。248 raise TypeError('actual_window需Interval或None')249 # 所有时长/计数是非负原生整数。250 durations = (self.window_duration_ns, self.value_valid_duration_ns, self.condition_known_duration_ns, self.selected_duration_ns, self.valid_sample_count)251 # 不把浮点秒数塞进ns字段。252 if any(type(value) is not int or value < 0 for value in durations):253 # 布尔计数也不允许。254 raise TypeError('指标时长及计数需非负原生int')255 # 覆盖不能超过实际观测范围。256 if self.value_valid_duration_ns > self.window_duration_ns or self.condition_known_duration_ns > self.window_duration_ns or self.selected_duration_ns > min(self.value_valid_duration_ns, self.condition_known_duration_ns):257 # 不自动裁剪不可信结果。258 raise ValueError('指标覆盖时长不一致')259 # 实际区间长度必须等于保存的观测窗口时长。260 if self.window_duration_ns != (0 if self.actual_window is None else self.actual_window.end_ns-self.actual_window.start_ns):261 # 没有观测不能声称正覆盖。262 raise ValueError('actual_window与window_duration矛盾')263 # 比例为有限0—1或明确零分母null。264 for coverage in (self.value_coverage, self.condition_coverage, self.requested_coverage):265 # 不用NaN代表零分母。266 if coverage is not None and (type(coverage) is not float or not isfinite(coverage) or not 0 <= coverage <= 1):267 # 正常JSON结果可无歧义表达。268 raise ValueError('coverage需0—1有限float或None')269 # 诊断范围与来源必须保持冻结typed结构。270 if any(not isinstance(ranges, tuple) or any(not isinstance(interval, Interval) for interval in ranges) for ranges in (self.value_unknown_ranges, self.condition_unknown_ranges)):271 # 未知不以自由字符串或被吞空数组代替。272 raise TypeError('未知范围需tuple Interval')273 # 原字段身份不可被自由路径文本冒充。274 if not isinstance(self.source_refs, tuple) or any(not isinstance(ref, SourceRef) for ref in self.source_refs):275 # 不从另一个运行全局对象继承来源。276 raise TypeError('结果来源需tuple SourceRef')277278279@dataclass(frozen=True)280class CalculationConversion:281 """保存归约结束后的一次单位转换,不保存样本或额外结果副本。282283 factor和offset为输出值=归约值*factor+offset,整数系数保持精确整数。284 version固定为当前规则;实例在一次计划中不可修改。285 """286287 unit: str # 输出单位,与转换后的数值一起使用。288 factor: int | float # 一次乘法系数,不重复上游DBC或绑定缩放。289 offset: int | float = 0 # 绝对温度归约可用;积分输出不允许偏移。290 version: int = 4 # 当前单位规则,旧版本明确拒绝。291292 def __post_init__(self):293 """检查不可变转换描述符,拒绝无效版本和不可执行系数。294295 参数: 无,读取unit、factor、offset和version。296 返回: None。异常: TypeError/ValueError为文本、类型、有限性或版本错误。297 """298 # 精确整数可超过float范围;其数值可用性由目标dtype及仿射核判断。299 require_text(self.unit, 'calculation_conversion.unit')300 if type(self.version) is not int or self.version != 4:301 raise ValueError('单位转换版本必须为4')302 if any(type(item) not in (int, float) or type(item) is float and not isfinite(item)303 for item in (self.factor, self.offset)) or self.factor <= 0:304 raise ValueError('单位倍率须为正,倍率与偏移须为有限数值')305306 def dtype(self, input_dtype):307 """在执行前确定输出类型,不根据某条样本是否整除改变类型。308309 参数: input_dtype为归约值或select原列的SignalDType。310 返回: 保持原类型,或在小数倍率/偏移时返回FLOAT64。311 异常: ValueError为BOOL/ENUM请求数值单位变换。312 """313 # 恒等变换保留布尔/枚举;其余变换只作用于数值。314 if self.factor == 1 and self.offset == 0:315 return input_dtype316 if input_dtype not in (D.INT64, D.UINT64, D.FLOAT64):317 raise ValueError('BOOL/ENUM不支持数值单位换算')318 if input_dtype is D.FLOAT64:319 return D.FLOAT64320 return input_dtype if type(self.factor) is int and type(self.offset) is int else D.FLOAT64321322323def calculation_conversion(spec, input_unit):324 """取得完整输出换算,包括均值/极值允许的绝对温度偏移。325326 参数: spec为CalculationDefinition;input_unit为表达式当前值的单位。327 返回: CalculationConversion,数值后端在归约后应用一次;空结果不转换。328 异常: ValueError为不兼容量种、偏移积分或未知单位转换;TypeError为模型错误。329 """330 # 只接受已验证的当前计算定义,不为旧版本选择另一套单位解释。331 if not isinstance(spec, CalculationDefinition):332 raise TypeError('单位换算需要CalculationDefinition')333 # 注册表只在编译边界使用,运行不为每个样本创建单位对象。334 from .unit_registry import resolve_unit, canonical_unit, conversion, multiply, output_label335 if spec.operation == 'sample_count':336 # 计数没有输入物理量纲,不允许把个数换成工程单位。337 if spec.output_unit not in (None, '1'):338 raise ValueError('样本计数的输出单位只能是1')339 return CalculationConversion('1', 1, version=4)340 if spec.operation == 'duration':341 if input_unit != '1' or spec.output_unit not in (None, 's'):342 raise ValueError('duration需无量纲BOOL输入及s输出')343 return CalculationConversion('s', 1, version=4)344 source = resolve_unit(input_unit)345 if spec.operation in MOMENTS:346 source = moment_source_unit(spec.operation, source)347 if source is None:348 # 不通过相同未知标签推断新的物理关系;保持原保守单位处理。349 unit = input_unit + '*s' if spec.operation == 'integral' else input_unit350 if spec.output_unit not in (None, unit):351 raise ValueError(f'未知单位不能转换:{unit}→{spec.output_unit}')352 return CalculationConversion(unit, 1, version=4)353 if spec.operation == 'integral':354 # degC*s必须先明确改用绝对尺度,不能给面积最后只加273.15。355 source = multiply(source, resolve_unit('s'))356 target = resolve_unit(spec.output_unit) if spec.output_unit is not None else (357 source if spec.operation in MOMENTS else canonical_unit(source))358 if target is None:359 raise ValueError(f'指标目标单位未登记:{spec.output_unit}')360 scale, shift = conversion(source, target)361 if spec.operation in MOMENTS and shift:362 raise ValueError('方差、标准差和RMS输出不允许偏移换算')363 # 整数比例与偏移保留整数,防止2**53以上的整数极值无故转float。364 factor = int(scale.factor) if scale.pi_power == 0 and scale.factor.denominator == 1 else scale.to_float()365 offset = int(shift) if shift.denominator == 1 else float(shift)366 if spec.operation == 'integral' and offset:367 raise ValueError('积分结果不支持带偏移的单位转换')368 if spec.operation == 'integral' or spec.operation in MOMENTS:369 # 浮点归约的目标倍率必须可以表示;显式目标只转换一次。370 factor = scale.to_float()371 # 原子别名归一,倍率1的复合单位用规范名;不把内部括号描述作为输出标签。372 label = output_label(target, spec.output_unit or target.symbol)373 return CalculationConversion(label, factor, offset, 4)374375376def calculation_unit(spec, input_unit):377 """提供没有加法偏移的单位/倍率接口,兼容旧调用和积分归约。378379 参数: spec为CalculationDefinition,input_unit为当前值单位。380 返回: (输出单位, 一次乘法系数)。381 异常: ValueError为需要加法偏移或单位关系非法;不静默丢弃偏移。382 """383 # 均值/极值/select使用完整描述符;旧二元消费者不能忽略摄氏偏移。384 result = calculation_conversion(spec, input_unit)385 if result.offset:386 raise ValueError('该单位转换含偏移,请使用calculation_conversion')387 return result.unit, float(result.factor)388389390def merge_ranges(ranges):391 """合并同一种未知证据的相邻区间,仅用于诊断序列化。392393 参数: 已排序正Interval序列,不是真实Event序列。394 返回: Interval tuple,不修改原活动/条件或填平已知缺口。395 异常: 模型错误传播,无数值分析副作用。396 """397 # 原未知边界完整保留,不跨known缺口。398 result = []399 # 此循环处理输出诊断区间,不是NumPy数值内核。400 for interval in ranges:401 # 只同类相邻未知范围可紧凑表示。402 if result and result[-1].end_ns == interval.start_ns:403 # 不改变选中集或真实活动边界。404 result[-1] = Interval(result[-1].start_ns, interval.end_ns)405 else:406 # 中间有已知部分时必须保留独立范围。407 result.append(interval)408 # 不返回可变内部诊断列表。409 return tuple(result)410411412def classify_calculation_value(spec, value, observed_duration_ns, known_duration_ns, selected_duration_ns,413 selected_unknown_duration_ns, *, all_false=False, calculation_failed=False, sample_count=0):414 """按既有政策分类数值,供完整结果和精简内部结果共用。415416 参数: spec为已校验请求;各时长参数均为实际覆盖整数纳秒,value为候选值;417 all_false表示完整已知空选择,calculation_failed表示数值归约失败。418 返回: (value, status, reason)。异常: 内部参数错误原样传播;不构造范围或追溯记录。419 """420 # 请求窗口不随EOF裁剪重写。421 requested_duration_ns = spec.window.end_ns - spec.window.start_ns422 # 无交集明确0,零长度actual仍不是充分观测非零选择。423 # 必要条件未知、选中值未知或请求被裁剪均为不完整。424 incomplete = known_duration_ns < observed_duration_ns or selected_unknown_duration_ns > 0 or observed_duration_ns < requested_duration_ns425 # 正常候选值与结果状态由覆盖政策一起解释。426 status, reason = 'complete', 'ok'427 # 零请求不能借全FALSE返回正常0。428 if requested_duration_ns == 0:429 # 无面积与无选择充分观测严格分开。430 value, status, reason = None, 'indeterminate', 'zero_length_window'431 # 有请求但没有实际连续观测。432 elif observed_duration_ns == 0:433 # 不把空数组sum的0当真值。434 value, status, reason = None, 'indeterminate', 'no_observation'435 # 非有限归约/转换不能报正常结果。436 elif calculation_failed:437 # 保留覆盖/原因,但数值不能使用。438 value, status, reason = None, 'indeterminate', 'calculation_failed'439 # 完整观测且条件始终为FALSE时,积分为零,样本统计保留None。440 elif spec.operation == 'sample_count':441 # 真实样本计数可恰为0。只要有确定的选择范围(含明确FALSE)就有442 # 部分计数依据,全部条件未知或全部所选值未知时不能宣称完整空集。443 from .counts_v4 import classify_count444 value, status, reason, _ = classify_count(value or 0, spec.partial_policy,445 observed=True, determinate=known_duration_ns > selected_unknown_duration_ns, complete=not incomplete)446 elif all_false and observed_duration_ns == requested_duration_ns:447 # 输入数值无效诊断仍保留,不是结果需要的选中值未知。448 value, reason = (0.0, 'empty_selection') if spec.operation in ('integral', 'duration') else (None, 'empty_selection')449 # 没有可核实选中数据,不区分真假补零。450 elif spec.ddof == 1 and sample_count == 1:451 value, status, reason = None, 'indeterminate', 'insufficient_samples'452 elif value is None:453 # 已知无样本/全未知/条件未知原因明确。454 status, reason = 'indeterminate', 'unknown_selection' if known_duration_ns < observed_duration_ns else 'no_samples' if spec.operation != 'integral' and selected_duration_ns > 0 else 'no_valid_selected_data'455 # 有候选部分值仍按政策附明确状态。456 elif incomplete:457 # strict不返回部分正常值,但事实覆盖照常保存。458 status, reason = ('indeterminate', 'insufficient_coverage') if spec.partial_policy == 'strict' else ('partial', 'partial_coverage')459 # report保留已核实部分面积/样本统计。460 if spec.partial_policy == 'strict':461 # 不用零填补unknown或EOF外时间。462 value = None463 return value, status, reason464465466def classify_coverage(spec,observed_duration_ns,value_duration_ns,known_duration_ns):467 """只在请求门槛时判断三种真实覆盖,不影响report/strict的数值政策。468469 参数: spec及已有实际范围/有效/已知时长。返回: sufficient或indeterminate。470 异常: 无主动异常;内部量由各数值核独立求得,不构造区间明细。471 """472 requested_duration_ns=spec.window.end_ns-spec.window.start_ns473 ratios=(value_duration_ns/observed_duration_ns if observed_duration_ns else None,474 known_duration_ns/observed_duration_ns if observed_duration_ns else None,observed_duration_ns/requested_duration_ns if requested_duration_ns else None)475 return 'sufficient' if all(value is not None and value>=spec.minimum_coverage for value in ratios) else 'indeterminate'476477478def make_calculation_result(spec, series, value, unit, actual, value_duration_ns, known_duration_ns, selected_duration_ns,479 selected_unknown_duration_ns, sample_count, value_unknown, condition_unknown, quality,480 *, all_false=False, calculation_failed=False):481 """把独立计算的数值与覆盖事实归类,生成可追溯指标结果。482483 参数:484 spec、series: 当前请求和输入;value为候选值,unit为结果单位。485 actual: 请求与观测范围的交集,无交集时为None。486 value_duration_ns、known_duration_ns、selected_duration_ns: 值有效、条件已知、487 有效值与TRUE选择相交的时长,均为整数纳秒;时长计算还要求主BOOL为TRUE。488 selected_unknown_duration_ns: 条件TRUE但值未知的时长,单位为纳秒。489 sample_count: 已选真实有效样本数;value_unknown、condition_unknown为未知区间。490 quality: 完整质量原因;all_false默认False,表示是否已有全程FALSE证据。491 calculation_failed: 默认False,表示归约或输出转换是否失败。492 返回:493 CalculationResult,保留覆盖、来源与原因。完整观测且全程FALSE时积分为零,494 空选择的样本统计为None;其他缺失情形不能据空归约推断正常零。495 异常:496 输出事实不满足CalculationResult要求时传播模型的TypeError或ValueError。497 流程:498 先判断零窗口、无观测和计算失败,再处理空选择与未知数据;499 有部分候选值时,report保留并标partial,strict清为None。500 本函数不计算积分或样本归约,各时长和候选值由调用者独立计算。501 """502 # 与精简内部结果共享分类;只有本完整结果入口继续构造覆盖明细。503 requested_duration_ns = spec.window.end_ns - spec.window.start_ns504 observed_duration_ns = 0 if actual is None else actual.end_ns - actual.start_ns505 value, status, reason = classify_calculation_value(spec, value, observed_duration_ns, known_duration_ns,506 selected_duration_ns, selected_unknown_duration_ns, all_false=all_false, calculation_failed=calculation_failed,507 sample_count=sample_count)508 # 三种既定覆盖率同时满足质量门槛才充分;零分母保持无法判断。509 coverages = (value_duration_ns / observed_duration_ns if observed_duration_ns else None,510 known_duration_ns / observed_duration_ns if observed_duration_ns else None,511 observed_duration_ns / requested_duration_ns if requested_duration_ns else None)512 coverage_status = 'sufficient' if all(item is not None and item >= spec.minimum_coverage for item in coverages) else 'indeterminate'513 # 最低门槛不改动以上数值政策或事实时长。514 return CalculationResult(spec, value, unit, status, reason, actual, observed_duration_ns, value_duration_ns, known_duration_ns,515 selected_duration_ns, value_duration_ns / observed_duration_ns if observed_duration_ns else None,516 known_duration_ns / observed_duration_ns if observed_duration_ns else None, observed_duration_ns / requested_duration_ns if requested_duration_ns else None,517 sample_count, merge_ranges(value_unknown), merge_ranges(condition_unknown), quality,518 series.binding_identity, series.source_refs, coverage_status)519