Files
SystemSimulationApp/tests/manual/mechanical_event_variant.py
ljz 7611f13208 修复循环信号与事件采样并接入 LSTP 接触定位,补充八路验证及复用实验
相较上一版 Jacobian 确定性复用更新,本次补齐事件边界一致性、结果两侧采样及接触事件定位;保留已有物性复用和组件力学公式。

- 统一 UD00 信号求值与下一事件查询的绝对时间边界,修复循环边界浮点舍入导致的阶段错位、重复或漏报,并覆盖零时长、多阶段及长周期场景。
- 引入原生输出语义 v2:保留规则网格真实时间,补充内部时间事件和状态事件的左邻及事件后采样,按保存时间、状态和离散模式重放结果。
- 两条代码生成路径均发出 LSTP 接触描述,默认定位间隙过零及非负力模式的力截断;仅在接受事件时更新防重复记录,增加 contactEvents 诊断计数。
- 补充 MASS/LSTP 独立事件实验、八路全曲线与驱动阶段配对评估,以及 Amesim 不连续点输出对照和力差定位报告;MASS 新增释放机制仍保留为独立实验。
- 保存局部 probe、context 访问与回退、shadow replay、R288 real skip/typed replay 及阀门数值尾部诊断工具和报告;未证明净收益的实验不启用为生产默认优化。
- 更新原生运行说明和元件建模规范,补充信号边界、输出语义、接触事件和实验依赖回归测试。

验证:五组专项回归共 34 项全部通过;37 个待提交 Python 文件语法检查通过;git diff --cached --check 通过。
2026-09-17 23:50:13 +08:00

146 lines
8.6 KiB
Python

"""Isolated mechanical-event builds; never edits production numerics or cache.
Augment the generated program with local contact descriptors and an optional
read-only release-drive evaluator. The ordinary RHS, Jacobian, property reuse,
state layout, output layout, and component formulas stay byte-for-byte intact.
"""
from dataclasses import replace
import hashlib
import json
from pathlib import Path
import re
import shutil
from app.simulation.components.amesim.semantics import contact_stiffness
HERE = Path(__file__).resolve().parent
MODES = ('off', 'mass', 'lstp', 'all')
def replace_once(text, old, new):
if text.count(old) != 1:
raise ValueError(f'Experiment patch no longer matches the reviewed runtime: {old[:100]}')
return text.replace(old, new, 1)
def prepare_runtime(original, destination):
original, destination = Path(original), Path(destination)
shutil.copytree(original, destination, dirs_exist_ok=True)
shutil.copyfile(HERE/'mechanical_events_runtime.c', destination/'runtime/experimental_events.c')
common = destination/'runtime/common.c'
source = common.read_text(encoding='utf-8')
source = replace_once(source, 'int native_accept(', '#include "experimental_events.c"\n\nint native_accept(')
previous_size=('2*(NSTOPS+NFRICTIONS+NCONTACTS+1)' if '2*(NSTOPS+NFRICTIONS+NCONTACTS+1)' in source
else '2*(NSTOPS+NFRICTIONS+1)')
event_size=previous_size[:-2]+'NEXPERIMENT_CONTACTS+NEXPERIMENT_RELEASES+1)'
source = source.replace(previous_size,event_size)
for field in ('bounds','restitution','thresholds'):
declaration=field+'['+event_size+']'
if declaration+'={0}' not in source:
source=replace_once(source,declaration,declaration+'={0}')
source = replace_once(source, ' double stop=next;', ''' if (!experiment_candidates(r,t,next,old,trial,dense,context,when,indices,friction,&count)) return -1;
double stop=next;''')
if 'if(friction[i]>=0) continue;' in source:
source = replace_once(source, 'if(friction[i]>=0) continue;', 'if(friction[i]!=-1) continue;')
elif 'if(friction[i]!=-1) continue;' not in source:
raise ValueError('Unknown hard-stop event dispatch')
source = source.replace('if(friction[i]<0 &&', 'if(friction[i]==-1 &&')
source = replace_once(source, ' if (!native_append(r,stop,accepted_state)) return -1;', ''' experiment_commit(r,stop,accepted_state,when,indices,friction,count);
if (!native_append(r,stop,accepted_state)) return -1;''')
common.write_text(source, encoding='utf-8')
header = destination/'include/runtime.h'
source = header.read_text(encoding='utf-8')
source = replace_once(source, ' NativePropertyWarning property_warnings[6];', ''' double experiment_last[2*NEXPERIMENT_CONTACTS+NEXPERIMENT_RELEASES+1];
int experiment_direction[2*NEXPERIMENT_CONTACTS+NEXPERIMENT_RELEASES+1];
int experiment_pending[2*NEXPERIMENT_CONTACTS+NEXPERIMENT_RELEASES+1];
unsigned long experiment_checks, experiment_dense, experiment_roots, experiment_rhs;
unsigned long experiment_mass, experiment_lstp, experiment_clipping, experiment_release;
NativePropertyWarning property_warnings[6];''')
header.write_text(source, encoding='utf-8')
main = destination/'runtime/main.c'
source = main.read_text(encoding='utf-8')
source = replace_once(source, ' fprintf(f,",\\\"propertyWarnings\\\":"); native_property_warnings_json(r,f);', ''' fprintf(f,",\\\"experimentalEvents\\\":{\\\"checks\\\":%lu,\\\"denseCalls\\\":%lu,\\\"rootIterations\\\":%lu,\\\"releaseRhsCalls\\\":%lu,\\\"massContactTransitions\\\":%lu,\\\"lstpContactTransitions\\\":%lu,\\\"forceClipTransitions\\\":%lu,\\\"massReleaseTransitions\\\":%lu}",
r->experiment_checks,r->experiment_dense,r->experiment_roots,r->experiment_rhs,
r->experiment_mass,r->experiment_lstp,r->experiment_clipping,r->experiment_release);
fprintf(f,",\\\"propertyWarnings\\\":"); native_property_warnings_json(r,f);''')
main.write_text(source, encoding='utf-8')
def event_program(program, network, mode):
if mode not in MODES:
raise ValueError(mode)
# Reproduce opt-in experiments after production LSTP integration too:
# descriptors remain available, but only the selected experimental events
# run. This also keeps the tests portable when the frozen snapshot is absent.
program=replace(program,header=re.sub(r'#define NCONTACTS \d+','#define NCONTACTS 0',program.header))
# The off program uses the unmodified runtime and generated program.
if mode == 'off':
return program, dict(mode=mode, contacts=[], releases=[])
slots = {v.key: i for i, v in enumerate(program.variables)}
assignments = {}
for w, y in re.findall(r'w\[(\d+)\]\s*=\s*y\[(\d+)\]\s*;', program.source):
if int(w) in assignments and assignments[int(w)] != int(y):
raise ValueError('Ambiguous kinematic state alias')
assignments[int(w)] = int(y)
def velocity(key):
vi = assignments[slots[key+'.v']]
if assignments[slots[key+'.x']] != vi+1:
raise ValueError('Experiment requires the reviewed contiguous v/x state layout')
return vi
contacts = []
for c in network.components.values():
if c.model_type == 'amesim_lstp00a' and mode in ('lstp', 'all'):
contacts.append(dict(name=c.name, values=(3, velocity(c.name+'.port_1'), velocity(c.name+'.port_2'),
c.gap0, contact_stiffness(c), c.rcont, c.Pdis, int(c.discContactOption))))
if c.model_type == 'amesim_mecmas21' and int(c.stoptype) == 2 and mode in ('mass', 'all'):
for kind, side in ((1, 'min'), (2, 'max')):
contacts.append(dict(name=c.name+'.'+side, values=(kind, velocity(c.name), -1,
getattr(c,'x'+side), getattr(c,'Kb'+side), getattr(c,'Db'+side),
getattr(c,'Pd'+side), int(c.discContactOption))))
stop_count = int(re.search(r'#define NSTOPS (\d+)', program.header)[1])
releases = list(range(stop_count)) if mode in ('mass', 'all') else []
header = f'''
#define NEXPERIMENT_CONTACTS {len(contacts)}
#define NEXPERIMENT_RELEASES {len(releases)}
typedef struct {{ int kind, v1, v2; double boundary, stiffness, damping, pdis; int signed_force; }} ExperimentContact;
extern const ExperimentContact experiment_contacts[{max(1,len(contacts))}];
int model_experiment_release_drives(double t,const double *y,double *drives);
'''
data = ','.join('{'+','.join(str(v) if isinstance(v,int) else repr(float(v)) for v in c['values'])+'}' for c in contacts)
additions = f'\nconst ExperimentContact experiment_contacts[{max(1,len(contacts))}] = {{{data or "{0,0,0,0,0,0,0,0}"}}};\n'
if releases:
start = program.source.index('static int model_eval_internal(')
body_start = program.source.index('{', start)
depth, end = 1, body_start+1
while depth:
depth += (program.source[end] == '{') - (program.source[end] == '}')
end += 1
function = program.source[start:end]
function = replace_once(function, 'model_eval_internal(', 'model_experiment_release_internal(')
pos = function.index(') {')
function = function[:pos] + ',double *release_drives' + function[pos:]
found = []
def capture(match):
vi = int(re.search(r'&dy\[(\d+)\]', match[0])[1])
index = len(found)
found.append(vi)
return f'release_drives[{index}]=dy[{vi}];'
function = re.sub(r'native_stop_motion\([^;]+\);', capture, function)
if len(found) != stop_count:
raise ValueError('Release evaluator does not match generated stop count')
extended = 'ModelJacobianWorkspace' in function
args = '0,NULL,NULL,NULL,drives' if extended else 'NULL,drives'
additions += function + '\nint model_experiment_release_drives(double t,const double *y,double *drives) {double dy[NSTATES],w[NOUTPUTS];return model_experiment_release_internal(t,y,dy,w,'+args+');}\n'
else:
additions += 'int model_experiment_release_drives(double t,const double *y,double *drives) {(void)t;(void)y;(void)drives;return 1;}\n'
# Add declarations before the include guard closes, preserving all generated
# RHS and Jacobian source bytes as an exact prefix.
at = program.header.rfind('#endif')
modified = replace(program, source=program.source+additions, header=program.header[:at]+header+program.header[at:])
assert modified.source.startswith(program.source)
return modified, dict(mode=mode, contacts=contacts, releases=releases,
originalModelSourceSha256=hashlib.sha256(program.source.encode()).hexdigest(),
jacobianStructure=program.jacobian_structure)