Chronaxis用户手册English
本页目录

chronaxis.planning.nodes

源码基线:8904c2f744951857659abd40aaa3535d57faf53c

1"""检查计算条件、窗口和指标配置,保存静态依赖与边界声明。23输入是节点配置及当前信号绑定,输出RulePlan、WindowPlan和CalculationPlan。4表达式只允许受控语法,条件结果必须为布尔类型;窗口区分持续活动与时间点,5指标区分全会话、原事件和合并片段范围。检查引用、类型、单位及同步声明后,6节点按用户顺序保存。本模块不读来源或DBC,不运行时序规则或积分;7可配置的节点不等于运行接入完成,实际能力由计划报告说明。8"""910# 类型与静态单位校验均来自已验核心,不运行样本数值。11from ..core.expression import validate_expression12from ..core.calculations import CalculationDefinition, calculation_conversion13from ..core.models import Interval14from ..core.types import SignalDType as D15from ..core.unit_registry import resolve_unit16from ..core.window_delta import WindowDeltaSpec17from ..core.counts_v4 import COUNT_OPERATIONS, OBJECT_COUNTS18from ..core.moments import MOMENTS, WITH_DDOF, moment_ddof19from ..core.quantiles import SAMPLE_QUANTILES, quantile_probability20from ..core.stability import StabilitySpec21from ..core.start_end import StartEndSpec22# 严格JSON字段与可定位异常。23from .diagnostic_collection import usable_node, collect_node_error24from .checks import PlanValidationError, array_at, choice_at, duration_at, model_at, number_at, object_at, text_at25from .models import CalculationPlan, RulePlan, WindowPlan262728def expression_at(text, path, bindings, derived, *, boolean=False, unit=None):29    """检查表达式声明,并将语法、类型或时钟错误关联到配置位置。3031    参数:32        text (str): 使用已绑定简单别名的表达式文本。33        path (str): 表达式对应的配置位置。34        bindings (DeclarationBindings): 当前会话的名称、来源和时间声明关系。35        derived (tuple): 全部派生定义,供依赖展开和循环检查。36        boolean (bool): 默认False;True时要求结果为布尔类型。37        unit (str | None): 可选输出单位,默认None,按表达式推导。38    返回:39        StaticExpression。保留类型、单位和依赖,尚未读取数据或执行计算。40    异常:41        PlanValidationError: 受控语法、名称、依赖、类型、单位或同步声明不合法。42    """43    from .diagnostic_collection import require_expression_dependencies, expression_scope44    require_expression_dependencies(text, path, derived)45    bindings, derived = expression_scope(text, bindings, derived)46    # 所有静态配置表达式走同一严格白名单。47    result = model_at(validate_expression, path, text, bindings, derived=derived, unit=unit)48    # 不借数值truthiness生成条件。49    if boolean and result.root.dtype is not D.BOOL:50        # 编译完成也必须符合所在节点角色。51        raise PlanValidationError(path, 'condition必须为bool表达式')52    # 调用者可保存依赖/单位/静态身份。53    return result545556def register_id(raw, path, ids) -> str:57    """将一个合法节点ID登记到本计划的名称集合。5859    参数:60        raw (str): 用户声明的节点ID。61        path (str): ID对应的配置位置。62        ids (set[str]): 当前计划局部集合,供规则、窗口和指标共用。63    返回:64        str。原ID保持不变,并加入ids。65    异常:66        PlanValidationError: ID非法或已经存在,不自动改名或覆盖。67    副作用:68        只修改传入的局部集合,不注册全局名称。69    """70    # ID也禁止关键字/函数名称。71    name = text_at(raw, path, identifier=True)72    # 不同节点类型也不能悄悄使用同一稳定计划ID。73    if name in ids:74        # 第二定义位置准确报告。75        raise PlanValidationError(path, f'计划ID重复:{name}')76    # 只更新解析器局部集合,不用全局注册表。77    ids.add(name)78    # 不自动添加后缀。79    return name808182def parse_rules(value, bindings, derived, ids) -> tuple[RulePlan, ...]:83    """检查普通布尔规则或数值窗口规则,保存参数与静态依赖。8485    参数:86        value (list): 规则配置数组。87        bindings (DeclarationBindings): 当前来源和名称声明关系。88        derived (tuple): 完整派生定义。89        ids (set[str]): 本计划节点ID集合,成功登记的规则ID加入其中。90    返回:91        tuple[RulePlan, ...]。保持规则顺序,参数按kind互斥;不产生事件或活动状态。92    异常:93        PlanValidationError: 字段、ID、kind、条件类型、初始触发或持续参数错误。94    """95    # 不排序规则,结果保持声明次序。96    result = []97    # 每种kind只接受自己的字段,不以大并集容忍无效参数。98    for index, raw in enumerate(array_at(value, '$.rules')):99        # 精确数组项路径。100        path = f'$.rules[{index}]'101        if not usable_node(path, raw):102            continue103        try:104            path = f'$.rules[{index}]'105            # 基本对象先检查,防止任意对象方法调用。106            if type(raw) is not dict:107                # 不将字符串规则当自由DSL。108                raise PlanValidationError(path, 'rule必须为对象')109            # 仅接受下列支持的规则类型,未知类型在加载配置时拒绝。110            rule_type = choice_at(raw.get('rule_type'), ('condition_duration', 'rising_edge', 'falling_edge', 'start_only', 'window_delta', 'debounce', 'hysteresis', 'start_end'), path + '.rule_type')111            if rule_type == 'start_end':112                raw = object_at(raw, path, ('name', 'rule_type', 'start_condition', 'end_condition'), ('timeout', 'emit_initial'))113                rule_id = register_id(raw['name'], path + '.name', ids)114                condition = expression_at(raw['start_condition'], path + '.start_condition', bindings, derived, boolean=True)115                end_condition = expression_at(raw['end_condition'], path + '.end_condition', bindings, derived, boolean=True)116                model_at(bindings.require_synchronized, path, tuple(sorted(set(condition.dependencies + end_condition.dependencies))))117                timeout = raw.get('timeout')118                timeout_ns = None if timeout is None else duration_at(timeout, path + '.timeout', positive=True)119                spec = StartEndSpec(timeout_ns, choice_at(raw.get('emit_initial', True), (False, True), path + '.emit_initial'))120                result.append(model_at(RulePlan, path, rule_id, rule_type, condition, None, None, False, None,121                                       exit_condition=end_condition, start_end=spec))122                continue123            if rule_type in ('debounce', 'hysteresis'):124                # 迟滞用明确的两个BOOL条件;普通防抖只需要一个条件和相反方向。125                required = ('name', 'rule_type', 'condition') if rule_type == 'debounce' else ('name', 'rule_type', 'enter_condition', 'exit_condition')126                raw = object_at(raw, path, required, ('enter_delay', 'exit_delay'))127                rule_id = register_id(raw['name'], path + '.name', ids)128                field = 'condition' if rule_type == 'debounce' else 'enter_condition'129                condition = expression_at(raw[field], path + '.' + field, bindings, derived, boolean=True)130                exit_condition = (expression_at(raw['exit_condition'], path + '.exit_condition', bindings, derived, boolean=True)131                                  if rule_type == 'hysteresis' else None)132                dependencies = condition.dependencies + (() if exit_condition is None else exit_condition.dependencies)133                # 两条件即使分别合法,联合判断仍须证明时钟可比较。134                model_at(bindings.require_synchronized, path, tuple(sorted(set(dependencies))))135                spec = StabilitySpec(duration_at(raw.get('enter_delay', '0s'), path + '.enter_delay'),136                                     duration_at(raw.get('exit_delay', '0s'), path + '.exit_delay'))137                result.append(model_at(RulePlan, path, rule_id, rule_type, condition, None, None, False, None,138                                       stability=spec, exit_condition=exit_condition))139                continue140            if rule_type == 'window_delta':141                # 数值窗口先生成条件,重触发另由边沿检测承担;不向DSL加入有状态函数。142                raw = object_at(raw, path, ('name', 'rule_type', 'signal', 'window', 'direction', 'threshold'),143                                ('monotonic', 'coverage_policy', 'emit_initial', 'retrigger'))144                rule_id = register_id(raw['name'], path + '.name', ids)145                signal = expression_at(raw['signal'], path + '.signal', bindings, derived)146                spec = model_at(WindowDeltaSpec, path,147                    duration_at(raw['window'], path + '.window', positive=True),148                    number_at(raw['threshold'], path + '.threshold'),149                    choice_at(raw['direction'], ('rise', 'fall', 'any'), path + '.direction'),150                    choice_at(raw.get('monotonic', False), (False, True), path + '.monotonic'),151                    choice_at(raw.get('coverage_policy', 'full'), ('full',), path + '.coverage_policy'))152                emit = choice_at(raw.get('emit_initial', False), (False, True), path + '.emit_initial')153                retrigger = choice_at(raw.get('retrigger', 'edge'), ('edge', 'once'), path + '.retrigger')154                result.append(model_at(RulePlan, path, rule_id, rule_type, signal, None, None, emit, None, spec, retrigger))155                continue156            # condition_duration与start_only有不同必须持续参数。157            required = ('name', 'rule_type', 'condition') + (('minimum_duration', 'duration_comparison') if rule_type == 'condition_duration' else ('window_duration',) if rule_type == 'start_only' else ())158            # condition_duration不接emit_initial,避免错误活动政策。159            raw = object_at(raw, path, required, ('selection_origin',) if rule_type == 'condition_duration' else ('emit_initial',))160            # ID和DSL都静态校验,不读取来源。161            rule_id = register_id(raw['name'], path + '.name', ids)162            # 条件必须为bool,并检查依赖来源是否声明共同时间基准;这不核实真实时钟误差。163            condition = expression_at(raw['condition'], path + '.condition', bindings, derived, boolean=True)164            # 仅condition_duration使用选择起点。165            selection = choice_at(raw.get('selection_origin', 'onset'), ('onset', 'confirmed'), path + '.selection_origin') if rule_type == 'condition_duration' else None166            # 开关严格bool,不能把文本false当True。167            emit = choice_at(raw.get('emit_initial', False), (False, True), path + '.emit_initial')168            # falling_edge不支持初始已知TRUE触发。169            if rule_type == 'falling_edge' and emit:170                # 不偷换首次状态的下降沿含义。171                raise PlanValidationError(path + '.emit_initial', 'falling不接受emit_initial=true')172            # condition_duration允许0,固定窗口必须正数。173            duration = duration_at(raw['minimum_duration'], path + '.minimum_duration') if rule_type == 'condition_duration' else None174            # 固定抑制不能以0长度循环重触发。175            fixed = duration_at(raw['window_duration'], path + '.window_duration', positive=True) if rule_type == 'start_only' else None176            # 保存声明,不在解析器内创建时序活动状态。177            comparison = (choice_at(raw['duration_comparison'], ('>=', '>'), path + '.duration_comparison')178                          if rule_type == 'condition_duration' else '>=')179            result.append(RulePlan(rule_id, rule_type, condition, duration, selection, emit, fixed,180                                   duration_comparison=comparison))181        except PlanValidationError as error:182            collect_node_error(error, path, raw)183    # 不保留用户可变数组。184    return tuple(result)185186187def parse_windows(value, rules, ids) -> tuple[WindowPlan, ...]:188    """检查事件窗口锚点、前后扩展和合并参数,不改变原事件声明。189190    参数:191        value (list): 窗口配置数组。192        rules (tuple[RulePlan, ...]): 可引用的已检查规则。193        ids (set[str]): 当前计划节点ID集合。194    返回:195        tuple[WindowPlan, ...]。持续规则只能使用activity锚点,其余使用point。196        exit_delay要求activity且post为0,start_only的post须等于固定窗口时长。197    异常:198        PlanValidationError: 引用、ID、锚点、扩展时间或合并参数冲突。199    副作用:200        登记窗口ID,但不运行合并或生成片段。201    """202    # 引用只允许当前计划规则。203    catalog = {item.rule_id: item for item in rules}204    # 结果保持声明顺序。205    result = []206    # 每项范围和合并约束都先于执行。207    for index, raw in enumerate(array_at(value, '$.windows')):208        # 错误关联具体窗口。209        path = f'$.windows[{index}]'210        if not usable_node(path, raw):211            continue212        try:213            path = f'$.windows[{index}]'214            # 没有自由metadata或未定义延迟参数。215            raw = object_at(raw, path, ('name', 'rule'), ('anchor', 'pre', 'post', 'merge', 'exit_delay'))216            # 窗口ID不能与其他节点冲突。217            window_id = register_id(raw['name'], path + '.name', ids)218            # 事件关联不是Python属性语法。219            event = text_at(raw['rule'], path + '.rule', identifier=True)220            # 未知规则不能通过空窗口回退。221            if event not in catalog:222                # 不自动生成新规则或选择首个事件。223                raise PlanValidationError(path + '.rule', f'未知规则:{event}','invalid_reference',224                    hint='填写rules中已声明的name:'+', '.join(catalog))225            # 活动锚点和真实point严格区分。226            # condition_duration及迟滞/防抖输出活动;窗口变化与普通边沿输出点。227            expected = 'activity' if catalog[event].temporal_rule_type == 'condition_duration' else 'point'228            # 事件已唯一决定锚点,只有显式填写时才需要核对一致性。229            anchor = choice_at(raw.get('anchor', expected), ('point', 'activity'), path + '.anchor')230            # 不能靠post窗口把point假称活动。231            if anchor != expected:232                # 保存事件本来语义。233                raise PlanValidationError(path + '.anchor', f'{catalog[event].rule_type}要求anchor={expected}')234            # pre/post不允许负数,包括量化前负小值。235            pre = duration_at(raw.get('pre', '0s'), path + '.pre')236            # 固定窗口抑制以post声明为准。237            # start_only的窗口继承规则时长,其他窗口未扩展的一侧为零。238            default_post = f'{catalog[event].window_duration_ns}ns' if catalog[event].rule_type == 'start_only' else '0s'239            post = duration_at(raw.get('post', default_post), path + '.post')240            # 合并政策只接受设计明确列出的种类。241            merge = choice_at(raw.get('merge', 'none'), ('none', 'overlap', 'overlap_and_adjacent', 'exit_delay'), path + '.merge')242            # exit_delay不能与其他merge模式一起隐藏。243            if (merge == 'exit_delay') != ('exit_delay' in raw):244                # 默认值不能使缺关键参数悄悄成功。245                raise PlanValidationError(path + '.exit_delay', 'exit_delay字段仅且必须用于同名merge模式')246            # exit_delay活动只扩合并上下文,不写回原事件统计区间。247            if merge == 'exit_delay' and (anchor != 'activity' or post != 0):248                # 不允许post再次扩大single分组。249                raise PlanValidationError(path + '.merge', 'exit_delay要求activity且post=0')250            # 关联start_only固定post不能改变抑制时长。251            if catalog[event].rule_type == 'start_only' and post != catalog[event].window_duration_ns:252                # 终点到期不自动重触发的运行政策由WINDOW验证。253                raise PlanValidationError(path + '.post', 'start_only关联post必须等于window_duration')254            # 延迟存在才解析,保留未使用None。255            delay = duration_at(raw['exit_delay'], path + '.exit_delay') if merge == 'exit_delay' else None256            # 不运行活动或合并事件,仅保存参数。257            result.append(WindowPlan(window_id, event, anchor, pre, post, merge, delay))258        except PlanValidationError as error:259            collect_node_error(error, path, raw)260    # 完整冻结声明顺序。261    return tuple(result)262263264def parse_calculations(value, bindings, derived, rules, windows, execution, ids) -> tuple[CalculationPlan, ...]:265    """检查指标统计范围、信号、条件、单位和覆盖政策。266267    参数:268        value (list): 指标配置数组。269        bindings (DeclarationBindings): 当前名称及来源同步声明。270        derived (tuple): 完整派生定义,用于静态依赖展开。271        rules (tuple[RulePlan, ...]): 原事件范围可引用的规则。272        windows (tuple[WindowPlan, ...]): 片段范围可引用的窗口。273        execution (ExecutionPlan): 提供默认partial_policy。274        ids (set[str]): 当前计划节点ID集合。275    返回:276        tuple[CalculationPlan, ...]。保存全会话、原事件或片段范围,明确样本统计、277        时间统计和选择行等口径;只检查声明,不生成数值或结果行。278    异常:279        PlanValidationError: 算子、ID、引用、信号类型、条件、单位或覆盖政策错误。280    处理流程:281        范围条件、数值信号和选择条件的原始依赖一起检查同步声明。282        select保留原单位;积分仅接受左侧保持'zoh';其他算子拒绝method字段。283    """284    # 当前计划范围引用不继承其他任务。285    rule_map, window_map = {item.rule_id: item for item in rules}, {item.window_id: item for item in windows}286    # 节点顺序与声明一致。287    result = []288    # 逐配置节点处理,不是逐样本数值计算。289    for index, raw in enumerate(array_at(value, '$.calculations')):290        # 数组错误位置明确。291        path = f'$.calculations[{index}]'292        if not usable_node(path, raw):293            continue294        try:295            path = f'$.calculations[{index}]'296            # 非integral的method字段稍后明确拒绝。297            version = getattr(bindings, 'config_version', 4)298            count_target = None299            is_object = isinstance(raw, dict) and raw.get('operation') in OBJECT_COUNTS300            if is_object:301                target_key = 'window' if raw['operation'] == 'segment_count' else 'rule'302                raw = object_at(raw, path, ('name', 'operation', target_key), ('scope', 'unit', 'partial_policy'))303                count_target = text_at(raw[target_key], path + '.' + target_key)304                targets = window_map if target_key == 'window' else rule_map305                if count_target not in targets:306                    raise PlanValidationError(path + '.' + target_key, '对象计数引用未声明的规则或窗口')307                if target_key == 'rule':308                    activity = rule_map[count_target].temporal_rule_type == 'condition_duration'309                    if activity != (raw['operation'] == 'activity_count'):310                        raise PlanValidationError(path + '.rule', '被计规则须产生相应的点事件或活动')311            else:312                optional = ('scope', 'expr', 'condition', 'unit', 'method', 'partial_policy', 'minimum_coverage')313                if isinstance(raw, dict) and raw.get('operation') in WITH_DDOF:314                    optional += ('ddof',)315                if isinstance(raw, dict) and raw.get('operation') == 'sample_quantile':316                    optional += ('q',)317                raw = object_at(raw, path, ('name', 'operation'), optional)318            # 所有节点ID唯一,不能覆盖窗口或规则。319            calculation_id = register_id(raw['name'], path + '.name', ids)320            # 模糊mean/min/max不进入执行计划。321            supported = ('select', 'sample_minimum', 'sample_maximum', 'sample_mean', 'min_time', 'max_time', 'time_weighted_mean', 'duration', 'integral')322            extensions = bindings.extensions.reductions if bindings.extensions is not None else {}323            operation = choice_at(raw['operation'], supported + (COUNT_OPERATIONS + MOMENTS + SAMPLE_QUANTILES) + tuple(extensions), path + '.operation')324            hosted=extensions.get(operation)325            q = raw.get('q')326            if operation in SAMPLE_QUANTILES:327                model_at(quantile_probability, path + '.q', operation, q)328            ddof = model_at(moment_ddof, path + '.ddof', operation,329                            number_at(raw['ddof'], path + '.ddof', integer=True) if 'ddof' in raw else None)330            # scope是配置前缀语法,不交给PythonDSL解析。331            scope = text_at(raw.get('scope', 'session'), path + '.scope')332            if is_object and scope != 'session':333                raise PlanValidationError(path + '.scope', '对象计数的首版范围为session')334            # 定义统计范围的条件依赖也加入来源同步声明检查。335            dependencies = set()336            # 全会话范围使用session,只有事件或片段范围需要引用ID。337            if scope != 'session':338                # 仅支持明确event/segment前缀和现有ID。339                prefix, separator, target = scope.partition(':')340                # 引用前缀和目标必须正确且唯一。341                if not separator or prefix not in ('event', 'segment') or target not in (rule_map if prefix == 'event' else window_map):342                    # 不自动选首个事件/片段。343                    raise PlanValidationError(path + '.scope', 'scope须为session或已定义的{rule: 名称}/{window: 名称}',344                        'invalid_reference',hint='核对统计范围;可用值:'+', '.join(345                            ['session']+['{rule: '+name+'}' for name in rule_map]+['{window: '+name+'}' for name in window_map]))346                # segment实际由窗口所关联规则产生。347                event = target if prefix == 'event' else window_map[target].event348                # 指标值与产生范围的条件也必须同步。349                dependencies.update(rule_map[event].dependencies)350            # 明确选择政策,不将低覆盖补成已知。351            partial = choice_at(raw.get('partial_policy', execution.partial_policy), ('report', 'strict'), path + '.partial_policy')352            # minimum_coverage只作质量门槛,保留原数值。353            coverage = number_at(raw.get('minimum_coverage', 0), path + '.minimum_coverage', minimum=0, maximum=1)354            # condition可选,但存在时必须BOOLDSL。355            condition = expression_at(raw['condition'], path + '.condition', bindings, derived, boolean=True) if 'condition' in raw else None356            # expr统一接收简单别名或白名单表达式;依赖和单位仍按现有编译器检查。357            name = text_at(raw['expr'], path + '.expr') if 'expr' in raw else None358            # 只编译受限表达式,不执行Python或属性访问。359            signal = expression_at(name, path + '.expr', bindings, derived,unit=hosted.input.unit if hosted else None) if name is not None else None360            if hosted and signal is not None and signal.root.dtype is not hosted.input.dtype:361                raise PlanValidationError(path+'.expr','宿主归约输入 dtype 与描述不符')362            # duration选择BOOL信号或明确条件,避免两个持续条件含义歧义。363            if is_object:364                unit = choice_at(raw.get('unit', '1'), ('1',), path + '.unit')365            elif operation == 'duration':366                # BOOL值不经过数值truthiness。367                if (signal is None) == (condition is None) or signal is not None and signal.root.dtype is not D.BOOL:368                    # 一项必要条件必须明确。369                    raise PlanValidationError(path, 'duration必须且只能提供BOOL expr或condition')370                # 设计首版持续结果统一秒。371                unit = choice_at(raw.get('unit', 's'), ('s',), path + '.unit')372            else:373                # select和数值统计都必须有实际列声明。374                if signal is None:375                    # 条件本身不能替代数值输入。376                    raise PlanValidationError(path + '.expr', '该指标必须提供expr')377                # select允许保留BOOL/ENUM行,其余数值口径明确排除。378                if operation != 'select' and signal.root.dtype not in (D.INT64, D.UINT64, D.FLOAT64):379                    # 不把业务标签或bool隐式数值化。380                    raise PlanValidationError(path + '.expr', '数值指标必须int64/uint64/float64')381                # 选择行流与标量使用同一完整转换描述符检查目标单位。382                version = getattr(bindings, 'config_version', 4)383                if operation in ('integral', 'sample_rms', 'time_rms'):384                    # 默认显示规范化不能绕过摄氏直接积分限制;显式to_unit(K)是用户已选尺度。385                    input_root = signal.root386                    while input_root.operator == 'unit_convert' and not input_root.unit_explicit:387                        input_root = input_root.args[0]388                    input_unit = model_at(resolve_unit, path + '.expr', input_root.unit)389                    if input_unit is not None and input_unit.offset:390                        raise PlanValidationError(path + '.expr', f'摄氏绝对温度{operation}请先显式to_unit(...,"K")')391                if hosted is not None:392                    # 已选真实样本的单位转换在输入表达式完成;归约输出可能属于不同量纲。393                    unit_spec = CalculationDefinition(calculation_id,'sample_minimum',Interval(0,0),394                        output_unit=raw.get('unit',hosted.output.unit),config_version=4)395                    output = model_at(calculation_conversion,path+'.unit',unit_spec,hosted.output.unit)396                    model_at(output.dtype,path+'.unit',hosted.output.dtype)397                    unit=output.unit398                else:399                    spec = model_at(CalculationDefinition, path, calculation_id, operation, Interval(0, 0),400                                    partial, raw.get('unit'), config_version=version, ddof=ddof, q=q)401                    conversion = model_at(calculation_conversion, path + '.unit', spec, signal.root.unit)402                    # 在开始读取前拒绝BOOL/ENUM换数值单位,目标值类型由系数静态决定。403                    if operation == 'select':404                        model_at(conversion.dtype, path + '.unit', signal.root.dtype)405                    unit = conversion.unit406            # method只允许积分zoh,不静默忽略其他算子字段。407            if 'method' in raw and operation != 'integral':408                # 模糊method可能改变业务统计含义。409                raise PlanValidationError(path + '.method', 'method仅用于integral')410            # 首版积分固定左侧保持,不开放未实现插值。411            method = choice_at(raw.get('method', 'zoh'), ('zoh',), path + '.method') if operation == 'integral' else None412            # 值与条件依赖都包含在当前节点同步证明中。413            for expression in (signal, condition):414                # 未配置条件不制造额外来源。415                if expression is not None:416                    # 使用已经展开的原信号依赖。417                    dependencies.update(expression.dependencies)418            # 条件和统计范围跨来源时,也须声明共同时间起点。419            model_at(bindings.require_synchronized, path, tuple(sorted(dependencies)))420            # 保存明确统计范围,窗口合并不能改变原事件指标的区间。421            result.append(CalculationPlan(calculation_id, scope, operation, signal, condition, unit, partial, coverage,422                                          method, count_target, ddof=ddof, q=q))423        except PlanValidationError as error:424            collect_node_error(error, path, raw)425    # 后续运行只读这些声明,不持有用户数组。426    return tuple(result)427
Chronaxis 用户手册 · 离线阅读