Chronaxis用户手册English
本页目录

chronaxis.execution.transform_stream

源码基线:8904c2f744951857659abd40aaa3535d57faf53c

1"""在原始记录流及下游规则之间插入按需的时序变换。23每一层只持有自己的最新输入、滚动窗口,以及固定周期等待后帧确认时尚未4交付的有界原组。待同一时刻的原更新与变换值均齐全,再原子交给下层。5无全会话网格、输入重扫或对照时间列;预算、取消和真实EOF沿主执行器。6"""7from collections import deque8from dataclasses import replace9from fractions import Fraction10from ..core.numeric import ScalarValue11from ..core.types import QualityFlag as Q, SignalDType as D12from ..core.models import SourceRef13from ..results.budget import _reserve_owned_ram, _release_owned_ram, _cleanup_owner14from .driver_expression import DriverExpression15from .groups import DecodedTimeGroup16from .history_records import estimate_record_bytes17from .record_sample import RecordSample18from .state import LatestSignalState19from .transform_window import TransformSpan, TransformWindow, first_grid202122class TransformStage:23    """拥有一层的状态与前瞻,向唯一后继按实际时间递增地交付组。"""2425    def __init__(self, plan, item, defs, choice, period, ends, cursors, budget, emit):26        """参数: 计划/变换、有效定义、原时钟/周期、EOF/游标、预算及后继。返回: owner。异常: 预算/绑定错误。"""27        self.item,self.backend,self._budget,self.emit=item,plan.execution.backend,budget,emit28        self._held_cost=self._fixed_cost=self._queue_cost=self._work_cost=029        self.state=self.driver=self.history=None30        self.host = None31        self.host_finished = False32        self.waiting=deque()33        self.epoch=self.next_grid=self.last=None34        self.last_point=self.last_right=self.last_deadline=None35        self.last_update=False36        self.adjacent=self.clock_start=None37        self.adjacent_weight=Fraction(0)38        self.adjacent_known=True39        self.sequence=040        self.ends,self.cursors=ends,cursors41        self.period,self.clock=period,choice.driver42        self.sources={plan.bindings.resolve(n).source for n in item.physical}43        if not self.sources:44            self.sources=set(cursors)45        self.domain=plan.bindings.domain46        self.ref=SourceRef(plan.dataset.session_id,item.output.source,item.output.field)47        self.names=set(item.expression.dependencies)|({self.clock} if self.clock else set())48        self.closed=self.failed=False49        try:50            _reserve_owned_ram(self,65536+1024*len(self.names),'_fixed_cost')51            self.state=LatestSignalState(budget,tuple(d for d in defs if d.name in self.names),plan.dataset.session_id)52            # 时钟决定区间长度,数值始终按输入表达式的真实更新独立推进。53            self.driver=DriverExpression(item.expression,self.state,None,backend=self.backend,54                derived=tuple(d.definition for d in plan.derived),observation_ends=ends)55            self.history=TransformWindow(budget,item.window_ns or item.max_gap_ns,period)56            if item.host_spec is not None:57                from ..extension_runtime import HostedTransform58                self.host = HostedTransform(item.host_spec,self.backend,budget)59        except BaseException as error:60            if self.host is None and getattr(error,'extension_cleanup_owner',None) is not None:61                self.host = error.extension_cleanup_owner62            if self.driver is None:63                self.driver=getattr(error,'driver_expression_cleanup_owner',None)64            if self.state is None:65                self.state=getattr(error,'state_cleanup_owner',None)66            _cleanup_owner(self,error,'transform_cleanup_owner','时序变换')67            raise6869    def _ensure_active(self):70        """拒绝关闭或失败后的继续推进,避免重发未完成网格。参数: 无。返回: None。异常: 生命周期错误。"""71        from ..results.errors import ResultClosedError72        if self.closed or self.failed:73            raise ResultClosedError('时序变换已关闭或失败')7475    def bounds(self):76        """取得实际可观测起点和已知终点。参数: 无。返回: 起点/可选终点。异常: 游标错误。"""77        start=min((self.cursors[n].observed_start for n in self.sources78                   if self.cursors[n].observed_start is not None),default=None)79        complete=all(self.cursors[n]._finished for n in self.sources)80        end=max((self.cursors[n].observed_end for n in self.sources if self.cursors[n].observed_end is not None),81                default=None) if complete else None82        if self.domain is not None:83            if start is not None:84                start=max(start,self.domain.start_ns)85            end=self.domain.end_ns if end is None else min(self.domain.end_ns,end)86        if complete and end is not None:87            self.ends[self.item.output.source]=end88        return start,end8990    def _invalid(self,quality):91        """构造明确未知的输出。参数: 原因。返回: ScalarValue。异常: 非法质量位。"""92        return ScalarValue(None,self.item.output.dtype,False,quality or Q.MISSING)9394    def _sample(self,t,value):95        """构造离散输出。参数: 网格/实际样本坐标和值。返回: RecordSample。异常: 模型错误。"""96        sample=RecordSample(self.item.name,t,self.sequence,value,self.item.output.unit,self.ref)97        self.sequence+=198        return sample99100    def _at_grid(self,t,point,update):101        """从原区间读取[t-W,t)及变化率端点。参数: 网格时刻、可选同刻点和更新。返回: ScalarValue。异常: 预算/数值错误。"""102        values = weights = fit = span = left_value = right_value = value = None103        try:104            op=self.item.operation105            values,weights,fit=[],[],[]106            valid=Fraction(0)107            width=Fraction(0)108            quality=Q.NONE109            left_value=right_value=None110            partial=False111            for span,left,right,ratio in self.history.slices(t):112                w=(right-left)*ratio113                if right_value is None:114                    right_value=span.value if span.deadline is None or t<span.deadline else self._invalid(Q.STALE)115                left_value=span.value if span.deadline is None or left<span.deadline else self._invalid(Q.STALE)116                good=max(0,min(right,span.deadline)-left)*ratio if span.deadline is not None else w117                if span.value.valid and good:118                    valid+=good119                    partial |= span.value.partial120                    weights.append(good)121                    values.append(span.value.value)122                if not span.value.valid:123                    quality|=span.value.quality124                elif good<w:125                    quality|=Q.STALE126                width+=w127                if op=='sample_fit_slope' and span.update and span.start>=left and span.point.valid:128                    fit.append((-width,span.point))129                    partial |= span.point.partial130            # 斜率会使用右端新读数;即使此前整个窗口都有完整实测值,新读数也可能是估值。131            if point is not None and point.partial and (op=='window_rate' or op=='sample_fit_slope' and update):132                partial=True133                quality|=Q.ESTIMATED134            window=self.item.window_ns135            if width<window:136                quality|=Q.MISSING137            partial |= valid<window138            if (not valid or partial and self.item.partial_policy=='strict'139                    or valid < Fraction(str(self.item.minimum_coverage))*window):140                return self._invalid(quality)141            if self.host is not None:142                from ..extension_runtime import advance_grid143                value = advance_grid(self.host,t,window,values,weights,self.item.conversion)144            elif op.startswith('rolling_'):145                from ..core.rolling_values import rolling_value146                value=rolling_value(op,self.backend,values,weights,self.item.conversion,self.item.expression.root.dtype)147            else:148                from ..core.rate_transforms import rate_difference, rate_fit149                right_value=point if point is not None else right_value150                if op=='window_rate':151                    if width!=window or left_value is None or right_value is None:152                        return self._invalid(Q.MISSING)153                    value=rate_difference(self.backend,left_value,right_value,window)154                else:155                    fit.reverse()156                    if update and point is not None and point.valid:157                        fit.append((0,point))158                    value=rate_fit(self.backend,[p for p,_ in fit],[s for _,s in fit])159                value=self._convert_rate(value)160            return replace(value,partial=True) if value.valid and partial else value161        finally:162            # 网格费用由调用层释放,本层错误回溯不再持有窗口临时列表或最后一段。163            # 只清自身局部引用;历史与用户回调仍保有的数据不会被修改。164            values = weights = fit = span = left_value = right_value = point = value = None165166    def _convert_rate(self,value):167        """换算变化率目标单位。参数: 自然单位结果。返回: 新标量。异常: 转换错误。"""168        if self.item.conversion.factor==1:169            return value170        if self.backend=='reference':171            from ..core.affine import affine_reference172            return affine_reference(value,self.item.conversion.factor,0,D.FLOAT64)173        from ..core.affine_numpy import affine_numpy174        from .where_state import scalar_columns175        from .calculation_values import scalar_from_column176        return scalar_from_column(affine_numpy(scalar_columns({'v':value})['v'],self.item.conversion.factor,0,D.FLOAT64),0)177178    def _deliver(self,group,t,value=None):179        """同刻真实更新与离散输出一次交付。参数: 可选原组、实际时刻、可选结果。返回: 无。异常: 下游错误。"""180        if value is None:181            if group is not None:182                self.emit(group)183        else:184            samples=() if group is None else group.samples185            self.emit(DecodedTimeGroup(t,(*samples,self._sample(t,value))))186187    def _drain(self,group,point=None,update=False,*,through=None,unresolved=False):188        """按网格和原组时间归并。参数: 当前组/点、真实更新或EOF截止。返回: 无。异常: 下游/预算错误。"""189        self._ensure_active()190        t=group.time_ns if group is not None else through191        start,end=self.bounds()192        if self.next_grid is None and start is not None:193            self.next_grid=first_grid(start,self.item.origin_ns,self.item.step_ns)194        limit=t if end is None else min(t,end)195        while True:196            clock=group.time_ns if group is not None else None197            tick=self.next_grid if self.next_grid is not None and self.next_grid<=limit else None198            if clock is None and tick is None:199                break200            when=clock if tick is None else tick if clock is None else min(clock,tick)201            value=None202            if tick==when:203                failure = None204                try:205                    # 申请也在保护域内,扣账后取消仍有本阶段的实际费用槽。206                    _reserve_owned_ram(self,8192+1024*len(self.history.spans),'_work_cost')207                    value=self._invalid(Q.MISSING) if unresolved else self._at_grid(when,point if when==t else None,208                                                                                 update and when==t)209                except BaseException as error:210                    failure = error211                    self.failed = True212                    error.transform_cleanup_owner = self213                    raise214                finally:215                    try:216                        _release_owned_ram(self,self._work_cost,'_work_cost')217                    except BaseException as cleanup:218                        self.failed = True219                        original = cleanup if failure is None else failure220                        original.transform_cleanup_owner = self221                        if failure is None:222                            raise223                        failure.add_note('时序变换网格工作区释放失败: ' + type(cleanup).__name__)224                self.next_grid+=self.item.step_ns225            self._deliver(group if clock==when else None,when,value)226            if clock==when:227                group=None228229    def _append_span(self,t,pair_width=None):230        """闭合上一条原始保持。参数: 后继实际时刻和所属驱动对跨度。返回: 无。异常: 预算/顺序错误。"""231        if self.last is not None and self.last<t:232            self.history.append(TransformSpan(self.last,t,self.last_right,self.last_deadline,self.last_update,233                                              self.last_point,pair_width))234235    def push(self,group):236        """消费上游完整组。参数: 原子组。返回: 无;向后继交付。异常: 原输入/预算/求值错误。"""237        self._ensure_active()238        t=group.time_ns239        if self.period is None:240            self._process(group,None)241            return242        drive=any(s.name==self.clock for s in group.samples)243        if self.clock_start is None:244            if drive:245                self.clock_start=t246            self._process(group,None,unresolved=True)247            return248        if drive:249            width=t-self.clock_start250            self._flush_waiting(width)251            self._process(group,width)252            self.clock_start=t253        elif self.ends.get(self.state.definitions[self.clock].source,t)>=t:254            # 等原驱动右端确定后,以原时间顺序回放这几组;每个真实值只求值一次。255            cost=1024+sum(estimate_record_bytes(s) for s in group.samples)256            _reserve_owned_ram(self,cost,'_queue_cost')257            self.waiting.append((group,cost))258        else:259            self._flush_waiting(None)260            self._process(group,None,unresolved=True)261262    def _flush_waiting(self,width):263        """以已确认的原对处理所有待定组。参数: 驱动实际跨度或EOF的None。返回: 无。异常: 交付/退款错误。"""264        while self.waiting:265            group,cost=self.waiting[0]266            self._process(group,width,unresolved=width is None)267            self.waiting.popleft()268            _release_owned_ram(self,cost,'_queue_cost')269270    def _process(self,group,width,*,unresolved=False):271        """按已知计量推进原更新和网格。参数: 完整组、原驱动跨度、计量未知标志。返回: 无。异常: 原错。"""272        t=group.time_ns273        start,end=self.bounds()274        if start is None or t<start:275            self.emit(group)276            return277        if self.item.operation!='adjacent_rate' and self.last is None and self.period is None and start<t:278            self.last=start279            self.last_right,self.last_deadline=self.driver.evaluate_at(start)280            self.last_point=self.last_right281        # 后帧可能刚揭示来源EOF;在覆盖旧状态之前,按真实末端重新确定左侧有效截止。282        if self.last is not None:283            self.last_right,self.last_deadline=self.driver.evaluate_at(self.last)284        selected=tuple(s for s in group.samples if s.name in self.names)285        if selected:286            self.state.apply(DecodedTimeGroup(t,selected))287        updated=self.driver.actual_update_at(t)288        point,_=self.driver.evaluate_at(t,continuous=False)289        if self.item.operation=='adjacent_rate':290            output=None291            if self.last is not None:292                elapsed=t-self.last293                if not unresolved:294                    self.adjacent_weight+=Fraction(elapsed*self.period,width) if width else elapsed295                self.adjacent_known &= (not unresolved and self.last_right.valid and296                                        (self.last_deadline is None or self.last_deadline>=t))297            if updated and (end is None or t<=end):298                from ..core.rate_transforms import rate_difference299                ok=(self.adjacent is not None and t-self.adjacent[0]<=self.item.max_gap_ns300                    and self.adjacent_known and self.adjacent_weight>0)301                output=(self._convert_rate(rate_difference(self.backend,self.adjacent[1],point,self.adjacent_weight))302                        if ok else self._invalid(point.quality or Q.MISSING))303                if output.valid and (point.partial or self.adjacent[1].partial):304                    output=replace(output,partial=True)305                self.adjacent,self.adjacent_known,self.adjacent_weight=(t,point),point.valid,Fraction(0)306            self.last=t307            self._deliver(group,t,output)308            return309        if not unresolved:310            self._append_span(t,width)311        self._drain(group,point,updated,unresolved=unresolved)312        self.last,self.last_point,self.last_update=t,point,updated313        self.last_right,self.last_deadline=self.driver.evaluate_at(t)314        self.history.trim()315316    def finish(self):317        """完成真正EOF后标记宿主状态正常结束。参数: 无。返回: None。异常: 最后网格失败仍通知取消。"""318        self._ensure_active()319        self._finish_grid()320        self.host_finished = True321322    def _finish_grid(self):323        """按真实来源EOF补最后可定网格,缺少后继原帧不外推周期。参数: 无。返回: 无。异常: 交付错误。"""324        start,end=self.bounds()325        self._flush_waiting(None)326        if self.item.operation=='adjacent_rate' or start is None or end is None:327            return328        if self.period is None:329            if self.last is None:330                self.last=start331                self.last_right,self.last_deadline=self.driver.evaluate_at(start)332                self.last_point=self.last_right333            self.last_right,self.last_deadline=self.driver.evaluate_at(self.last)334            self._append_span(end)335        self._drain(None,through=end,unresolved=self.period is not None)336337    def close(self):338        """清理等待组、真实区间和最新值。参数: 无。返回: 无。异常: 退款失败可重试。"""339        if self.closed:340            return341        for owner in (self.driver,self.state,self.history):342            if owner is not None:343                owner.close()344        if self.host is not None:345            self.host.close(cancelled=not self.host_finished)346            self.host = None347        self.waiting.clear()348        self.emit=self.last_point=self.last_right=None349        for field in ('_work_cost','_queue_cost','_fixed_cost'):350            _release_owned_ram(self,getattr(self,field),field)351        self.closed=True352
Chronaxis 用户手册 · 离线阅读