Chronaxis用户手册English
本页目录

chronaxis.execution.record_time_calculation

源码基线:8904c2f744951857659abd40aaa3535d57faf53c

1"""用有限记录计算时间统计和真实样本统计;仅时间均值暂存必要值/权重。23输入与RecordIntegral共享点缓冲、递增时间和失效处理。复用原时间/样本数值核;4样本统计保真实更新标志,不把条件/TTL更新当新样本,样本均值使用原序求和。5均值首遍仅写值与权重,实际权重为int64纳秒,周期权重为float秒;6次遍按已知总权重规范化,避免先求面积溢出。7临时文件由本owner关闭时删除,不默认留现场;结果不附历史、贡献或时间对照。8调用者必须先确认来源EOF及关闭成功再finish,本对象不替代公共AnalysisRunner发布。9"""1011from ..core.moments import SAMPLE_MOMENTS12from ..core.quantiles import SAMPLE_QUANTILES13from dataclasses import dataclass14from pathlib import Path15from struct import Struct16from tempfile import TemporaryFile1718import numpy as np1920from ..core.calculations import CalculationDefinition, CalculationSeries, classify_calculation_value21from ..core.calculations import classify_coverage22from ..core.models import Interval23from ..core.types import SignalDType as D, QualityFlag as Q24from ..core.time_reduction import require_time_config, TimeReductionResult25from ..core.time_reduction_reference import ReferenceTimeReduction26from ..core.time_reduction_numpy import NumpyTimeReduction27from ..core.conditional_time_reduction_reference import ReferenceConditionalTimeReduction28from ..core.conditional_time_reduction_numpy import NumpyConditionalTimeReduction29from ..core.integral_sum_reference import ReferenceIntegralSum30from ..core.integral_sum_numpy import NumpyIntegralSum31from ..core.mean_weights_reference import normalized_mean_terms as reference_mean32from ..core.mean_weights_numpy import normalized_mean_terms as numpy_mean33from ..results.budget import ResourceBudget, _reserve_owned_ram, _release_owned_ram, _cleanup_owner34from ..results.errors import BudgetExceededError35from .history import _metadata_bytes36from .record_point_buffer import RecordPointBuffer37from ..core.sample_reduction import require_sample_config38from ..core.calculation_output_reference import convert_result_reference39from ..core.calculation_output_numpy import convert_result_numpy40from ..core.sample_reduction_reference import ReferenceSampleReduction41from ..core.sample_reduction_numpy import NumpySampleReduction42from .record_sample_series import sample_series434445@dataclass(frozen=True)46class RecordTimeResult:47    """保存时间统计的必要输出,整数极值不转成浮点数。"""4849    value: int | float | None50    unit: str51    status: str52    reason: str53    quality: Q54    coverage_status: str | None = None555657class RecordTimeCalculation(RecordPointBuffer):58    """拥有一份点缓冲、数值状态及可选均值临时文件,失败后只能清理。"""5960    def __init__(self, budget, spec, input_unit, dtype, step_ns, *, backend='numpy',61                 batch_intervals=64, partial_limit=4096, conditioned=False, work_dir=None,hosted=None):62        """预付有限状态,均值的文件延迟到首批有效项才创建。6364        参数: 共享budget、时间统计spec、已求值类型/单位;step_ns为None实际差或正周期;65            后端和批上限明确;66            conditioned表示外BOOL条件,time_weighted_mean需要既存work_dir。67        返回: 新owner。异常: 配置/预算失败;持续清理故障附record_time_cleanup_owner。68        """69        if not isinstance(budget, ResourceBudget) or not isinstance(spec, CalculationDefinition):70            raise TypeError('记录时间统计需要共享预算和CalculationDefinition')71        if backend not in ('reference', 'numpy') or type(conditioned) is not bool:72            raise ValueError('时间统计需要明确后端及bool条件开关')73        if type(batch_intervals) is not int or not 1 <= batch_intervals <= 4096:74            raise ValueError('记录批上限需要1到4096')75        self._samples=spec.operation in ('sample_count','sample_minimum','sample_maximum','sample_mean', *SAMPLE_MOMENTS, *SAMPLE_QUANTILES)76        # 宿主归约自行管理其状态;只有内建精确分位数在本层预估样本保留费用。77        self._builtin_quantile = hosted is None and spec.operation in SAMPLE_QUANTILES78        if hosted is not None:79            from ..core.calculations import calculation_conversion80            from ..core.unit_registry import resolve_unit,conversion81            scale,shift=conversion(resolve_unit(input_unit),resolve_unit(hosted.input.unit))82            if (not self._samples or step_ns is not None or dtype is not hosted.input.dtype or83                    scale.factor!=1 or scale.pi_power or shift):84                raise ValueError('宿主样本归约需要声明的输入dtype/单位和真实更新')85            self._conversion=calculation_conversion(spec,hosted.output.unit)86        elif self._samples:87            if step_ns is not None:raise ValueError('样本统计不使用固定或DBC周期')88            self._conversion=require_sample_config(spec,input_unit,dtype,partial_limit)89        else:90            self._conversion = require_time_config(spec, input_unit, dtype, None, partial_limit, step_ns=step_ns)91        self._unit,self._factor=self._conversion.unit,self._conversion.factor92        self._work_dir = None93        if spec.operation == 'time_weighted_mean':94            if work_dir is None or not Path(work_dir).is_dir():95                raise ValueError('均值需要已存在的专用工作目录')96            self._work_dir = Path(work_dir)97        self._budget, self._spec, self._input_unit, self._dtype = budget, spec, input_unit, dtype98        self._step_ns, self._limit, self._conditioned = step_ns, batch_intervals, conditioned99        self._backend, self._partial_limit = backend, partial_limit100        self._held_cost = self._fixed_cost = self._workspace_cost = 0101        self._closed = self._closing = self._failed = self._finished = False102        self._rows = self._reduction = self._result = self._file = self._mean_sum = None103        self._last_time = None104        self._covered_ns = self._known_ns = self._selected_ns = self._selected_unknown_ns = self._false_ns = 0105        self._quality = Q.NONE106        self._transform_partial = False107        self._coverage_value_ns=0 if spec.minimum_coverage>0 else None108        self._record_count = 0109        self._codec = Struct('>dq' if step_ns is None else '>dd')  # 仅值/权重,实际纳秒不降为浮点。110        try:111            _reserve_owned_ram(self, 65536 + partial_limit * 512 + (batch_intervals+1)*1024112                + 16*_metadata_bytes((spec, input_unit)), '_fixed_cost')113            self._rows = []114            kernel = ((ReferenceConditionalTimeReduction if backend == 'reference' else NumpyConditionalTimeReduction)115                      if conditioned else (ReferenceTimeReduction if backend == 'reference' else NumpyTimeReduction))116            if hosted is not None:117                from .hosted_sample_reduction import HostedSampleReduction118                self._reduction=HostedSampleReduction(spec,input_unit,dtype,hosted,self._conversion,backend,budget,partial_limit)119            else:120                self._reduction = ((ReferenceSampleReduction if backend=='reference' else NumpySampleReduction)121                    (spec,input_unit,dtype,partial_limit=partial_limit,quantile_budget=budget) if self._samples else122                    kernel(spec, input_unit, dtype, step_ns=step_ns, partial_limit=partial_limit))123        except BaseException as exc:124            self._reduction=getattr(exc,'hosted_sample_cleanup_owner',self._reduction)125            _cleanup_owner(self, exc, 'record_time_cleanup_owner', '记录时间统计')126            raise127128    def _append_mean(self, values, weights):129        """只写本批选中的必要均值项;费用先预约,不缓存完整输入。130131        参数: 已由核校验的有限数值/权重。返回: None。异常: 配额、短写、IO或取消传播。132        """133        if len(values) == 0:134            return135        if self._file is None:136            self._file = TemporaryFile(mode='w+b', buffering=0, dir=self._work_dir, prefix='mean-')137        payload = bytearray(len(values)*self._codec.size)138        view = None139        try:140            for index, (value, weight) in enumerate(zip(values, weights)):141                self._codec.pack_into(payload, index*self._codec.size, float(value),142                    int(weight) if self._step_ns is None else float(weight))143            self._budget.reserve_spool(len(payload))144            view = memoryview(payload)145            position = 0146            while position < len(view):147                written = self._file.write(view[position:])148                if type(written) is not int or not 0 < written <= len(view)-position:149                    raise OSError('均值暂存短写或无写入进展')150                position += written151            self._record_count += len(values)152        finally:153            if view is not None:154                view.release()155            values = weights = payload = view = None156157    def _flush(self):158        """在共享预算内分块处理样本统计,保留每块的共同右端。159160        参数: 无,使用已预付的点缓冲。返回: None。161        异常: 最小两点工作区也放不下时报告计算项与用途;数值、取消原错传播。162        不降低每行估算、不放宽限额,也不丢弃、重排或重复累计真实样本。163        """164        if not self._samples:165            return self._flush_block()166        remaining = None167        try:168            while len(self._rows) >= 2:169                # 读取队列和其他指标仍存活,不能假定本指标可独占整个预算。170                # 系数沿用原样本核对最多两个TTL插入点的保守预付。171                available = self._budget.memory_budget_bytes - self._budget.ram_bytes172                count = min(len(self._rows), (available - 65536) // 36864)173                if self._builtin_quantile:174                    # 临时工作区与本块新增的持久样本同时存活。右端不属于半开块,175                    # TTL插入点也不是真实样本,故至多保留count-1个值。按缓冲器176                    # 的实际计费规则找最大可用块,不用固定百分比吞掉剩余预算。177                    lower, upper = 0, max(0, count)178                    while lower < upper:179                        middle = (lower + upper + 1) // 2180                        needed = (65536 + middle * 36864181                                  + self._reduction._quantile._append_memory_bytes(max(0, middle - 1)))182                        if needed <= available:183                            lower = middle184                        else:185                            upper = middle - 1186                    count = lower187                if count < 2:188                    minimum = 139264189                    if self._builtin_quantile:190                        minimum += self._reduction._quantile._append_memory_bytes(1)191                    raise BudgetExceededError(192                        f'计算项 {self._spec.calculation_id}({self._spec.operation})'193                        f'样本统计工作区不足:最小两点及必要样本存储需要{minimum}字节,当前可用{available}字节;'194                        '已尝试按可用预算分块,请减少同时运行的计算项或读取批量,'195                        '核实必要状态后再调整memory_budget_bytes')196                if count == len(self._rows):197                    self._flush_block()198                    break199                # 两份短列表的引用槽由既有点缓冲1024字节/行覆盖;不复制值对象。200                # 右端在前块半开范围内不计样本,下一块再作为左端处理一次。201                remaining = self._rows[count - 1:]202                self._rows = self._rows[:count]203                self._flush_block()204                self._rows, remaining = remaining, None205        finally:206            # 中途失败不能持有已退款缓冲以外的尾部引用;外层负责标失败并关闭。207            remaining = None208209    def _flush_block(self):210        """计算完整记录对,只留共享右端;均值写选中项,其他指标不写盘。211212        参数: 无。返回: None。异常: 预算/数值结构/IO/取消,失败后不得交付前缀。213        """214        if len(self._rows) < 2:215            return216        series = block = facts = None217        primary = None218        try:219            _reserve_owned_ram(self, 65536 + len(self._rows)*(36864 if self._samples else 12288), '_workspace_cost')220            times = tuple(row[0] for row in self._rows)221            values = tuple(row[1] for row in self._rows)222            deadlines = tuple(row[2] for row in self._rows) if any(row[2] is not None for row in self._rows) else None223            conditions = tuple(row[3] for row in self._rows) if self._conditioned else None224            options = {'condition_valid_until_ns': tuple(row[4] for row in self._rows)} if self._conditioned else {}225            series = (sample_series(self._rows,self._input_unit,self._conditioned) if self._samples else226                CalculationSeries(Interval(times[0], times[-1]), times, values, (True,)*len(times), self._input_unit, conditions=conditions))227            mean = self._spec.operation == 'time_weighted_mean'228            block = (self._reduction.push(series) if self._samples else229                self._reduction.push(series, valid_until_ns=deadlines,230                    retain_unknown_ranges=False, retain_mean_terms=mean, **options))231            facts = block.facts if mean else block232            from .transform_partial import partial_in_block233            if partial_in_block(self._rows,self._spec.window,samples=self._samples):234                self._transform_partial=True235            if self._coverage_value_ns is not None:236                self._coverage_value_ns+=facts.value_valid_duration_ns237            if mean:238                self._append_mean(block.values, block.weights)239            span = 0 if facts.actual_window is None else facts.actual_window.end_ns-facts.actual_window.start_ns240            self._covered_ns += span241            self._known_ns += facts.condition_known_duration_ns if self._conditioned else span242            self._selected_ns += facts.selected_duration_ns243            self._selected_unknown_ns += facts.selected_value_unknown_duration_ns if self._conditioned else span-facts.value_valid_duration_ns244            if self._conditioned:245                self._false_ns += facts.false_duration_ns246            self._quality |= facts.quality247            self._rows[:] = self._rows[-1:]248        except BaseException as exc:249            primary, self._failed = exc, True250            raise251        finally:252            series = block = facts = times = values = deadlines = conditions = options = None253            try:254                _release_owned_ram(self, self._workspace_cost, '_workspace_cost')255            except BaseException as cleanup:256                self._failed = True257                if primary is None:258                    raise259                primary.add_note('时间统计临时费用释放失败:'+type(cleanup).__name__)260261    def _finish_mean(self):262        """分批回读必要数值/权重,复用原归一化及原序求和,最终只换单位一次。263264        参数: 无。返回: TimeReductionResult。异常: 临时数据短缺/多出、IO、预算或取消。265        """266        if self._record_count == 0:267            return TimeReductionResult(None, self._unit, False, Q.NONE, 0)268        denominator = (self._reduction.selected_duration_ns if self._step_ns is None269                       else self._reduction.selected_weight_seconds)270        self._file.seek(0)271        self._mean_sum = (ReferenceIntegralSum if self._backend == 'reference' else NumpyIntegralSum)(self._partial_limit)272        remaining, quality = self._record_count, Q.NONE273        payload = pairs = values = weights = terms = keep = None274        try:275            while remaining:276                count = min(remaining, self._limit)277                payload = self._file.read(count*self._codec.size)278                if len(payload) != count*self._codec.size:279                    raise ValueError('均值必要暂存数据不完整')280                if self._backend == 'reference':281                    pairs = tuple(self._codec.iter_unpack(payload))282                    values, weights = tuple(item[0] for item in pairs), tuple(item[1] for item in pairs)283                    terms, failure = reference_mean(values, weights, denominator)284                    keep = (True,)*len(terms)285                else:286                    pairs = np.frombuffer(payload, dtype=[('value', '>f8'),287                        ('weight', '>i8' if self._step_ns is None else '>f8')])288                    values, weights = pairs['value'], pairs['weight']289                    terms, failure = numpy_mean(values, weights, denominator)290                    keep = np.ones(len(terms), dtype=bool)291                    keep.setflags(write=False)292                quality |= failure293                if quality == Q.NONE:294                    self._mean_sum.push_values(terms, keep)295                remaining -= count296            if self._file.read(1):297                raise ValueError('均值必要暂存含未登记数据')298            if quality != Q.NONE:299                return TimeReductionResult(None, self._unit, True, quality, self._record_count)300            # v4先完成原量纲均值,再由所选后端一次换单位;偏移不能随块数重复。301            number = self._mean_sum.finish(self._input_unit,302                                          1.0)303            if not number.calculation_failed:304                convert = convert_result_reference if self._backend == 'reference' else convert_result_numpy305                value, quality = convert(number.value, self._dtype, self._conversion)306                return TimeReductionResult(value, self._unit, quality != Q.NONE, quality, self._record_count)307            return TimeReductionResult(number.value, self._unit, number.calculation_failed, number.quality, self._record_count)308        finally:309            payload = pairs = values = weights = terms = keep = None310311    def finish(self):312        """上游确认EOF后结算,并关闭删除均值临时文件,再交付精简结果。313314        参数: 无。返回: RecordTimeResult,重复调用同一结果。异常: 失败拒绝前缀,close可重试。315        """316        self._require_open()317        if self._finished:318            return self._result319        primary = None320        try:321            self._flush()322            workspace = 65536 + (self._limit+1)*12288 if self._spec.operation == 'time_weighted_mean' else 4096323            _reserve_owned_ram(self, workspace, '_workspace_cost')324            number = self._finish_mean() if self._spec.operation == 'time_weighted_mean' else self._reduction.finish()325            value, status, reason = classify_calculation_value(self._spec, number.value, self._covered_ns,326                self._known_ns, self._selected_ns, self._selected_unknown_ns,327                all_false=self._conditioned and self._covered_ns>0 and self._false_ns==self._covered_ns,328                calculation_failed=number.calculation_failed,329                sample_count=number.valid_sample_count if self._samples else 0)330            if self._file is not None:331                self._file.close()332                self._file = None333            coverage=None if self._coverage_value_ns is None else classify_coverage(334                self._spec,self._covered_ns,self._coverage_value_ns,self._known_ns)335            self._result = RecordTimeResult(value, self._unit, status, reason, self._quality | number.quality,coverage)336            if self._transform_partial or getattr(self._reduction,'partial',False):337                from .transform_partial import partial_result338                self._result=partial_result(self._result,self._spec.partial_policy)339            self._finished = True340            return self._result341        except BaseException as exc:342            primary, self._failed = exc, True343            raise344        finally:345            number = None346            try:347                _release_owned_ram(self, self._workspace_cost, '_workspace_cost')348            except BaseException as cleanup:349                self._failed = True350                if primary is None:351                    raise352                primary.add_note('时间统计结算费用释放失败:'+type(cleanup).__name__)353354    @property355    def closed(self):356        """读取自有文件/费用是否全部释放。参数: 无。返回: bool。异常: 无。"""357        return self._closed358359    def close(self):360        """清理自有数值状态和临时文件,释放失败保留重试入口,不碰输入文件。361362        参数: 无。返回: None。异常: 文件关闭/费用释放失败可重试;重复关闭安全。363        """364        if self._closed:365            return366        self._closing = True367        if self._samples and self._reduction is not None:368            self._reduction.close()369        self._rows = self._reduction = self._mean_sum = self._result = None370        if self._file is not None:371            self._file.close()372            self._file = None373        _release_owned_ram(self, self._workspace_cost, '_workspace_cost')374        _release_owned_ram(self, self._fixed_cost, '_fixed_cost')375        self._closed = True376
Chronaxis 用户手册 · 离线阅读