chronaxis.extensions
源码基线:8904c2f744951857659abd40aaa3535d57faf53c
1"""由宿主Python程序显式声明可信扩展,纯配置只引用其已注册名称。23逐点函数声明固定输入输出端口;样本归约与网格变换分别使用具有生命周期4的工厂。注册表冻结名称、版本、类型、单位和后端能力,以稳定摘要标识协议。5本模块不发现模块、不读取配置中的文件或导入路径,也不在注册时创建运行状态。6"""78from contextlib import contextmanager9from contextvars import ContextVar10from dataclasses import dataclass11from hashlib import sha25612import json13from types import MappingProxyType1415from .core.types import SignalDType as D16from .core.unit_registry import resolve_unit17from .core.validation import require_identifier, require_text18from .core.function_contracts import builtin_function_specs192021_BUDGET = ContextVar('chronaxis_host_extension_budget',default=None)22_BUILTINS = frozenset(builtin_function_specs()) | frozenset("""23duration integral select legacy_sum sample_mean24sample_minimum sample_maximum min_time max_time time_weighted_mean sample_count point_event_count25activity_count segment_count sample_variance sample_stddev sample_rms sample_median sample_quantile26time_variance time_stddev time_rms rolling_time_mean rolling_minimum rolling_maximum rolling_integral27adjacent_rate window_rate sample_fit_slope28condition_duration rising_edge falling_edge start_only window_delta debounce hysteresis start_end29""".split())303132@dataclass(frozen=True)33class ValueSpec:34 """一个固定类型与数值尺度的端口。BOOL使用单位1,枚举代码需在DSL中显式取出。"""3536 dtype: D37 unit: str3839 def __post_init__(self):40 """核对宿主声明的类型与尺度。参数: 无。返回: None。异常: ValueError为非法端口。"""41 if not isinstance(self.dtype,D) or self.dtype is D.ENUM or resolve_unit(self.unit) is None:42 raise ValueError('扩展端口须声明数值或BOOL类型及已登记单位')43 if self.dtype is D.BOOL and self.unit != '1':44 raise ValueError('扩展BOOL端口的单位必须为1')454647class _BackendSpec:48 """三类协议共享名称/版本与显式能力验证,不共享数值或状态回调。"""4950 def require_backend(self, backend):51 """取得请求后端自身的实现。参数: backend为reference或numpy。返回: 回调。异常: 缺实现报ValueError。"""52 if backend not in ('reference','numpy'):53 raise ValueError('扩展后端须为reference或numpy')54 implementation = self.reference if backend == 'reference' else self.numpy55 if implementation is None:56 raise ValueError(f'扩展 {self.name} {self.version} 不支持后端 {backend}')57 return implementation5859 def _check(self, inputs):60 """校验名称、版本、端口与声明的具体后端。参数: 固定输入tuple。返回: None。异常: 非法描述报错。"""61 require_identifier(self.name,'extension.name')62 require_text(self.version,'extension.version')63 if self.name in _BUILTINS:64 raise ValueError(f'扩展名称与内置能力冲突:{self.name}')65 if type(inputs) is not tuple or not inputs or any(type(p) is not ValueSpec for p in (*inputs,self.output)):66 raise TypeError('扩展需要非空固定输入及明确的ValueSpec输出')67 if self.reference is None and self.numpy is None or any(68 f is not None and not callable(f) for f in (self.reference,self.numpy)):69 raise ValueError('扩展至少需要一个可调用的后端实现')707172@dataclass(frozen=True)73class FunctionSpec(_BackendSpec):74 """纯逐点函数,impl(args,resources)返回同一形状的ScalarValue或NumericArray。7576 固定位置参数逐一按端口单位及dtype编译,任何必要输入无效都传递到结果;77 无历史,真实输入更新沿用普通表达式政策。78 """79 name: str80 version: str81 inputs: tuple[ValueSpec,...]82 output: ValueSpec83 reference: object = None84 numpy: object = None8586 def __post_init__(self):87 """参数: 无。返回: None。异常: 声明或实现不合法时拒绝注册。"""88 self._check(self.inputs)899091@dataclass(frozen=True)92class ReductionSpec(_BackendSpec):93 """标量样本归约协议;factory(resources)建立每范围独立的状态。9495 consume接收已选真实样本,finalize返回ScalarValue,close退还外部资源;96 mergeable=True时还必须提供merge(other)。cancel和close均为必需方法,分别处理取消及资源释放。97 """98 name: str99 version: str100 input: ValueSpec101 output: ValueSpec102 reference: object = None103 numpy: object = None104 state_bytes: int = 0105 mergeable: bool = False106107 def __post_init__(self):108 """参数: 无。返回: None。异常: 资源/合并能力或端口不合规时拒绝。"""109 self._check((self.input,))110 if type(self.state_bytes) is not int or self.state_bytes < 0 or type(self.mergeable) is not bool:111 raise ValueError('归约需要非负state_bytes和布尔mergeable')112 if self.input.dtype is D.BOOL or self.output.dtype is D.BOOL:113 raise ValueError('标量样本归约需要数值输入和输出')114115116@dataclass(frozen=True)117class TransformSpec(_BackendSpec):118 """有状态网格变换;factory(resources)为本次会话创建状态。119120 advance(time_ns,window_ns,values,weights_ns)仅接收本窗口有效的ZOH区间,121 返回实际网格时刻的ScalarValue。history_ns限制回看窗口的实际或周期122 计量宽度,interval_modes声明该实现能正确使用的时间权重。123 """124 name: str125 version: str126 input: ValueSpec127 output: ValueSpec128 reference: object = None129 numpy: object = None130 state_bytes: int = 0131 history_ns: int = 0132 interval_modes: tuple[str,...] = ('actual','fixed','dbc')133134 def __post_init__(self):135 """参数: 无。返回: None。异常: 非数值端口、非正历史或非法资源费时拒绝。"""136 self._check((self.input,))137 if (type(self.state_bytes) is not int or self.state_bytes < 0 or type(self.history_ns) is not int138 or not 0 < self.history_ns < 1 << 63 or self.input.dtype is D.BOOL or self.output.dtype is D.BOOL):139 raise ValueError('网格扩展需要数值端口、非负state_bytes及正int64历史时长')140 if (type(self.interval_modes) is not tuple or not self.interval_modes141 or any(m not in ('actual','fixed','dbc') for m in self.interval_modes)142 or len(set(self.interval_modes)) != len(self.interval_modes)):143 raise ValueError('时序扩展的interval_modes须明确列出互不重复的actual、fixed或dbc')144145146@dataclass(frozen=True,eq=False,init=False)147class ExtensionRegistry:148 """不可变的宿主能力集合;同次运行必须提供与编译时相同的注册对象。"""149 functions: object150 reductions: object151 transforms: object152 identity: str153154 def __init__(self,functions=(),reductions=(),transforms=()):155 """登记三种互斥的typed描述。参数: 三类描述符tuple。返回: 新注册表。异常: 重名或模型错误。"""156 records, names = [], set()157 for kind,entries,typ in (('functions',functions,FunctionSpec),('reductions',reductions,ReductionSpec),158 ('transforms',transforms,TransformSpec)):159 if type(entries) is not tuple or any(type(spec) is not typ for spec in entries):160 raise TypeError(f'{kind}需要对应描述符的tuple')161 by_name = {}162 for spec in entries:163 if spec.name in names:164 raise ValueError(f'重复的扩展名称:{spec.name}')165 by_name[spec.name] = spec166 names.add(spec.name)167 ports = spec.inputs if kind == 'functions' else (spec.input,)168 records.append((kind,spec.name,spec.version,[(p.dtype.value,p.unit) for p in ports],169 (spec.output.dtype.value,spec.output.unit),spec.reference is not None,170 spec.numpy is not None,getattr(spec,'state_bytes',0),getattr(spec,'mergeable',False),171 getattr(spec,'history_ns',0),sorted(getattr(spec,'interval_modes',()))))172 object.__setattr__(self,kind,MappingProxyType(by_name))173 # 身份取声明和实现版本,跨进程不使用函数地址;换实现的宿主须递增版本。174 payload = json.dumps(['chronaxis-host-v1',sorted(records)],ensure_ascii=False,separators=(',',':'))175 object.__setattr__(self,'identity',sha256(payload.encode('utf-8')).hexdigest())176177178@contextmanager179def using_extension_budget(budget):180 """让当前调用栈的逐点扩展使用实际运行账本,嵌套及异常后恢复原上下文。181182 参数: budget为本次ResourceBudget。返回: 上下文管理器。异常: 回调/执行异常原样传播。183 """184 token = _BUDGET.set(budget)185 try:186 yield187 finally:188 _BUDGET.reset(token)189