Chronaxis用户手册English
本页目录

chronaxis.results.signal_exports

源码基线:8904c2f744951857659abd40aaa3535d57faf53c

1"""将指定信号的已解码历史写为原始采样及可选连续量网格CSV长表。23输入为同次读取生成的HistorySample/HistoryProgress流及显式导出范围,不打开日志4或DBC;输出original.csv及清单,启用线性时另写resampled.csv。异步信号分别5一行,状态/计数不补点;显式连续量交给独立有界网格owner。6值无效时留空并保质量原因和完整来源引用;原无效值以既有安全编码保存。7逐行缓冲和编码先付预算,输入正常/失败/取消均关闭,整个目录最终独占发布。8"""910import csv11from dataclasses import dataclass12from io import StringIO13import json1415from ..core.models import Interval16from ..core.validation import require_identifier, require_text17from ..execution.record_sample import RecordSample18from ..execution.history_records import HistorySample, HistoryProgress, estimate_record_bytes19from ..execution.history import _metadata_bytes20from .budget import _reserve_owned_ram, _release_owned_ram, _cleanup_owner21from .codec import _pack22from .export_files import ExportFiles23from .signal_grid import SignalGridSpec, SignalGrid242526@dataclass(frozen=True)27class SignalCSVExportSpec:28    """指定待导出的已绑定别名;字段含义和单位沿用原绑定,不由文件名推断。"""2930    signals: tuple[str,...]  # 唯一非空别名,最多4096,保持配置顺序。31    resampling: SignalGridSpec | None = None  # 默认原始采样,显式连续量才进入线性网格。3233    def __post_init__(self):34        """只核静态选择。参数: 无。返回: None。异常: 类型/数量/重复或非法别名。35        """36        if type(self.signals) is not tuple or not 1 <= len(self.signals) <= 4096:37            raise ValueError('信号CSV需要1到4096个显式别名')38        for name in self.signals:39            require_identifier(name,'signal_csv_export.signal')40        if len(set(self.signals)) != len(self.signals):41            raise ValueError('信号CSV别名重复')42        if self.resampling is not None and (type(self.resampling) is not SignalGridSpec43                or any(name not in self.signals for name in self.resampling.continuous)):44            raise ValueError('重采样必须是所选信号的明确子集')454647_COLUMNS = ('time_ns','source','signal','field','value','unit','valid','quality','status',48            'sequence','source_ref_json','original_value_json')495051def _csv_bytes(values):52    """编码一个有界CSV行。参数: values有限标量列表。返回: UTF8 bytes。异常: CSV/Unicode/内存错。53    """54    # newline交由CSV处理,引号、逗号和换行按标准转义,不拼接未经转义的来源文字。55    stream = StringIO(newline='')56    csv.writer(stream,lineterminator='\n').writerow(values)57    return stream.getvalue().encode('utf-8')585960def _sample_row(sample, compact=False):61    """提取原样本可读列及完整证据。参数: sample已校验HistorySample,compact关闭原文/来源列。返回: 行tuple。异常: 安全编码错。62    """63    value = sample.value64    if compact:65        return (sample.time_ns,sample.name,value.value if value.valid else '',sample.unit,int(value.quality))66    return (sample.time_ns,sample.source_ref.source_name,sample.name,sample.source_ref.field,67            value.value if value.valid else '',sample.unit,value.valid,int(value.quality),68            'observed' if value.valid else 'invalid',sample.sequence,69            json.dumps(_pack(sample.source_ref),ensure_ascii=False,allow_nan=False,separators=(',',':')),70            json.dumps(_pack(sample.original_value),ensure_ascii=False,allow_nan=False,separators=(',',':'))71                if sample.original_present else '')727374def _reference_json(sample):75    """编码实际端点引用。参数: sample或None。返回: JSON文本或空字符串。异常: 安全编码错。76    """77    return '' if sample is None else json.dumps(_pack(sample.source_ref),ensure_ascii=False,78                                                allow_nan=False,separators=(',',':'))798081def _write_grid(owner, rows, counts, compact=False):82    """有界编码网格行及两端引用。参数: owner/rows/固定计数字典及compact引用开关。返回: None。异常: 编码/预算/I/O/取消错。83    """84    owner.select_file('resampled.csv')85    for row in rows:86        _reserve_owned_ram(owner,8192+_metadata_bytes(row)*16,'_row_cost')87        try:88            # 左右时间和引用均来自实际采样,目标时间不伪装成原行记录。89            left,right=row.left,row.right90            values=(row.time_ns,row.name,'' if row.value is None else row.value,row.unit,91                row.status,row.reason,row.quality)92            if not compact:93                values+=('' if left is None else left.time_ns,'' if right is None else right.time_ns,94                    _reference_json(left),_reference_json(right))95            owner.append(_csv_bytes(values))96            counts[row.status]+=197        finally:98            _release_owned_ram(owner,owner._row_cost,'_row_cost')99100101class SignalCSVWriter(ExportFiles):102    """复用原写文件和线性网格,支持逐样本推进而不强制暂存整份输入。"""103104    def __init__(self,spec,interval,destination,budget,*,device_label,check_cancel=None,105                 definitions=(),backend='numpy',compact=False,integrity=True,timestamped=False):106        """预付输出状态并建立所需表。参数: 导出规格/范围/目标/预算与后端。返回: owner。异常: 原构造错。"""107        if type(compact) is not bool:108            raise TypeError('compact必须为bool')109        if type(spec) is not SignalCSVExportSpec or type(interval) is not Interval:110            raise TypeError('信号导出需要选择及明确Interval')111        require_text(device_label, 'device_label')112        if (backend not in ('reference','numpy')113                or check_cancel is not None and not callable(check_cancel)):114            raise ValueError('导出需要有效后端及可选取消回调')115        super().__init__(destination,budget,integrity=integrity,timestamped=timestamped)116        self.spec,self.interval,self.definitions=spec,interval,definitions117        self.device_label,self.backend,self.compact=device_label,backend,compact118        self._check_cancel=check_cancel119        self._count=self._invalid=0120        self._grid_counts={'observed':0,'estimated':0,'missing':0}121        self._selected=None122        try:123            _reserve_owned_ram(self,4096+sum(len(n.encode('utf-8'))*16+256 for n in spec.signals),'_fixed_cost')124            self._selected=set(spec.signals)125            if spec.resampling is not None:126                try:127                    self.worker=SignalGrid(spec.resampling,definitions,interval,budget,128                        backend=backend,check_cancel=check_cancel)129                except BaseException as error:130                    self.worker=getattr(error,'signal_grid_cleanup_owner',None);raise131                self.open_file('resampled.csv')132                self.append(_csv_bytes(('time_ns','signal','value','unit','status','reason','quality')+133                    (() if compact else ('left_time_ns','right_time_ns','left_ref_json','right_ref_json'))))134            if not compact or spec.resampling is None or self._selected-set(spec.resampling.continuous):135                self.select_file('original.csv')136                self.append(_csv_bytes(('time_ns','signal','value','unit','quality') if compact else _COLUMNS))137        except BaseException as error:138            self._failed = True139            _cleanup_owner(self,error,'export_cleanup_owner','信号CSV构造');raise140141    def push(self,sample):142        """写一个已验证样本或推进原网格。参数: HistorySample/Progress。返回: None。异常: 原输入/写入错。"""143        if self._closed:144            raise ValueError('信号导出已关闭')145        if self._check_cancel is not None: self._check_cancel()146        if type(sample) is HistoryProgress: return147        if type(sample) not in (HistorySample,RecordSample):148            raise TypeError('信号导出只接受已解码历史记录')149        if sample.name not in self._selected or not self.interval.start_ns<=sample.time_ns<self.interval.end_ns:150            return151        if self.worker is not None and sample.name in self.spec.resampling.continuous:152            _write_grid(self,self.worker.push(sample),self._grid_counts,self.compact)153            return154        _reserve_owned_ram(self,estimate_record_bytes(sample)*2,'_row_cost')155        try:156            self.select_file('original.csv')157            self.append(_csv_bytes(_sample_row(sample,self.compact)))158        finally:159            _release_owned_ram(self,self._row_cost,'_row_cost')160        self._count+=1;self._invalid+=not sample.value.valid161162    def observed_end(self,end_ns):163        """在线调用方在末组交付前收紧EOF;不能用它改已经写出的记录。164165        参数: 实际末端,须在初始范围中。返回: None。异常: 越界。166        """167        if not self.interval.start_ns<=end_ns<=self.interval.end_ns:168            raise ValueError('导出末端超出声明范围')169        self.interval=Interval(self.interval.start_ns,end_ns)170        if self.worker is not None: self.worker.interval=self.interval171172    def complete(self):173        """结束网格、写一次必要清单并原子发布。参数: 无。返回: 输出Path。异常: 原完成错。"""174        interval,spec=self.interval,self.spec175        metadata={'export_type':'signal_csv','device_label':self.device_label,'signals':spec.signals,176            'range_ns':(interval.start_ns,interval.end_ns),'resampling':False,177            'rows':self._count,'observed':self._count-self._invalid,'invalid':self._invalid}178        if self.worker is not None:179            _write_grid(self,self.worker.finish(),self._grid_counts,self.compact)180            grid=spec.resampling181            metadata.update(resampling={'mode':'linear','continuous':grid.continuous,182                'period_ns':grid.period_ns,'origin_ns':grid.origin_ns,'max_gap_ns':grid.max_gap_ns,183                'causal':False,'numeric':'float64_estimate','ordering':'per_signal_monotonic'},184                resampled_counts=self._grid_counts,backend=self.backend,185                bindings=tuple({'name':d.name,'source':d.source,'field':d.field,'unit':d.unit,186                    'dtype':d.dtype.value,'same_time_policy':d.same_time_policy}187                    for d in self.definitions if d.name in grid.continuous))188        if self.compact: metadata['row_format']='compact-v2'189        return self.finish(metadata)190191192def export_signal_csv(records,spec,interval,destination,budget,*,device_label,check_cancel=None,193                      definitions=(),backend='numpy',compact=False,integrity=True,file_consumer=None):194    """消费既有记录流并复用在线写出器,保原公开调用与关闭责任。195196    参数: 可关闭记录流、规格/范围/目标/预算;设备、取消、绑定、后端、精简开关。197    integrity只在请求时计算文件摘要;file_consumer接收写入时取得的有限文件身份,供内部登记复用。198    返回: 公开ExportManifest含实际路径;内部file_consumer模式返回Path。异常: 原格式/输入/预算/取消错;清理失败附真实owner。199    """200    if file_consumer is not None and not callable(file_consumer):201        raise TypeError('file_consumer须为回调')202    owner=SignalCSVWriter(spec,interval,destination,budget,device_label=device_label,203        check_cancel=check_cancel,definitions=definitions,backend=backend,compact=compact,integrity=integrity,204        timestamped=file_consumer is None)205    try:206        owner.reader=iter(records)207        for sample in owner.reader: owner.push(sample)208        output=owner.complete()209        if file_consumer is not None: file_consumer(tuple(owner.files))210        return output211    except BaseException as error:212        owner._failed = True213        _cleanup_owner(owner,error,'export_cleanup_owner','信号CSV导出');raise214
Chronaxis 用户手册 · 离线阅读