Chronaxis用户手册English
本页目录

chronaxis.fast.totals

源码基线:8904c2f744951857659abd40aaa3535d57faf53c

1"""按独立文件模板的声明顺序汇总同一业务指标。只在请求 --total 时使用。"""23from dataclasses import dataclass4import json5import math6from pathlib import Path789def _semantics(spec):10    """比较业务绑定、计算指令、时间范围及解码选项。单文件路径和展示格式可不同。"""11    settings = {key: value for key, value in spec.items()12                if key not in ('path', 'dbc', 'table_format', '_database', 'inputs')}13    if 'inputs' in spec:14        settings['input_options'] = [{k:v for k,v in item.items() if k != 'path'} for item in spec['inputs']]15    return Path(spec['dbc']).resolve(), json.dumps(settings, sort_keys=True, allow_nan=False)161718@dataclass(frozen=True)19class FastTotals:20    """在开始读文件前冻结可求和的指标,以及这些指标应当来自哪些任务。"""2122    members: tuple23    metrics: tuple2425    @classmethod26    def from_specs(cls, specs, *, template):27        """specs 为按模板输入顺序排列的 job_id→fast 声明。返回只读求和计划。2829        只接受同模板、可核实相同绑定和累计口径的不同文件。失败发生在任务分派前。30        """31        if template is not True:32            raise ValueError('--total 仅支持同一分析模板展开的独立文件')33        if not specs:34            raise ValueError('--total 没有预期文件')35        first = next(iter(specs.values()))36        expected = _semantics(first)37        metrics = tuple((item['name'], item['unit']) for item in first['metrics'])38        if not metrics or len(metrics) != len({name for name, _ in metrics}):39            raise ValueError('批量总计需要互不重复的计算项')40        files = set()41        for job, spec in specs.items():42            if _semantics(spec) != expected:43                raise ValueError(f'{job} 与首文件的 DBC、绑定、公式、方向或时间口径不同')44            path = Path(spec['path']).resolve()45            if path in files:46                raise ValueError(f'{job} 重复选择了同一个输入文件')47            files.add(path)48        return cls(tuple(specs), metrics)4950    def finish(self, outcomes):51        """按计算项和方向桶分别求和。失败/取消/未运行任务须显式写为 None。5253        outcomes 的正常值是 worker 返回的未舍入行。成员或行不符合计划属于协议错误。54        数值缺失导致该项总计不可用。有限加数之和溢出时抛出 OverflowError。55        """56        if set(outcomes) != set(self.members):57            raise ValueError('批量结果成员与启动前冻结的文件清单不一致')58        schema = dict(self.metrics)59        sums = dict.fromkeys(schema, 0.0)60        unavailable = {}61        for job in self.members:62            rows = outcomes[job]63            if rows is None:64                for name in schema:65                    unavailable[name] = 'member_not_completed'66                continue67            seen = set()68            for row in rows:69                name = row['name']70                if name not in schema or name in seen or row['unit'] != schema[name]:71                    raise ValueError(f'{job} 返回了未知、重复或单位不符的计算项 {name}')72                seen.add(name)73                value = row['value']74                if value is None and row['status'] == 'indeterminate':75                    unavailable.setdefault(name, 'member_value_unknown')76                elif row['status'] != 'complete' or type(value) not in (float, int):77                    raise ValueError(f'{job} 的 {name} 数值与状态不符')78                elif not math.isfinite(value) or float(value) != value:79                    raise ValueError(f'{job} 的 {name} 不能无损表示为有限 float64')80                else:81                    sums[name] += value82            if seen != schema.keys():83                raise ValueError(f'{job} 缺少预期计算项')84        result = []85        for name, unit in self.metrics:86            reason = unavailable.get(name)87            value = None88            if reason is None:89                value = sums[name]90                if not math.isfinite(value):91                    raise OverflowError(f'批量总计 {name} 溢出')92            result.append(dict(name=name, unit=unit, value=value,93                               status='complete' if reason is None else 'indeterminate', reason=reason))94        return result95
Chronaxis 用户手册 · 离线阅读