Chronaxis用户手册English
本页目录

chronaxis.execution.driver_expression

源码基线:8904c2f744951857659abd40aaa3535d57faf53c

1"""从已提交的同刻信号状态取得所选驱动的数值或BOOL表达式,不保存输入历史。23输入是经过来源校验、归一化和同刻政策处理的LatestSignalState,以及静态4表达式声明;每个驱动更新最多返回一个(time, value, valid_until)计算点。5按实际更新时间判断valid_for,参考及生产逐点数值核独立;真正批量数组入口不变。固定周期6不进入此组件。最新值归state持有,临时求值空间在共享预算中预付。7这是新按需执行的内部接点,不生成CompiledExpression或伪造EOF/绑定证据,8不改变公开表达式入口拒绝StaticExpression的契约。来源读取/同刻归组由调用方负责。9"""1011from dataclasses import replace12from ..core import expression_reference13from ..core.expression import StaticExpression, validate_expression14from ..core.expression_point import PointProgram15from ..core.lookup import pack_requested_lookups16from ..core.where_updates import contains_where17from ..core.enum_codes import expire_value18from ..core.declarations import DeclarationBindings19from ..core.numeric import ScalarValue20from ..core.types import SignalDType as D, QualityFlag as Q21from ..results.budget import _reserve_owned_ram, _release_owned_ram, _cleanup_owner22from ..results.errors import ResultClosedError23from ..timebase.conversion import require_int64_ns24from .history import _metadata_bytes25from .static_metadata import static_metadata_bytes26from .state import LatestSignalState27from .where_state import before_context, group_updates, group_active282930class DriverExpression:31    """借用最新状态,持有已核实的表达式树与依赖;不跨组缓存计算值。"""3233    def __init__(self, static, state, driver, *, backend='numpy', derived=(), observation_ends=None):34        """核对声明与实际状态映射,预付树和依赖引用费用。3536        参数: static为StaticExpression;state为已校验来源的共享最新状态;37            driver为所选原信号别名;None用于实际间隔,每个完整必要更新组求值。38            backend选择数值核;derived为原派生声明。39        返回: 新owner。异常: 声明不一致、非数值/BOOL表达式、预算或取消错误;40            构造清理失败提供driver_expression_cleanup_owner重试句柄。41        """42        if (type(static) is not StaticExpression or type(static.bindings) is not DeclarationBindings43                or not isinstance(state, LatestSignalState)):44            raise TypeError('驱动求值需要静态表达式及真实最新状态')45        if backend not in ('reference', 'numpy'):46            raise ValueError('驱动求值后端需要reference或numpy')47        if static.root.dtype not in (D.FLOAT64, D.INT64, D.UINT64, D.BOOL,D.ENUM):48            raise ValueError('驱动求值需要数值、BOOL或ENUM表达式')49        self._budget, self._state = state.budget, state50        self._held_cost = self._fixed_cost = self._work_cost = 051        self._closed = self._closing = self._failed = False52        self._static = self._definitions = self._point_program = None53        self._last_revision = 054        self._last_group_time = None55        self._invalidated_at = None56        self._last_value = None57        self._driver, self._backend = driver, backend58        self._observation_ends = observation_ends59        try:60            # 新校验树、派生展开、点求值索引仍预付;bindings借用state/原计划,61            # 不再为每个指标复制计入整份信号声明。32倍只覆盖本次真实构造的树和临时结构。62            _reserve_owned_ram(self, 65536 + 32 * static_metadata_bytes(63                (static.root, static.text, static.normalized_ast, static.dependencies, derived)), '_fixed_cost')64            checked = validate_expression(static.text, static.bindings, derived=derived, unit=static.root.unit)65            if checked != static:66                raise ValueError('驱动表达式与原声明不一致')67            if static.bindings.session_id != state.session_id:68                raise ValueError('驱动表达式不属于当前会话')69            names = set(static.dependencies) | ({driver} if driver is not None else set())70            for name in names:71                declared = static.bindings.resolve(name)72                actual = state.definitions.get(name)73                # 唯一允许的启动补全是待DBC的有效期;字段、转换及其他身份仍逐项相同。74                if declared.valid_for_ns is None and actual is not None:75                    declared = replace(declared, valid_for_ns=actual.valid_for_ns)76                if actual != declared:77                    raise ValueError('驱动表达式与实际信号定义不同:' + str(name))78            static.bindings.require_synchronized(tuple(sorted(names)))79            # 固定申请已覆盖完整点对及每点16字节的只读列,参考后端直接二分原点对。80            self._static = pack_requested_lookups(checked) if backend == 'numpy' else checked81            # 内建单点直接计算;NumPy宿主只在自己的列接口边界装箱。82            # 原固定预算已预付节点/常量/查表引用,准备结果随当前owner关闭。83            self._point_program = PointProgram(self._static.root) if backend == 'numpy' else None84            self._has_where = contains_where(checked.root)85            self._definitions = tuple((name, state.definitions[name]) for name in static.dependencies)86            # 费用随表达式复杂度和依赖数增长,不随输入记录数增长。87            nodes, pending = 0, [checked.root]88            self._boolean_short_circuit = False89            while pending:90                node = pending.pop()91                nodes += 192                self._boolean_short_circuit |= node.operator in ('and', 'or')93                pending.extend(node.args)94            self._workspace = 65536 + nodes * 4096 + len(self._definitions) * 409695        except BaseException as exc:96            _cleanup_owner(self, exc, 'driver_expression_cleanup_owner', '驱动表达式')97            raise98        finally:99            # 构造失败保留异常栈时,不让临时编译树在退款后继续存活。100            checked = pending = node = static = state = derived = None101102    def _require_open(self):103        """拒绝失败或关闭后的求值。参数: 无。返回: None。异常: ResultClosedError。"""104        if self._closed or self._closing or self._failed or self._state.closed:105            raise ResultClosedError('驱动表达式或借用状态已关闭/失败')106107    def _context_at(self, time_ns, *, continuous=False):108        """读取当前持有值在实际时刻的有效性。参数: time_ns整数ns。返回: context及未来到期点。异常: 模型/分配错误。"""109        context, deadlines = {}, []110        for name, definition in self._definitions:111            sample = self._state.get(name)112            end = None if self._observation_ends is None else self._observation_ends.get(definition.source)113            if sample is None:114                # bool 的 choices 是输入编码到真假值的映射,只有 enum 才携带文字域。115                # 首样本未到时保持 MISSING,不把配置映射误当成观测值或枚举标签。116                value = ScalarValue(None, definition.dtype, False, Q.MISSING,117                                    tuple(dict.fromkeys(label for _, label in definition.choices))118                                    if definition.dtype is D.ENUM else ())119            elif end is not None and (time_ns >= end if continuous else time_ns > end):120                value = expire_value(sample.value, Q.MISSING)121            elif time_ns - sample.time_ns >= definition.valid_for_ns:122                value = expire_value(sample.value, Q.STALE)123            else:124                value = sample.value125                expires = sample.time_ns + definition.valid_for_ns126                available = value.valid or value.code is not None and value.code.valid127                if available and expires < (1 << 63):128                    deadlines.append(expires)129                if available and end is not None:130                    deadlines.append(end)131            context[name] = value132        return context, deadlines133134    def _evaluate_values(self, context, unobserved=None):135        """使用独立逐点实现。参数: typed值及可选规则缺项集合。返回: ScalarValue。异常: 原计算/分配错误。"""136        value = names = None137        cache = {}138        try:139            if self._backend == 'reference':140                value = expression_reference._evaluate(self._static.root, context, cache).value141                names = (node.name for node in cache if node.operator == 'name')142            else:143                value = self._point_program.evaluate(context, cache).value144                names = self._point_program.used_names(cache)145            # 缺项和部分质量只来自实际访问的分支,不能扫描静态依赖全集。146            partial = value.partial147            for name in names:148                if unobserved is not None and self._state.get(name) is None:149                    unobserved.add(name)150                partial |= context[name].partial151            if value.valid and not value.partial and partial:152                value = replace(value,partial=True)153            return value154        finally:155            context = value = cache = names = None156157    def actual_update_at(self, time_ns):158        """检查真实数值更新。参数: 完整组纳秒。返回: bool。异常: 状态/预算/后端错误。"""159        return self._point_evidence(time_ns, 'updated')160161    def active_end_at(self, time_ns):162        """检查活动来源末端。参数: 查询纳秒。返回: bool。异常: 状态/预算/后端错误。"""163        return self._point_evidence(time_ns, 'source_end')164165    def actual_unknown_at(self, time_ns, point):166        """区分真实中断与并列 TTL。参数: 完整组纳秒、已知无效点值。返回: bool。异常: 状态/求值错误。"""167        return self._point_evidence(time_ns, 'invalid', point)168169    def _point_evidence(self, time_ns, kind, point=None):170        """检查点上的活动更新、未知原因或活动来源末端,不推进驱动。171172        参数: 查询纳秒、证据种类,invalid 另传当前实际点。返回: bool。173        异常: 状态、预算及后端求值错误;失败后只允许关闭。174        """175        self._require_open()176        source_end = kind == 'source_end'177        if not source_end and self._state.last_time_ns != time_ns:178            return False179        names = self._static.dependencies180        ends = self._observation_ends or {}181        if source_end and not any(ends.get(d.source) == time_ns for _, d in self._definitions):182            return False183        if not self._has_where:184            return source_end or any((sample := self._state.get(name)) is not None and sample.time_ns == time_ns for name in names)185        primary = None186        try:187            _reserve_owned_ram(self, self._workspace, '_work_cost')188            current, _ = self._context_at(time_ns)189            if source_end:190                return group_active(self._static.root, current,191                    {name: ends.get(d.source) == time_ns for name, d in self._definitions}, self._backend,192                    point_program=self._point_program)193            flags = {name: (sample := self._state.get(name)) is not None and sample.time_ns == time_ns for name in names}194            if kind == 'invalid':195                # 判定实际中断使用“同一 t 排除真实更新”而非左侧保持;否则 TTL 到期196                # 与旁列真实有效更新恰好同刻,会被误判为样本导致的中断。197                invalid = {n: real and not self._state.get(n).value.valid for n, real in flags.items()}198                if group_active(self._static.root, current, invalid, self._backend, point_program=self._point_program):199                    return True200                prior = before_context(self._state, names, time_ns, ends=self._observation_ends)201                old = self._evaluate_values(prior)202                return old.valid or bool(point.quality & ~old.quality)203            previous = before_context(self._state, names, time_ns, ends=self._observation_ends, left_limit=True)204            return group_updates(self._static.root, current, previous, flags, self._backend,205                                 point_program=self._point_program)206        except BaseException as error:207            primary, self._failed = error, True208            raise209        finally:210            try:211                _release_owned_ram(self, self._work_cost, '_work_cost')212            except BaseException as cleanup:213                self._failed = True214                if primary is None:215                    raise216                primary.add_note('活动依赖检查费用释放失败:' + type(cleanup).__name__)217218    def _output_deadline(self, time, output, deadlines):219        """选取首次可见失效。参数: 当前纳秒、输出、TTL/来源候选末端。220        返回: 整数截止或 None。异常: 候选边界上的求值错误。221        """222        if not self._has_where and not (output.dtype is D.BOOL and self._boolean_short_circuit):223            return min(deadlines, default=None)224        if output.valid:225            for candidate in sorted(set(deadlines)):226                if candidate >= time:227                    future_context, _ = self._context_at(candidate, continuous=True)228                    future = self._evaluate_values(future_context)229                    if not future.valid or future.value != output.value:230                        return candidate231        return None232233    def evaluate(self):234        """仅在最新完整组包含驱动更新时求值一次;其他组仅由state保存最新值。235236        参数: 无,读取借用state的当前完整组。返回: None或(time_ns, ScalarValue,237            实际有效截止);None截止表示所有依赖在int64时间域内没有未来过期点。238        异常: 跳过未消费组、预算、计算结构或取消错误;失败后不再交付结果。239        约束: 必须在每次完整组提交后调用,不能用最新状态补算丢掉的历史组。240        """241        self._require_open()242        context = result = output = sample = None243        primary = None244        try:245            revision = self._state.revision246            if revision == self._last_revision:247                return None248            if revision != self._last_revision + 1:249                raise ValueError('驱动求值不能跳过已提交的完整组')250            time = self._state.last_time_ns251            previous_time, self._last_group_time = self._last_group_time, time252            self._invalidated_at = None253            # 非驱动依赖的无效更新也立即停止旧值保持,不能等下一条驱动才处理。254            # 只传必要的截止,不制造一个会参与N−1计数的驱动点。255            for name, _ in self._definitions:256                changed = self._state.get(name)257                definition = self._state.definitions[name]258                ended = None if self._observation_ends is None else self._observation_ends.get(definition.source)259                if ended is not None and ended <= time and (previous_time is None or ended > previous_time):260                    self._invalidated_at = ended if self._invalidated_at is None else min(ended,self._invalidated_at)261                if changed is not None and changed.time_ns == time and not changed.value.valid:262                    self._invalidated_at = time if self._invalidated_at is None else min(time,self._invalidated_at)263            driver_sample = self._state.get(self._driver) if self._driver is not None else None264            is_driver = self._driver is None or driver_sample is not None and driver_sample.time_ns == time265            # 已知FALSE and UNKNOWN等不能因为无关分支失效而被整体改成UNKNOWN。266            check_condition = (self._static.root.dtype is D.BOOL and self._boolean_short_circuit267                               and self._invalidated_at is not None)268            if not is_driver and not check_condition and not self._has_where:269                self._last_revision = revision270                return None271            _reserve_owned_ram(self, self._workspace, '_work_cost')272            context, deadlines = self._context_at(time, continuous=not is_driver)273            output = self._evaluate_values(context)274            if self._has_where:275                # EOF 可能随下一条真实记录才确定。按该时间上仍生效的旧组求值,276                # 避免把新分支的失效倒填到旧区间,或让未选来源提前截止。277                self._invalidated_at = None278                ends = self._observation_ends or {}279                cutoffs = {ends[d.source] for _, d in self._definitions if d.source in ends and280                           previous_time is not None and previous_time <= ends[d.source] <= time}281                for cutoff in sorted(cutoffs):282                    point = before_context(self._state, self._static.dependencies, cutoff, ends=ends)283                    if not group_active(self._static.root, point,284                            {n: ends.get(d.source) == cutoff for n, d in self._definitions}, self._backend,285                            point_program=self._point_program):286                        continue287                    prior = before_context(self._state, self._static.dependencies, cutoff, ends=ends, continuous=True)288                    if not self._evaluate_values(prior).valid:289                        self._invalidated_at = cutoff290                        break291                if self._invalidated_at is None and not output.valid:292                    self._invalidated_at = time293            if not is_driver:294                if (self._last_value is not None and output.valid and self._last_value.valid295                        and output.value == self._last_value.value):296                    self._invalidated_at = None297                self._last_revision = revision298                return None299            deadline = self._output_deadline(time, output, deadlines)300            if output.dtype is D.BOOL:301                if output.valid and self._last_value is not None and self._last_value.valid and output.value == self._last_value.value:302                    self._invalidated_at = None303            self._last_value = output if output.dtype is D.BOOL else None304            self._last_revision = revision305            return time, output, deadline306        except BaseException as exc:307            primary = exc308            self._failed = True309            raise310        finally:311            context = columns = result = output = sample = driver_sample = value = changed = None312            deadlines = future_context = future = None313            try:314                _release_owned_ram(self, self._work_cost, '_work_cost')315            except BaseException as cleanup:316                self._failed = True317                if primary is None:318                    raise319                primary.add_note('驱动表达式清理失败:' + type(cleanup).__name__)320321    @property322    def invalidated_at(self):323        """当前已消费组使数值依赖失效的实际时刻。参数: 无。返回: int或None。异常: 关闭/失败。"""324        self._require_open()325        return self._invalidated_at326327    def evaluate_at(self, time_ns, *, continuous=True, unobserved=None):328        """在当前最新状态上查询实际边界,供规则的TTL和等待调度使用。329330        参数: time_ns不得早于已提交组;continuous=False查询实际点,True查询右侧保持;331            unobserved可收集当前活动路径从未观测的别名,不记录未来推算中的缺项。332        返回: (ScalarValue, 下一次可能改变已知结果的实际截止或None)。不提交组、333            不改变驱动游标,不制造报文,也不保存查询轨迹。334        异常: 时间/状态/预算/数值错误;失败后仅可关闭,费用释放失败仍保原责任。335        """336        self._require_open()337        context = output = future_context = future = deadlines = None338        primary = None339        try:340            time = require_int64_ns(time_ns,'expression.query_time')341            if type(continuous) is not bool or self._state.last_time_ns is not None and time < self._state.last_time_ns:342                raise ValueError('边界查询需要bool视图且不能回读已覆盖的旧状态')343            _reserve_owned_ram(self,self._workspace,'_work_cost')344            context,deadlines = self._context_at(time,continuous=continuous)345            output = self._evaluate_values(context, unobserved)346            deadline = None347            if output.valid:348                for candidate in sorted(set(value for value in deadlines if value > time)):349                    if not self._boolean_short_circuit and not self._has_where:350                        deadline = candidate351                        break352                    future_context,_ = self._context_at(candidate,continuous=True)353                    future = self._evaluate_values(future_context)354                    if not future.valid or future.value != output.value:355                        deadline = candidate356                        break357            return output,deadline358        except BaseException as error:359            primary,self._failed = error,True360            raise361        finally:362            context = output = future_context = future = deadlines = None363            try:364                _release_owned_ram(self,self._work_cost,'_work_cost')365            except BaseException as cleanup:366                self._failed=True367                if primary is None:368                    raise369                primary.add_note('规则边界求值费用释放失败:'+type(cleanup).__name__)370371    def close(self):372        """释放自有树/费用,借用state保持原所有者负责。参数: 无。返回: None。异常: 释放失败可重试。"""373        if self._closed:374            return375        self._closing = True376        self._static = self._definitions = self._state = self._last_value = self._observation_ends = self._point_program = None377        _release_owned_ram(self, self._work_cost, '_work_cost')378        _release_owned_ram(self, self._fixed_cost, '_fixed_cost')379        self._closed = True380
Chronaxis 用户手册 · 离线阅读