Chronaxis用户手册English
本页目录

chronaxis.sources.models

源码基线:8904c2f744951857659abd40aaa3535d57faf53c

1"""定义分析层接收的来源描述、读取请求和独立信号批次。23调用者提供来源元数据和已经归一化的信号数组,本模块检查名称、类型、4时间、序号与质量,再输出冻结模型。数组复制为模型自身拥有的只读数据,5嵌套元数据深度复制为只读映射和元组;不读取文件或DBC,也不注册全局来源。67SignalSeries只表示一次传输中的一列,区别于core.state中的完整内存历史。8每列允许为空,时间非递减,同时间更新完整保留;跨批身份和完整时间组9由BatchValidator及后续执行层处理。valid是有效性掩码,quality保存无效10原因;无效值的存储占位不能参与正常计算,original_values可另存原始诊断。11nbytes只统计五个数值数组的存储,Python容器和全任务预算须另外核算。12"""1314# 标准库冻结结构和JSON元数据,元数据不含任意可执行对象。15from collections.abc import Mapping16# 数据类字段冻结不替代数组所有权与内容校验。17from dataclasses import dataclass, field18# JSON浮点元数据必须有限,原始无效样本另有专用诊断字段。19from math import isfinite20# 深冻结断开调用者后续字典修改。21from types import MappingProxyType22# 联合类型表明元数据不是无类型object占位。23from typing import TypeAlias24# NumPy只负责列布局,不负责CAN解码。25import numpy as np2627# 原样本身份和半开范围使用既有已验证逻辑模型。28from ..core.models import Interval, SourceRef29# 复用数组结构校验,不把其数值计算作为独立参考内核。30from ..core.numeric_numpy import NumericArray31# dtype和质量是通用类型,来源模型不绑定业务信号名称。32from ..core.types import SignalDType33# 共享名称/不可变标量校验,不隐式cast错误输入。34from ..core.validation import require_identifier, require_scalar, require_text35# 所有观测和watermark坐标均为明确int64纳秒。36from ..timebase.conversion import require_int64_ns3738# 冻结后的自由元数据只包含JSON标量、tuple数组和字符串键Mapping。39JsonValue: TypeAlias = str | int | float | bool | None | tuple['JsonValue', ...] | Mapping[str, 'JsonValue']404142def freeze_metadata(value: JsonValue) -> JsonValue:43    """递归复制JSON元数据,避免共享调用者的可变容器。4445    参数:46        value (JsonValue): JSON标量、字符串键映射、list或tuple;不接受任意对象。47    返回:48        JsonValue。映射变为新的只读Mapping,数组变为tuple,标量保持原值。49    异常:50        TypeError: 出现非JSON对象或非字符串键。51        ValueError: 浮点元数据包含NaN或无穷值。52    副作用:53        无。复制容器,不修改传入结构。54    """55    # JSON对象逐层冻结,不能仅冻结外层而共享嵌套字典。56    if isinstance(value, Mapping):57        # 所有键必须原生字符串,不把整数键静默变成文本。58        if any(type(key) is not str for key in value):59            # 与字段定位及JSON序列化含义保持一致。60            raise TypeError('metadata键必须为字符串')61        # 新字典与调用者脱离;递归只接受明确JSON类型。62        return MappingProxyType({key: freeze_metadata(item) for key, item in value.items()})63    # 元数据数组冻结为tuple,不产生object数值数组。64    if type(value) in (list, tuple):65        # 顺序是元数据身份,不能排序或去重。66        return tuple(freeze_metadata(item) for item in value)67    # 只有原生JSON标量可保留;bool与int分别明确支持。68    if type(value) not in (str, int, float, bool, type(None)):69        # 禁止任意对象repr或反射序列化。70        raise TypeError('metadata仅接受JSON类型')71    # JSON数值不能包含NaN/Infinity,避免清单不兼容。72    if type(value) is float and not isfinite(value):73        # 无效采样值不通过metadata伪装正常配置。74        raise ValueError('metadata浮点必须有限')75    # 已验证不可变标量不需要改变原值。76    return value777879@dataclass(frozen=True)80class FieldLocation:81    """保存原字段在来源中的位置,区分原字段与表达式别名。8283    source是来源名,field保存表列或内存字段原文,允许含点号。84    locator保存额外的JSON定位信息,并在构造时深度冻结。85    定位信息只说明如何查找字段,不能证明对应文件或旧来源能力已经核实。86    """8788    source: str  # 来源名,不从路径猜测。89    field: str  # 列或已解码字段原文,允许点号。90    locator: Mapping[str, JsonValue] = field(default_factory=dict)  # profile额外定位证据。9192    def __post_init__(self) -> None:93        """检查来源和原字段,冻结附加定位信息。9495        参数:96            无。读取source、field和locator。97        返回:98            None。将locator替换为独立的只读快照。99        异常:100            TypeError或ValueError: 名称、字段文本或JSON定位信息不合法。101        """102        # 来源名参与当前计划名称空间,原字段不改名。103        require_identifier(self.source, 'field.source')104        # 字段文本保持原定位,不能解释为DSL属性。105        require_text(self.field, 'field.field')106        # 定位必须是对象,不能接孤立列表或字符串。107        if not isinstance(self.locator, Mapping):108            # 错误结构不假装为空定位。109            raise TypeError('locator必须为Mapping')110        # 所有嵌套层与外部调用者断开共享。111        object.__setattr__(self, 'locator', freeze_metadata(self.locator))112113114@dataclass(frozen=True)115class FieldDescription:116    """描述一个可请求的输出别名及其原字段、类型和单位。117118    name是输出别名,field保留原定位;dtype与unit描述归一化后的输出。119    metadata保存只读转换和可选语义声明,不强制普通分析加入领域目录。120    枚举类型必须在metadata.labels中声明非空、唯一的标签集合,供跨批核对。121    同一原字段可以有不同显式转换,各别名分别保存描述。122    """123124    name: str  # iter_batches可请求的输出别名。125    field: FieldLocation  # 完整原定位,区别于semantic_id。126    dtype: SignalDType  # 已归一化输出的逻辑类型。127    unit: str  # 已归一化输出单位,未知标签不自动verified。128    metadata: Mapping[str, JsonValue] = field(default_factory=dict)  # 可选声明与转换证据。129130    def __post_init__(self) -> None:131        """校验字段描述,并冻结用于跨批核对的元数据。132133        参数:134            无。读取name、field、dtype、unit和metadata。135        返回:136            None。metadata成为只读快照;不会读取来源来核实单位或定位。137        异常:138            TypeError或ValueError: 名称、定位、类型、单位文本、JSON结构或枚举标签域错误。139        """140        # 输出别名遵守首版安全名称规则。141        require_identifier(self.name, 'field.name')142        # 字段位置和dtype不可用无类型字典/字符串代替。143        if not isinstance(self.field, FieldLocation) or not isinstance(self.dtype, SignalDType):144            # 配置层应先显式解析类型。145            raise TypeError('field需要FieldLocation和SignalDType')146        # 标签保留原文;物理单位契约另由绑定检查。147        require_text(self.unit, 'field.unit')148        # 自由元数据只接受对象结构。149        if not isinstance(self.metadata, Mapping):150            # 不自动将错误输入转换为空对象。151            raise TypeError('metadata必须为Mapping')152        # 深冻结先规范元数据数组,enum标签域必须可跨批核对。153        metadata = freeze_metadata(self.metadata)154        # enum的逻辑标签在描述中明确,不能每批重新猜编码顺序。155        if self.dtype is SignalDType.ENUM:156            # labels属于该dtype的必要元数据,普通业务键仍可自由声明。157            labels = metadata.get('labels', ())158            # 只接受非空唯一文本域,不放行字符串本身或重复码。159            if not isinstance(labels, tuple) or not labels or any(type(item) is not str or not item.strip() for item in labels) or len(set(labels)) != len(labels):160                # enum无域就无法证明跨批类型契约。161                raise ValueError('enum描述metadata需要非空唯一labels')162        # 保存不可变描述,不从上一次计划继承。163        object.__setattr__(self, 'metadata', metadata)164165166@dataclass(frozen=True)167class SourceCapabilities:168    """逐项记录来源适配器声明的能力和限制。169170    decoded_output、raw_export、repeat_scan和random_access分别表示已解码输出、171    原始导出、可重复读取和随机访问能力,必须使用真实布尔值。172    diagnostics与limitations是不可变说明元组。模型只校验声明结构,173    不会运行来源来证明能力,也不因repeat_scan为True自动增加旧日志扫描。174    """175176    decoded_output: bool  # 是否实际能返回逐次已解码信号。177    raw_export: bool  # 原路径已支持原始导出时才可为True。178    repeat_scan: bool  # 可重复调用的能力,不授权多次旧扫描。179    random_access: bool  # 是否可随机定位,普通迭代不等同此能力。180    diagnostics: tuple[str, ...] = ()  # 已实际暴露的诊断。181    limitations: tuple[str, ...] = ()  # 未获得的元数据或兼容限制。182183    def __post_init__(self) -> None:184        """检查能力开关及诊断、限制文本的结构。185186        参数:187            无。读取四个能力开关、diagnostics和limitations。188        返回:189            None。保留原声明,不推断或测试来源能力。190        异常:191            TypeError或ValueError: 开关不是bool,文本列表不是tuple,或说明文本为空。192        """193        # 禁止把整数1当作已经审核的能力。194        if any(type(value) is not bool for value in (self.decoded_output, self.raw_export, self.repeat_scan, self.random_access)):195            # 不用truthiness扩大声明。196            raise TypeError('capability必须为bool')197        # 诊断和限制只作为事实文本,不能执行。198        for items in (self.diagnostics, self.limitations):199            # 不共享可变列表或不明确顺序的集合。200            if not isinstance(items, tuple):201                # 错误形状在构造前拒绝。202                raise TypeError('diagnostics/limitations必须为tuple')203            # 每条非空说明保留大小写及原语义。204            for item in items:205                # 不能用空文本冒充限制已说明。206                require_text(item, 'capability说明')207208209@dataclass(frozen=True)210class SourceDescription:211    """保存一个来源的静态字段、时钟及能力声明。212213    name标识来源,fields保存唯一输出别名,timebase_id标识声明的时间基准。214    observation_start_ns和observation_end_ns保存已知观测边界,未知端点各自为None。215    capabilities记录能力说明,legacy_profile可保存旧来源的契约标识。216    metadata_status区分'verified'已核实、'declared'仅声明和'unavailable'不可用。217    模型校验不会读取来源,也不会自动将声明状态升级为已核实。218    """219220    name: str  # 当前计划来源名。221    fields: tuple[FieldDescription, ...]  # 已描述的输出别名和原定位。222    timebase_id: str  # 明确来源时基ID。223    observation_start_ns: int | None  # 未知开始保留None。224    observation_end_ns: int | None  # 未知终点不补周期。225    capabilities: SourceCapabilities  # 逐项审核能力。226    legacy_profile: str | None = None  # 仅旧来源需要冻结profile身份。227    metadata_status: str = 'declared'  # verified/declared/unavailable。228229    def __post_init__(self) -> None:230        """检查描述归属、别名唯一性和已知的观测范围。231232        参数:233            无。读取来源名称、字段元组、时钟、观测端点、能力和证据状态。234        返回:235            None。已知双端点按时间区间检查,未知端点保持None。236        异常:237            TypeError或ValueError: 描述类型、归属、重复名称、状态或旧契约标识错误。238            OverflowError: 时间坐标或区间跨度超出范围。239        副作用:240            无输入读取;名称合法不表示来源实际存在。241        """242        # 来源和时基各有明确安全名称。243        require_identifier(self.name, 'description.name')244        # 相同ID不等于共同起点,同步仍由计划验证。245        require_identifier(self.timebase_id, 'description.timebase_id')246        # 全部字段有类型,不能只有类名和dict占位。247        if not isinstance(self.fields, tuple) or any(not isinstance(item, FieldDescription) for item in self.fields) or not isinstance(self.capabilities, SourceCapabilities):248            # 错误描述不能进入执行能力检查。249            raise TypeError('description需要typed fields/capabilities')250        # 输出别名唯一且原字段确实属于当前来源。251        if len({item.name for item in self.fields}) != len(self.fields) or any(item.field.source != self.name for item in self.fields):252            # 不取第一个冲突或使用他源字段。253            raise ValueError('description字段重名或来源冲突')254        # 未知边界分别保留;已知才规范精确纳秒。255        for value in (self.observation_start_ns, self.observation_end_ns):256            # None不是0,也不表示完整覆盖。257            if value is not None:258                # bool/float坐标在模型边界拒绝。259                require_int64_ns(value)260        # 双端都有事实才判断完整范围。261        if self.observation_start_ns is not None and self.observation_end_ns is not None:262            # 同时检查反向和跨度溢出。263            Interval(self.observation_start_ns, self.observation_end_ns)264        # 证据状态是固定词汇,不能自由标verified。265        if self.metadata_status not in ('verified', 'declared', 'unavailable'):266            # 错误版本/状态应执行前失败。267            raise ValueError('未知metadata_status')268        # 旧profile非空,但不假装已经登记/可调用。269        if self.legacy_profile is not None:270            # profile登记和哈希是adapter的另一个责任。271            require_text(self.legacy_profile, 'legacy_profile')272273274@dataclass(frozen=True)275class ReadRequest:276    """说明本次会话要读取的信号别名、分析范围和批次上限。277278    session_id绑定当前会话,signal_names指定列顺序并允许空投影。279    domain_start_ns与domain_end_ns是分析请求坐标,不表示来源实际观测范围。280    batch_rows限制每批全部信号列的样本数之和,不能按宽表行数计算。281    请求范围不授权修改旧读取链;适配器保留分析开始前的必要更新及真实终点。282    同一时间组可以跨批,分批上限不代表该组已经完整。283    """284285    session_id: str  # 明确会话身份。286    signal_names: tuple[str, ...]  # 当前绑定输出别名,允许空投影。287    domain_start_ns: int | None  # 分析域起点;纯消息整文件请求可未声明。288    domain_end_ns: int | None  # 分析域终点;与起点同时未声明。289    batch_rows: int  # 所有series样本数总和上限。290291    def __post_init__(self) -> None:292        """检查请求会话、唯一别名、时间范围和正整数批次上限。293294        参数:295            无。读取session_id、signal_names、domain_start_ns、domain_end_ns和batch_rows。296        返回:297            None。保持别名顺序,不去重或裁剪来源数据。298        异常:299            TypeError或ValueError: 名称、容器、重复别名、时间范围或批次大小错误。300            OverflowError: 时间范围超出整数纳秒约束。301        """302        # 会话不能从全局当前任务猜测。303        require_identifier(self.session_id, 'request.session_id')304        # 投影顺序明确,同一别名不能重复计样本。305        if not isinstance(self.signal_names, tuple) or len(set(self.signal_names)) != len(self.signal_names):306            # 不自动去重。307            raise ValueError('signal_names需要唯一tuple')308        # 每个名字必须是当前计划可以安全绑定的简单别名。309        for name in self.signal_names:310            # 来源字段的点号不是表达式别名。311            require_identifier(name, 'request.signal_name')312        # 半开范围和极值跨度按统一时间契约。313        if self.domain_start_ns is None or self.domain_end_ns is None:314            if self.domain_start_ns is not None or self.domain_end_ns is not None:315                raise ValueError('未声明时间域请求必须双端同时为None')316        else:317            Interval(self.domain_start_ns, self.domain_end_ns)318        # 不能把True当成单行预算,也不能允许零缓冲。319        if type(self.batch_rows) is not int or self.batch_rows <= 0:320            # 限额不靠默认值掩盖错误。321            raise ValueError('batch_rows必须为正整数')322323324@dataclass(frozen=True)325class SignalSeries:326    """保存一批中的单个信号列及每个样本的来源身份。327328    time_ns、values、valid、quality和sequence都是等长一维数组,形状为(N,)。329    time_ns与sequence使用int64,时间单位为纳秒;时间非递减,序号非负且列内唯一。330    valid明确值是否有效,quality保存原因;不能凭values中的占位判断有效性。331    name、dtype和unit说明别名、逻辑类型及输出单位;枚举用整数码和labels表示。332    source_ref与可选original_values均与样本等长,保留位置及原始诊断。333334    构造时复制数组并设为只读,调用者修改原数组不影响本列。335    本类不折叠同时间冲突,也不执行valid_for保持或过期判断;这些属于后续时间驱动。336    """337338    name: str  # 简单表达式别名。339    dtype: SignalDType  # 逻辑类型,enum存储int64 code。340    unit: str  # 已显式归一化输出单位。341    time_ns: np.ndarray  # shape(N,), int64,当前会话分析坐标ns。342    values: np.ndarray  # shape(N,), 精确dtype。343    valid: np.ndarray  # shape(N,), bool,不能单用值占位判断。344    quality: np.ndarray  # shape(N,), uint16九位原因。345    sequence: np.ndarray  # shape(N,), int64,原来源稳定非负序号。346    source_ref: tuple[SourceRef, ...]  # 每行实际来源/字段/会话。347    labels: tuple[str, ...] = ()  # enum固定域,其他类型为空。348    original_values: tuple[int | float | bool | str | None, ...] | None = None  # 原无效诊断可保留。349    code: NumericArray | None = None  # 请求 enum_code 时携带同坐标整数原码。350351    def __post_init__(self) -> None:352        """检查单列布局、有效性和样本身份,再复制为只读数组。353354        参数:355            无。读取信号描述、五个数组、source_ref、labels和original_values。356        返回:357            None。时间、序号及数值数组成为本模型独立拥有的只读数据。358        异常:359            TypeError或ValueError: 名称、数组形状或类型、质量、时间顺序、序号、360                来源引用或原始诊断不符合要求;不通过广播或隐式转换修正。361        副作用:362            分配数组副本,不修改调用者输入。无效原浮点可保留在诊断中。363        """364        # 输出别名和单位仍需明确,不推断字段语义。365        require_identifier(self.name, 'series.name')366        # 标签有效性与转换一致性由上游绑定提供。367        require_text(self.unit, 'series.unit')368        # 数值结构校验包括enum域、非有限正常值与禁止原因。369        numeric = NumericArray(self.dtype, self.values, self.valid, self.quality, self.labels, self.code)370        # 时间与稳定序号始终分列int64,禁止混宽表float。371        for array in (self.time_ns, self.sequence):372            # ndarray子类或隐式cast均不接受。373            if type(array) is not np.ndarray or array.ndim != 1 or array.dtype != np.dtype('int64') or len(array) != len(numeric.values):374                # 错误shape不能广播补齐。375                raise TypeError('time/sequence需要同长1维int64列')376        # 先比较不相减,避免极值坐标之差wrap。377        if np.any(self.time_ns[1:] < self.time_ns[:-1]):378            # 不排序输入来掩盖来源乱序。379            raise ValueError('series时间回退')380        # 来源序号非负且列内唯一;无需用时间替代它。381        if np.any(self.sequence < 0) or len(np.unique(self.sequence)) != len(self.sequence):382            # 保留相同时间不同序号,拒绝重复同一观测身份。383            raise ValueError('series sequence负值或重复')384        # 原定位每行等长,批次归属另外在SignalBatch检查。385        if not isinstance(self.source_ref, tuple) or len(self.source_ref) != len(self.time_ns) or any(not isinstance(ref, SourceRef) for ref in self.source_ref):386            # 少一个来源位置也不能假设复用第一条。387            raise ValueError('source_ref需要同长typed tuple')388        # 原诊断可省略,但存在时必须严格等长且不可变。389        if self.original_values is not None:390            # 不接受可变list或不同长度的原始事实。391            if not isinstance(self.original_values, tuple) or len(self.original_values) != len(self.time_ns):392                # 不能截断诊断或用值占位重建。393                raise ValueError('original_values长度/容器错误')394            # 无效原浮点可保留NaN,正常数值已由valid校验。395            for value in self.original_values:396                # 禁止可变对象进入跨规则共享输入。397                require_scalar(value)398        # NumericArray已复制校验列,不再重复制相同三个数组。399        for name in ('values', 'valid', 'quality'):400            # 安全只读列仍携带各自精确dtype。401            object.__setattr__(self, name, getattr(numeric, name))402        # 时间与序号独立复制,调用者更改原数组无影响。403        for name in ('time_ns', 'sequence'):404            # 新数组独立拥有存储,其复制后大小由nbytes反映。405            copied = getattr(self, name).copy()406            # 只读约定防止普通写操作污染其他规则。407            copied.flags.writeable = False408            # 只在构造期安装新数组。409            object.__setattr__(self, name, copied)410411    @property412    def nbytes(self) -> int:413        """统计本列五个数组的存储字节数。414415        参数:416            无。读取time_ns、values、valid、quality和sequence的nbytes。417        返回:418            int。数组字节总和;不含枚举标签、来源引用、原诊断或其他Python容器。419        异常:420            无主动异常;数组结构已在构造时检查。421        """422        # 时间与序号各占8字节,有效性占1字节、质量占2字节;值按类型占1或8字节,标签另计。423        total = sum(array.nbytes for array in (self.time_ns, self.values, self.valid, self.quality, self.sequence))424        return total if self.code is None else total + self.code.values.nbytes + self.code.valid.nbytes + self.code.quality.nbytes425426427@dataclass(frozen=True)428class SignalBatch:429    """传送一个来源在当前会话中的异步信号列和读取进度。430431    source与session_id确定归属,series按输出别名保存列,允许零列。432    watermark_ns是进度承诺:后续不会再出现小于或等于该坐标的更新。433    它可以落后于当前尚未完整的时间组,不能当作实际观测终点。434    source_eof只表示整个配置来源耗尽;observed_end_ns保存观测终点事实,435    未知时保留None。单批结构检查不替代BatchValidator的跨批校验。436    """437438    source: str  # 已解码来源名称。439    session_id: str  # 本次读取的明确会话。440    series: Mapping[str, SignalSeries]  # 输出别名到真实列,允许零列。441    watermark_ns: int | None  # 已完整的时间组进度,否则None。442    source_eof: bool  # 整个来源终点,不能仅单文件EOF。443    observed_end_ns: int | None  # 观测未知保留None,不补周期。444445    def __post_init__(self) -> None:446        """检查批次归属、列名称和观测终点,冻结列映射。447448        参数:449            无。读取来源、会话、series、watermark_ns、source_eof和observed_end_ns。450        返回:451            None。series成为新只读映射,列数组保持已有独立所有权。452        异常:453            TypeError或ValueError: 映射或结束标记类型、别名、样本归属或观测边界错误。454            OverflowError: 已知时间坐标超出整数纳秒范围。455        """456        # 来源与会话必须真实、明确。457        require_identifier(self.source, 'batch.source')458        # 会话边界不会从上个请求继承。459        require_identifier(self.session_id, 'batch.session_id')460        # 列须为已定义的信号模型,来源结束标记须为bool,不能用整数代替。461        if not isinstance(self.series, Mapping) or type(self.source_eof) is not bool:462            # 不将未知输入当作空批或已结束。463            raise TypeError('batch需要Mapping series与bool EOF')464        # 所有输出名字与列自己的绑定一致。465        for name, column in self.series.items():466            # 未绑定名字/错误列结构在执行前拒绝。467            if not isinstance(column, SignalSeries) or name != column.name:468                # 不能用键映射把其他信号冒充当前列。469                raise ValueError('batch列类型/名称冲突')470            # 每个样本来源引用都属于本batch会话和来源。471            if any(ref.session_id != self.session_id or ref.source_name != self.source for ref in column.source_ref):472                # 即使值相同也不能丢失真实归属。473                raise ValueError('batch sample来源/会话冲突')474        # unknown端点和watermark独立保留None。475        for value in (self.watermark_ns, self.observed_end_ns):476            # 不将None转成0或当前时间。477            if value is not None:478                # 所有进度和观测时间遵守精确ns协议。479                require_int64_ns(value)480        # 已知观测终点必须包含该批真实末点。481        if self.observed_end_ns is not None and any(len(column.time_ns) and int(column.time_ns[-1]) > self.observed_end_ns for column in self.series.values()):482            # 不能在自称结束之后继续提供真实样本。483            raise ValueError('样本超出observed_end')484        # 冻结外部别名字典,列内容已复制只读。485        object.__setattr__(self, 'series', MappingProxyType(dict(self.series)))486487    @property488    def sample_count(self) -> int:489        """按所有列的样本数总和计算本批大小。490491        参数:492            无。读取series中各列的长度。493        返回:494            int。空批为0;同一时间的多个信号分别计数,用于核对batch_rows上限。495        异常:496            无主动异常;列结构已在构造时检查。497        """498        # 同时间多信号仍各算一个样本,同刻可跨传输批次。499        return sum(len(column.time_ns) for column in self.series.values())500
Chronaxis 用户手册 · 离线阅读