Chronaxis用户手册English
本页目录

chronaxis.fast.config

源码基线:8904c2f744951857659abd40aaa3535d57faf53c

1"""把一个CAN来源编译成fast会话,共用Chronaxis的单位、类型与来源位置规则。23会话保存完整输入序列、合并方式及资源额度,工作进程接收其中一个文件的声明。4"""5from dataclasses import asdict6from hashlib import sha2567from pathlib import Path89from ..configuration import ConfigurationError, load_configuration10from ..configuration.diagnostics import locate_error11from ..configuration.imports import expand_document_imports12from ..legacy.binding import dbc_candidates13from ..planning.checks import PlanValidationError, configuration_error14from ..planning.dbc_defaults import load_dbc, scoped_dbc_cache15from ..planning.plan import AnalysisPlan16from ..sources.asc import ASCSpec17from ..sources.can_spec import LegacyCANSpec, can_field_from_key18from .compute import encode_program, _prepare_program_metric192021def _unsupported(path, message):22    """构造可由已有配置诊断和 CLI 消费的错误。"""23    return PlanValidationError(path, message, 'unsupported_fast', hint='修改配置或使用通用 chronaxis run')242526def _capabilities(raw):27    """仅检查显式请求,避免把通用计划的默认设置误判为用户要求。"""28    errors = []29    for key in ('derived', 'lookups', 'rules', 'windows', 'sequences', 'calculations', 'presets',30                'aggregations', 'transforms', 'intervals', 'preset_bindings'):31        if raw.get(key):32            errors.append(_unsupported('$.' + key, 'fast 只执行当前帧数值公式'))33    for key in ('execution', 'diagnostics'):34        group = raw.get(key)35        if isinstance(group, dict):36            for field, value in group.items():37                if key == 'execution' and field in ('process_count', 'memory_budget_bytes', 'spool_quota_bytes'):38                    continue39                if key == 'diagnostics' and value is False:40                    continue41                errors.append(_unsupported(f'$.{key}.{field}', 'fast 不执行该资源或诊断策略'))42    outputs = raw.get('outputs')43    if isinstance(outputs, dict):44        for key in ('signal_csv', 'can_frames'):45            if outputs.get(key) is not None:46                errors.append(_unsupported('$.outputs.' + key, 'fast 只发布指标汇总'))47        if outputs.get('contributions', 'none') != 'none':48            errors.append(_unsupported('$.outputs.contributions', 'fast 不生成逐帧明细'))49    sources = raw.get('sources')50    if isinstance(sources, list):51        if len(sources) != 1:52            errors.append(_unsupported('$.sources', '当前 fast 任务须有一个来源'))53        for i, src in enumerate(sources):54            if not isinstance(src, dict):55                continue56            if 'legacy_profile' in src:57                errors.append(_unsupported(f'$.sources[{i}].legacy_profile', '显式锁定的 reader 与 fast 冲突'))58            reader = src.get('reader')59            if isinstance(reader, dict):60                for key, value in reader.items():61                    if (key,value) not in (('input_check','auto'), ('decode_scope','requested'), ('decode_policy','strict')):62                        errors.append(_unsupported(f'$.sources[{i}].reader.{key}', 'fast 仅支持按需读取与严格长度检查'))63    metrics = raw.get('message_calculations')64    if not metrics:65        errors.append(_unsupported('$.message_calculations', '至少需要一项逐消息累计'))66    elif isinstance(metrics, list):67        for i, item in enumerate(metrics):68            if isinstance(item, dict) and 'profile' in item:69                errors.append(_unsupported(f'$.message_calculations[{i}].profile', '显式锁定的解码器与 fast 冲突'))70    return errors717273def _source(plan):74    """展开同一来源的文件和时间解释,不打开日志。7576    参数: 静态AnalysisPlan。返回: 可序列化会话声明。异常: 不支持来源或解码策略。77    """78    source, = plan.sources79    cfg = source.config80    if source.source_type not in ('can_blf', 'can_asc', 'asc_or_blf'):81        raise _unsupported('$.sources[0]', 'fast只读取ASC/BLF')82    if cfg.decode_policy != 'strict':83        raise _unsupported('$.sources[0].reader', 'fast要求严格帧长')84    inputs = []85    if isinstance(cfg, LegacyCANSpec):86        inputs = [dict(path=str(path), offset_ns=cfg.offset_ns) for path in cfg.input_paths]87    else:88        for item in cfg.inputs:89            if isinstance(item, ASCSpec):90                inputs.append(dict(path=str(item.path), asc_options={k:v for k,v in asdict(item).items() if k != 'path'}))91            else:92                inputs.append(dict(path=str(item.path), offset_ns=item.offset_ns))93    if not inputs:94        raise _unsupported('$.sources[0].paths', 'fast需要至少一个输入文件')95    domain = plan.dataset.domain96    return dict(**inputs[0], inputs=inputs, dbc=str(cfg.dbc_path), channel=cfg.channel + 1,97                domain_ns=[domain.start_ns, domain.end_ns] if domain else None,98                table_format=plan.outputs.tables, file_merge=cfg.file_merge,99                process_count=plan.execution.process_count,100                memory_budget_bytes=plan.execution.memory_budget_bytes,101                spool_quota_bytes=plan.execution.spool_quota_bytes)102103104def _flag(selector, fields, name):105    """合并同一公式中每个信号和选择器的身份限制;显式矛盾不可合并。"""106    flags = {getattr(v,name) for v in (selector,*fields)} - {None}107    if len(flags) > 1:108        raise ValueError('同一帧公式的 ' + name + ' 互相冲突')109    return next(iter(flags), None)110111112def _metric(plan, item, database):113    """静态验证本次请求的信号及原单位,将已检查的表达式树编码为简单数值指令。"""114    required = set(item.expr.dependencies)115    if item.disjunction:116        required.add(item.disjunction.signal)117    signals, fields = {}, {}118    for name in sorted(required):119        definition = plan.bindings.resolve(name)120        if definition.dtype.value not in ('int64','uint64','float64'):121            raise ValueError(name + ' 需要数值类型')122        if definition.valid_range or definition.invalid_values:123            raise ValueError(name + ' 声明了 fast 尚不支持的有效范围或无效值')124        if definition.unit_status != 'verified':125            raise ValueError(name + ' 未验证单位或换算')126        field = can_field_from_key(definition.field)127        fields[name] = field128        signals[name] = dict(signal=field.signal, dtype=definition.dtype.value,129                             scale=definition.scale, offset=definition.offset)130        if definition.input_dtype is not None:131            signals[name]['input_dtype'] = definition.input_dtype.value132    selector = item.update_message133    extended = _flag(selector, fields.values(), 'is_extended')134    fd = _flag(selector, fields.values(), 'is_fd')135    messages = dbc_candidates(database, selector.frame_id, [v.signal for v in fields.values()], extended, fd)136    if len(messages) != 1:137        raise ValueError('DBC 候选不唯一,请补充扩展帧或 FD 标志')138    message, = messages139    if message.is_container or message.is_multiplexed():140        raise ValueError('fast 首版不支持 container 或 multiplex 报文')141    for name, field in fields.items():142        unit = message.get_signal_by_name(field.signal).unit143        if unit and unit != plan.bindings.resolve(name).input_unit:144            raise ValueError(name + ' 的 DBC 单位与声明的输入单位不符')145    direction = item.disjunction146    spec = dict(name=item.calculation_id,frame_id=selector.frame_id,is_extended=message.is_extended_frame,147                is_fd=fd,expr=item.expr.text,unit=item.unit,signals=signals,148                parameters={p.name:p.value for p in item.parameters},149                direction=dict(signal=fields[direction.signal].signal,bucket=direction.bucket) if direction else None,150                output_scale=1,program=encode_program(item.expr.root))151    # 公共编译器已检查原式;只绑定已编码指令,避免第二套文本语法重新解释同一公式。152    _prepare_program_metric(spec)153    return spec, message154155156def _compile_plan(plan, with_database):157    """编译公共静态计划的单会话声明,保留全部分卷;仅加载本计划的DBC元数据。"""158    import cantools159    errors = []160    result = _source(plan)161    try:162        before = sha256(Path(result['dbc']).read_bytes()).hexdigest()163        database = load_dbc(result['dbc'])164        if sha256(Path(result['dbc']).read_bytes()).hexdigest() != before:165            raise ValueError('编译期间DBC内容变化')166        result['_dbc_digest'] = before167    except (OSError, ValueError, cantools.database.errors.Error) as exc:168        raise PlanValidationError('$.sources[0].dbc_by_channel', str(exc)) from exc169    metrics = []170    bindings = {}171    for i, item in enumerate(plan.message_calculations):172        try:173            metric, message = _metric(plan,item,database)174            # 同一实际帧只解码一次。不同公式若各自选中不同定义,不能等到 worker 才拒绝。175            fd = metric['is_fd']176            for actual_fd in ((False, True) if fd is None else (fd,)):177                key = (metric['frame_id'], metric['is_extended'], actual_fd)178                decoder = None if actual_fd and not message.is_fd else message179                if bindings.setdefault(key,decoder) is not decoder:180                    raise ValueError(f'ID {key[0]:#x} 在同一实际帧型中对应多个 DBC 定义')181            metrics.append(metric)182        except (TypeError, ValueError, OverflowError) as exc:183            errors.append(_unsupported(f'$.message_calculations[{i}]', str(exc)))184    if errors:185        raise PlanValidationError.from_diagnostics(errors)186    result['metrics'] = metrics187    if with_database:188        result['_database'] = database189    return result190191192def compile_template_plan(plan, *, template_path=None, _with_database=False):193    """为 batch.template_jobs 展开的一个真实文件编译 fast 任务。194195    只使用原模板中显式声明的选项判断能力冲突,不把通用缺省值解释为用户要求。196    模板绑定错误带模板路径及当前文件说明。此步骤不执行分析或打开原始日志。197    """198    from ..planning.checks import json_value199    raw = json_value(plan.raw_config)200    try:201        errors = _capabilities(raw)202        if errors:203            raise PlanValidationError.from_diagnostics(errors)204        return _compile_plan(plan, _with_database)205    except PlanValidationError as exc:206        if template_path is not None:207            source = raw['sources'][0]208            input_path = source.get('path', source.get('paths', ''))209            for diagnostic in exc.diagnostics:210                diagnostic.source = str(template_path)211                diagnostic.hint = f'模板输入 {input_path}。' + (diagnostic.hint or '')212                diagnostic.refresh_text()213            exc.source = str(template_path)214            exc.refresh_text()215        raise216217218@scoped_dbc_cache()219def compile_config(path, *, _with_database=False):220    """读取配置和导入定义,验证DBC与fast子集。默认返回可序列化会话声明。221222    参数 path 是 JSON/YAML 文件路径;_with_database 仅给同进程立即执行时复用数据库。223    错误带配置路径。此入口只准备计算,不打开日志或创建输出目录。224    """225    target = Path(path).absolute()226    try:227        doc = expand_document_imports(load_configuration(target))228    except ConfigurationError as exc:229        raise configuration_error(exc) from exc230    try:231        if not isinstance(doc.data,dict):232            raise _unsupported('$', '需要单任务分析配置')233        errors = _capabilities(doc.data)234        if errors:235            raise PlanValidationError.from_diagnostics(errors)236        plan = AnalysisPlan.from_dict(doc.data,base_dir=target.parent)237        return _compile_plan(plan, _with_database)238    except PlanValidationError as exc:239        raise locate_error(exc,doc)240
Chronaxis 用户手册 · 离线阅读