Chronaxis用户手册English
本页目录

chronaxis.execution.same_frame_integrals

源码基线:8904c2f744951857659abd40aaa3535d57faf53c

1"""把同一物理报文的简单数值表达式直接交给现役实际时间积分器。23本对象只保存一个待完成同刻组;不构造信号数组、历史或通用最新状态。4读取、DBC投影、范围推进、积分器及结果发布仍由原会话负责。只有首帧前5证明绑定一致的内部调用方使用本入口,运行错误不切换路径或重读输入。6"""78from __future__ import annotations910from dataclasses import dataclass, replace11from math import isnan12from typing import Callable, Mapping, Sequence, TYPE_CHECKING1314from ..core.binding import SignalDefinition15from ..core.expression import validate_expression16from ..core.expression_reference import _evaluate17from ..core.numeric import normalize_reference, ScalarValue18from ..core.scalar import _scalar_from_validated_fields19from ..core.types import SignalDType as D20from ..legacy.records import field_from_dict21from ..legacy.updates import LegacyDecodedUpdate22from ..planning.backend_selection import simple_numeric_expression23from ..results.budget import ResourceBudget, _reserve_owned_ram, _release_owned_ram, _cleanup_owner24from ..results.errors import ResultClosedError25from ..sources.can_spec import can_field_from_key26from .history import _metadata_bytes27from .static_metadata import static_metadata_bytes2829if TYPE_CHECKING:30    from ..core.expression import StaticExpression31    from ..planning.plan import AnalysisPlan32    from .record_integral import RecordIntegral333435def direct_integral_inputs(plan: AnalysisPlan) -> tuple[SignalDefinition, ...] | None:36    """检查直接积分的完整静态边界,不读取DBC或数据。3738    参数: 已选reference的计划。返回: 本次必要定义tuple;不适用为None。39    异常: 非法typed结构,不把运行错误当成选择其他路径的理由。40    """41    if (plan.execution.backend != 'reference' or plan.extensions is not None42            or plan.rules or plan.windows or plan.sequences or plan.transforms or plan.aggregations43            or plan.message_calculations or not plan.calculations44            or plan.outputs.signal_csv is not None or plan.outputs.can_frames is not None45            or plan.diagnostics.trace or plan.diagnostics.integrity):46        return None47    choices, used = dict(plan.intervals.resolved), set()48    for item in plan.calculations:49        if (item.operation != 'integral' or item.scope != 'session' or item.method != 'zoh'50                or item.condition is not None or not simple_numeric_expression(item.signal)51                or item.calculation_id not in choices or choices[item.calculation_id].mode != 'actual'52                or choices[item.calculation_id].driver is not None):53            return None54        used.update(item.signal.dependencies)55    definitions = tuple(item.definition for item in plan.signals if item.definition.name in used)56    if {item.name for item in definitions} != used or len({item.source for item in definitions}) != 1:57        return None58    source = next((item for item in plan.sources if item.name == definitions[0].source), None)59    if source is None or source.source_type not in ('can_blf', 'can_asc', 'asc_or_blf', 'raw_can_csv'):60        return None61    fields = tuple(can_field_from_key(item.field) for item in definitions)62    if len({(f.channel, f.frame_id, f.is_extended, f.is_fd) for f in fields}) != 1:63        return None64    if any(item.dtype not in (D.INT64, D.UINT64, D.FLOAT64) or item.capture_code for item in definitions):65        return None66    return definitions676869@dataclass(frozen=True)70class SameFrameBinding:71    """保存同次DBC证明的共同定位;fd可为None,真实歧义继续由原投影器检查。"""7273    source: str  # 当前计划中的来源名称。74    channel: int  # 已转换为内部零起序号的通道。75    frame_id: int  # DBC和原配置共同定位的报文编号。76    extended: bool  # 同次DBC字段事实已经确定的标准/扩展标记。77    fd: bool | None  # 原配置的CAN/FD约束;None保留原投影器的自动识别。78    dbc_fd: bool  # DBC声明的FD属性,沿用经典CAN兼容规则。7980    def matches(self, update: LegacyDecodedUpdate) -> bool:81        """匹配已投影更新。参数: 更新。返回: bool。异常: 非法内部对象属性错误。"""82        return (update.source == self.source and update.channel == self.channel83                and update.frame_id == self.frame_id and update.is_extended == self.extended84                and (self.fd is None or update.is_fd == self.fd)85                and (self.dbc_fd or not update.is_fd)86                and not update.is_remote and not update.is_error)878889def same_frame_binding(definitions: tuple[SignalDefinition, ...], facts: Sequence[Mapping]) -> SameFrameBinding | None:90    """复用同次DBC字段事实证明唯一共同绑定,不加载文件。9192    参数: 实际所需信号定义和metadata.fields。返回: 共同定位或None(交回普通路径)。93    异常: 已有元数据结构损坏;不能将损坏当不支持而吞错。94    """95    if not definitions:96        return None97    by_field = {field_from_dict(dict(fact['locator'])).key: fact for fact in facts}98    chosen = None99    for definition in definitions:100        if definition.dtype not in (D.FLOAT64, D.INT64, D.UINT64) or definition.capture_code:101            return None102        field = can_field_from_key(definition.field)103        fact = by_field.get(definition.field)104        if fact is None:105            return None106        if 'dbc_frames' in fact:107            candidates = tuple(tuple(pair) for pair in fact['dbc_frames'])108        elif field.is_extended is not None and field.is_fd is not None:109            # 显式帧型沿用现有READY结构,不要求读取器增加重复候选字段。110            # 字段事实已由同次DBC核对;仅单向FD兼容时使用已有兼容提示。111            dbc_fd = field.is_fd or 'frame_format_compatibility' in fact112            candidates = ((field.is_extended, dbc_fd),)113        else:114            return None115        if len(candidates) != 1:116            return None117        extended, fd = candidates[0]118        if field.is_extended is not None and field.is_extended != extended:119            return None120        binding = SameFrameBinding(definition.source, field.channel, field.frame_id, extended, field.is_fd, fd)121        if chosen is not None and chosen != binding:122            return None123        chosen = binding124    return chosen125126127def _same_value(left: ScalarValue, right: ScalarValue) -> bool:128    """沿用同刻事实比较。参数: 归一化标量。返回: bool。异常: 无外部输入转换。"""129    return (left.valid == right.valid and left.quality == right.quality and left.code == right.code130            and type(left.value) is type(right.value)131            and (left.value == right.value or type(left.value) is float and isnan(left.value) and isnan(right.value)))132133134class SameFrameIntegrals:135    """拥有同刻选择和表达式上下文;借用积分器,关闭时不代替父会话关闭它们。"""136137    def __init__(self, budget: ResourceBudget, definitions: tuple[SignalDefinition, ...],138                 expressions: tuple[StaticExpression, ...], calculations: tuple[RecordIntegral, ...],139                 binding: SameFrameBinding, session_id: str, before_group: Callable[[], None], *, derived: tuple = ()) -> None:140        """检查静态表达式与实定义,预付有界状态,不读取输入。141142        参数: 共享预算、已补齐TTL的定义、静态表达式tuple、对应积分器tuple、143            已证明共同定位、会话ID、积分缓冲结算前刷新已观测范围的同步回调;derived为原派生声明。144        返回: owner。异常: 声明/预算/取消;未返回时附same_frame_cleanup_owner。145        """146        if not isinstance(budget, ResourceBudget) or type(definitions) is not tuple:147            raise TypeError('同帧积分需要预算和定义tuple')148        if (type(expressions) is not tuple or type(calculations) is not tuple149                or not expressions or len(expressions) != len(calculations)150                or type(binding) is not SameFrameBinding or not callable(before_group)):151            raise TypeError('同帧积分需要对应表达式/积分器、已证明定位和范围回调')152        self._budget = budget153        self._held_cost = self._fixed_cost = self._group_cost = self._work_cost = 0154        self._closed = self._closing = self._failed = self._finished = False155        self._definitions = self._fields = self._expressions = self._calculations = None156        self._chosen = self._seen = self._before_group = None157        self._time = self._last_time = None158        self._binding, self._session = binding, session_id159        try:160            # bindings由父计划持有;这里只计校验树、当前字段索引和必要同刻槽。161            trees = tuple((expr.root, expr.text, expr.normalized_ast, expr.dependencies) for expr in expressions)162            _reserve_owned_ram(self, 131072 + 32 * static_metadata_bytes((definitions, trees, derived)), '_fixed_cost')163            by_name = {item.name: item for item in definitions}164            if len(by_name) != len(definitions) or any(type(item) is not SignalDefinition165                    or item.dtype not in (D.INT64, D.UINT64, D.FLOAT64) or item.capture_code166                    or item.valid_for_ns is None for item in definitions):167                raise ValueError('同帧积分需要唯一数值定义及已知有效期')168            fields = tuple((item, can_field_from_key(item.field).signal) for item in definitions)169            for definition in definitions:170                field = can_field_from_key(definition.field)171                if (definition.source != binding.source or field.channel != binding.channel172                        or field.frame_id != binding.frame_id173                        or field.is_extended is not None and field.is_extended != binding.extended174                        or field.is_fd != binding.fd):175                    raise ValueError('同帧实际定义与已证明定位不一致')176            for expression, calculation in zip(expressions, calculations):177                if not simple_numeric_expression(expression):178                    raise ValueError('同帧积分仅接受简单内建数值表达式')179                checked = validate_expression(expression.text, expression.bindings, derived=derived, unit=expression.root.unit)180                if checked != expression or expression.bindings.session_id != session_id:181                    raise ValueError('同帧表达式与当前声明或会话不一致')182                for name in expression.dependencies:183                    declared = expression.bindings.resolve(name)184                    actual = by_name.get(name)185                    if declared.valid_for_ns is None and actual is not None:186                        declared = replace(declared, valid_for_ns=actual.valid_for_ns)187                    if declared != actual:188                        raise ValueError('同帧表达式与实际定义不一致:' + name)189                expression.bindings.require_synchronized(expression.dependencies)190                if (calculation._step_ns is not None or calculation._conditioned191                        or calculation._dtype != expression.root.dtype192                        or calculation._input_unit != expression.root.unit):193                    raise ValueError('同帧表达式与实际时间积分器不一致')194            self._definitions, self._fields = by_name, fields195            self._expressions, self._calculations = expressions, calculations196            self._chosen, self._seen = {}, set()197            self._before_group = before_group198            # 输入原值仍由借用的消息持有;这里只构造数值标量和有限表达式工作区。199            self._workspace = 65536 + 32 * static_metadata_bytes(tuple(expr.root for expr in expressions))200        except BaseException as error:201            _cleanup_owner(self, error, 'same_frame_cleanup_owner', '同帧积分构造')202            raise203        finally:204            definitions = expressions = calculations = fields = by_name = checked = expression = calculation = trees = None205            definition = declared = actual = field = binding = before_group = derived = None206207    def _require_open(self) -> None:208        """失败后仅允许关闭。参数: 无。返回: None。异常: ResultClosedError。"""209        if self._closed or self._closing or self._failed or self._finished:210            raise ResultClosedError('同帧积分已关闭、失败或完成')211212    def consume(self, update: LegacyDecodedUpdate) -> None:213        """接收一条已投影报文,完整同刻组才推进积分。214215        参数: 当前LegacyDecodedUpdate。返回: None。异常: 身份、同刻冲突、预算、计算原错。216        无效字段覆盖本次值,不沿用前条。未选择的帧只由原读取器维护观测范围。217        """218        self._require_open()219        values = value = None220        primary = None221        try:222            if not isinstance(update, LegacyDecodedUpdate) or update.source_ref.session_id != self._session:223                raise ValueError('同帧更新不属于当前会话')224            if not self._binding.matches(update):225                return226            if update.time_ns is None or self._last_time is not None and update.time_ns < self._last_time:227                raise ValueError('同帧积分需要非递减实际时间')228            if self._time is not None and update.time_ns > self._time:229                self._flush()230            if update.sequence in self._seen:231                raise ValueError('完整组同别名原sequence重复')232            # 共同投影器对本帧解码失败会插入None;缺少键说明绑定证明失效,不能偷用旧值。233            if any(name not in update.values for _, name in self._fields):234                raise ValueError('同帧投影没有交付全部已证明字段')235            _reserve_owned_ram(self, self._workspace, '_work_cost')236            values = {}237            for definition, name in self._fields:238                value = normalize_reference(definition, update.values[name])239                if not value.valid:240                    value = _scalar_from_validated_fields(None, value.dtype, False, value.quality)241                previous = self._chosen.get(definition.name)242                if previous is not None and definition.same_time_policy == 'error' and not _same_value(previous[1], value):243                    raise ValueError(f'同信号同时间冲突:{definition.name}@{update.time_ns}ns')244                values[definition.name] = (update.sequence, value) if previous is None or update.sequence > previous[0] else previous245            # 只保一套选定值及同刻原序号;集合每新增一项先付费,不随总文件长度增长。246            _reserve_owned_ram(self, 512 + 16 * _metadata_bytes(update.sequence), '_group_cost')247            self._seen.add(update.sequence)248            self._chosen = values249            values = None250            self._last_time = self._time = update.time_ns251        except BaseException as error:252            primary, self._failed = error, True253            raise254        finally:255            values = value = update = previous = definition = None256            try:257                _release_owned_ram(self, self._work_cost, '_work_cost')258            except BaseException as cleanup:259                self._failed = True260                if primary is None:261                    raise262                primary.add_note('同帧积分临时费用清理失败:' + type(cleanup).__name__)263264    def _flush(self) -> None:265        """提交完整同刻值,复用原表达式核及积分器。参数: 无。返回: None。异常: 原计算/预算错误。"""266        if self._time is None:267            return268        context = cache = value = None269        primary = None270        try:271            # 点缓冲只在满批时使用范围计算面积;不为每条报文重建整份计划。272            # 即将结算前必须先取得已观测范围,尾部则由finish统一更新。273            if any(len(calculation._rows) >= calculation._limit for calculation in self._calculations):274                self._before_group()275            _reserve_owned_ram(self, self._workspace, '_work_cost')276            context = {name: item[1] for name, item in self._chosen.items()}277            cache = {}278            for expression, calculation in zip(self._expressions, self._calculations):279                value = _evaluate(expression.root, context, cache).value280                deadlines = (self._time + self._definitions[name].valid_for_ns281                             for name in expression.dependencies if context[name].valid)282                deadline = min((time for time in deadlines if time < (1 << 63)), default=None)283                calculation.consume(((self._time, value, deadline),))284            self._chosen.clear()285            self._seen.clear()286            self._time = None287            _release_owned_ram(self, self._group_cost, '_group_cost')288        except BaseException as error:289            primary, self._failed = error, True290            raise291        finally:292            context = cache = value = deadlines = expression = calculation = None293            try:294                _release_owned_ram(self, self._work_cost, '_work_cost')295            except BaseException as cleanup:296                self._failed = True297                if primary is None:298                    raise299                primary.add_note('同帧表达式工作区清理失败:' + type(cleanup).__name__)300301    def finish(self, observed_end: int | None) -> None:302        """在原读取器确认EOF并关闭后,补算到来源真实末端的保持尾段。303304        参数: 来源实际末端或None。返回: None。异常: 原数值、预算或取消错误。305        不替代EOF证明,不把domain.end当采集终点,不虚构固定周期。306        """307        self._require_open()308        try:309            self._before_group()310            self._flush()311            if observed_end is not None:312                for calculation in self._calculations:313                    if calculation._rows and calculation._last_time < observed_end:314                        time, value, deadline, _, _ = calculation._rows[-1]315                        deadline = observed_end if deadline is None else min(deadline, observed_end)316                        calculation._rows[-1] = (time, value, deadline, None, None)317                        calculation.consume(((observed_end, value, deadline),))318            self._finished = True319        except BaseException:320            self._failed = True321            raise322        finally:323            calculation = value = None324325    def close(self) -> None:326        """释放自有上下文及费用,借用积分器由父会话关闭。参数: 无。返回: None。异常: 释放失败可重试。"""327        if self._closed:328            return329        self._closing = True330        self._chosen = self._seen = self._definitions = self._fields = None331        self._expressions = self._calculations = self._before_group = self._binding = None332        _release_owned_ram(self, self._work_cost, '_work_cost')333        _release_owned_ram(self, self._group_cost, '_group_cost')334        _release_owned_ram(self, self._fixed_cost, '_fixed_cost')335        self._closed = True336
Chronaxis 用户手册 · 离线阅读