chronaxis._control_models
源码基线:8904c2f744951857659abd40aaa3535d57faf53c
1"""三条内部控制通道的固定模型转换;协议只允许这里显式列出的构造器。"""2from collections.abc import Mapping3from math import isfinite4from pathlib import Path56_SIGNAL = ('name source field dtype unit valid_for_ns input_unit scale offset choices valid_range '7 'invalid_values sample_policy same_time_policy semantic_id semantic_contract_version '8 'mapping_version sign_convention measurement_location input_sign_convention direction_factor '9 'config_version input_dtype code_dtype capture_code').split()10_ASC = 'path parser_version encoding time_reference timezone_offset_minutes offset_ns'.split()11_CSV_TIME = 'format unit offset_ns timezone_offset_minutes rollover_min_drop_ns'.split()12_COLUMN = 'column fixed value_map strip'.split()13_LAYOUT = 'fields encoding delimiter has_header skip_rows layout_version'.split()14_RULE = 'channel frame_id continuous is_extended is_fd protected same_time_policy'.split()15_RESULT = ('job_id status plan_path plan_identity dataset_id entity_id session_id input_identity run_id '16 'output_dir manifest_path error_type error_message error_chain elapsed_seconds memory_budget_bytes worker_pid').split()17_OPTIONS = set(('classify_unselected cycle_fields metadata_fields integrity trace retain_source_time selected_ids '18 'time_frame_ids emit_frame_ids blf_time_scope legacy_blf_decode').split())192021def native(value, depth=0):22 """复制控制值为有限JSON树。2324 参数: value原生值或已冻结映射,depth从0累计且最多320层。25 返回: 独立list/dict及原生标量。26 异常: 类型、深度或非有限数错误。27 """28 if depth > 320:29 raise ValueError('控制数据嵌套超过320层')30 if value is None or type(value) in (str, bool, int):31 return value32 if type(value) is float:33 if not isfinite(value):34 raise ValueError('控制数据需要有限数值')35 return value36 if isinstance(value, Mapping):37 if any(type(key) is not str for key in value):38 raise ValueError('控制映射键需要文本')39 return {key: native(item, depth+1) for key,item in value.items()}40 if type(value) in (list, tuple):41 return [native(item, depth+1) for item in value]42 raise TypeError('控制数据含未声明类型')434445def keys(data, names):46 """核对模型的完整字段集合。4748 参数: data字典,names允许且必需的字段名序列。49 返回: 原字典。50 异常: 缺项、多项或非字典时ValueError。51 """52 if type(data) is not dict or set(data) != set(names):53 raise ValueError('控制模型字段不符:' + ','.join(names))54 return data555657def extract(value, names):58 """摘取固定模型的明确字段。5960 参数: value已知模型,names调用点写定的字段名序列。61 返回: 字段名到原值的dict。62 异常: 缺少已声明属性时AttributeError。63 """64 return {name: getattr(value, name) for name in names}656667def _scalar(value):68 """编码信号无效值标记。6970 参数: value原生标量,float允许NaN/Inf。71 返回: 二元素列表,分别为固定种类及无损原值。72 异常: 未支持类型时ValueError。73 """74 if type(value) is float:75 return ['float', value.hex()]76 if value is None or type(value) in (str, bool, int):77 return ['value', value]78 raise ValueError('信号无效值类型不可传输')798081def _restore_scalar(data):82 """恢复信号无效值标记。8384 参数: data固定二元素列表。85 返回: 原生标量,保留float与整数区别。86 异常: 种类、数量或浮点文本非法时ValueError。87 """88 if type(data) is not list or len(data) != 2:89 raise ValueError('信号无效值编码非法')90 if data[0] == 'float' and type(data[1]) is str:91 return float.fromhex(data[1])92 if data[0] == 'value' and (data[1] is None or type(data[1]) in (str,bool,int)):93 return data[1]94 raise ValueError('信号无效值编码非法')959697def pack_signal(signal):98 """展开已冻结信号声明。99100 参数: signal为SignalDefinition。101 返回: JSON原生字段字典。102 异常: 模型类型或字段值非法。103 """104 from .core.binding import SignalDefinition105 if type(signal) is not SignalDefinition:106 raise TypeError('CAN控制信号需要SignalDefinition')107 value = extract(signal, _SIGNAL)108 for name in ('dtype', 'input_dtype', 'code_dtype'):109 value[name] = value[name].value if value[name] is not None else None110 value['invalid_values'] = [_scalar(item) for item in value['invalid_values']]111 return native(value)112113114def restore_signal(value):115 """恢复明确的信号声明。116117 参数: value必须包含_SIGNAL全部字段。118 返回: SignalDefinition,其派生单位状态由构造重新核对。119 异常: 字段、dtype或模型约束错误。120 """121 from .core.binding import SignalDefinition122 from .core.types import SignalDType123 value = dict(keys(value, _SIGNAL))124 for name in ('dtype', 'input_dtype', 'code_dtype'):125 value[name] = SignalDType(value[name]) if value[name] is not None else None126 value['choices'] = tuple(tuple(item) for item in value['choices'])127 value['valid_range'] = tuple(value['valid_range']) if value['valid_range'] is not None else None128 value['invalid_values'] = tuple(_restore_scalar(item) for item in value['invalid_values'])129 return SignalDefinition(**value)130131132def pack_input(item):133 """展开一个CAN文件输入。134135 参数: item为ASCSpec、BLFSpec或CANCSVSpec。136 返回: 固定种类及字段字典的二元素列表。137 异常: 未知输入类型时TypeError。138 """139 from .sources.asc import ASCSpec140 from .sources.blf import BLFSpec141 from .sources.raw_csv import CANCSVSpec142 if type(item) is ASCSpec:143 value = extract(item, _ASC)144 value['path'] = str(value['path'])145 return ['asc', native(value)]146 if type(item) is BLFSpec:147 return ['blf', dict(path=str(item.path), offset_ns=item.offset_ns)]148 if type(item) is CANCSVSpec:149 layout = extract(item.layout, _LAYOUT)150 layout['fields'] = {name: extract(mapping, _COLUMN) for name,mapping in item.layout.fields.items()}151 return ['csv', native(dict(path=str(item.path), layout=layout,152 time=extract(item.time,_CSV_TIME), id_base=item.id_base, length_base=item.length_base,153 channel_origin=item.channel_origin, payload_style=item.payload_style))]154 raise TypeError('CAN控制输入类型未声明')155156157def restore_input(data):158 """恢复一个CAN文件输入。159160 参数: data为固定种类及字段字典。161 返回: ASCSpec、BLFSpec或CANCSVSpec,不读取输入。162 异常: 种类、字段或原模型约束错误。163 """164 from .sources.asc import ASCSpec165 from .sources.blf import BLFSpec166 from .sources.raw_csv import CANCSVSpec, CSVTimeSpec167 from .sources.raw_csv_layout import CANCSVLayout, ColumnMapping168 if type(data) is not list or len(data) != 2:169 raise ValueError('CAN输入声明结构非法')170 kind, value = data171 if kind == 'asc':172 value = dict(keys(value, _ASC))173 value['path'] = Path(value['path'])174 return ASCSpec(**value)175 if kind == 'blf':176 keys(value, ('path','offset_ns'))177 return BLFSpec(Path(value['path']),value['offset_ns'])178 if kind == 'csv':179 value = dict(keys(value, ('path','layout','time','id_base','length_base','channel_origin','payload_style')))180 layout = dict(keys(value['layout'], _LAYOUT))181 if type(layout['fields']) is not dict:182 raise ValueError('CAN CSV列映射需要对象')183 layout['fields'] = {name:ColumnMapping(**keys(mapping,_COLUMN)) for name,mapping in layout['fields'].items()}184 value.update(path=Path(value['path']), layout=CANCSVLayout(**layout),185 time=CSVTimeSpec(**keys(value['time'],_CSV_TIME)))186 return CANCSVSpec(**value)187 raise ValueError('CAN输入类型未知')188189190def pack_source(source):191 """展开单次CAN来源。192193 参数: source为CANTextSpec。194 返回: 原生字段字典,路径按现有文本保留。195 异常: 类型或嵌套模型错误。196 """197 from .sources.text_can_spec import CANTextSpec198 if type(source) is not CANTextSpec:199 raise TypeError('CAN控制来源需要CANTextSpec')200 return dict(name=source.name,inputs=[pack_input(item) for item in source.inputs],201 dbc_path=str(source.dbc_path), channel=source.channel,202 timebase=dict(timebase_id=source.timebase.timebase_id,time_type=source.timebase.time_type.value,203 origin=source.timebase.origin),204 decode_policy=source.decode_policy,input_check=source.input_check,file_merge=source.file_merge)205206207def restore_source(value):208 """恢复单文件CAN来源。209210 参数: value为固定来源字段字典,inputs必须恰好一份。211 返回: CANTextSpec,不打开DBC或日志。212 异常: 字段、时间类型或输入数量错误。213 """214 from .sources.text_can_spec import CANTextSpec215 from .core.models import TimebaseSpec216 from .core.types import TimeType217 value = dict(keys(value, ('name','inputs','dbc_path','channel','timebase','decode_policy','input_check','file_merge')))218 base = keys(value['timebase'], ('timebase_id','time_type','origin'))219 if type(value['inputs']) is not list or len(value['inputs']) != 1:220 raise ValueError('CAN工作进程需要恰好一个输入')221 value.update(inputs=tuple(restore_input(item) for item in value['inputs']),222 dbc_path=Path(value['dbc_path']),223 timebase=TimebaseSpec(base['timebase_id'],TimeType(base['time_type']),base['origin']))224 return CANTextSpec(**value)225226227def pack_options(options):228 """展开CAN读取关键字。229230 参数: options仅包含_OPTIONS白名单。231 返回: 原生字典,周期定位显式展开,ID集合变列表。232 异常: 回调、未知关键字或未支持值类型错误。233 """234 from .legacy.records import field_to_dict235 if type(options) is not dict or not set(options) <= _OPTIONS:236 raise ValueError('CAN控制选项不在固定白名单')237 result = dict(options)238 for name in ('cycle_fields','metadata_fields'):239 if name in result:240 result[name] = [field_to_dict(value) for value in result[name]]241 for name in ('selected_ids','time_frame_ids','emit_frame_ids'):242 if result.get(name) is not None:243 result[name] = list(result[name])244 return native(result)245246247def restore_options(options):248 """恢复CAN读取关键字。249250 参数: options为白名单原生字典。251 返回: 含固定LegacyCANField元组及ID元组的dict。252 异常: 未知选项或非法定位模型错误。253 """254 from .legacy.records import field_from_dict255 if type(options) is not dict or not set(options) <= _OPTIONS:256 raise ValueError('CAN控制选项不在固定白名单')257 result = dict(options)258 for name in ('cycle_fields','metadata_fields'):259 if name in result:260 result[name] = tuple(field_from_dict(value) for value in result[name])261 for name in ('selected_ids','time_frame_ids','emit_frame_ids'):262 if result.get(name) is not None:263 result[name] = tuple(result[name])264 return result265266267def pack_frame_context(value):268 """展开所需帧重建声明。269270 参数: value为None或规则元组、周期开关、完整性开关。271 返回: None或固定三元素列表。272 异常: 非原生字段错误。273 """274 if value is None:275 return None276 rules, cycles, integrity = value277 return [native([extract(rule,_RULE) for rule in rules]), cycles, integrity]278279280def restore_frame_context(value):281 """恢复固定帧重建上下文。282283 参数: value为None或三元素列表。284 返回: None或FrameRebuildRule元组与两个bool。285 异常: 字段、开关或规则校验错误。286 """287 from .results.frame_patch import FrameRebuildRule288 if value is None:289 return None290 if type(value) is not list or len(value) != 3 or type(value[1]) is not bool or type(value[2]) is not bool:291 raise ValueError('CAN帧重建上下文非法')292 rules = []293 for row in value[0]:294 row = dict(keys(row,_RULE))295 row['continuous'], row['protected'] = tuple(row['continuous']), tuple(row['protected'])296 rules.append(FrameRebuildRule(**row))297 return tuple(rules), value[1], value[2]298299300def pack_result(result):301 """展开无活动资源的批量结果。302303 参数: result为recovery为None的BatchJobResult。304 返回: 固定原生结果字典。305 异常: 未知类型或仍持恢复句柄时TypeError。306 """307 from .batch.models import BatchJobResult308 if type(result) is not BatchJobResult or result.recovery is not None:309 raise TypeError('批量结果只能发送无活动恢复对象的事实')310 value = extract(result,_RESULT)311 for name in ('output_dir','manifest_path'):312 value[name] = str(value[name]) if value[name] is not None else None313 return native(value)314315316def restore_result(value):317 """恢复批量任务结果事实。318319 参数: value包含_RESULT全部字段。320 返回: BatchJobResult,指定路径字段恢复为Path,恢复对象仍为空。321 异常: 字段或结果模型约束错误。322 """323 from .batch.models import BatchJobResult324 value = dict(keys(value,_RESULT))325 for name in ('output_dir','manifest_path'):326 value[name] = Path(value[name]) if value[name] is not None else None327 value['error_chain'] = tuple(value['error_chain'])328 return BatchJobResult(**value)329330331def pack_message(family, message):332 """展开固定控制命令的参数。333334 参数: family为can/fast/batch,message为现有命令tuple。335 返回: JSON原生参数列表。336 异常: 未支持对象或固定模型错误。337 """338 operation, *arguments = message339 if family == 'can' and operation == 'file':340 source, signals, options, limit, context = arguments341 arguments = [pack_source(source),[pack_signal(item) for item in signals],342 pack_options(options),limit,pack_frame_context(context)]343 elif family == 'batch' and operation == 'result':344 result, pending = arguments345 arguments = [pack_result(result),pending]346 elif family == 'batch' and operation == 'fast_result':347 # fast工作结果是已知字段dict,Path只在这两个固定结果位置允许。348 arguments[0] = dict(arguments[0])349 for name in ('output_dir','manifest_path'):350 value = arguments[0].get(name)351 if isinstance(value,Path):352 arguments[0][name] = str(value)353 return native(arguments)354355356def restore_message(family, operation, arguments):357 """恢复固定命令参数。358359 参数: family通道、operation命令、arguments原生参数列表。360 返回: 现有消费者使用的tuple,仅恢复写定模型。361 异常: 数量、类型或模型校验错误。362 """363 if type(arguments) is not list:364 raise ValueError('控制消息参数需要列表')365 if family == 'can' and operation == 'file':366 if len(arguments) != 5:367 raise ValueError('CAN文件命令参数数量不符')368 source, signals, options, limit, context = arguments369 if type(signals) is not list or len(signals) > 4096:370 raise ValueError('CAN信号声明数量非法')371 if type(limit) is not int or limit <= 0:372 raise ValueError('CAN工作预算非法')373 arguments = [restore_source(source),tuple(restore_signal(item) for item in signals),374 restore_options(options),limit,restore_frame_context(context)]375 elif family == 'batch' and operation == 'result':376 if len(arguments) != 2 or type(arguments[1]) is not bool:377 raise ValueError('批量结果参数非法')378 arguments = [restore_result(arguments[0]),arguments[1]]379 elif family == 'batch' and operation == 'fast_result':380 if len(arguments) not in (2,3) or type(arguments[0]) is not dict or type(arguments[1]) is not bool:381 raise ValueError('fast批量结果参数非法')382 result = dict(arguments[0])383 for name in ('output_dir','manifest_path'):384 if result.get(name) is not None:385 result[name] = Path(result[name])386 if 'error_chain' in result:387 result['error_chain'] = tuple(result['error_chain'])388 arguments[0] = result389 elif family == 'can' and operation in ('next','close') and arguments:390 raise ValueError('CAN无参数命令包含多余参数')391 return (operation, *arguments)392393394395_FAST_KEYS = set(('path dbc channel offset_ns asc_options domain_ns table_format file_merge process_count '396 'memory_budget_bytes spool_quota_bytes metrics inputs _dbc_digest _profile').split())397_METRIC_KEYS = set(('name frame_id is_extended is_fd expr unit signals parameters direction output_scale program').split())398399400def _object(value, required, allowed):401 """核对有限对象字段。402403 参数: value字典,required必需字段,allowed全部允许字段。404 返回: None,不改输入。405 异常: 缺项、未知字段或类型错误。406 """407 if type(value) is not dict or not set(required)<=set(value) or not set(value)<=set(allowed):408 raise ValueError('控制对象字段不符')409410411def _program(value, depth=0):412 """核对已有数值指令形状。413414 参数: value固定指令dict,depth从0累计且最多128。415 返回: None,不重新编译文本表达式。416 异常: 未知操作、dtype、参数或深度错误。417 """418 if depth>128:419 raise ValueError('fast控制指令深度超过128')420 _object(value,('op','dtype'),('op','dtype','name','value','args','scale','offset'))421 operation=value['op']422 if value['dtype'] not in ('int64','uint64','float64'):423 raise ValueError('fast控制指令数值类型非法')424 if operation=='name':425 keys(value,('op','dtype','name'))426 if type(value['name']) is not str:raise ValueError('fast指令名称非法')427 elif operation=='const':428 keys(value,('op','dtype','value'))429 if type(value['value']) not in (int,float):raise ValueError('fast指令常量非法')430 elif operation in ('add','sub','mul','div','abs','neg','to_float','unit_convert'):431 names=('op','dtype','args','scale','offset') if operation=='unit_convert' else ('op','dtype','args')432 keys(value,names)433 expected=2 if operation in ('add','sub','mul','div') else 1434 if type(value['args']) is not list or len(value['args'])!=expected:435 raise ValueError('fast指令参数数量非法')436 if operation=='unit_convert' and any(type(value[k]) not in (int,float) for k in ('scale','offset')):437 raise ValueError('fast指令换算系数非法')438 for argument in value['args']:_program(argument,depth+1)439 else:440 raise ValueError('fast控制指令未知')441442443def check_fast_spec(value):444 """核对有限fast文件声明。445446 参数: value原生spec字典。447 返回: None,不加载数据库或运行公式。448 异常: 未知字段、缺项或固定指令错误。449 """450 _object(value,('path','dbc','channel','metrics'),_FAST_KEYS)451 if type(value['path']) is not str or type(value['dbc']) is not str or type(value['channel']) is not int:452 raise ValueError('fast路径或通道类型非法')453 if type(value['metrics']) not in (tuple,list) or not value['metrics']:454 raise ValueError('fast需要非空计算项列表')455 for item in value['metrics']:456 _object(item,('name','frame_id','expr','unit','signals'),_METRIC_KEYS)457 if any(type(item[key]) is not str for key in ('name','expr','unit')) or type(item['frame_id']) is not int:458 raise ValueError('fast计算项字段类型非法')459 if type(item['signals']) is not dict:460 raise ValueError('fast信号需要对象')461 for signal in item['signals'].values():462 _object(signal,('signal','dtype'),('signal','dtype','input_dtype','scale','offset'))463 if type(signal['signal']) is not str or signal['dtype'] not in ('int64','uint64','float64'):464 raise ValueError('fast信号字段非法')465 if 'parameters' in item and (type(item['parameters']) is not dict466 or any(type(v) not in (int,float) for v in item['parameters'].values())):467 raise ValueError('fast参数需数值映射')468 if item.get('direction') is not None:469 keys(item['direction'],('signal','bucket'))470 if 'program' in item:_program(item['program'])471 if 'inputs' in value:472 if type(value['inputs']) is not list:raise ValueError('fast输入需列表')473 for item in value['inputs']:474 _object(item,('path',),_FAST_KEYS-{'inputs'})475 if 'asc_options' in value:476 _object(value['asc_options'],(),set(_ASC)-{'path'})477478479480def _check_fast_rows(rows):481 """检查快速批量总量所用的固定五字段结果。482483 参数: rows为未请求/任务失败时的None,或当前累计器产出的行列表。484 返回: None;不改数值、名称和顺序,也不限制合法任务的计算项数量。485 异常: 未知字段、类型或结果状态错误。486 """487 if rows is None:488 return489 if type(rows) is not list:490 raise ValueError('fast批量累计结果需要列表或None')491 for row in rows:492 keys(row,('name','unit','value','status','reason'))493 if type(row['name']) is not str or type(row['unit']) is not str:494 raise ValueError('fast累计名称及单位需要文本')495 value=row['value']496 if value is not None and (type(value) not in (int,float) or not isfinite(value)):497 raise ValueError('fast累计数值需要有限数或None')498 if row['status'] not in ('complete','indeterminate') or row['reason'] not in (499 None,'no_target_frames','no_selected_direction'):500 raise ValueError('fast累计状态或原因非法')501502503def check_message_fields(family, operation, arguments):504 """核对大消息的固定字段。505506 参数: family通道、operation命令、arguments参数序列。507 返回: None,用户config仍交原AnalysisPlan验证。508 异常: 固定字段、计数、结果状态或指令错误。509 """510 if family=='fast' and operation in ('file','program_file'):511 check_fast_spec(arguments[1])512 elif family=='fast' and operation=='eof':513 facts,clocks=arguments[2],arguments[3]514 keys(facts,('raw','targets','valid','selected','kinds','compatible'))515 if type(facts['raw']) is not int or facts['raw']<0:516 raise ValueError('fast原帧计数非法')517 counts=[]518 for key in ('targets','valid','selected','kinds','compatible'):519 if type(facts[key]) not in (list,tuple) or any(type(v) is not int or v<0 for v in facts[key]):520 raise ValueError('fast计数或身份列表非法')521 if key!='compatible':counts.append(len(facts[key]))522 if len(set(counts))>1:523 raise ValueError('fast各计算项计数长度不符')524 allowed={'compute_seconds','credit_wait_seconds','send_seconds','write_seconds',525 'read_seconds','decode_seconds','formula_seconds'}526 if not set(clocks)<=allowed or any(type(v) not in (int,float) or not isfinite(v) or v<0 for v in clocks.values()):527 raise ValueError('fast计时字段非法')528 elif family=='batch' and operation in ('run','fast'):529 packet=arguments[0]530 if operation=='run':531 names=('job_id','path') if 'path' in packet else ('job_id','config','base_dir','identity')532 keys(packet,names)533 for key in set(names)-{'config'}:534 if type(packet[key]) is not str:raise ValueError('batch任务文本字段非法')535 if 'config' in packet and type(packet['config']) is not dict:536 raise ValueError('batch内存配置需要对象')537 else:538 keys(packet,('job_id','path','spec','total'))539 if type(packet['job_id']) is not str or packet['path'] is not None and type(packet['path']) is not str or type(packet['total']) is not bool:540 raise ValueError('fast批量任务字段非法')541 check_fast_spec(packet['spec'])542 elif family=='batch' and operation in ('result','fast_result'):543 if operation=='fast_result' and len(arguments)==3:544 _check_fast_rows(arguments[2])545 row=arguments[0]546 _object(row,('job_id','status'),_RESULT)547 if row['status'] not in ('completed','failed','cancelled'):548 raise ValueError('batch结果状态非法')549 for key in set(_RESULT)-{'error_chain','elapsed_seconds','memory_budget_bytes','worker_pid'}:550 if row.get(key) is not None and type(row[key]) is not str:551 raise ValueError('batch结果文本字段非法')552 if 'error_chain' in row and (type(row['error_chain']) is not list or any(type(v) is not str for v in row['error_chain'])):553 raise ValueError('batch错误链需要文本列表')554 if 'elapsed_seconds' in row and (type(row['elapsed_seconds']) not in (int,float) or not isfinite(row['elapsed_seconds']) or row['elapsed_seconds']<0):555 raise ValueError('batch结果耗时非法')556 for key in ('memory_budget_bytes','worker_pid'):557 if row.get(key) is not None and (type(row[key]) is not int or row[key]<1):558 raise ValueError('batch结果计数非法')559