相较上一版 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 通过。
100 lines
5.3 KiB
Python
100 lines
5.3 KiB
Python
"""Event output sides, replay, and phase pairing independent of output errors."""
|
|
import math
|
|
from pathlib import Path
|
|
import tempfile
|
|
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.runner import execute_native
|
|
from tests.manual.event_phase_comparison import pair_saved_phases
|
|
from tests.test_native_catalog import Circuit
|
|
|
|
|
|
class PhasePairingTests(unittest.TestCase):
|
|
def test_roundoff_different_sides_match_reference_phase_using_one_row(self):
|
|
boundary = 32.400000000000006
|
|
nt = [32.4, boundary, 32.41]
|
|
rt = [32.40000000000221, 32.41]
|
|
ni, ai, records = pair_saved_phases(nt, rt,
|
|
[[49000., 2.], [1e17, 2.], [1e17, 2.]], [[1e17, 2.], [1e17, 2.]], [32.4, 32.41], .01)
|
|
self.assertEqual(ni.tolist(), [1, 2])
|
|
self.assertEqual(ai.tolist(), [0, 1])
|
|
self.assertEqual(records[0]['status'], 'matched-other-event-side')
|
|
|
|
def test_reference_before_event_uses_saved_left_side(self):
|
|
left = math.nextafter(.8, -math.inf)
|
|
ni, _, _ = pair_saved_phases([left, .8], [.8], [[1e17], [49000.]], [[1e17]], [.8], .01)
|
|
self.assertEqual(ni.tolist(), [0])
|
|
|
|
def test_missing_side_or_one_different_signal_is_not_interpolated_or_hidden(self):
|
|
ni, _, records = pair_saved_phases([.79, .8, .81], [.8],
|
|
[[1., 1.], [2., 1.], [2., 2.]], [[2., 2.]], [.8], .01)
|
|
self.assertEqual(ni.tolist(), [-1])
|
|
self.assertEqual(records[0]['status'], 'unmatched-forcing-phase')
|
|
ni, _, records = pair_saved_phases([.79, .81], [.8], [[1.], [1.]], [[1.]], [.8], .01)
|
|
self.assertEqual(ni.tolist(), [-1])
|
|
self.assertEqual(records[0]['status'], 'missing-saved-sample')
|
|
|
|
|
|
class NativeEventOutputTests(unittest.TestCase):
|
|
def test_off_grid_step_keeps_regular_samples_and_both_sides(self):
|
|
b = Circuit()
|
|
b.add('amesim_step0', 'step', time=.055, initial=2., final=4.)
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
with patch.object(builder, 'CACHE', root/'build'), patch.object(result_storage, 'RESULT_ROOT', root/'results'):
|
|
build = builder.build_native(compile_native_program(b.net))
|
|
try:
|
|
for method in ('BDF', 'RK45'):
|
|
result = execute_native(build, SolveIVPConfig(t_stop=.2, method=method, max_step=1.),
|
|
.01, run_dir=root/method)
|
|
self.assertTrue(result['success'], result['message'])
|
|
times = result['series']['time']
|
|
expected = sorted({i*.01 for i in range(21)} | {.055, math.nextafter(.055, -math.inf)})
|
|
self.assertEqual(times, expected)
|
|
self.assertEqual(result['series']['step.y'], [2. if t<.055 else 4. for t in times])
|
|
self.assertEqual(result['solverStarts'], 2)
|
|
finally:
|
|
build.close()
|
|
|
|
def test_collision_sides_and_recording_do_not_change_integration(self):
|
|
b = Circuit()
|
|
mass = b.add('amesim_mecmas21', 'mass', mass=1, stoptype=3,
|
|
xmin=0, xmax=1, x0=.9, v0=1, restcoeff=.5, restdvel=.001, useFriction=1)
|
|
for i in (1, 2):
|
|
free = b.add('amesim_f000', 'free'+str(i))
|
|
b.connect(mass, 'port_'+str(i), free, 'port_1')
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
with patch.object(builder, 'CACHE', root/'build'), patch.object(result_storage, 'RESULT_ROOT', root/'results'):
|
|
build = builder.build_native(compile_native_program(b.net))
|
|
try:
|
|
for method in ('BDF', 'RK45'):
|
|
config = SolveIVPConfig(t_stop=.25, method=method, max_step=.03, rtol=1e-8)
|
|
result = execute_native(build, config, .007, run_dir=root/method)
|
|
control = execute_native(build, config, .007, run_dir=root/(method+'-solve-only'), record_samples=False)
|
|
self.assertTrue(result['success'], result['message'])
|
|
self.assertEqual(result['finalState'], control['finalState'])
|
|
for key in ('acceptedSteps', 'rejectedSteps', 'solverStarts', 'stateTransitions', 'nfev'):
|
|
self.assertEqual(result[key], control[key], key)
|
|
series = result['series']
|
|
times, speeds = series['time'], series['mass.v']
|
|
self.assertTrue(all(a < z for a, z in zip(times, times[1:])))
|
|
idx = next(i for i in range(1, len(times)) if speeds[i] < 0 <= speeds[i-1])
|
|
self.assertEqual(times[idx-1], math.nextafter(times[idx], -math.inf))
|
|
self.assertAlmostEqual(times[idx], .1, delta=1e-6)
|
|
self.assertAlmostEqual(speeds[idx-1], 1., places=10)
|
|
self.assertAlmostEqual(speeds[idx], -.5, places=10)
|
|
self.assertAlmostEqual(series['mass.x'][idx], 1., places=12)
|
|
finally:
|
|
build.close()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|