修复循环信号与事件采样并接入 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 通过。
This commit is contained in:
1 parent
1aac220084
commit
7611f13208
85 files changed
+11358
-33
No files matched your search
@@ -0,0 +1,176 @@
|
||||
"""Production LSTP event regression against analytic dynamics and Amesim."""
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from dataclasses import replace
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import numpy as np
|
||||
|
||||
from app.simulation.config import SolveIVPConfig
|
||||
from app.simulation.native_codegen import build as builder, result_storage
|
||||
from app.simulation.native_codegen.compiler import compile_native_program
|
||||
from app.simulation.native_codegen.contacts import contact_table
|
||||
from app.simulation.native_codegen.extended import compile_extended_program
|
||||
from app.simulation.native_codegen.runner import execute_native
|
||||
from tests.test_mechanical_event_experiment import contact_pair
|
||||
from tests.test_native_catalog import Circuit
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
OUT = ROOT/'test/lstp-mainline-20260917/unit'
|
||||
|
||||
|
||||
class NativeContactEventsTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
cls.tag = str(time.time_ns())
|
||||
|
||||
def run_case(self, b, method='BDF', stop=.7, h=.02, compiler=compile_native_program,
|
||||
disabled=False, label='', record=True, step=.002):
|
||||
program = compiler(b.net)
|
||||
if disabled:
|
||||
program = replace(program, header=re.sub(r'#define NCONTACTS \d+', '#define NCONTACTS 0', program.header))
|
||||
path = OUT/self._testMethodName/self.tag/(method+compiler.__name__+str(disabled)+label)
|
||||
with patch.object(builder, 'CACHE', OUT/'cache'), \
|
||||
patch.object(result_storage, 'RESULT_ROOT', OUT/'results'), \
|
||||
patch.object(builder, 'ThreadPoolExecutor', lambda **kw: ThreadPoolExecutor(max_workers=1)):
|
||||
build = builder.build_native(program)
|
||||
try:
|
||||
r = execute_native(build, SolveIVPConfig(t_stop=stop, method=method, max_step=h, rtol=1e-9),
|
||||
step, run_dir=path, record_samples=record, timeout=60)
|
||||
finally:
|
||||
build.close()
|
||||
self.assertTrue(r['success'], r['message'])
|
||||
events = []
|
||||
for line in (path/'worker.log').read_text(encoding='utf-8').splitlines():
|
||||
try: e = json.loads(line)
|
||||
except ValueError: continue
|
||||
if e.get('phase') == 'mechanical-event': events.append(e)
|
||||
return r, events
|
||||
|
||||
def test_elastic_collision_analytic_and_conservation(self):
|
||||
for compiler in (compile_native_program, compile_extended_program):
|
||||
for method in ('BDF', 'RK45'):
|
||||
for h in (.06, .015):
|
||||
r, events = self.run_case(contact_pair(), method, h=h, compiler=compiler, label=str(h))
|
||||
self.assertEqual([e['direction'] for e in events], [1, -1])
|
||||
np.testing.assert_allclose([e['time'] for e in events], [.2, .2+math.pi/math.sqrt(200)], atol=2e-6, rtol=0)
|
||||
v1=np.array(r['series']['left.v']);v2=np.array(r['series']['right.v'])
|
||||
p=np.maximum(-np.array(r['series']['contact.gap']),0)
|
||||
np.testing.assert_allclose(v1+v2,1,atol=1e-9,rtol=0)
|
||||
np.testing.assert_allclose(.5*(v1*v1+v2*v2)+50*p*p,.5,atol=2e-6,rtol=0)
|
||||
self.assertAlmostEqual(r['final']['left.v'],0,delta=2e-6)
|
||||
self.assertAlmostEqual(r['final']['right.v'],1,delta=2e-6)
|
||||
|
||||
def test_nonnegative_force_clips_before_detachment(self):
|
||||
for method in ('BDF','RK45'):
|
||||
r,events=self.run_case(contact_pair(10,2),method)
|
||||
clips=[e for e in events if e['kind']=='force-clip']
|
||||
self.assertEqual(len(clips),1)
|
||||
self.assertAlmostEqual(clips[0]['time'],.2+math.pi/20,delta=3e-6)
|
||||
self.assertGreater(clips[0]['penetration'],0)
|
||||
self.assertGreaterEqual(min(r['series']['contact.force']),0)
|
||||
|
||||
def test_geometry_stiffness_uses_same_event_parameters(self):
|
||||
b=contact_pair();contact=b.net.components['contact']
|
||||
# Independent geometry: G*d^4/(8*D^3*n) = 100 N/m exactly.
|
||||
for name,value in dict(stiffmode=2,G=51200,sdiam=.5,wdiam=.25,na=2,kcont=1e9).items():
|
||||
setattr(contact,name,value)
|
||||
contact.set_parameter_values({p.name:getattr(contact,p.name) for p in contact.PARAMETERS})
|
||||
for method in ('BDF','RK45'):
|
||||
r,events=self.run_case(b,method)
|
||||
np.testing.assert_allclose([e['time'] for e in events],[.2,.2+math.pi/math.sqrt(200)],atol=2e-6,rtol=0)
|
||||
self.assertAlmostEqual(r['final']['left.v'],0,delta=2e-6)
|
||||
self.assertAlmostEqual(r['final']['right.v'],1,delta=2e-6)
|
||||
|
||||
def test_exponential_damping_modes(self):
|
||||
for option in (1,2):
|
||||
for method in ('BDF','RK45'):
|
||||
r,events=self.run_case(contact_pair(2,option,.01),method,stop=1.1,label=str(option))
|
||||
self.assertGreaterEqual(len(events),2)
|
||||
self.assertLess(len(events),15)
|
||||
self.assertNotIn('experimentalEvents',r)
|
||||
|
||||
def test_no_events_preserve_trajectory(self):
|
||||
for label,b,stop in [('apart',contact_pair(v1=.1,v2=.1),.3),
|
||||
('touch',contact_pair(x2=0,v1=0,v2=0),.3),
|
||||
('inside',contact_pair(x2=-.1,v1=0,v2=0),.01)]:
|
||||
r,events=self.run_case(b,stop=stop,label=label)
|
||||
base,_=self.run_case(b,stop=stop,disabled=True,label=label)
|
||||
self.assertEqual(events,[])
|
||||
for key in ('series','finalState','solverStarts','nfev'):
|
||||
self.assertEqual(r[key],base[key])
|
||||
|
||||
def test_output_replay_does_not_change_events_or_state(self):
|
||||
for method in ('BDF','RK45'):
|
||||
r,events=self.run_case(contact_pair(),method)
|
||||
bare,bare_events=self.run_case(contact_pair(),method,label='bare',record=False)
|
||||
for key in ('finalState','contactEvents','nfev','acceptedSteps','solverStarts'):
|
||||
self.assertEqual(r[key],bare[key])
|
||||
self.assertEqual(events,bare_events)
|
||||
times=r['series']['time']
|
||||
self.assertTrue(all(a<b for a,b in zip(times,times[1:])))
|
||||
for event in events:
|
||||
self.assertIn(event['time'],times)
|
||||
self.assertIn(math.nextafter(event['time'],-math.inf),times)
|
||||
|
||||
def test_existing_amesim_joint_mechanical(self):
|
||||
cases=json.loads((ROOT/'tests/fixtures/amesim-component-semantics-2404.json').read_bytes())['cases']
|
||||
case=next(c for c in cases if c['name']=='joint_mechanical')
|
||||
b=Circuit()
|
||||
for c in case['components']:b.add(c['type'],c['name'],**c['parameters'])
|
||||
for left,right in case['connections']:b.net.connect(*left,*right)
|
||||
r,_=self.run_case(b,stop=3,h=.002,step=.001)
|
||||
for key,expected in case['series'].items():
|
||||
actual=np.interp(case['time'],r['series']['time'],r['series'][key])
|
||||
atol=.002 if key.split('.')[-1] in ('a','Ffric','Fmin','Fmax','force') else 3e-6
|
||||
np.testing.assert_allclose(actual,expected,atol=atol,rtol=0,err_msg=key)
|
||||
|
||||
def test_dense_excursion_tangent_and_expression_association(self):
|
||||
path=OUT/self._testMethodName/self.tag;path.mkdir(parents=True)
|
||||
_,header=contact_table([(0,2,0,100,1,0,1,0)])
|
||||
(path/'model.h').write_text('#define NSTATES 4\n#define NOUTPUTS 1\n#define NSTOPS 0\n'+header,encoding='ascii')
|
||||
code=r'''
|
||||
#include "runtime.h"
|
||||
#include <math.h>
|
||||
#include "contact_events.h"
|
||||
const NativeContact model_contacts[1]={{0,2,0,100,1,0,1,0}};
|
||||
static int polynomial(void *context,double t,double *y) {
|
||||
y[0]=1-2*t;y[1]=(*(int*)context?0:.01)-(t-.5)*(t-.5);y[2]=y[3]=0;return 1;
|
||||
}
|
||||
int main(void) {
|
||||
for(int tangent=0;tangent<2;tangent++) {
|
||||
NativeRun r={0};double a[4],b[4],when[8],t=0;int indices[8],kinds[8],count=0;
|
||||
polynomial(&tangent,t,a);polynomial(&tangent,1,b);
|
||||
if(!contact_candidates(&r,t,1,a,b,polynomial,&tangent,when,indices,kinds,&count))return 1;
|
||||
if(tangent) {if(count)return 2;continue;}
|
||||
if(count!=1 || fabs(when[0]-.4)>1e-12)return 3;
|
||||
t=when[0];polynomial(&tangent,t,a);contact_commit(&r,t,a,when,indices,kinds,count);
|
||||
count=0;
|
||||
if(!contact_candidates(&r,t,1,a,b,polynomial,&tangent,when,indices,kinds,&count))return 4;
|
||||
if(count!=1 || fabs(when[0]-.6)>1e-12)return 5;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
'''
|
||||
(path/'check.c').write_text(code,encoding='ascii')
|
||||
cc,_,_=builder.toolchain();exe=path/('check.exe' if sys.platform=='win32' else 'check')
|
||||
builder._command([cc,*builder.COMPILER_FLAGS,'-I',str(path),'-I',str(ROOT/'native/include'),
|
||||
'-I',str(ROOT/'native/runtime'),str(path/'check.c'),'-lm','-o',str(exe)],log=[])
|
||||
r=subprocess.run([str(exe)],capture_output=True,text=True,timeout=15)
|
||||
self.assertEqual(r.returncode,0,r.stderr)
|
||||
# Both schedules must register their own arithmetic association.
|
||||
compact=compile_native_program(contact_pair().net)
|
||||
extended=compile_extended_program(contact_pair().net)
|
||||
self.assertIn('0.0,100.0,0.0,0.0,1,1}',compact.source)
|
||||
self.assertIn('0.0,100.0,0.0,0.0,1,0}',extended.source)
|
||||
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
Reference in new issue
Block a user