Chronaxis用户手册English
本页目录

chronaxis.planning.transforms

源码基线:8904c2f744951857659abd40aaa3535d57faf53c

1"""解析 v4 时序变换的名称图、时间网格、输入表达式及输出单位。23每个离散序列作为独立的分析来源声明,不向真实 reader 增加字段。先按表达式4引用拓扑校验所有声明(包括不用的声明),再给消费者提供同一套最终绑定。5本模块不扫描数据、不生成网格点,也不创建历史。6"""78import ast9from dataclasses import dataclass, field, replace10from hashlib import sha25611from ..core.binding import SignalDefinition12from ..core.calculations import CalculationDefinition, CalculationConversion, calculation_conversion13from ..core.declarations import DeclaredSource14from ..core.expression import StaticExpression, validate_expression15from ..core.models import Interval16from ..core.types import SignalDType as D, TimeType17from .checks import (PlanValidationError, array_at, canonical, choice_at, coordinate_at, duration_at,18                     model_at, number_at, object_at, text_at)1920ROLLING = ('rolling_time_mean', 'rolling_minimum', 'rolling_maximum', 'rolling_integral')21RATES = ('adjacent_rate', 'window_rate', 'sample_fit_slope')22MAX_TRANSFORMS = 64  # 防止用户定义无界依赖链和运行回调栈。232425@dataclass(frozen=True)26class TemporalTransformPlan:27    """按拓扑序保存一个已验证的离散变换,预算与运行状态在任务启动时创建。"""2829    name: str30    operation: str31    expression: StaticExpression32    output: SignalDefinition33    conversion: CalculationConversion34    physical: tuple[str, ...]35    window_ns: int | None36    step_ns: int | None37    origin_ns: int38    max_gap_ns: int | None39    partial_policy: str40    minimum_coverage: float41    host_spec: object = field(default=None,repr=False,compare=False)  # 仅运行宿主注册的实际网格操作。424344def _names(text, path):45    """提取引用边。参数: 表达式文本及位置。返回: 名称集合。异常: 带位置的语法错误。"""46    try:47        tree = ast.parse(text, mode='eval')48    except (SyntaxError, TypeError) as error:49        raise PlanValidationError(path, '变换输入须为合法表达式') from error50    return {n.id for n in ast.walk(tree) if isinstance(n, ast.Name)}515253def build_transforms(raw, bindings, derived, *, timezone=None):54    """解析全图。参数: 配置项、物理声明、派生定义及时间区。返回: 最终声明和变换元组。异常: 配置错误。"""55    if bindings.config_version != 4:56        raise PlanValidationError('$.transforms', 'transforms需要config_version=4')57    rows, edges, plans = {}, {}, {}58    original = {s.name for s in bindings.signals}59    formulas = {d.name: d for d in derived}60    for d in derived:61        edges[d.name] = _names(d.expr, '$.derived.'+d.name)62    entries=array_at(raw,'$.transforms')63    if len(entries)>MAX_TRANSFORMS:64        raise PlanValidationError('$.transforms',f'最多支持{MAX_TRANSFORMS}个时序变换')65    for i, item in enumerate(entries):66        path = f'$.transforms[{i}]'67        base = object_at(item, path, ('name','operation','expr'), ('window','step','origin','max_gap','unit',68                         'minimum_coverage','partial_policy'))69        name = text_at(base['name'], path+'.name', identifier=True)70        if name in original or name in formulas or name in rows:71            raise PlanValidationError(path+'.name', '时序、派生与原信号名称必须唯一')72        registry = getattr(bindings,'extensions',None)73        registered = registry.transforms if registry is not None else {}74        op = choice_at(base['operation'], ROLLING+RATES+tuple(registered), path+'.operation')75        params = ('max_gap',) if op == 'adjacent_rate' else ('window','step')76        extra = ('unit',) if op == 'adjacent_rate' else ('unit','origin','minimum_coverage','partial_policy')77        item = object_at(base,path,('name','operation','expr',*params),extra)78        rows[name] = path,item79        edges[name] = _names(text_at(item['expr'],path+'.expr'),path+'.expr')8081    ordered, done, visiting = [], set(), set()82    def visit(name):83        """按依赖先后检查循环。参数: 原简单名称。返回: 无。异常: 包括派生/变换混合环。"""84        if name in visiting:85            raise PlanValidationError('$.transforms', '派生或时序变换循环依赖:'+name)86        if name in done or name not in edges:87            return88        visiting.add(name)89        for dep in sorted(edges[name]):90            visit(dep)91        visiting.remove(name)92        done.add(name)93        ordered.append(name)9495    for name in edges:96        visit(name)97    available = []98    source_names = {s.name for s in bindings.sources}99    for name in ordered:100        if name in formulas:101            available.append(formulas[name])102            continue103        path, item = rows[name]104        op = item['operation']105        local = replace(bindings,derived_names=tuple(d.name for d in available))106        host_spec = registered.get(op)107        expr = model_at(validate_expression,path+'.expr',item['expr'],local,derived=tuple(available),108                        unit=host_spec.input.unit if host_spec is not None else None)109        if host_spec is not None and expr.root.dtype is not host_spec.input.dtype:110            raise PlanValidationError(path+'.expr','宿主时序变换需要与声明端口一致的输入dtype')111        if expr.root.dtype not in (D.FLOAT64,D.INT64,D.UINT64):112            raise PlanValidationError(path+'.expr','时序数值变换需要数值输入')113        physical = tuple(dict.fromkeys(alias for dep in expr.dependencies114                    for alias in (plans[dep].physical if dep in plans else (dep,))))115        model_at(bindings.require_synchronized,path+'.expr',physical)116        grid = op != 'adjacent_rate'117        window = duration_at(item['window'],path+'.window',positive=True) if grid else None118        if host_spec is not None and window > host_spec.history_ns:119            raise PlanValidationError(path+'.window','时序窗口超过宿主注册的history_ns')120        step = duration_at(item['step'],path+'.step',positive=True) if grid else None121        gap = None if grid else duration_at(item['max_gap'],path+'.max_gap',positive=True)122        origin = coordinate_at(item['origin'],path+'.origin',absolute=bindings.timebase.time_type is TimeType.ABSOLUTE,123                               tz=timezone) if 'origin' in item else 0124        policy = choice_at(item.get('partial_policy','strict'),('strict','report'),path+'.partial_policy')125        coverage = number_at(item.get('minimum_coverage',0),path+'.minimum_coverage',minimum=0,maximum=1)126        unit = host_spec.output.unit if host_spec is not None else expr.root.unit127        if op in RATES:128            from ..core.rate_transforms import rate_unit129            unit = model_at(rate_unit,path+'.unit',unit)130        # 平均值/极值保留输入尺度,积分从规范输入推导面积;最后统一兼容目标。131        calculation = 'integral' if op=='rolling_integral' else 'sample_minimum'132        target = item.get('unit', None if calculation=='integral' else unit)133        if calculation == 'integral':134            from ..core.unit_registry import resolve_unit135            child = expr.root136            while child.operator=='unit_convert' and not child.unit_explicit:137                child = child.args[0]138            original_unit = resolve_unit(child.unit)139            if original_unit is not None and original_unit.offset:140                raise PlanValidationError(path+'.expr','绝对摄氏度滚动积分须先显式转换到K')141        spec = CalculationDefinition(name,calculation,Interval(0,0),output_unit=target,config_version=4)142        conversion = model_at(calculation_conversion,path+'.unit',spec,unit)143        dtype = conversion.dtype(expr.root.dtype) if op in ('rolling_minimum','rolling_maximum') else D.FLOAT64144        if host_spec is not None:145            dtype = conversion.dtype(host_spec.output.dtype)146        source = '_temporal_'+name147        while source in source_names:148            source = '_'+source149        source_names.add(source)150        identity = sha256(canonical((item,registry.identity) if host_spec is not None else item).encode('utf-8')).hexdigest()151        declared = DeclaredSource(source,bindings.timebase,'temporal:'+identity)152        output = SignalDefinition(name,source,'temporal.'+name,dtype,conversion.unit,step or gap,config_version=4)153        bindings = replace(bindings,sources=bindings.sources+(declared,),signals=bindings.signals+(output,),154                           temporal_inputs=bindings.temporal_inputs+((name,physical),))155        plans[name] = TemporalTransformPlan(name,op,expr,output,conversion,physical,window,step,origin,gap,policy,coverage,host_spec)156    # 重编译到最终绑定;执行时按原声明复核,不把临时拓扑身份交给 reader。157    return bindings, tuple(replace(p,expression=model_at(validate_expression,rows[p.name][0]+'.expr',158                p.expression.text,bindings,derived=derived,unit=p.host_spec.input.unit if p.host_spec is not None else None))159                           for p in plans.values())160161162def active_transforms(plan, aliases):163    """从消费者逆向收集必要变换。参数: 计划、输出别名集合。返回: 拓扑序元组。异常: 已建计划错误。"""164    needed = set(aliases)165    selected = []166    for item in reversed(plan.transforms):167        if item.name in needed:168            selected.append(item)169            needed.update(item.expression.dependencies)170    return tuple(reversed(selected))171172173def physical_dependencies(plan, aliases):174    """展开原采集依赖。参数: 计划和表达式输入别名。返回: 保序去重原别名。异常: 已建计划错误。"""175    transforms = {p.name:p.physical for p in plan.transforms}176    return tuple(dict.fromkeys(n for a in aliases for n in transforms.get(a,(a,))))177
Chronaxis 用户手册 · 离线阅读