From 7611f13208e41074e191777e9ed90bf80c5517de Mon Sep 17 00:00:00 2001 From: ljz <425868052@qq.com> Date: Thu, 17 Sep 2026 23:50:13 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=BE=AA=E7=8E=AF=E4=BF=A1?= =?UTF-8?q?=E5=8F=B7=E4=B8=8E=E4=BA=8B=E4=BB=B6=E9=87=87=E6=A0=B7=E5=B9=B6?= =?UTF-8?q?=E6=8E=A5=E5=85=A5=20LSTP=20=E6=8E=A5=E8=A7=A6=E5=AE=9A?= =?UTF-8?q?=E4=BD=8D=EF=BC=8C=E8=A1=A5=E5=85=85=E5=85=AB=E8=B7=AF=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E5=8F=8A=E5=A4=8D=E7=94=A8=E5=AE=9E=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 相较上一版 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 通过。 --- app/simulation/native_codegen/compiler.py | 7 + app/simulation/native_codegen/contacts.py | 24 + app/simulation/native_codegen/extended.py | 8 + .../LSTP主路接入与八路力差定位-2026-09-17.md | 151 +++++ ...与LSTP事件独立实验及八路对比-2026-09-17.md | 171 +++++ ...件输出语义修改与八路基线复测-2026-09-17.md | 95 +++ ...路曲线剩余差异占比与全程评价-2026-09-17.md | 132 ++++ ...八路模型正确性初评与展示建议-2026-09-17.md | 165 +++++ ...环UD00事件边界修复与独立验证-2026-09-17.md | 101 +++ ...机械元件事件覆盖与Amesim对照-2026-09-17.md | 136 ++++ .../component-model-authoring-spec-v1.md | 27 + native/README.md | 11 +- native/components/modules/signal.c | 98 ++- native/include/runtime.h | 11 + native/runtime/common.c | 37 +- native/runtime/contact_events.h | 105 +++ native/runtime/cvode_solver.c | 4 + native/runtime/main.c | 8 + native/runtime/rk45.c | 1 + tests/manual/analyze_context_access.py | 256 ++++++++ tests/manual/analyze_context_fallback.py | 191 ++++++ tests/manual/analyze_context_shadow.py | 109 +++ .../manual/analyze_fallback_profitability.py | 244 +++++++ tests/manual/analyze_local_probe_profile.py | 156 +++++ tests/manual/analyze_mql8_curve_coverage.py | 236 +++++++ tests/manual/analyze_real_skip.py | 144 ++++ tests/manual/analyze_typed_replay.py | 178 +++++ tests/manual/analyze_valve_tail.py | 102 +++ tests/manual/compare_local_probe.py | 120 ++++ tests/manual/context_access_checklists.md | 339 ++++++++++ tests/manual/context_access_diag.c | 69 ++ tests/manual/context_access_diag.h | 21 + tests/manual/context_access_diagnostic.md | 169 +++++ tests/manual/context_fallback_diag.c | 213 ++++++ tests/manual/context_fallback_diag.h | 24 + tests/manual/context_shadow_replay.c | 578 ++++++++++++++++ tests/manual/context_shadow_replay.h | 26 + tests/manual/context_shadow_report.md | 145 ++++ tests/manual/diagnose_context_access.py | 134 ++++ tests/manual/diagnose_context_fallback.py | 126 ++++ tests/manual/diagnose_context_shadow.py | 107 +++ tests/manual/diagnose_mql8_force_events.py | 328 ++++++++++ tests/manual/diagnose_valve_tail.py | 90 +++ tests/manual/evaluate_mechanical_events.py | 140 ++++ tests/manual/evaluate_mql8_correctness.py | 412 ++++++++++++ tests/manual/event_phase_comparison.py | 49 ++ .../fallback_profitability_intervals.md | 618 ++++++++++++++++++ .../fallback_profitability_operations.md | 352 ++++++++++ tests/manual/fallback_profitability_report.md | 219 +++++++ tests/manual/local_probe_experiment.md | 40 ++ tests/manual/local_probe_experiment.py | 292 +++++++++ tests/manual/local_probe_guard_test.c | 39 ++ tests/manual/local_probe_profile.c | 86 +++ tests/manual/local_probe_profile.h | 53 ++ tests/manual/local_probe_profile.md | 34 + tests/manual/local_probe_support.c | 133 ++++ tests/manual/mechanical_event_variant.py | 145 ++++ tests/manual/mechanical_events_experiment.md | 37 ++ tests/manual/mechanical_events_runtime.c | 178 +++++ tests/manual/mql8_comparison.py | 118 ++++ tests/manual/plot_mql8_evaluation.py | 112 ++++ tests/manual/probe_mql8_signal_boundaries.py | 51 ++ tests/manual/profile_local_probe.py | 218 ++++++ tests/manual/r288_real_skip_report.md | 137 ++++ tests/manual/r288_typed_replay_report.md | 212 ++++++ tests/manual/real_skip.h | 28 + tests/manual/real_skip_experiment.py | 180 +++++ tests/manual/real_skip_runtime.c | 223 +++++++ tests/manual/specialized_replay_experiment.py | 260 ++++++++ tests/manual/state_valve_tail_report.md | 237 +++++++ tests/manual/summarize_mechanical_events.py | 63 ++ tests/manual/summarize_output_semantics.py | 71 ++ tests/manual/typed_contract_check.inc | 57 ++ tests/manual/typed_oracle.inc | 62 ++ tests/manual/typed_replay_core.inc | 184 ++++++ tests/manual/typed_replay_hooks.h | 6 + tests/manual/typed_replay_runtime.inc | 40 ++ tests/manual/valve_tail_diag.c | 63 ++ tests/manual/valve_tail_diag.h | 35 + .../manual/verify_context_access_evidence.py | 56 ++ tests/test_local_probe_experiment.py | 45 ++ tests/test_mechanical_event_experiment.py | 246 +++++++ tests/test_native_contact_events.py | 176 +++++ tests/test_native_output_semantics.py | 99 +++ tests/test_native_signal_boundaries.py | 188 ++++++ 85 files changed, 11358 insertions(+), 33 deletions(-) create mode 100644 app/simulation/native_codegen/contacts.py create mode 100644 docs/other/LSTP主路接入与八路力差定位-2026-09-17.md create mode 100644 docs/other/MASS与LSTP事件独立实验及八路对比-2026-09-17.md create mode 100644 docs/other/事件输出语义修改与八路基线复测-2026-09-17.md create mode 100644 docs/other/八路曲线剩余差异占比与全程评价-2026-09-17.md create mode 100644 docs/other/八路模型正确性初评与展示建议-2026-09-17.md create mode 100644 docs/other/循环UD00事件边界修复与独立验证-2026-09-17.md create mode 100644 docs/other/机械元件事件覆盖与Amesim对照-2026-09-17.md create mode 100644 native/runtime/contact_events.h create mode 100644 tests/manual/analyze_context_access.py create mode 100644 tests/manual/analyze_context_fallback.py create mode 100644 tests/manual/analyze_context_shadow.py create mode 100644 tests/manual/analyze_fallback_profitability.py create mode 100644 tests/manual/analyze_local_probe_profile.py create mode 100644 tests/manual/analyze_mql8_curve_coverage.py create mode 100644 tests/manual/analyze_real_skip.py create mode 100644 tests/manual/analyze_typed_replay.py create mode 100644 tests/manual/analyze_valve_tail.py create mode 100644 tests/manual/compare_local_probe.py create mode 100644 tests/manual/context_access_checklists.md create mode 100644 tests/manual/context_access_diag.c create mode 100644 tests/manual/context_access_diag.h create mode 100644 tests/manual/context_access_diagnostic.md create mode 100644 tests/manual/context_fallback_diag.c create mode 100644 tests/manual/context_fallback_diag.h create mode 100644 tests/manual/context_shadow_replay.c create mode 100644 tests/manual/context_shadow_replay.h create mode 100644 tests/manual/context_shadow_report.md create mode 100644 tests/manual/diagnose_context_access.py create mode 100644 tests/manual/diagnose_context_fallback.py create mode 100644 tests/manual/diagnose_context_shadow.py create mode 100644 tests/manual/diagnose_mql8_force_events.py create mode 100644 tests/manual/diagnose_valve_tail.py create mode 100644 tests/manual/evaluate_mechanical_events.py create mode 100644 tests/manual/evaluate_mql8_correctness.py create mode 100644 tests/manual/event_phase_comparison.py create mode 100644 tests/manual/fallback_profitability_intervals.md create mode 100644 tests/manual/fallback_profitability_operations.md create mode 100644 tests/manual/fallback_profitability_report.md create mode 100644 tests/manual/local_probe_experiment.md create mode 100644 tests/manual/local_probe_experiment.py create mode 100644 tests/manual/local_probe_guard_test.c create mode 100644 tests/manual/local_probe_profile.c create mode 100644 tests/manual/local_probe_profile.h create mode 100644 tests/manual/local_probe_profile.md create mode 100644 tests/manual/local_probe_support.c create mode 100644 tests/manual/mechanical_event_variant.py create mode 100644 tests/manual/mechanical_events_experiment.md create mode 100644 tests/manual/mechanical_events_runtime.c create mode 100644 tests/manual/mql8_comparison.py create mode 100644 tests/manual/plot_mql8_evaluation.py create mode 100644 tests/manual/probe_mql8_signal_boundaries.py create mode 100644 tests/manual/profile_local_probe.py create mode 100644 tests/manual/r288_real_skip_report.md create mode 100644 tests/manual/r288_typed_replay_report.md create mode 100644 tests/manual/real_skip.h create mode 100644 tests/manual/real_skip_experiment.py create mode 100644 tests/manual/real_skip_runtime.c create mode 100644 tests/manual/specialized_replay_experiment.py create mode 100644 tests/manual/state_valve_tail_report.md create mode 100644 tests/manual/summarize_mechanical_events.py create mode 100644 tests/manual/summarize_output_semantics.py create mode 100644 tests/manual/typed_contract_check.inc create mode 100644 tests/manual/typed_oracle.inc create mode 100644 tests/manual/typed_replay_core.inc create mode 100644 tests/manual/typed_replay_hooks.h create mode 100644 tests/manual/typed_replay_runtime.inc create mode 100644 tests/manual/valve_tail_diag.c create mode 100644 tests/manual/valve_tail_diag.h create mode 100644 tests/manual/verify_context_access_evidence.py create mode 100644 tests/test_local_probe_experiment.py create mode 100644 tests/test_mechanical_event_experiment.py create mode 100644 tests/test_native_contact_events.py create mode 100644 tests/test_native_output_semantics.py create mode 100644 tests/test_native_signal_boundaries.py diff --git a/app/simulation/native_codegen/compiler.py b/app/simulation/native_codegen/compiler.py index fcebb4a..0a92e00 100644 --- a/app/simulation/native_codegen/compiler.py +++ b/app/simulation/native_codegen/compiler.py @@ -373,6 +373,11 @@ def _compile_storage_anchored_program(network: SimulationNetwork) -> NativeProgr raise NativeCapabilityError(f"Native output coverage is incomplete: {sorted(missing)}") stops = [(si(c, "v"), c.xmin, c.xmax) for c in masses if int(c.stoptype) == 1] + from .contacts import contact_table + contact_source, contact_header = contact_table([ + (si(mass_at(c, 'port_1'), 'v'), si(mass_at(c, 'port_2'), 'v'), + c.gap0, c.kcont, c.rcont, c.Pdis, int(c.discContactOption), 1) + for c in components if c.model_type == 'amesim_lstp00a']) stop_c = ",".join(f"{{{v},{_number(lo)},{_number(hi)},0,0,0,0}}" for v, lo, hi in stops) or "{0,0,0,0,0,0,0}" next_event = [] for index, c in signal_specs: @@ -384,6 +389,7 @@ def _compile_storage_anchored_program(network: SimulationNetwork) -> NativeProgr source = '\n'.join([ '#include "model.h"', '#include ', *declarations, f"const NativeStop model_stops[{max(1,len(stops))}] = {{{stop_c}}};", + contact_source, f"const double model_atol[NSTATES] = {{{','.join(map(state_absolute_tolerance, state_keys))}}};", "const char *const model_output_keys[NOUTPUTS] = {" + ",".join(json.dumps(v.key, ensure_ascii=True) for v in variables) + "};", "int model_init(double *y) {", *init, "return 1; }", @@ -408,6 +414,7 @@ int model_property_temperatures(double t,const double *y,NativePropertyTemperatu #define NSTATES {len(state_keys)} #define NOUTPUTS {len(variables)} #define NSTOPS {len(stops)} +{contact_header} extern const NativeStop model_stops[{max(1,len(stops))}]; extern const double model_atol[NSTATES]; extern const char *const model_output_keys[NOUTPUTS]; diff --git a/app/simulation/native_codegen/contacts.py b/app/simulation/native_codegen/contacts.py new file mode 100644 index 0000000..28f439f --- /dev/null +++ b/app/simulation/native_codegen/contacts.py @@ -0,0 +1,24 @@ +"""Emit state-only LSTP event descriptors for both native lowering paths.""" +from .compiler import _number + + +def contact_table(contacts): + """Rows: velocity indices, gap, stiffness, damping, depth, force option, + subtract-first flag. Keep the force expression's floating operation order. + Position follows velocity in each reviewed mechanical state pair. + """ + rows = [] + for v1, v2, gap, stiffness, damping, depth, option, subtract_first in contacts: + rows.append('{' + ','.join((str(v1), str(v2), _number(gap), + _number(stiffness), _number(damping), _number(depth), + str(option), str(subtract_first))) + '}') + count = len(rows) + header = f'''#define NCONTACTS {count} +typedef struct {{ + int velocity1, velocity2; + double gap0, stiffness, damping, depth; + int signed_force, subtract_first; +}} NativeContact; +extern const NativeContact model_contacts[{max(1, count)}];''' + source = f'const NativeContact model_contacts[{max(1, count)}] = {{' + (','.join(rows) or '{0,0,0,0,0,0,0,0}') + '};' + return source, header diff --git a/app/simulation/native_codegen/extended.py b/app/simulation/native_codegen/extended.py index 9164149..f235690 100644 --- a/app/simulation/native_codegen/extended.py +++ b/app/simulation/native_codegen/extended.py @@ -723,6 +723,12 @@ def compile_extended_program(network): policy='immutable canonical baseline for gas, PH inversion, density and pipe roots; changed inputs recomputed', verification='uncached canonical dense differences') np,ng,nq=max(1,len(pgroups)),max(1,gas_count),max(1,len(pneu)) + from .contacts import contact_table + contact_source, contact_header = contact_table([ + (states[mass_groups[groups.find(ep(c, 'port_1'))][0].name, 'v'], + states[mass_groups[groups.find(ep(c, 'port_2'))][0].name, 'v'], + c.gap0, contact_stiffness(c), c.rcont, c.Pdis, int(c.discContactOption), 0) + for c in components if c.model_type == 'amesim_lstp00a']) # Advanced-friction reference runs at 5 N and 7 N confirm Amesim 2404's # 0.1% breakaway hysteresis. Simple friction uses the exact static threshold. source='\n'.join(['#include "model.h"','#include ','#include ',*declarations, @@ -731,6 +737,7 @@ def compile_extended_program(network): num(sum(c.fstick*(1.001 if int(c.frictionType)==2 else 1) for c in dry))+'}' for group,dry,threshold in friction_groups) or '{0,0,0,0}')+'};', f'const NativeStop model_stops[{max(1,len(stops))}] = {{'+(','.join('{'+str(s[0])+','+','.join(num(v) for v in s[1:])+'}' for s in stops) or '{0,0,0,0,0,0,0}')+'};', + contact_source, 'const double model_atol[NSTATES] = {'+','.join(map(state_absolute_tolerance, state_keys or ['dummy']))+'};', 'const char *const model_output_keys[NOUTPUTS] = {'+(','.join(json.dumps(v.key,ensure_ascii=True) for v in variables) or '""')+'};', *jacobian.source_lines(), @@ -774,6 +781,7 @@ int model_property_temperatures(double t,const double *y,NativePropertyTemperatu #define NOUTPUTS {max(1,len(variables))} #define NSTOPS {len(stops)} #define NFRICTIONS {len(friction_groups)} +{contact_header} extern const NativeFriction model_frictions[{max(1,len(friction_groups))}]; int model_friction_drives(double t,const double *y,double *drives); extern const NativeStop model_stops[{max(1,len(stops))}]; diff --git a/docs/other/LSTP主路接入与八路力差定位-2026-09-17.md b/docs/other/LSTP主路接入与八路力差定位-2026-09-17.md new file mode 100644 index 0000000..dc28350 --- /dev/null +++ b/docs/other/LSTP主路接入与八路力差定位-2026-09-17.md @@ -0,0 +1,151 @@ +# LSTP 主路接入与八路力差定位 + +日期:2026-09-17。参照:本机 Amesim 2404、当前循环八路归档及上轮已审计的非循环归档。 + +## 1. 结论 + +**LSTP 事件已接入平台默认原生求解流程。循环、非循环八路的 238 条比较曲线均与上一轮“仅 LSTP”独立实验逐值相同,生成的原 RHS、雅可比结构和求解计数保持一致。MASS 的新增释放检查未合入。** + +此前循环八路约 **128.55 万 N** 的最大力差,主要来自事件附近的保存时刻差异:平台配对点尚在驱动跳变边界,Amesim 普通输出点已经向前演化约 **2.20 ps**。两边驱动值相同,但连续速度已经不同,`1e11 N/(m/s)` 的阻尼将这个速度差放大。 + +在 Amesim 副本中只开启 **Discontinuities printout(不连续点输出)** 后,沿用原有共同网格及同驱动阶段配对,循环八路最大力差降为 **1.312403 N**,力的相对差超过 5% 的样本归零。平台没有再次改变积分结果,Amesim 原有所有样本也逐值不变,只新增了事件保存点。 + +另一个约 **−2.084e14 N** 的原始力峰,在 Amesim 的事件输出中同样存在,来自理想硬限位碰撞后的速度重置与大阻尼。这是当前参照工况本身产生的瞬态量级,不能直接当成平台独有的数值误差。 + +## 2. 默认流程改动及验证 + +- 两条生成路径均发出 `NativeContact` 描述,直接引用两端质量组的速度/位移状态,涵盖数值/几何刚度模式。 +- 默认定位 LSTP 间隙过零;非负力模式还定位接触区内原始力过零。允许负力的模式不新增力截断事件。 +- 沿用已测试的接受步密集插值、二分定位及相对速度换向分段检查。接触事件不重置位置或速度,不额外调用整模型 RHS。 +- 防重复记录只在事件接受时更新,不参与力、雅可比或输出重放。保留已有输出语义 v2;结果增加 `contactEvents` 诊断计数。 +- 两条生成路径原有间隙表达式的浮点运算顺序分别保留。移除新增常量描述表后,生成的 C 源码 SHA-256 与上轮原模型相同;雅可比仍为 132 状态、27 色和原物性复用方案。 + +实现:[事件描述](../../app/simulation/native_codegen/contacts.py)、[公共事件处理](../../native/runtime/common.c)、[接触定位](../../native/runtime/contact_events.h)、[组件接入规范 12.3](../standard/component-model-authoring-spec-v1.md#123-lstp-接触状态事件)。已有 LSTP 实例自动生效,无需工程 JSON 另加事件开关。 + +| 主路复测 | 循环 | 非循环 | +| --- | ---: | ---: | +| 与独立 LSTP 实验的全部比较数组 | 完全相同 | 完全相同 | +| 接受步 | 16,934 | 9,980 | +| RHS 求值 | 81,067 | 54,412 | +| 求解器启动 | 21 | 6 | +| 新增 LSTP 接触事件 | 8,同一时刻合并重启一次 | 8,同一时刻合并重启一次 | +| 接触力截断事件 | 0 | 0 | +| 同阶段配对 | 5,001/5,001 | 5,001/5,001 | + +本轮未重新做五次性能测量,不将单次秒数解释为新性能变化;积分计数与上一轮独立实验一致。上轮性能结论见[MASS/LSTP 独立报告](MASS与LSTP事件独立实验及八路对比-2026-09-17.md)。 + +验证覆盖解析碰撞、守恒、两个最大步长、BDF/RK45、几何刚度、指数阻尼、力截断、同号间隙的中间接触/脱离、切触、无事件轨迹不变、事件输出及纯求解一致性,以及原有摩擦、硬限位和雅可比结构。 + +8 项 LSTP 专项及相关输出/信号、摩擦/结构回归合计 **27 个不同测试方法通过**。最初 Amesim 参考回路测试误用 2 ms 采样,参考含 1 ms 点,产生插值误差;恢复到参考的 1 ms 采样后原公差通过,没有放宽断言。另保留旧独立实验工具的兼容处理:使用新运行库时关闭主路接触注册,避免与实验事件重复执行。 + +测试日志:[LSTP 首批](../../test/lstp-mainline-unit.log)、[参考回路复测及输出/信号回归](../../test/lstp-mainline-regression.log)、[摩擦与结构](../../test/lstp-mainline-friction-structure.log)、[几何刚度](../../test/lstp-mainline-geometry.log)。八路原始结果:[主路汇总](../../test/lstp-mainline-20260917/baseline-summary.json)。 + +另有 1 项旧独立实验入口在新运行库上的[兼容回归](../../test/lstp-mainline-experiment-compatibility.log)通过;最终源码/结果隔离核验见[检查记录](../../test/lstp-mainline-20260917/final-verification.json)。 + +## 3. 将 32.4 s 的力差拆开 + +以第 2 路为例,接触力为: + +`F = K*p + R*(1-exp(-p/Pdis))*(v_branch-v_common)`。 + +此时穿透约 4.6866 μm,`Pdis=0.1 μm`,阻尼系数已基本达到 `R=1e11 N/(m/s)`。由两侧保存的间隙和速度分别重建接触力,均与各自输出完全一致。 + +| 原普通输出的配对量 | 平台 | Amesim | +| --- | ---: | ---: | +| 实际时间,s | 32.400000000000006 | 32.40000000000221 | +| 公共质量块速度,m/s | 0 | −1.2883209101e-5 | +| 第 2 路相对速度,m/s | −3.3024983e-12 | 1.2854894891e-5 | +| 弹性力,N | 468,662.995301 | 468,662.655106 | +| 阻尼力,N | −0.330250 | 1,285,489.489088 | +| 总接触力,N | 468,662.665051 | 1,754,152.144194 | + +**弹性项只差 0.340 N,约 128.55 万 N 的差异几乎全部是阻尼项。** 双方均在接触区,并不存在一边接触、一边脱离的模式错误。 + +此刻 UD00 驱动从 0 切换为 1e12 N,经负方向力源作用于公共质量块。质量为 170,000 kg,位于 0.37 m 上限,切换后向允许方向释放,加速度约 −5.882e6 m/s²。经过约 2.2e-12 s,速度变化达到约 1.3e-5 m/s,乘以大阻尼便得到百万牛级力变化。 + +本机参考代码中,[UD00](F:/AMESim2404/Amesim/libsig/submodels/UD00.c:593)更新周期阶段并调用时间不连续处理;[MECMAS21](F:/AMESim2404/Amesim/libmec/submodels/MECMAS21.c:547)将理想限位交给 `endst2_`;[LSTP00A](F:/AMESim2404/Amesim/libmec/submodels/LSTP00A.c:296)由穿透及其导数计算接触力。这些代码和本次输出支持上述因果链,但本报告不据此声称两套积分器内部算法完全相同。 + +## 4. 三种对照验证取样影响 + +### A. 使用相同实际时刻的连续状态 + +在独立运行库副本中增加 210 个只读密集输出查询,包括 Amesim 真正保存的时间。查询不改变步长、时间事件或接受状态;完整积分计数与主路运行一致。结果重放仍调用原模型公式。 + +| 第 2 路名义时刻 | 原配对绝对力差,N | 同一实际时刻绝对力差,N | 减少 | +| --- | ---: | ---: | ---: | +| 21.6 s | 387,825.741 | 7,758.635 | 98.00% | +| 32.4 s | 1,285,489.479 | 5,025.566 | 99.61% | +| 43.2 s | 29,254.603 | 3.685 | 99.99% | + +同一实际时刻仍有小残差,不能据此宣称皮秒瞬态完全一致。32.4 s 附近 binary64 时间间距约 7.105e-15 s;按此刻加速度和阻尼估算,一格时间变化对应约 4,180 N。主路日志在这些驱动边界也记录了积分器成功步未推进外部浮点时间的情况。 + +进一步用**同一已保存的边界状态**,在独立程序中将局部时间从 0 开始,只继续到上述 2.20 ps;原力学、热力学公式及驱动阶段不变。rtol=1e-8 和 1e-10 的第 2 路末力均约 **1,761,496.860 N**;默认绝对时间密集输出为 **1,749,126.578 N**,Amesim 普通输出为 **1,754,152.144 N**。该试验表明这一极短瞬态对时间原点和启动处理敏感,单纯再收紧相对容差不能解释或消除全部残差。局部时钟没有合入生产。 + +证据:[逐支路力分解与同时间对照](../../test/lstp-mainline-20260917/force-error-analysis.json)、[连续状态取样](../../test/lstp-mainline-20260917/trace/dense-outputs.json)、[局部时钟结果](../../test/lstp-mainline-20260917/rebase/summary.json)。 + +### B. 直接比较事件前后保存点 + +按本机 [Amesim `ameputsimopt`](F:/AMESim2404/Amesim/scripting/python/amesim.py:1093) 的定义,仅将副本 `.sim` 第二行的 `simOptions[2]` 从 0 改为 1,启用 `printDiscont`。模型、容差、时间区间、普通输出间隔及其他求解选项不变,重新运行 Amesim。 + +记录从 5,002 行增至 5,046 行,核验**所有原有时刻、全部原保存变量逐值不变**。32.4 s 第 2 路新增记录为: + +| Amesim 实际时刻,s | 阶段 | 公共质量块速度,m/s | 接触力,N | +| --- | --- | ---: | ---: | +| 32.400000000000006 | 驱动切换前 | 0 | 468,662.652083 | +| 32.40000000000001 | 驱动切换后立即 | 0 | 468,662.652083 | +| 32.40000000000221 | 原普通输出,已演化约 2.2 ps | −1.2883209101e-5 | 1,754,152.144194 | + +平台事件后的力为 **468,662.665051 N**。与 Amesim 的事件后立即值比较,差 **0.012968 N**。这直接证明“相同信号阶段”还不足以把事件边界与已经演化的普通输出当成同一个瞬态。 + +### C. 保持原比较算法,重新比较整组八路 + +平台结果不变;参照换成含事件点的 Amesim 输出,仍用相同名义 10 ms 网格、相同时间候选窗口、完整 10 个驱动信号配对,并让所有曲线共用一对行。没有根据力差选择样本,没有删除事件网格点。 + +Amesim 原始事件输出存在一处文件行序倒置:先写 0.8000000000000009 s 普通点,后写 0.8 s 事件点。本次诊断只按原始实际时间稳定排序,保留全部行、值和重复时间,并保存排序到原始行的映射;未改变平台导入器或原始结果文件。 + +| 循环八路指标 | 原普通 Amesim 输出 | Amesim 含事件点输出 | +| --- | ---: | ---: | +| 比较曲线/网格点 | 238 / 5,001 | 238 / 5,001 | +| 未配对时刻 | 0 | 0 | +| 全网格最大绝对力差,N | 1,285,489.485624 | **1.312403** | +| 最大力差时刻 | 32.4 s | 0.01 s | +| 力的相对差超过 5% 的曲线样本数 | 24 | **0** | +| 所有变量相对差超过 5% 的曲线样本数 | 32 | **6** | +| 最大压力差,Pa | 61.105785 | 61.105785 | +| 最大温度差,K | 0.389714 | 0.389714 | + +剩余 6 个为质量流量 4、焓流 2;5% 仍只是原有诊断线。32.4 s 八路在事件侧配对后的力差均小于 0.015 N。**1.312403 N 是这组共同网格和事件侧对照的最大差,不能作为网格之间所有极短瞬态误差的上界。** + +证据:[新对照汇总](../../test/lstp-mainline-20260917/event-output-comparison/summary.json)、[全部曲线](../../test/lstp-mainline-20260917/event-output-comparison/comparison.json)、[阶段配对](../../test/lstp-mainline-20260917/event-output-comparison/phase-pairing.json)、[原始行映射](../../test/lstp-mainline-20260917/event-output-comparison/ame-row-order.json)。 + +## 5. 约 2.084e14 N 的原始力峰来自哪里 + +该峰发生在约 32.4003551 s,即释放后约 0.355 ms,公共质量块从上限运动至下限。碰撞前,两端速度都约 −2083.9 m/s、相对速度较小;理想下限把公共质量块速度重置为 0,支路质量块仍保持约 −2083.9 m/s。因此大阻尼立即产生约 −2.084e14 N 的力。此模型允许负接触力,该值不会被截断。 + +| 第 2 路对应的下限碰撞后 | 平台 | Amesim | +| --- | ---: | ---: | +| 事件时刻,s | 32.40035510065042 | 32.400355100640695 | +| 公共质量块位移/速度 | 0 m / 0 m/s | 0 m / 0 m/s | +| 支路速度,m/s | −2083.913265366 | −2083.913208006 | +| 接触力,N | −2.083913259638e14 | −2.083913202278e14 | + +对应碰撞时间差约 **9.73 ps**,力值相对差约 **2.75e-6%**。单看绝对差仍有约 5.74e6 N,但这是在约 2.084e14 N 的峰值上。两边都出现同一种速度重置和阻尼峰,不能把这个峰本身归因于缺少 LSTP 事件。 + +这验证了当前参照模型的实现一致性;是否希望工程模型具有如此大的外力、速度及理想碰撞瞬态,应由工况与接触建模目标决定,不能用调小数值结果来替代模型选择。 + +证据:[平台碰撞前后样本](../../test/lstp-mainline-20260917/raw-force-peak.json)、[Amesim 事件输出及碰撞样本](../../test/lstp-mainline-20260917/amesim-event-output/mechanical-events.json)。 + +## 6. 后续建议及复现 + +初步平台展示与正确性评价,优先使用**开启不连续点输出的 Amesim 结果**作为参照,并保留共同事件侧配对、原时间戳及原普通输出的审计。当前证据不支持为消除原来的 128 万 N 配对差再调整 LSTP 刚度、阻尼,或将昂贵的 MASS 释放实验合入默认路径。 + +若下一步要验证皮秒尺度的峰形,再研究按事件相对时间表示/求解短瞬态、保存精度以及硬限位重置后的连续状态;本次只完成了独立局部时钟诊断,没有改生产时间处理。 + +[诊断脚本](../../tests/manual/diagnose_mql8_force_events.py)支持分阶段复现,完整执行使用新目录: + +```powershell +.\.venv-win\Scripts\python.exe -X utf8 -m unittest tests.test_native_contact_events -v +.\.venv-win\Scripts\python.exe -X utf8 tests/manual/diagnose_mql8_force_events.py --output test/lstp-mainline-new-run --stage all +``` + +该诊断依赖本工作站已对齐的工程、上一轮独立实验数据和 Amesim 2404。源码、测试、规范及本文为可跟踪交付;原始结果在 Git 忽略的 `test/lstp-mainline-20260917/`。Windows 编译器的偶发启动失败由现有重试恢复;诊断和新专项串行构建以减少干扰,生产构建调度未更改。 diff --git a/docs/other/MASS与LSTP事件独立实验及八路对比-2026-09-17.md b/docs/other/MASS与LSTP事件独立实验及八路对比-2026-09-17.md new file mode 100644 index 0000000..9cdc308 --- /dev/null +++ b/docs/other/MASS与LSTP事件独立实验及八路对比-2026-09-17.md @@ -0,0 +1,171 @@ +# MASS 与 LSTP 事件独立实验及八路对比 + +日期:2026-09-17。环境:Windows、本项目 Python 3.12、原生 C、CVODE BDF;组件专项同时覆盖 RK45。 + +## 1. 本次实验回答什么 + +以已经完成 UD00 边界修复、事件输出语义修改的当前平台为基线,独立比较 `off`、`mass`、`lstp`、`all` 四种配置。循环和非循环各运行 50 s;每种配置保留完整结果,再做 5 次轮换顺序的纯求解计时。 + +**实验实现放在 `tests/manual/`,没有把新事件启用到默认平台。已有信号修复、输出语义、组件公式、雅可比分色及物性复用保留。** 因此本文的“未修改”特指本轮新增机械事件关闭,不是回退到此前有信号边界问题的版本。 + +MASS 的新增事件在当前八路工况中没有带来精度改善,纯求解耗时中位数增加 **31%–34%**。LSTP 的非循环最大力差降低 **9.90%**,本次耗时没有明显增加,但循环工况 32.4 s 的主要力差没有改善,部分其他误差反而增大。两类一起开启,精度与只开 LSTP 相同,耗时增加 **22%–23%**。 + +## 2. 独立实现与隔离方式 + +当前 `native/` 冻结为 `test/mechanical-events-20260917/source-control/native/`。`off` 直接用这个副本;其他配置用另一份副本追加实验运行代码,编译缓存、可执行文件、结果存储均放在本次实验目录。 + +[实验程序构建](../../tests/manual/mechanical_event_variant.py)在原生成源码后追加事件描述与释放驱动力求值入口。每次构建均断言原生成源码仍是逐字相同的前缀,状态键、求值调度和雅可比稀疏结构一致。正常 RHS、Jacobian 和物性缓存路径没有替换。八路仍为 132 个状态、1,784 个输出、27 个雅可比颜色。 + +生产源码的前后 SHA-256 核验、原模型前缀核验与积分计数用于区分“新增事件改变积分轨迹”和“破坏已有优化”。这些检查不代表新事件本身没有计算成本。 + +最终核验:46 个生产源码文件前后哈希相同,控制副本与当前 `native/` 相同;所有配置仍采用 `colored-difference` 和输出语义 v2。两组 `off` 的全部比较数组与上一轮输出语义基线完全一致。见[隔离及结果核验](../../test/mechanical-events-20260917/baselines/evidence-verification.json)与[生产源码哈希](../../test/mechanical-events-20260917/baselines/production-verification.json)。 + +### MASS + +- 弹性上下限:分别检查 `xmin - x`、`x - xmax` 过零,定位接触和脱离。 +- 非负接触力模式:在接触区内额外检查未截断力 `K*p + D(p)*v_relative` 过零。允许负力的模式不添加这个截断事件。 +- 理想/恢复限位保持后的释放:在已贴限位、速度接近零时,检查不施加限位约束的加速度是否转向允许运动的方向。 +- 原有碰撞、恢复系数和摩擦事件继续按原逻辑处理。弹性事件不重置速度或位置;保持后的释放从约束位置、零速度继续。 + +为隔离已有优化,释放驱动力来自独立整模型求值,没有修改原 RHS 以共享中间量。`releaseRhsCalls` 单独记录这项成本,并计入 `nfev`。这是**本实验实现**的代价,不是所有 MASS 事件实现必然付出的代价。 + +### LSTP + +按原公式的同一运算顺序计算 `gap = gap0 + x2 - x1`,检查 `-gap` 的过零。非负力模式再检查接触区内的原始力过零;允许负力时不检查力截断。事件不施加硬碰撞式速度重置。 + +### 公共定位与输出 + +[实验运行库](../../tests/manual/mechanical_events_runtime.c)在积分接受步的密集插值上二分定位;浮点区间不可再分时停止。同一浮点时刻发生的事件合并提交并重启一次。仅在事件真正接受后更新防重复触发记录;试算、雅可比求值和输出重放不更新物理模式,输出仍由原公式计算。 + +相对速度换向时,先按换向点分段找间隙根,以覆盖步首尾间隙同号但中间接触/脱离的情况;孤立切触不制造虚假来回事件。这仍依赖积分步已经解析运动,不能保证捕获一个接受步内任意多次未解析的高频振荡。根的二分精度也不等于积分状态精度。 + +沿用上一轮 `outputSemantics.version=2`:记录事件左邻时刻和接受后的事件时刻。没有调整比较点来挑选误差更小的力值。 + +## 3. 组件独立验证 + +验证包括解析解、物理守恒、现有 Amesim 参考回路和无事件回归,代码见[专项测试](../../tests/test_mechanical_event_experiment.py)。 + +| 测试 | 验证依据 | +| --- | --- | +| MASS 弹性上下限 | 阻尼振子的解析接触持续时间及出界速度;两个最大步长、BDF/RK45 | +| MASS 保持后释放 | 连续斜坡驱动,1 s 释放;上下限镜像,最终速度和位置解析值 | +| MASS 原有恢复碰撞 | 恢复系数 0.5;与事件关闭版逐样本、最终状态和启动次数对照 | +| LSTP 两质量弹性碰撞 | 解析接触/脱离时刻、速度交换、总动量和动能加弹性势能 | +| MASS/LSTP 非负力 | 接触区内力截断事件、非负输出、MASS 截断时刻解析值 | +| 指数阻尼与两种力模式 | MASS/LSTP、BDF/RK45、extended 生成路径,检查完成且无事件抖振 | +| 无接触、初始贴边静止、持续接触 | 无多余重启,原始输出与关闭版完全一致 | +| 同号间隙与切触 | 已知二次多项式密集输出:准确找到 0.4/0.6 s 两根,切触不造事件 | +| 已有 Amesim 回路 | `friction_ideal`、`friction_elastic`、`joint_mechanical`,按现有参考值核验 | + +弹性接触时刻/速度允许误差 2e-6,连续释放时刻 1e-8 s,解析截断时刻 3e-6 s;守恒断言另设独立公差。Amesim 参考来自现有 `amesim-component-semantics-2404.json`,这里不声称重新测出了 Amesim 的内部根定位算法。八路则使用本轮重新运行的 Amesim 结果。 + +**9 个测试方法全部通过**,覆盖 38 种模型/配置/求解器组合,另含 2 个解析密集输出场景。按开发顺序分批执行,力截断条件补全后专门复跑相关测试;日志有重复方法,不能把各批数量直接相加。证据:[首批 6 项](../../test/mechanical-events-20260917/isolated-verified.log)、[截断复测及参考回归 3 项](../../test/mechanical-events-20260917/independent-reference-tests.log)、[恢复碰撞回归 1 项](../../test/mechanical-events-20260917/restitution-regression.log)。 + +## 4. 八路比较口径 + +循环参照为 `tests/data/test_mql.ame`,非循环参照为已审计的 `test/node-fixes-amesim-20260914/test_mql.ame`。输入用上轮已对齐的工程副本,原 AME 和用户工程不修改。每个工况重新执行一次 Amesim,作为四种配置共同的参考;平台 BDF、0–50 s、输出间隔 0.01 s、误差限数值 1e-8。 + +八路包含 10 个 MASS:9 个无限位、1 个理想限位,没有弹性限位 MASS;8 个 LSTP 均允许负力,接触刚度与阻尼均为 1e11,指数阻尼距离约 1e-7 m。因此八路不覆盖全部新增模式,必须结合前面的组件测试判断。 + +每个比较点先核验 8 个 STEP 和 2 个 UD00 的完整驱动组合,再让全部 238 条曲线共用同一对真实保存行,不跨信号跳变插值。另检查八个 `gap < 0` 标志。信号阶段匹配不等于两边浮点时间完全相同,也不代表验证了网格之间的全部接触事件。 + +全部 8 次准确性运行均正常结束,全部比较输出有限;各自 **5,001/5,001 个时刻成功配对,信号最大差为 0,接触/分离标志不匹配为 0**。没有删除未匹配点来改善指标。 + +## 5. 八路准确性结果 + +### 汇总 + +下表列 `off → lstp`。`mass` 在 238 条已比较曲线上与 `off` 完全一致,`all` 与 `lstp` 完全一致,因此不重复相同列。 + +| 指标 | 循环:off → lstp | 非循环:off → lstp | +| --- | --- | --- | +| 最大接触力绝对差,N | 1,285,489.483041 → 1,285,489.485624 | 1.456532 → **1.312403** | +| 上项发生时间,s | 32.4 → 32.4 | 0.01 → 0.01 | +| 平静段最大力差,N | 0.275890 → **0.212043** | 0.275890 → **0.154170** | +| 最大压力差,Pa | 65.686768 → **61.105785** | 65.686768 → **61.105785** | +| 最大温度差,K | 0.395395 → **0.389714** | 0.395395 → **0.389714** | +| 最大间隙差,m | 1.293005e-8 → **5.474060e-8,增大** | 1.293005e-8 → **1.581826e-8,增大** | +| 最大质量流量差,kg/s | 1.326797e-5 → **2.424649e-5,增大** | 5.663171e-6 → **2.424649e-5,增大** | +| 最大焓流差,W | 141.920203 → **18.688805** | 141.920203 → **18.688805** | +| 明确非零且相对差超过 5% 的曲线样本数 | 56 → **32** | 30 → **6** | + +“平静段”沿用已有定位统计:t≥0.1 s,避开信号边界前后 0.02 s;它不能替代包含事件点的完整指标。5% 是诊断筛查线,不是工程验收公差。循环的 32 个超线样本仍含接触力 24、速度 2、质量流 4、焓流 2;非循环剩余质量流 4、焓流 2。 + +最大压力差降低约 6.97%,温度差降低约 1.44%;循环/非循环平静段最大力差分别降低约 23.14%/44.12%。但间隙和质量流量的最大绝对差增大,因此不能称为所有指标全面改善。新增最大流量差发生在 0.09 s、`amesim_pn3node2_1.reference_mass_flow`;该点参考流量约 0.02098 kg/s,绝对差约为参考值的 0.116%。原有小流量样本的相对差计数减少,与这一绝对误差增大可以同时发生。 + +### 八个支路分别看 + +以下均为各支路完整 0–50 s 网格的最大绝对力差,单位 N。支路编号对应 `amesim_lstp00a_1…8`。循环均发生在 32.4 s,非循环均发生在 0.01 s。 + +| 支路 | 循环 off | 循环 lstp | 非循环 off | 非循环 lstp | +| --- | ---: | ---: | ---: | ---: | +| 1 | 1,285,489.480 | 1,285,489.479 | 1.450464 | 1.306552 | +| 2 | 1,285,489.483 | 1,285,489.479 | 1.456532 | 1.312398 | +| 3 | 1,285,489.482 | 1,285,489.486 | 1.456530 | 1.312403 | +| 4 | 1,285,489.478 | 1,285,489.480 | 1.450460 | 1.306567 | +| 5 | 1,285,489.475 | 1,285,489.483 | 1.447450 | 1.299400 | +| 6 | 1,285,489.475 | 1,285,489.481 | 1.446239 | 1.298499 | +| 7 | 1,285,489.477 | 1,285,489.478 | 1.446238 | 1.298504 | +| 8 | 1,285,489.478 | 1,285,489.477 | 1.447445 | 1.299415 | + +非循环八路均改善,循环八路的大力差均基本不变。完整精度数据见[汇总](../../test/mechanical-events-20260917/baselines/effect-summary.json)、[逐支路](../../test/mechanical-events-20260917/baselines/branch-force-summary.json)、[全部物理量分组](../../test/mechanical-events-20260917/baselines/group-summary.json)。 + +### 实际新增了哪些事件 + +两组工况都只有 8 个新增 LSTP 接触事件,全部发生在 **6.5966840909145208e-9 s**,合并为一次积分重启;之后没有检测到新的 LSTP 接触/脱离。当前采用允许负力模式,因此没有力截断事件。 + +两组都没有新增 MASS 弹性接触或连续释放事件。当前模型没有弹性限位 MASS;理想限位的相关驱动跳变已有时间事件负责重新启动,没有额外触发本次连续释放根。不能用“本模型未触发”推断其他驱动下不需要释放事件;连续斜坡测试已验证释放能正确发生。 + +循环 32.4 s 仍使用平台实际时刻 `32.400000000000006` 和 Amesim `32.40000000000221`。驱动阶段一致,但约 2.20e-12 s 的实际时间差与高刚度连续状态的影响仍未分离。新增的启动间隙事件不能替代对此的检查。 + +额外事件保存点的最大绝对接触力仍约为循环 **2.084e14 N**、非循环 **4.035e11 N**,与 `off` 基本同量级;这些点没有同分辨率 Amesim 参考,未计入上述准确性判断。 + +## 6. 求解耗时与成本来源 + +每组、每配置运行 **5 次纯求解**,轮换配置顺序。计时排除编译和结果重放/序列化;完整输出准确性运行另存。下表为墙钟中位数,括号为五次最小–最大值,百分比相对本次同工况 `off`,不与上轮不同时间运行的单次秒数混比。 + +| 配置 | 循环耗时,s | 相对 off | 非循环耗时,s | 相对 off | +| --- | ---: | ---: | ---: | ---: | +| off | 14.334(14.209–14.664) | — | 9.583(8.978–10.293) | — | +| mass | 19.220(18.035–20.428) | **+34.09%** | 12.565(12.190–14.599) | **+31.12%** | +| lstp | 13.605(13.369–15.221) | **−5.09%** | 8.871(8.661–9.044) | **−7.43%** | +| all | 17.697(17.003–19.600) | **+23.46%** | 11.653(11.246–11.852) | **+21.60%** | + +LSTP 的运行范围与基线有重叠,五次试验不构成普遍提速保证;本次没有明显增加耗时,且积分步数下降。MASS 的范围与基线明显分离,额外开销同时有计算次数支持。各配置五次计时的积分计数完全重复,并与完整输出运行一致。 + +| 工况/配置 | 接受步 | 拒步 | RHS 求值 nfev | 求解器启动 | 额外释放驱动力求值 | +| --- | ---: | ---: | ---: | ---: | ---: | +| 循环 off | 19,606 | 1,552 | 85,570 | 20 | 0 | +| 循环 mass | 19,606 | 1,552 | 122,240 | 20 | **36,670** | +| 循环 lstp | 16,934 | 1,137 | 81,067 | 21 | 0 | +| 循环 all | 16,934 | 1,137 | 112,331 | 21 | 31,264 | +| 非循环 off | 11,238 | 885 | 56,299 | 5 | 0 | +| 非循环 mass | 11,238 | 885 | 78,561 | 5 | **22,262** | +| 非循环 lstp | 9,980 | 742 | 54,412 | 6 | 0 | +| 非循环 all | 9,980 | 742 | 74,172 | 6 | 19,760 | + +LSTP 循环/非循环分别执行 135,472/79,840 次元件步检查、2,196/963 次密集插值调用,实际只增加一次事件重启;其检查直接使用状态,不调用整模型 RHS。MASS 单独开启时,轨迹、步数和原有重启次数没变,多出的 nfev 恰好等于释放驱动力整模型求值次数。这解释了为什么“没有新事件发生”仍可能有明显耗时。 + +CPU 时间中位数也支持同一方向:循环 off/mass/lstp/all 为 13.891/18.266/13.062/16.984 s,非循环为 9.328/11.969/8.609/11.281 s。全部独立计时见[循环](../../test/mechanical-events-20260917/baselines/full/timings.json)、[非循环](../../test/mechanical-events-20260917/baselines/noncyclic/timings.json)。 + +## 7. 结论适用范围与下一步 + +1. **当前八路不宜直接把本实验 MASS 释放检查作为默认路径。** 其新增事件在这两组工况中没有触发,结果不变而存在额外计算。对于连续驱动力导致的限位释放,专项解析测试证明事件机制有作用;后续若集成,应优先从已有 RHS 获取释放驱动力,或仅计算必要依赖,再重新计时。 +2. **LSTP 可以作为后续生产集成候选,但本次仍保留在实验中。** 它补齐了接触边界定位,部分误差降低,间隙和质量流量最大误差也有回退;不能据此声称循环八路的主要力差已解决,也不能把这一模型中的耗时结果推广到频繁接触/脱离的模型。正式集成前应根据展示关注量确认这些取舍。 +3. **循环力峰仍应检查同一实际时刻的连续状态。** 32.4 s 的信号阶段已经一致,新增间隙事件仍未消除差异。下一步应核对细时间窗中的位移、速度、间隙和力,区分实际保存时刻偏移、高刚度响应及已有硬限位处理的影响。 + +纳秒级启动事件的定位时刻是本次平台数值轨迹上的根,没有同分辨率 Amesim 根时刻验证。额外事件保存点的巨大力峰同样保留,没有平滑或删除,也没有把缺少同分辨率参考的峰值当成准确性结论。 + +## 8. 复现与证据 + +[运行说明](../../tests/manual/mechanical_events_experiment.md)、[八路运行脚本](../../tests/manual/evaluate_mechanical_events.py)、[指标汇总脚本](../../tests/manual/summarize_mechanical_events.py)。 + +```powershell +.\.venv-win\Scripts\python.exe -X utf8 -m unittest tests.test_mechanical_event_experiment -v +.\.venv-win\Scripts\python.exe -X utf8 tests/manual/evaluate_mechanical_events.py --output test/mechanical-events-new-run --repeats 5 +.\.venv-win\Scripts\python.exe -X utf8 tests/manual/summarize_mechanical_events.py test/mechanical-events-new-run +``` + +八路脚本依赖本工作站 Amesim 2404 及已有对齐工程;主输出目录必须是新目录。构建阶段出现过 Windows 缓存发布拒绝与 GCC 子进程启动/停滞,保留已完成运行后续跑,最后一次恢复使用 `--resume --serial-build`。串行选项只改变实验构建调度,不改变编译参数、模型、事件实现或求解器设置;构建及其等待均不计入上述纯求解时间。 + +本轮完整数据位于 Git 忽略的 `test/mechanical-events-20260917/`;代码、测试和本文可跟踪保存。每个配置目录有 `comparison.json`、`curves.npz`、`phase-pairing.json`、完整原始结果、事件日志及五次计时目录。 diff --git a/docs/other/事件输出语义修改与八路基线复测-2026-09-17.md b/docs/other/事件输出语义修改与八路基线复测-2026-09-17.md new file mode 100644 index 0000000..8fd537d --- /dev/null +++ b/docs/other/事件输出语义修改与八路基线复测-2026-09-17.md @@ -0,0 +1,95 @@ +# 事件输出语义修改与八路基线复测 + +日期:2026-09-17。Windows,本项目 Python 3.12、原生 C、CVODE BDF;独立测试同时覆盖 RK45。 + +## 1. 结论 + +**已完成输出语义修改、专项测试,以及循环/非循环两组 50 s 八路基线重跑。两组各 5,001 个比较时刻均找到同一信号驱动阶段的样本,信号比较误差归零;循环接触力尖峰差仍然存在。** + +本次修改只增加事件两侧的保存样本并明确比较规则,没有补充接触事件。前后对照均使用已经修复 UD00 的内核,避免把此前信号修复与本次输出修改混在一起归因。修改前的原生源码快照保存在 `test/output-semantics-20260917/control-source/native/`。 + +## 2. 输出语义怎样改变 + +- 规则采样时间仍按实际浮点数解释,不把网格点吸附到事件,不提前切换信号。 +- 对仿真区间内部的时间事件,保存 `nextafter(boundary, -INFINITY)` 和 `boundary`。前者是旧积分段实际到达的终点;后者保留连续状态,用事件后的信号重放输出。两者是相邻的真实浮点时刻,不是两个重名时间戳。 +- 对已注册的机械状态事件,在当前积分区间可用时,保存事件左邻时刻的插值状态,然后保存事件后的接受状态。这保留了碰撞前后速度及各自模式。初始即时事件没有新左侧区间,不倒填样本。 +- 同一实际时间依然以后一次接受状态为准;序列严格递增。结果增加 `outputSemantics.version=2`,已有状态/输出块格式和变量集合保持兼容。 +- 新增样本同样走既有有界缓冲、存储配额和输出重放流程。纯求解模式不保存这些样本。 + +主要实现:[公共事件采样](../../native/runtime/common.c)、[BDF](../../native/runtime/cvode_solver.c)、[RK45](../../native/runtime/rk45.c)、[结果元数据](../../native/runtime/main.c)。注册与重放要求见[组件开发规范 12.2](../standard/component-model-authoring-spec-v1.md#122-事件输出与重放)。 + +## 3. 如何保证比较阶段一致 + +当前八路有 8 个 STEP 和 2 个 UD00,UD00 各启用阶段均为常值。比较程序先取 Amesim 在名义网格附近实际保存的样本,再在平台保存样本中寻找相同的完整信号组合。 + +- 时间候选窗口仍为输出间隔的 `1e-7`,本次即 `1e-9 s`;它只用于匹配保存时间的舍入差,不修改求解时间或事件时间。 +- 10 个信号必须同时一致;所有 238 条曲线共用同一对样本行,不按接触力、压力或误差大小选择样本。 +- 不跨跳变插值;缺失对应阶段时明确记录未配对,不把错误配对纳入同阶段指标。旧的纯时间匹配指标另存为 `raw-time-comparison.json`,用于审计。 +- 本规则针对本次分段常值信号;脚本遇到 UD00 斜坡会明确拒绝,避免把“输出值相近”错误地推广为所有信号的阶段标识。 +- 额外核验八个接触件的带符号间隙:两组全部已配对点的 `gap<0` 标志一致。但这不是对网格之间所有接触/脱离时刻的验证,也不声称复原了 Amesim 的全部内部离散模式。 + +修改前循环工况缺少 4 个时刻的同阶段样本,非循环缺少 1 个;修改后两组均为 **5,001/5,001 配对,未配对 0 个**,没有删除事件点。循环改配时刻为 0.8、10.8、32.4、44 s;非循环为 0.8 s。真实时间、选中的信号值及原始行记录在各组 `phase-pairing.json`。 + +## 4. 第一次测试与回归 + +首先执行输出/相位配对、循环 UD00、存储专项共 18 项,全部通过;另补非网格时刻阶跃测试 1 项,通过。随后结果传输、HTTP 返回、取消/异常处理、恢复碰撞及摩擦保持回归 11 项通过,共 **30 项**。 + +关键验证包括:32.4 s 浮点边界前后分属不同阶段时正确配对;Amesim 保留事件前值时选用平台左侧样本;缺失阶段或任一驱动不同则明确拒绝;BDF/RK45 保存非网格阶跃两侧;碰撞前后速度分别为约 1 和 −0.5 m/s;有/无采样的积分最终状态及计数一致;二进制存储与 JSON 返回一致。 + +证据:[18 项首次通过日志](../../test/output-semantics-20260917/verified-first-tests.log)、[非网格阶跃](../../test/output-semantics-20260917/off-grid-test.log)、[11 项附加回归](../../test/output-semantics-20260917/additional-regression.log)。测试过程中一次解析碰撞时间断言过严:原设 1e-9 s,实际偏差约 2.61e-8 s;按该数值积分测试的目的改为 1e-6 s。事件两侧相邻浮点时间及速度断言未放宽。编译器偶发启动失败由既有重试恢复。 + +## 5. 八路基线结果 + +使用原有 AME 作为参照,在副本中审计并对齐参数、连接和运行设置。两组均重新执行 Amesim 归档中的程序及平台程序,没有用旧曲线代替本次运行。循环采用当前 `tests/data/test_mql.ame`;非循环采用此前已审计的 `test/node-fixes-amesim-20260914/test_mql.ame`。时间 0–50 s,输出间隔 0.01 s,两边误差限数值 1e-8。原始工程文件未修改。 + +下表“修改前”是保留了 UD00 修复的本次对照运行,采用旧的纯时间配对;“修改后”使用新增事件样本和同阶段配对。每项均包含事件网格点。 + +| 指标 | 循环:修改前 → 修改后 | 非循环:修改前 → 修改后 | +| --- | --- | --- | +| 信号最大绝对差 | 约 1e17 → **0** | 约 1e17 → **0** | +| 接触力最大绝对差 | 1,285,489.483 → **1,285,489.483 N** | 1.456532 → **1.456532 N** | +| 压力最大绝对差 | 65.686768 → **65.686768 Pa** | 65.686768 → **65.686768 Pa** | +| 温度最大绝对差 | 0.395395 → **0.395395 K** | 0.395395 → **0.395395 K** | +| 明确非零且相对差超过 5% 的曲线样本数 | 63 → **56** | 32 → **30** | +| 修改后信号阶段不匹配的比较点 | **0** | **0** | +| 修改后八路接触/分离标志不匹配的比较点 | **0** | **0** | + +5% 仍是诊断筛查线,不是工程验收公差。循环剩余 56 个超线曲线样本为接触力 24、速度 2、质量流 23、焓流 7;非循环剩余 30 个为质量流 23、焓流 7。事件之外的小流量/焓流差异未因本次修改消失。历史初评中的 61 个不能直接当作本次修改前计数,因为本次对照的 UD00 已修复。 + +### 32.4 s 为何仍需继续查 + +选中的平台时刻为 `32.400000000000006`,Amesim 为 `32.40000000000221`。两边 UD00 均为事件后的值,第 2 路间隙均为负、处于接触区,但力仍分别为 **468,662.661 N** 与 **1,754,152.144 N**。 + +本次证据只排除了“信号前值对信号后值”的误配,不能排除高刚度过程对约 `2.20e-12 s` 实际时间差的敏感性,也不能证明缺少接触事件就是该误差的唯一原因。弹性接触力取决于连续位移/速度;切换信号输出不会直接把这些连续状态改成 Amesim 的值。 + +共同 10 ms 网格仍不是原始瞬态的上界:循环额外事件点中约 **2.084e14 N** 的绝对力峰仍存在;非循环原始峰值约 **4.035e11 N**。这些原始峰未被平滑、删除或纳入不具备同分辨率参考的精度结论。循环在 t≥0.1 s 且避开信号边界前后 0.02 s 的定位统计中,最大力差约 **0.276 N**,不能替代完整事件段验收。 + +## 6. 是否改变求解与耗时 + +对两组修改前后原始结果逐项核验:全部旧样本仍保留原始时间,1,784 条原输出在这些时刻数值完全一致,最终状态和全部下列积分计数也一致。这支持本次为输出/对照语义修改,没有改变八路积分轨迹。 + +| 项目 | 循环:修改前 → 修改后 | 非循环:修改前 → 修改后 | +| --- | --- | --- | +| 实际保存点数 | 5,010 → 5,028 | 5,002 → 5,006 | +| 接受步数 | 19,606 → 19,606 | 11,238 → 11,238 | +| 拒步计数 | 1,552 → 1,552 | 885 → 885 | +| 方程求值 | 85,570 → 85,570 | 56,299 → 56,299 | +| 求解器启动次数 | 20 → 20 | 5 → 5 | +| 雅可比刷新 | 1,835 → 1,835 | 1,270 → 1,270 | +| 线性 setup 次数 | 6,028 → 6,028 | 3,603 → 3,603 | +| 求解耗时,单次 | 11.945 → 11.572 s | 7.357 → 7.469 s | +| 平台进程耗时,单次 | 18.339 → 17.851 s | 13.638 → 13.869 s | + +这些单次耗时有运行波动,不能据此声称性能提升或给出可靠开销百分比;可以确认没有新增积分重启与 RHS 求值。新增保存和输出重放仍有实际工作量。 + +## 7. 后续判断与复现 + +输出阶段误配已消除,可将这两组作为接下来修改的基线。继续判断循环接触力峰前,应在同一实际时刻或足够细的共同时间窗口核对位移、速度、间隙和接触力,区分保存时间偏移、动态响应及接触事件定位的影响;现有结果不支持直接把全部力峰差归因于接触事件缺失。 + +汇总证据:[前后效果对照](../../test/output-semantics-20260917/effect-summary.json)、[修改后运行汇总](../../test/output-semantics-20260917/after/summary.json)、[循环阶段配对](../../test/output-semantics-20260917/after/full/phase-pairing.json)、[非循环阶段配对](../../test/output-semantics-20260917/after/noncyclic/phase-pairing.json)。 + +```powershell +.\.venv-win\Scripts\python.exe -X utf8 tests/manual/evaluate_mql8_correctness.py --output test/output-semantics-rerun --ame-home F:/AMESim2404/Amesim --align-cyclic-from-ame --noncyclic-ame test/node-fixes-amesim-20260914/test_mql.ame --noncyclic-project tests/data/test-mql-8-corrected.json --profiles full noncyclic +``` + +输出目录必须是新目录。`--native-source` 可指定本次冻结源码作修改前对照;`tests/manual/summarize_output_semantics.py` 核验 before/after 两组结果。原始结果位于 Git 忽略的 `test/output-semantics-20260917/`;代码、测试与本报告为可跟踪交付。 diff --git a/docs/other/八路曲线剩余差异占比与全程评价-2026-09-17.md b/docs/other/八路曲线剩余差异占比与全程评价-2026-09-17.md new file mode 100644 index 0000000..d28b833 --- /dev/null +++ b/docs/other/八路曲线剩余差异占比与全程评价-2026-09-17.md @@ -0,0 +1,132 @@ +# 八路曲线剩余差异占比与全程评价 + +日期:2026-09-17。 + +## 1. 结论与适用范围 + +**在目前 0–50 s、10 ms 共同输出网格上,两侧主要运行过程的数值一致性较好,剩余流量和焓流差异对整条曲线的影响很小。这个结论支持当前平台的初步功能展示和基线一致性说明。** + +循环工况的 238 条曲线中,只有 3 条曲线、6 个“曲线×时刻”样本超过原来的 5% 相对差筛查线;它们集中在 0.30、0.31、0.32 s 三个时刻,占 5,001 个共同输出时刻的 **0.0600%**。不过,不能据此说其余时间完全没有问题:参考值接近零时,另有零星绝对差异;把这类点也计入,涉及 **34 / 5,001 个时刻,即 0.6799%**。 + +比占比更有说服力的是幅值和累计指标:质量流量各曲线中最差的全程相对 L2 误差为 **0.008566%**,焓流为 **0.007368%**;逐条曲线的累计绝对差与参考累计绝对值之比,最差分别为 **0.013934%** 和 **0.013814%**。这两种指标不会仅因为增加长时间的双方零输出而变小。 + +本次只读取已有结果,不修改元件、求解器、配对逻辑或工况,不重新求解。统计不包含共同网格之间所有自适应步或所有机械事件点,因此不构成未采样的极短瞬态误差上界。流量和焓流的差异尚未完成根因定位;本次量化的是影响范围和量级。 + +## 2. 数据来源与评价口径 + +- 循环:上一轮 LSTP 已接入主路的结果,对比开启不连续点输出的 Amesim 结果;238 条曲线,5,001 个共同网格点,全部成功配对。 +- 不循环:同一轮主路基线的已有结果,Amesim 参照仍为普通输出,采用原有信号阶段配对;主要用来交叉检查剩余流量和焓流差异是否重复出现。 +- 每个网格点、所有变量沿用同一对原始记录,不根据误差大小重新选择事件侧,没有平滑、插值或删除异常点。 +- 原相对差口径为 `100 × |平台−Amesim| / |Amesim|`。质量流量只有在 `|Amesim| > 1e-6 kg/s` 时计算该比值;焓流门槛为 `1 W`。这些是已有报告的诊断门槛,不是工程验收公差。 +- 接近零的区间单列:`|Amesim| ≤ 门槛` 且 `|平台−Amesim| > 门槛`。避免除以极小数,也避免把这些点直接忽略。 +- “全程占比”使用曲线样本数或不重复时刻数,分母分别注明。连续标记点折合时间只是 10 ms 网格上的估计,不是已经定位的超差起止时刻。 + +令差值为 `e`、参考值为 `r`、梯形积分权重为 `w`: + +- 全程相对 L2 误差:`100 × sqrt(Σw·e² / Σw·r²)`,即相对均方根误差。 +- 全程相对 L1 误差:`100 × Σw·|e| / Σw·|r|`,避免正负误差相互抵消。 +- 累计输送量误差:对流量或焓流差值做梯形积分;另保存绝对差积分和任意时刻累计有符号差的最大值。 + +积分均为已有 10 ms 网格上的估算。每条节点通道独立计算,不把串联节点的流量相加当成系统总输送量,也不把这些积分当成完整的守恒验证。 + +## 3. 超过 5% 的差异有多大占比 + +每类流量都有 16 条对照曲线,每条 5,001 个点。 + +| 变量类别 | 超过 5% 的曲线数 | 超过 5% 的样本数 | 占该类全部 80,016 个样本 | 占该类高于近零门槛的样本 | +| --- | ---: | ---: | ---: | ---: | +| 质量流量 | 2 | 4 | 0.004999% | 4 / 576 = 0.6944% | +| 焓流 | 1 | 2 | 0.002500% | 2 / 426 = 0.4695% | + +下面按单条受影响曲线计算,防止其他无差异曲线稀释占比: + +| 曲线 | 超过 5% 的时刻,s | 占本曲线全部样本 | 占本曲线高于近零门槛的样本 | 最大相对差 | +| --- | --- | ---: | ---: | ---: | +| `amesim_pn3node2_2.reference_mass_flow` | 0.30 | 0.0200% | 1 / 32 = 3.125% | 11.111% | +| `amesim_pn3node2_3.reference_mass_flow` | 0.30、0.31、0.32 | 0.0600% | 3 / 32 = 9.375% | 32.690% | +| `amesim_p4node2_4.reference_enthalpy_flow` | 0.31、0.32 | 0.0400% | 2 / 35 = 5.714% | 11.360% | + +所以,单说“占全程很小”不够:最差流量曲线高于近零门槛的样本中,仍有 9.375% 超过相对差筛查线。关键是它们全部位于衰减尾部,实际量值很小: + +- 最大相对流量差位于 0.32 s:Amesim **3.66657 mg/s**,平台 **2.46797 mg/s**,差 **1.19860 mg/s**;该曲线参考峰值约 **249,299 mg/s**。 +- 最大相对焓流差位于 0.31 s:Amesim **−3.08080 W**,平台 **−2.73081 W**,差 **0.34999 W**;该曲线参考绝对峰值约 **522,749 W**。 +- 0.30–0.32 s 并非本基线登记的 STEP/UD00 信号切换时刻。当前不能把这些剩余差异直接归因为之前的事件侧配对问题。 + +若仅查看参考幅值超过各自峰值 1% 的主要幅值区间,并同时要求高于近零门槛,质量流量最大逐点相对差为 **0.11559%**,焓流为 **0.10395%**。1% 峰值是用于区分主要幅值和衰减尾部的辅助口径,不替代上面的全样本统计。 + +门槛敏感性也支持差异集中于小量区间:近零门槛缩小到原来的 0.1 倍,超过 5% 的样本增至流量 79、焓流 29;扩大到 10 倍,两类都为 0。本报告保留原门槛,未靠改门槛消除差异。 + +## 4. 接近零的区间没有被忽略 + +| 类别 | 接近零但绝对差超过门槛的样本 | 占该类全部样本 | 这些点上的最大绝对差 | +| --- | ---: | ---: | ---: | +| 质量流量 | 44 | 0.054989% | 7.24440 mg/s | +| 焓流 | 2 | 0.002500% | 8.01591 W | + +这些异常涉及 31 个不重复时刻,网格折合约 0.31 s,占全程约 0.62%。多数是零散点,最长连续标记段为 45.80–45.84 s 的 5 个样本,网格折合约 0.05 s。焓流两个点分别在 1.41 s 和 46.18 s,并与流量异常时刻重合。 + +连同相对差超过 5% 的三个时刻,共涉及 34 个时刻,网格折合约 0.34 s、约 0.68%。**99.32% 的共同网格时刻未触发上述任一筛查条件**,但这不表示误差严格为零。 + +按运行阶段分组,流量/焓流的主要绝对差仍在初始阶段;后续循环中没有再次出现高于原近零门槛且相对差超过 5% 的点: + +| 时间区间,s | 最大流量绝对差,mg/s | 最大焓流绝对差,W | 流量近零异常样本 | 焓流近零异常样本 | +| --- | ---: | ---: | ---: | ---: | +| [0, 1) | 24.2465 | 18.6888 | 2 | 0 | +| [1, 10.8) | 6.5727 | 8.0159 | 9 | 1 | +| [10.8, 21.6) | 0.2172 | 0.02551 | 0 | 0 | +| [21.6, 32.4) | 7.2444 | 0.26117 | 5 | 0 | +| [32.4, 43.2) | 4.5750 | 0.17762 | 8 | 0 | +| [43.2, 50] | 6.3927 | 5.08374 | 20 | 1 | + +## 5. 全程幅值与累计影响 + +以下每一列均独立取该类曲线中的最差值,因此同一行不同列可能来自不同曲线。 + +| 变量类别 | 最大全程相对 L2 误差 | 最大全程相对 L1 误差 | 最大逐点绝对差 | +| --- | ---: | ---: | ---: | +| 接触力,8 条 | 0.000008651% | 0.000004595% | 1.31240 N | +| 质量流量,16 条 | 0.008566% | 0.013934% | 24.2465 mg/s | +| 焓流,16 条 | 0.007368% | 0.013814% | 18.6888 W | +| 压力,56 条 | 0.00001109% | 0.000004617% | 61.1058 Pa | +| 温度,56 条 | 0.002601% | 0.002407% | 0.389714 K | +| 接触间隙,8 条 | 0.133016% | 0.045482% | 54.7406 nm | + +接触间隙的最大逐点相对差为 1.16799%,但对应约 55 nm 的绝对差;因此除近零流量/焓流外,也不应宣称所有变量逐点都小于 1%。其余类别的完整逐曲线结果保存在统计 JSON 中。 + +质量流量的绝对差积分最大为 **7.82056e−7 kg = 0.782056 mg**;同一条曲线的参考绝对流量积分为 **5.61262 g**,比值 **0.013934%**。所有流量曲线中,任意时刻累计有符号差的最大绝对值为 **0.364845 mg**。 + +焓流的绝对差积分最大为 **0.491310 J**;该通道参考绝对焓流积分约 **12,521.9 J**。逐条比较误差积分与各自参考积分,最差比例为 **0.013814%**,来自另一条较小幅值通道。所有焓流曲线中,任意时刻累计有符号差的最大绝对值为 **0.307860 J**。 + +这些结果表明,在已保存输出的尺度上,剩余差异没有表现为显著的累计质量输送或能量输送偏差。 + +模型中另有异常量级的机械位移/容积输出,例如某位移参考值达到约 1.12e17 m;其绝对差虽大,相对差很小。本报告评价两套实现的一致性,不把这类模型本身的异常物理量级解释为工程工况合理,也不笼统宣称所有量的绝对差都小。 + +## 6. 不循环工况交叉检查 + +不循环工况仍然是相同三个时刻、相同三条曲线、相同六个相对差超过 5% 的样本。质量流量/焓流最差全程相对 L2 误差分别为 **0.008262% / 0.007048%**,相对 L1 误差分别为 **0.010307% / 0.010197%**。 + +接近零的绝对异常少于循环工况:流量 11 个样本,焓流 1 个;加上相对异常,共涉及 13 个不重复时刻,占 5,001 个时刻的 **0.25995%**。这支持主要剩余相对差属于初始衰减尾部,不是循环越多便持续积累的整体偏离。 + +## 7. 展示用结论与复现 + +可以在当前基线展示中使用以下表述: + +> 在与 Amesim 对齐工况和事件输出阶段后,0–50 s、10 ms 共同输出网格上的主要响应曲线具有良好的数值一致性。质量流量和焓流最差全程相对均方根误差分别约为 0.0086% 和 0.0074%;较大的逐点相对差主要集中于近零衰减尾部,对累计输送量的影响很小。该评价不覆盖共同网格之间的全部极短瞬态。 + +本次不需要为整体展示而优先修改流量或焓流公式。若后续目标是低流量尾部、微小泄漏或每一事件内部瞬态的精确复现,再针对这些区间增加密集采样与根因定位;现有占比不应被当作这类目标已经通过的证据。 + +![全程曲线、近零尾部与绝对误差包络](../../test/mql8-curve-coverage-20260917/coverage.png) + +- [完整统计:两种工况、所有曲线、区间及异常点](../../test/mql8-curve-coverage-20260917/coverage.json) +- [只读分析脚本](../../tests/manual/analyze_mql8_curve_coverage.py) +- [上一步力差与事件输出定位报告](LSTP主路接入与八路力差定位-2026-09-17.md) + +复现统计无需重新仿真: + +```powershell +.\.venv-win\Scripts\python.exe -X utf8 tests/manual/analyze_mql8_curve_coverage.py +# 本机已有 matplotlib 的环境可同时生成图: +F:\Anaconda\python.exe -X utf8 tests/manual/analyze_mql8_curve_coverage.py --plots +``` + +脚本核验:全部输入有限、共同网格完整配对、时间严格递增、积分权重合计 50 s;逐条曲线重算的相对异常数和近零异常数与此前报告完全一致;读取前后的输入 SHA-256 不变。原平台代码与仿真结果未改动。 diff --git a/docs/other/八路模型正确性初评与展示建议-2026-09-17.md b/docs/other/八路模型正确性初评与展示建议-2026-09-17.md new file mode 100644 index 0000000..ee8a4eb --- /dev/null +++ b/docs/other/八路模型正确性初评与展示建议-2026-09-17.md @@ -0,0 +1,165 @@ +# 八路模型正确性初评与展示建议 + +日期:2026-09-17。代码提交:`1aac2200843009a86066271d3bfb1053bd8d6b2d`。本机 Windows / Python 3.12.3 / GCC 8.1 / SUNDIALS 7.4.0。 + +## 评价结论 + +**平台已具备初步展示核心仿真功能的条件,但尚不能表述为“全部结果与 Amesim 一致”或“已完成工程精度验证”。** + +本轮重新执行当前 Amesim 模型程序和当前平台代码,共完成六组对照,每组比较 238 条关键曲线。20 项相关自动化回归全部通过。压力、温度、常规行程和质量守恒表现较好;当前仍需处理默认模型配置不一致、循环信号边界不一致,以及小流量/焓流的局部偏差。 + +原始模型输入和 143 个已跟踪生产文件的 SHA-256 在评估前后均未变化。本轮新增评估脚本、图片和报告,没有修改组件公式、求解器或默认工程。 + +## 1. 首先发现:当前两份默认模型并非相同工况 + +| 项目 | 当前平台 JSON | 当前 Amesim AME | +| --- | --- | --- | +| 文件 | `tests/data/test-mql-8-corrected.json` | `tests/data/test_mql.ame` | +| 两个 UD00 信号 | 非循环,`iscyclic=0` | 循环,Amesim 枚举值为 2 | +| 仿真终点 | 10 s | 50 s | +| 输出间隔 | 0.01 s | 0.05 s | +| 误差限数值 | 后端默认 rtol=1e-8 | tolerance=1e-7 | + +结构审计得到 **157 个元件、178 条逐端口连接、1,092 项公开参数**;只有两个 UD00 的循环参数不匹配,连接差异为 0。当前 AME 的 SHA-256 为 `0ad9117d88c64c743fcfc41c7b1c5d10854527ab6478cc76ffa092913e52ada3`,与 9 月 14 日旧报告的 `251b2f…` 不同,不能直接复用旧报告作为当前输入的结论。 + +正式对照在新建 JSON 副本中,仅将这两个循环参数按 Amesim 语义设为 1;再次审计后 **1,092 项参数及全部连接一致**,并完成 1,015 项图纸参数与归档运行参数交叉核验。运行时间、输出间隔、误差限在两边的副本中统一。原文件保持原样。 + +证据:[原始审计](../../test/evaluation-mql8-20260917/audit-original/audit.json)、[两个参数的对齐记录](../../test/evaluation-mql8-20260917/alignment-changes.json)、[对齐后审计](../../test/evaluation-mql8-20260917/audit-aligned/audit.json)。 + +## 2. 本轮实际运行与比较口径 + +| 工况 | 信号 | 时长 | 输出间隔 | 两边误差限数值 | 结果 | +| --- | --- | ---: | ---: | ---: | --- | +| 对齐后的短演示 `default` | 循环 | 10 s | 0.01 s | 1e-8 | 两边完成 | +| 当前模型全程 `full` | 循环 | 50 s | 0.01 s | 1e-8 | 两边完成 | +| 启动细采样 `startup` | 循环 | 0.15 s | 0.0001 s | 1e-8 | 两边完成 | +| 启动容差收紧 `startup-refined` | 循环 | 0.15 s | 0.0001 s | 1e-10 | 两边完成 | +| 最初压缩过程 `volume-startup` | 循环 | 2 μs | 2 ns | 1e-10 | 两边完成 | +| 非循环对照 `noncyclic` | 非循环 | 50 s | 0.01 s | 1e-8 | 两边完成 | + +非循环对照使用已保存、重新审计的旧版非循环 AME 与当前默认 JSON,只在副本中统一运行设置;同样重新执行程序,没有读取旧运行曲线代替新结果。其余五组使用当前 AME。 + +Amesim 端提取源归档内的可执行程序与运行输入后重新运行;没有重新编译 Amesim。除 `.sim` 前五个运行设置外,提取文件逐字节校验不变。平台走 JSON→System XML→组件网络→当前原生 C 编译/运行。平台使用 CVODE BDF,最大步长为 1e30;Amesim 保留其余原运行选项。相同误差限数字不代表两套求解器的误差权重和算法完全相同。 + +每组映射压力、温度、位移、速度、8 路接触力与间隙、节点焓流/质量流、气室质量/容积/容积功、活塞容积及信号,共 **238 条曲线**。平台完整输出为 **1,784 条**;其余输出检查了有限性,未逐条声明与 Amesim 相符。节点焓流由保存的各支路流量和流入焓按符号重构;Amesim 气室体积功按其保存的压力、连接容积变化率及容积下限条件重构。 + +单位统一为 SI:Amesim 表压加 101300 Pa,g 转 kg,cm³ 转 m³,L/min 转 m³/s,间隙 mm 转 m。映射来自元件拓扑和端口审计,没有交换元件、拟合时间轴或任意改符号。 + +**时间比较方法:**在共同规则网格上,若原始保存时刻距名义采样时刻不超过输出间隔的 1e-7,直接读取最近保存值;其他点线性插值。保留原记录的事件侧别,不平均、不平滑尖峰。直接对 `1e17→49000` 的巨大跳变做插值,会因输出时间浮点漂移在 0.81 s 伪造 57888 等尾值。本轮已用原始样本消除这一比较器伪差;原始结果、原始时刻及朴素插值诊断仍保留。 + +所有“最大差”均指所述采样网格,不是连续时间极值。事件点不从主统计中删除;另外的事件外指标仅用于定位问题。 + +## 3. 主要结果 + +| 指标 | 50 s 循环全程,10 ms 采样 | 启动 0.15 s,0.1 ms 采样 | +| --- | ---: | ---: | +| 最大绝对压力差 | **65.687 Pa** | **4,641.503 Pa** | +| 最大温差 | **0.3954 K** | **0.4575 K** | +| 压力最大逐点相对差 | 0.000319% | 0.02022% | +| 温度最大逐点相对差 | 0.004197% | 0.007854% | +| 节点焓流最大绝对差 | 141.920 W | 27,064.123 W | +| 节点质量流最大绝对差 | 1.3268e-5 kg/s | 0.390305 kg/s,位于 0.04 s 阶跃点 | +| 8 路接触力最大绝对差 | **1,285,489.483 N**,32.4 s | 105.579 N | +| 普通质量块最大位移差,排除特殊 #9 | 1.2930e-8 m | 7.4362e-13 m | + +表中各指标取该类全部映射曲线中的最大值,最大绝对差与最大相对差可能属于不同曲线和时刻。 + +![主要曲线和误差](../../test/evaluation-mql8-20260917/comparison.png) + +### 完成状态与基本物理检查 + +- 六组均完成指定终点;所有读取的结果数值有限,输出时间有序。50 s 循环运行到达 50 s,没有再出现早期的 10.8 s 提前退出。 +- 56 个气体质量状态合计初值约 **5.566893 kg**。六组中最大总质量漂移为 **4.3521e-14 kg**,相对漂移约 **7.82e-15**。 +- 所检查的保存结果中气体质量、绝对压力和温度均为正。质量守恒通过不等于能量及所有本构关系均已验证。 +- 20 项回归全部通过,覆盖默认非循环八路 10 s 完整运行、节点能量与容积传递、容积功和下限、氦气物性与反解、UD00 信号链路、压力单位、CSV 导出。首次运行中的部分 GCC 启动失败经现有重试机制恢复;最终没有测试失败。 + +### 容积下限的快速过程 + +普通 10 ms 采样看不到最初快速压缩,不能只拿恒定容积说明实现正确。2 ns 采样下,双方第 15 个气室都在 32 ns 的首个下限采样点达到 **0.15 L**,此前初值为 15 L。八个气室最大容积差约 **1.716e-13 m³**;体积功率最大绝对差约 **5.306e5 W**,最大逐点相对差约 **4.786e-6%**,参考功率本身约为 1e13 W 量级。 + +![容积与体积功率](../../test/evaluation-mql8-20260917/volume-startup.png) + +该八路文件中的活塞直接连接气室;经节点传递容积与非零预设容积变化率的覆盖来自本轮通过的专项测试,不能仅由这张图推断。 + +## 4. 仍不能验收为全部一致的部分 + +### 4.1 循环事件与采样边界:已确认存在实现缺陷 + +当前原生信号函数分别通过 `fmod` 判断周期位置、通过周期乘法定位事件。本轮直接编译调用当前源文件,得到: + +| 事件函数返回时刻 | 该时刻信号求值 | 应进入的阶段值 | 下一个 double 时刻的值 | +| --- | ---: | ---: | ---: | +| 44 s | 1e17 | 49000 | 49000 | +| 54 s,补充探针 | 49000 | 1e17 | 1e17 | + +这证明同一数值时刻上的事件定位和信号求值不一致;不需要积分器即可复现。定位文件:[signal.c](../../native/components/modules/signal.c)。证据:[探针输出](../../test/evaluation-mql8-20260917/signal-probe/signal-boundaries.csv)。 + +32.4 s 的大接触力差还包含另一层问题:平台保存时间为 `32.4`,此时 UD00 为 49000;Amesim 保存时间为 `32.40000000000221`,UD00 为 1e17。第 2 路接触力分别为 **468662.661 N** 和 **1754152.144 N**。显示相同的小数时间并不保证处于跳变的同一侧,不能把该差额直接认定为接触力本构公式错误。 + +仅作定位:保留 t≥0.1 s 并避开各信号边界前后 0.02 s 后,8 路接触力最大差为 **0.276 N**,普通质量块速度最大差约 **1.35e-7 m/s**。这不能替代包括事件点的整段验收。 + +此外,平台原始额外事件样本在约 `32.40035510065 s` 保存了约 **−2.084e14 N** 的接触力;共同 10 ms 网格未包含这个样本,Amesim 本轮保存点也未覆盖同一时刻。因此本轮无法对该窄峰完成同侧、同分辨率定量验收,不能把共同网格最大差当作全部原始事件样本的上界。 + +### 4.2 小流量与焓流:绝对差小,但局部相对差仍明显 + +沿用此前报告的诊断分界:焓流参考绝对值超过 1 W、质量流超过 1e-6 kg/s 时计算逐点相对误差;参考更接近零时单列原值和绝对差。**±5% 是本轮筛查线,不是用户批准的工程公差。** 其他物理量分界及分界乘/除 10 的敏感性保存在指标文件中。 + +| 50 s 工况中的样本 | Amesim | 当前平台 | 绝对差 | 相对差 | +| --- | ---: | ---: | ---: | ---: | +| P4NODE2_1 参考焓流,0.32 s | −2.375850 W | −1.641047 W | 0.734803 W | −30.928% | +| P4NODE2_2 参考质量流,0.32 s | 3.260972e-6 kg/s | 8.351735e-6 kg/s | 5.090763e-6 kg/s | +156.112% | + +50 s 循环共同网格中,明确非零的超 5% 样本为 **61 个**:接触力 24、速度 2、信号 5、焓流 7、质量流 23。其中 **30 个位于信号事件点之外**,全部属于上述焓流/质量流;不能将所有差异解释成事件侧别。参考近零的异常另计,没有因为不算相对误差而自动通过。 + +启动段 0.04 s 的质量流约 0.390305 kg/s 差异位于阶跃点;收紧双方误差限后,该点差异消失,启动质量流最大差降为 0.0009686 kg/s。另一方面,启动焓流过零附近仍有约 0.32 W 差异。收紧容差后最大压差仍约 **4.551 kPa**,最大温差为 **0.5214 K**,说明剩余偏差没有简单随误差限收紧消失。 + +本轮没有证明管阻公式、近零流量闭合或积分误差中的哪一项是剩余偏差的唯一原因。应在相同状态输入下逐项比较,再决定修改位置。 + +![事件与小量差异](../../test/evaluation-mql8-20260917/diagnostics.png) + +### 4.3 输入本身属于极端工况,数值对齐不等于物理有效 + +模型包含 1e17 量级激励;无限位、无摩擦的特殊质量块 #9 在循环 50 s 时位移约 **1.12e17 m**。其最大绝对位移差约 **3.056e10 m**,最大逐点相对差约 **9.78e-5%**。该量及相连活塞的巨大几何容积必须单独列出,不能与普通行程的绝对误差混合评价。 + +启动细采样的气体温度达到约 **11759 K**;当前物性诊断也报告状态方程有效上限 1500 K 被越过,并报告内部理想热量学查询约 39.44 K 低于其 200 K 下限。39.44 K 是内部物性查询温度,不是初始气室温度。这些提示来自本次实际运行。 + +因此该输入适合展示复杂网络、刚性系统、事件和诊断能力;若用于展示某个真实设备的预测精度,需要另行明确合理的载荷、约束和物性适用工况,并以实验或可靠工程基准验证。 + +## 5. 建议修改优先级与验收方式 + +| 优先级 | 修改对象 | 依据 | 修改后应如何验收 | +| --- | --- | --- | --- | +| P1,展示前 | 默认 AME/JSON 的工况管理 | 两个循环开关、终点、采样间隔不同 | 明确主展示工况;保存成对的 AME/JSON 与哈希;参数、端口和运行设置审计一致 | +| P1,正确性 | UD00 的阶段判定、事件定位和输出侧别 | 当前 44 s/54 s 探针直接失败;循环接触力事件点差异大 | 为所有边界检查前一个 double、边界、本身之后一个 double;约定事件前/后输出;比较事件时刻、同侧力与速度及窄窗口原始样本 | +| P2,精度 | 启动残差与小流量/焓流 | 事件之外仍有偏差,收紧容差后未完全消失 | 先做同状态物性、管流、节点输入输出探针;再做分辨率/容差收敛;同时报告绝对误差和非零相对误差 | +| P2,展示模型 | 极端载荷、自由质量块与物性适用域 | 巨大位移、超温、纳秒级压缩 | 保留当前模型作为压力测试;另建有合理量级和约束的展示工况,重新对齐两平台,不能仅缩坐标轴隐藏异常 | + +现有结果不足以支持“先更换积分器”或“继续优化求解器即可修复曲线差异”。已证明的边界缺陷和输入一致性应优先处理。 + +## 6. 可用于初步展示的内容 + +建议展示顺序为:加载八路工程 → 查看元件、连接与信号设置 → 运行 → 叠加两平台压力/温度/普通行程 → 展示误差曲线 → 查看物性诊断与事件差异 → 导出结果。 + +可直接导入的评估副本:[10 s 对齐工程](../../test/evaluation-mql8-20260917/default/platform.json)、[50 s 循环工程](../../test/evaluation-mql8-20260917/full/platform.json)。两者已与当前 AME 的物理输入对齐。 + +可用表述:“已完成 157 元件、132 状态八路网络的原生求解及结果输出,对 238 条关键曲线进行了 Amesim 对照;主要连续状态吻合较好,事件边界和小流量精度仍在完善。” + +本轮没有执行真实浏览器中的拖拽、导入按钮、持久化恢复、绘图及取消全流程,不将后端测试通过等同于前端展示流程已经验收。单次 50 s 循环平台求解约 12.41 s,Amesim 完整进程约 7.59 s;计时边界不同且未重复测量,**不据此计算性能优劣比**。 + +## 7. 交付与复现 + +全部本机证据在 `test/evaluation-mql8-20260917/`,该目录按仓库规则被 Git 忽略;报告及四个脚本为新增可跟踪文件。 + +- [总指标与运行状态](../../test/evaluation-mql8-20260917/summary.json)、[输入/代码哈希](../../test/evaluation-mql8-20260917/manifest.json)、[评估结束哈希核验](../../test/evaluation-mql8-20260917/source-verification.json)。 +- [1,428 条分工况曲线指标 CSV](../../test/evaluation-mql8-20260917/curve-errors.csv)、[18 条展示变量的双平台曲线 CSV](../../test/evaluation-mql8-20260917/showcase-curves.csv)。每个工况另有完整 `curves.npz`、`comparison.json`、映射、原始结果、日志和事件邻近样本。 +- [20 项回归日志](../../test/evaluation-mql8-20260917/regression.log)、[信号边界探针](../../test/evaluation-mql8-20260917/signal-probe/signal-boundaries.csv)、[采样匹配检查](../../test/evaluation-mql8-20260917/sampling-verification.json)。 +- [主评估脚本](../../tests/manual/evaluate_mql8_correctness.py)、[曲线映射](../../tests/manual/mql8_comparison.py)、[边界探针脚本](../../tests/manual/probe_mql8_signal_boundaries.py)、[绘图脚本](../../tests/manual/plot_mql8_evaluation.py)。 + +从仓库根目录使用新的输出目录重跑当前循环模型五组对照: + +```powershell +.\.venv-win\Scripts\python.exe tests/manual/evaluate_mql8_correctness.py --output test/evaluation-mql8-rerun --ame-home F:/AMESim2404/Amesim --align-cyclic-from-ame +.\.venv-win\Scripts\python.exe tests/manual/probe_mql8_signal_boundaries.py --output test/evaluation-mql8-rerun/signal-probe +``` + +可选追加 `--noncyclic-ame test/node-fixes-amesim-20260914/test_mql.ame --noncyclic-project tests/data/test-mql-8-corrected.json`,重跑第六组非循环对照;该历史副本只存在于当前工作站证据目录。绘图脚本需要 Matplotlib,本轮使用已安装该库的 `F:/Anaconda/python.exe`。主评估脚本仅需项目既有运行依赖与 NumPy。 diff --git a/docs/other/循环UD00事件边界修复与独立验证-2026-09-17.md b/docs/other/循环UD00事件边界修复与独立验证-2026-09-17.md new file mode 100644 index 0000000..2cd5715 --- /dev/null +++ b/docs/other/循环UD00事件边界修复与独立验证-2026-09-17.md @@ -0,0 +1,101 @@ +# 循环 UD00 事件边界修复与独立验证 + +日期:2026-09-17。环境:Windows,项目 `.venv-win`,MinGW GCC 8.1.0,原生 C 运行库,CVODE 7.4.0 BDF / RK45。 + +## 1. 本次完成的第一步 + +修复 UD00 的信号求值与事件时间计算不一致。实现位于 [signal.c](../../native/components/modules/signal.c),新增回归位于 [test_native_signal_boundaries.py](../../tests/test_native_signal_boundaries.py)。 + +原来信号值用 `fmod(t - tstart, period)` 判断当前阶段,事件查询另用绝对时间计算。两种浮点运算可能落在边界两侧。例如阶段时长为 0.8 s 和 10 s 时,事件查询已经返回 44 s,而取模仍小于 0.8,信号在该时刻仍输出上一段的 `1e17`。 + +现在 `native_signal()` 与 `native_signal_break()` 共用 `signal_interval()`,共同确定当前阶段、阶段起点及下一边界。阶段内部仍按线性插值计算;周期结束统一使用下一周期的起点,避免同一事件出现两个相邻浮点时间。 + +本次没有修改八路工程 JSON、Amesim 文件、积分算法或结果采样协议。已有 UD00 的调用接口保持不变,两条原生生成路径均自动使用修复后的内核。 + +## 2. 修改后的事件定位及处理 + +### 时间边界定位 + +设信号开始时间为 `s`,各段累计时长为 `offset[i]`,周期为 `P`:普通阶段边界按 `(s + offset[i]) + k * P` 计算;周期终点统一采用下一周期起点 `s + (k + 1) * P`。这些表达式的浮点运算顺序也是合同的一部分,求值与事件查询共同使用同一函数。 + +当前周期由估算值开始,再依据绝对周期起点修正。阶段采用左闭右开区间:严格早于边界时仍在前段,恰好等于边界时进入后段;零时长阶段被跳过。查询返回严格晚于当前时间的最近事件,无后续事件或事件超过仿真终点时返回仿真终点。 + +### 运行库处理 + +1. 生成的 `model_next_break(t, end)` 汇总各信号的下一边界,选择最早一个。 +2. BDF / RK45 在这个时间边界处分段积分。对仿真内部的边界,旧段积分终点是边界左侧相邻浮点数 `nextafter(boundary, -INFINITY)`。 +3. 到达边界后保留连续状态,将时间推进到该边界,并重新启动积分。此时信号求值已切换到新段,导数计算使用新信号。 +4. 结果输出仍按保存的实际时间和状态调用 `model_eval()` 重放。因为信号阶段是时间的纯函数,求解试算、回退和重放不会推进或污染一个全局阶段计数器。 + +第 1~4 项的运行库流程原来已经存在;本次让第 3、4 项使用的信号值与第 1 项报告的边界保持一致。连续状态如位置、速度不会仅因信号切换被重置;代数量如输出信号和力可以立即改变。机械碰撞、摩擦等由状态触发的事件仍使用各自的状态事件处理。 + +### 与 Amesim 对齐的范围 + +本次确立平台自身一致的右连续规则。Amesim 的 UD00 还维护当前阶段及 `TNEXT`,通过离散时间事件调用更新阶段;本次没有完整复制这套离散调用和结果保存协议。 + +尤其是普通浮点数 `32.4` 与本工况计算出的事件时间 `32.400000000000006` 不相等。前者仍属于边界前,测试专门保留这一点,没有使用全局 epsilon 提前切换。后续对照 Amesim 的原始采样时,应继续区分实际时间和事件前/后侧;此次不能直接推出此前八路模型在 32.4 s 的力尖峰差异已经消失。 + +## 3. 独立信号测试结果 + +测试网络只有一个循环 UD00:第一段 `1e17`,持续 0.8 s;第二段 `49000`,持续 10 s;从 0 s 开始循环。两种求解器均运行至 60 s,以 0.01 s 保存结果,共 6,001 个采样点。该模型没有物理动态状态,运行库使用一个内部常量状态执行真实求解及输出重放流程。 + +| 验证项 | 结果 | +| --- | --- | +| BDF 独立仿真 | 成功完成 60 s;6,001 点逐点符合冻结边界期望值 | +| RK45 独立仿真 | 成功完成 60 s;6,001 点逐点符合冻结边界期望值 | +| 两种求解器比较 | 全部采样值相同 | +| 事件分段 | 两种求解器均启动 12 次:初始启动一次,11 个内部时间事件后分别重启 | +| 44 s 信号 | 修改前 `1e17`;修改后 `49000`,符合后一段 | +| 54 s 信号 | 修改前 `49000`;修改后 `1e17`,符合下一周期 | +| 八段信号 1,000 周期 | 连续检查 8,000 个边界,无漏报、重复;边界左邻/本身/右邻均正确 | +| 非零起点与小周期 | 3 组工况,各 900 个边界通过 | +| 含零时长阶段的循环 | 连续 2,000 个边界通过;周期尾部零时长段不泄漏瞬时值 | +| 其他 | 斜坡、非循环终值、零总时长、单段锯齿、乱序重放通过 | + +新增 7 个测试方法及相关 3 个既有测试全部通过(最终版本 10 / 10,39.033 s,包含积分启动次数断言)。修改前先运行关键边界测试,已复现 44 s、54 s 及多阶段循环边界失败,详见保留日志。独立测试用于确认信号内核和求解/重放接入,不替代八路气动与机械系统的精度验收。 + +测试过程中曾遇到测试子进程返回空输出及 GCC 进程启动重试。边界查询已合并为单次批量调用,数值判定保持严格;最终定向测试通过。相关中间日志保留在验证目录,不把进程空输出当成边界数值错误,也不将一次成功重跑解释为环境问题已修复。 + +### 完整回归的结果与限制 + +另执行 `python -m unittest discover -s tests -v`,共运行 458 项,耗时 1,534.126 s,报告 7 个错误、25 项跳过,不能记为整库回归通过。新增 7 项信号测试、既有 UD00 XML、阶跃重启、信号驱动力以及当前八路工程保存配置的完整运行测试均通过。 + +| 错误来源 | 实际失败点与处理 | +| --- | --- | +| 管道多变过程测试 | `model --init` 输出不能解析为 JSON,尚未进入数值断言;单独复核又遇到 GCC 预处理超时 | +| 冻结参考网络 38 | 发布编译缓存时目录重命名报 Windows `WinError 5` | +| 雅可比固定状态夹具 | 夹具保存的输入哈希与当前八路 JSON 不一致,类初始化被主动阻止 | +| 物性警告测试 | GCC 预处理超时;残留预处理进程退出后测试序列继续 | +| 工程输入合同的三项测试 | 默认 GBK 解码 UTF-8 JSON 失败;仅加 `-X utf8` 单独重跑,3 / 3 通过 | + +夹具期望输入 SHA-256 为 `670977bef67e62d9c66e8af497bada208bd72a7301be45128d185d47282cf288`;实际工作树及 `HEAD` 中八路 JSON 的 SHA-256 均为 `60ed16c71b72be4ae71ca7980f4f8cdcdf6b4c4c4c8950863c2390960b71ced4`,确认这项输入不匹配不是本次修改引入。没有为使回归通过而更新参考夹具、修改其他内核或放宽断言。本次仅在 Windows 验证,未声明 Linux 验收通过。 + +验证文件: + +- [修改前失败日志](../../test/ud00-boundaries-20260917/before-tests.log) +- [最终定向测试通过日志](../../test/ud00-boundaries-20260917/final-verified-tests.log) +- [边界及左右相邻浮点值](../../test/ud00-boundaries-20260917/after-probe/signal-boundaries.csv) +- [双求解器独立运行摘要](../../test/ud00-boundaries-20260917/isolated-summary.json) +- [6,001 点结果及期望值](../../test/ud00-boundaries-20260917/isolated-signal-samples.csv) +- [完整回归日志](../../test/ud00-boundaries-20260917/regression-tests.log) +- [UTF-8 编码复核日志](../../test/ud00-boundaries-20260917/utf8-recheck.log) + +复现定向测试: + +```powershell +.\.venv-win\Scripts\python.exe -m unittest tests.test_native_signal_boundaries tests.test_amesim_ud00_xml tests.test_amesim_semantics.NativeComponentSemanticTests.test_signal_endpoints_and_exponential_contact_law_in_c -v +``` + +## 4. 新注册类似信号组件时需要什么 + +| 情况 | 事件方面的工作 | +| --- | --- | +| 新增现有 UD00 实例或调整其参数 | 无需额外注册,直接使用已有求值和事件内核 | +| 新型号复用 UD00 分段语义 | 接入已有 `native_signal` 和 `native_signal_break`,并把后者汇入 `model_next_break` | +| 新阶跃、脉冲、分段周期信号 | 除输出公式,还要提供下一时间事件;与输出共享边界计算和相等判定 | +| 无分段的光滑信号,如正弦 | 通常无需专门时间事件 | +| 由压力、位置等状态触发的切换 | 需要相应状态事件定位与切换机制,时间事件表不足以实现 | + +仅在 `library.py` 注册类型,或填写 `PORTS` / `PARAMETERS` / `RESULT_VARIABLES`,不会自动实现这些事件。默认在 `native_codegen/extended.py` 接入;若新型号也进入 `compiler.py` 紧凑路径,两条路径都要覆盖。新增 C 函数时同步公共声明与模块导出/依赖。 + +新组件通常只负责描述事件时刻及切换语义,由运行库统一执行积分分段与重启。相关要求已写入[元件开发规范第 12.1 节](../standard/component-model-authoring-spec-v1.md#121-分段信号的时间事件)。 diff --git a/docs/other/机械元件事件覆盖与Amesim对照-2026-09-17.md b/docs/other/机械元件事件覆盖与Amesim对照-2026-09-17.md new file mode 100644 index 0000000..9843fc6 --- /dev/null +++ b/docs/other/机械元件事件覆盖与Amesim对照-2026-09-17.md @@ -0,0 +1,136 @@ +# 机械元件事件覆盖与 Amesim 对照 + +本次核查基于当前平台源码、八路工程的实际生成代码、本机 Amesim 2404 子模型源码及随安装提供的帮助。没有修改机械内核,也没有重新运行 Amesim;数值验收证据引用上一轮修复后的回归日志。 + +## 1. UD00 修复的影响范围 + +生产代码改动仅在 `native/components/modules/signal.c`。MASS、接触力公式、机械事件定位及求解器重启代码没有修改。已有机械事件仍然生效,不需要因为这次信号修复而重新注册。 + +若机械系统由 UD00 驱动,修正信号边界的取值会影响驱动力、后续轨迹和碰撞时刻;积分步分布也可能变化。因此“机械事件机制没有改动”不代表耦合系统的每一个数值结果都保持不变。纯机械模型若没有使用 UD00,则没有这条直接数值调用依赖。 + +## 2. 逐类比较 + +| 元件或模式 | 当前平台 | Amesim 2404 本机证据 | 判断 | +| --- | --- | --- | --- | +| MECMAS21 理想限位,`stoptype=1` | 注册 `NativeStop`;在接受步内用密集插值和二分定位上下界;命中后位置置于界面,速度置零,再重启积分 | `MECMAS21.c` 调用 `endst2_`;帮助说明其碰撞处理为位置等于限位、速度为零,并维护状态标记 | 撞击事件已具备,无需重复添加;应核验脱离限位、初始就在限位及同时事件 | +| MECMAS21 恢复碰撞,`stoptype=3` | 注册相同的限位定位;入射速度超过阈值时按恢复系数反向,否则置零 | 调用 `endstrc_`;帮助说明恢复系数及防止无限反弹的低速阈值 | 核心撞击事件已具备;保留并补边界组合验收 | +| MECMAS21 弹性限位,`stoptype=2` | 计算上下限穿透及弹簧阻尼力,未向 `NativeStop` 或其他通用事件表注册接触/脱离 | 允许负力时调用 `endst1b_`,否则调用 `mecEndStop_` 且 `handle_disc=1`;帮助说明维护接触状态 | 建议补充上下界接触/脱离事件,非负力模式还应处理原始接触力的截断边界 | +| LSTP00A 弹性接触 | 用间隙计算非负穿透,再调用 `native_contact`;没有接触状态事件注册 | 与弹性限位复用上述接触工具;`mecEndStop_` 明确启用不连续处理 | 当前八路模型应优先补充这一项 | +| 静摩擦、库仑摩擦、Karnopp/Stribeck 模式 | 已有 `NativeFriction`、保存的离散模式、停滑与起滑定位;密集插值定位速度阈值和净驱动力超过静摩擦阈值的时刻 | `MECMAS21.c` 调用摩擦工具;`linert1c` 帮助说明低速区、起滑阈值及保存的模式标记 | 已有基础,不是缺失整套事件;需核验换向、阈值相等、与限位/信号同时触发 | +| 无限位 MASS,`stoptype=4` | 不注册限位;若启用干摩擦仍有摩擦事件 | 同样绕过限位工具,保留所选摩擦模型 | 无需人为添加上下限位事件 | +| 普通双向线性弹簧、黏性阻尼 | 当前公开机械目录主要提供 LSTP00A 接触件;不能把它等同于普通双向弹簧阻尼 | Amesim `SD0000.c` 直接计算连续的弹簧、阻尼力及导数 | 将来新增这类光滑模型通常不需要事件;出现间隙、单向作用、饱和或摩擦切换时再添加 | + +平台依据: + +- [机械力公式](../../native/components/modules/mechanics.c):`native_contact`、`native_stop_motion`。 +- [机械事件定位与状态更新](../../native/runtime/common.c):`locate`、`locate_breakaway`、`native_accept`。 +- [生成器的接触方程、摩擦与限位注册](../../app/simulation/native_codegen/extended.py):接触力约在 559 行,MASS 弹性力约在 571 行,限位注册约在 625 行。 +- [紧凑生成路径](../../app/simulation/native_codegen/compiler.py):支持的限位模式也生成 `NativeStop`;其他模式由扩展生成路径处理。 + +Amesim 依据: + +- [MECMAS21.c](F:/AMESim2404/Amesim/libmec/submodels/MECMAS21.c:547):理想、弹性、恢复碰撞和摩擦模式的实际调用分支。 +- [LSTP00A.c](F:/AMESim2404/Amesim/libmec/submodels/LSTP00A.c:294):接触工具调用及 `handle_disc=1`。 +- [SD0000.c](F:/AMESim2404/Amesim/libmec/submodels/SD0000.c:267):普通弹簧阻尼连续方程。 +- 本机 [amehelp.qch](F:/AMESim2404/Amesim/doc/amehelp.qch) 内的 `libmec/doc/html/utils/endst1b.html`、`mecEndStop.html`、`endst2.html`、`endstrc.html`、`linert1c.html`。这些帮助页通过只读查询 Qt 帮助数据库取得。 + +本机安装未提供上述接触工具的完整内部源代码。因此可以确认 Amesim 的接触状态管理和不连续处理接口,但不据此声称已完全复原其全部根函数、内部容差或同一时刻的执行顺序。 + +## 3. 当前八路工程的优先级 + +从 `tests/data/test-mql-8-corrected.json` 实际生成得到: + +- 共 10 个 MECMAS21:9 个无限位,1 个理想限位;理想限位元件为 `amesim_mecmas21_10`,范围 `[0, 0.37] m`。 +- 共 8 个 LSTP00A,均为数值刚度、允许负接触力;`kcont=1e11 N/m`、`rcont=1e11 N·s/m`、`Pdis≈1e-7 m`。 +- 生成头文件为 `NSTATES=132`、`NSTOPS=1`、`NFRICTIONS=0`。这 1 组 `NativeStop` 包含上下两个位置界面,不代表只有一个事件时刻。 +- `model_next_break` 只汇总信号时间事件,没有注册这 8 个接触件的间隙事件。接触力仍被计算,只是没有单独定位接触/脱离时刻。 + +建议优先顺序: + +1. **先补 LSTP00A 的接触/脱离定位。** 它直接覆盖当前八路模型的 8 个接触件。 +2. **复用同一机制覆盖 MASS 弹性限位。** 当前八路工程没有启用该模式,但这是平台组件能力中明显未显式覆盖的事件。 +3. **完善已有理想/恢复限位和摩擦的组合边界。** 当前理想限位通过 `native_stop_motion` 的条件方程处理保持与离开,没有单独注册“约束反力消失/自由净驱动力反向”的根函数;应核验连续驱动力下的离开时刻,再补充需要的释放事件。 + +缺少接触事件不等于当前接触力公式错误。自适应积分可以通过缩小步长处理部分分段变化,已有温和工况也能通过对照。显式事件的目的,是更稳定地确定作用阶段、接触时刻及峰值附近的积分历史。此次核查不能证明之前八路模型的全部力峰差异均由这一缺口导致。 + +## 4. 应怎样添加 + +这类事件由状态决定,不能像 UD00 那样只向 `model_next_break(t,end)` 添加预定时间。应扩展状态事件接口,允许组件提供事件面、穿越方向、有效模式及事件后的操作。 + +- **接触边界:** LSTP00A 使用带符号间隙 `gap = gap0 + x2 - x1` 的 `gap=0`;MASS 弹性限位使用 `x-xmin=0`、`x-xmax=0`。不要用已截成非负的 penetration 作为唯一找根量,否则脱离区间整段为零,失去跨越方向。 +- **非负接触力边界:** 仅在 `discContactOption=2` 且处于接触区时,处理未截断力 `Fraw=0` 的分支切换。允许负力的模式没有这个截断开关,不应因力过零就重启。 +- **事件动作不同:** 弹性接触只重新确认分段并重启积分,位置和速度保持连续;理想碰撞、恢复碰撞才按其模型重置状态。不能把弹性接触事件直接当成硬碰撞。 +- **不需要的事件:** `Pdis` 是指数过渡尺度,不是“穿透达到它就突然切换”的阈值;也不应在 `3*Pdis` 处造事件。数值刚度/几何刚度是初始化参数模式,若运行中不切换,也不产生事件。 +- **防止重复触发:** 初始恰好接触、贴边不动、离开后重新接触、同一时刻多个接触和信号跳变都需明确阶段与方向,并保证试探求值不提交离散模式。 + +最低验收应覆盖:单 MASS 上下碰撞及释放、单弹性限位、双质量块 LSTP00A 接触/脱离、允许/禁止负力两种模式、非零/零阻尼过渡尺度,并对 BDF/RK45、不同最大步长比较事件时刻和接触峰值。与 Amesim 对照时还需区分事件前后值。 + +## 5. 已有验证能说明什么 + +上一轮修复后的[完整回归日志](../../test/ud00-boundaries-20260917/regression-tests.log)中,恢复碰撞双求解器测试、LSTP00A 接触测试、MASS 摩擦保持以及 `test_amesim_stick_slip_endstops_and_joint_circuits` 均通过。 + +最后一项使用独立 Amesim 2404 运行生成的参考数据,覆盖理想限位、弹性限位、摩擦与联合机械等场景;每个参考场景检查 40 个选定时刻,测试设置最大步长 0.002 s。它支持“相关常规数值行为仍可用”,不足以证明当前八路模型的高刚度接触全过程、所有边界组合都与 Amesim 严格一致。 + +## 6. 补充接触/脱离事件的求解时间成本 + +### 结论及适用范围 + +事件检查、定位和处理会增加计算工作,但总求解时间不一定增加。若它减少了跨接触边界的拒步、非线性迭代和反复缩步,这些节省可能抵消部分甚至全部开销。当前尚未实现接触事件,不能给出已测得的变慢百分比。 + +以下分析针对前文建议的“状态事件定位后重启积分”实现。单纯记录零点与切换接触阶段不是同一件事,也不应假设所有事件定位都必然要求相同的重启方式。 + +### 成本来自哪里 + +| 环节 | 当前代码依据及新增成本 | 判断 | +| --- | --- | --- | +| 每个接受步检查接触状态 | `native_accept` 已经检查硬限位/摩擦;新增 LSTP00A 间隙可直接由两个位置状态及常数计算 | 对 8 个接触件,轻量状态判断预计成本较低;这是算法量级判断,不是计时结果 | +| 定位实际跨越时刻 | 现有 `locate` 固定执行 60 次二分,每次调用密集插值;BDF 使用 `CVodeGetDky`,RK45 计算状态插值多项式 | 纯间隙根函数不需要重算整个气动网络;定位次数和精度决定成本 | +| 截断并重启积分 | `native_accept` 可把已接受步截回事件;BDF 调用 `CVodeReInit`,RK45 重新计算导数和初始步长 | 会放弃事件之后那部分试算工作,并产生重新起步的工作;BDF 的历史重建尤其值得关注 | +| 雅可比及线性求解 | BDF 原生路径使用差分雅可比和稠密线性求解器;新起步需要相应初始化与后续数值建立工作 | 成本作用于整个耦合系统,而非只作用于触发事件的那一个弹簧 | +| 保存事件点及输出重放 | 若沿用现有状态事件存储流程,每个新增事件时刻会保存状态,输出阶段再次调用 `model_eval` | 通常少量事件时不突出;大量事件会增加结果体积与后处理时间,需要与纯求解耗时区分 | + +CVODE 7.4.0 的官方实现中,`CVodeReInit` 将当前阶次重设为 1,并重置步数/历史起点;下一次积分进入初始设置与初始步长计算。它复用已有内存,不是重新编译模型或重新分配整个求解器。见 [CVODE 7.4.0 官方源代码](https://github.com/LLNL/sundials/blob/v7.4.0/src/cvode/cvode.c#L518)。平台对应入口见 [cvode_solver.c](../../native/runtime/cvode_solver.c:357)、[rk45.c](../../native/runtime/rk45.c:57) 和 [common.c](../../native/runtime/common.c:137)。 + +因此,主要风险是频繁切换造成的整系统重新起步,不是“多写了几个 if 判断”。重启不清除从仿真开始以来的物理运动结果,而是从事件处的状态继续积分。 + +### 既有八路运行数据能支持什么判断 + +早先正确性评估中的 **50 s 循环工况**记录如下,来源为 [native-summary.json](../../test/evaluation-mql8-20260917/full/native-summary.json)。这是 UD00 修复前的历史样本,仅用于估计工作量,不能作为当前代码已经完成的性能 A/B 对比。 + +| 指标 | 历史样本 | +| --- | ---: | +| 求解壁钟时间 `solveSeconds` | 12.414 s | +| 接受步数 | 19,412 | +| 误差测试失败次数 `rejectedSteps` | 1,511 | +| 方程求值计数 `nfev` | 84,268 | +| 其中雅可比差分求值 | 50,512 | +| 雅可比刷新次数 | 1,804 | +| 线性求解器 setup 次数,输出字段 `nlu` | 5,903 | +| 求解器启动次数,含初次启动 | 20 | +| 已注册状态事件的触发批次数 | 9 | + +该生成模型包含 132 个状态、1,784 个输出和 27 个雅可比分色组。历史数据中每次雅可比刷新对应 28 次方程求值(27 组扰动加一个基准求值),合计 `1804 × 28 = 50512`。这说明多余的整模型求值、雅可比刷新和重新起步比单纯间隙算术更值得控制;不能把方程调用次数占比直接当作耗时占比。 + +若接受步数暂按历史样本不变估算,8 个接触件每步检查一次是 `8 × 19412 = 155296` 次简单间隙检查;两个端点均重算则约为其两倍。间隙直接读取位置即可,不需要为这些检查各自求解气动物性、管道流量和全部输出。 + +另一方面,历史的 `stateTransitions=9` 不包含当时没有注册的 LSTP00A 接触/脱离,所以不能据此预测新增事件数量。8 个接触件可能长期保持接触,也可能反复接触和脱离;真正影响重启成本的是整个仿真期间的切换次数,以及是否出现数值反复触发。 + +不能将 12.414 s 除以启动次数来推算单次事件成本,也不能把平台求解时间与报告中的 Amesim 完整进程时间直接比较。 + +### 建议采用的成本控制措施 + +1. **专门生成轻量事件函数。** 直接读取位置/速度状态,批量计算间隙和需要的局部接触力;避免为了事件检查调用完整 `model_eval`。在不需要新增物理动态量时,不人为增加连续状态维度。 +2. **先筛查,后定位。** 只对可能跨界的区间执行找根;同时覆盖区间内转向等情况,不能为了省时漏掉端点同号但中间跨界的情形。定位达到所需精度或浮点区间不再可分后停止,不必机械重复满 60 次。 +3. **仅在阶段实际切换时处理。** 保持接触期间不反复重启;明确初始贴边、穿越方向和离开后的再次触发规则。不能以放大物理间隙、移动事件时间或粗暴忽略真实小振荡来换速度。 +4. **同一时刻的多个事件批量提交、一次重启。** 包括多个接触件,以及与信号边界重合的情况;不得为了减少启动次数把有真实先后关系的事件强行合并。 +5. **只注册启用模式所需的边界。** 当前八路的接触件均允许负力,不额外注册非负力截断事件;也不在 `Pdis` 或 `3*Pdis` 处造事件。 + +按上述实现,接触/脱离次数较少时,额外成本预期可控;频繁真实碰撞或边界数值反复触发时,成本可能明显增加。事件处理不会消除模型本身的高刚度,也不能保证 RK45 因此适合所有高刚度工况。 + +### 如何验证增量成本 + +完成实现后,在同一份 JSON、求解器、容差、最大步长和输出设置下,对修改前后分别重复运行,使用已构建的可执行文件,比较中位数并记录波动。分别记录纯求解时间、进程总时间、事件检查/定位次数、实际切换数、启动次数、接受步/拒步、`nfev`、`njev` 和线性 setup 次数。 + +平台 `solveSeconds` 不包含后续全部输出重放和 JSON 写出,所以事件点增多的输出成本还应检查总进程时间。现有 `nfev` 也不统计纯密集插值调用,新增定位计数/耗时才能解释这部分成本。 + +除当前八路模型外,至少检查长期不接触、持续接触、单次接触/脱离及反复碰撞四种情况;在事件时刻与接触力精度得到验证的前提下评估速度。本轮仅完成成本评估,没有修改机械事件实现或进行新增事件的性能实测。 diff --git a/docs/standard/component-model-authoring-spec-v1.md b/docs/standard/component-model-authoring-spec-v1.md index 7776b7b..b01d03b 100644 --- a/docs/standard/component-model-authoring-spec-v1.md +++ b/docs/standard/component-model-authoring-spec-v1.md @@ -414,6 +414,33 @@ def create( 9. 保持 SI、端口符号和适用的质量/能量守恒;按需编译及缓存由内容失效机制管理,测试完整模型命中、参数变化、模块变化及遗漏导出错误。 10. C 代码、构建依赖、进程与文件操作遵守[跨平台交付约定](跨平台交付约定.md)。分别记录 Linux 与 Windows 实际结果,不把 Linux 模拟测试写成 Windows 验收。 +### 12.1 分段信号的时间事件 + +新增阶跃、脉冲或循环分段信号时,元数据注册不会自动建立时间事件。除信号值方程外,还须在原生生成器中提供下一事件时间,汇入 `model_next_break(t, end)`。默认接入 `extended.py`;若该型号也走 `compiler.py` 紧凑路径,则两条路径都须实现。连续光滑信号没有这项分段要求;由压力、位置等状态触发的切换需要相应状态事件支持,不能仅凭时间表处理。 + +- 求值与事件查询必须共享同一套绝对时间边界。可复用 UD00 的 `native_signal` / `native_signal_break`;避免一边以取模判断阶段,另一边独立计算事件。仅新增现有 UD00 实例或修改参数无需再次注册事件。 +- 对 `t < end`,返回严格晚于 `t` 的最近边界,超过仿真终点或没有后续事件时返回 `end`。包含开始时刻、阶段端点、周期回绕,以及模型需要定位的斜率变化;同一时刻的零时长阶段按合同合并处理。 +- UD00 使用右连续阶段规则:`t < boundary` 属于前一段,`t == boundary` 属于后一段。周期终点统一使用下一周期起点。不能用任意全局 epsilon 把真实边界前的时刻提前切换。 +- 求值必须支持试探步、回退和结果重放;不得在普通 `model_eval` 调用中推进全局阶段索引。UD00 当前由时间纯函数查找阶段。需要离散状态的组件,应另行实现可回退、可重放的状态及事件提交机制。 +- 运行库按最早边界分段积分,在内部边界左侧结束旧段,再于边界处保留连续状态并重新启动积分。新组件通常复用该流程,不为每个型号新增求解器分支。名义采样时间仍按其实际浮点值求值;“界面显示同一小数”不等于内部时间完全相等。 +- 新内核函数同步公共声明及 `native_codegen/modules.py` 导出/依赖。测试边界前一个浮点数、边界本身及后一个浮点数,并覆盖延迟开始、周期回绕、多周期、退化参数和 BDF/RK45 最小系统的结果重放。参照 `tests/test_native_signal_boundaries.py`。 + +### 12.2 事件输出与重放 + +原生输出语义 version 2 保留实际时间的规则采样,并自动保存内部时间事件的左侧相邻浮点时刻及事件时刻。已有状态重置事件在当前积分区间可用时保存左邻插值状态,再保存重置后的状态;同一实际时间以后一次接受状态为准。时间序列保持严格递增,样本数量可能超过规则网格点数。初始即时事件不倒填时间,仿真终点不额外制造时间事件。 + +组件接入已有时间/状态事件接口后无需自行写入输出文件。结果重放必须仅依赖保存时间、完整状态和模型参数;模式影响输出时,必须有可保存、可回退、可重放的表示,不能读取运行结束时的全局模式。接触状态接口同样需要遵守这项约束。相关运行库合同见 [native/README.md](../../native/README.md),测试见 `tests/test_native_output_semantics.py`。 + +外部对照不能把显示为同一小数时刻当作同一事件侧。当前八路评估以全部分段常值信号核验驱动阶段,所有变量共用同一对保存样本;保留双方真实时间及无法配对记录。相同阶段也不等于实际时间完全相同,高刚度快速过程仍需进一步核验时间差的影响。 + +### 12.3 LSTP 接触状态事件 + +LSTP00A 已在两条原生生成路径默认注册:`native_codegen/contacts.py` 发出两端机械速度/位移状态索引、间隙、刚度、阻尼、阻尼距离、力符号模式及原公式运算顺序;`native/runtime/contact_events.h` 在接受步的密集插值上定位间隙过零,并在非负力模式定位接触区内的原始力过零。新增现有 LSTP 实例或修改其参数无需额外注册。 + +此接口只覆盖已审查的 LSTP 状态形式,不会因新组件名称或元数据相似而自动适用。新增类似组件时,须明确事件函数及依赖、根方向、初始贴边/切触、连续状态是否重置、同时事件和防重复提交规则;生成的事件表达式应与力公式保持一致,包括浮点运算顺序。检测不能悄悄引入整模型 RHS 求值或改变已有雅可比/物性复用。MASS 弹性限位及连续释放仍为独立实验,未在本次加入默认路径。 + +接触事件的运行期记录只用于定位及防止重复触发,不影响力求值。若新模式需要真正的离散物理状态,应按 12.2 保存和重放,不能借用这个定位记录作为不可重放的力开关。验证应包括解析或独立参考、步长变化、接触与脱离、力截断、无事件轨迹保持和 BDF/RK45;参考 `tests/test_native_contact_events.py`。 + ## 13. 模型实现示例 可参照 [气瓶声明](../../app/simulation/components/experimental/storage/cylinder.py)、[气腔声明](../../app/simulation/components/amesim/storage/chambers.py)、[C 数值模块](../../native/components/modules/) 与 [系统 C 生成器](../../app/simulation/native_codegen/extended.py)。完整开发顺序见 [注册示例与验证](component-registration-example-v1.md)。 diff --git a/native/README.md b/native/README.md index 935ab85..a8b4bd7 100644 --- a/native/README.md +++ b/native/README.md @@ -34,6 +34,14 @@ bash bat/setup-native-linux.sh 通用诊断包含 `stateCount`、`sampleCount`。`pressureFlow`、`stream` 属于 Python 后端的可选诊断;C 后端不报告未计算的方程残差,网页仅在该值存在时显示它。 +### 事件输出语义(version 2) + +原生结果的 `outputSemantics` 描述采样合同。规则网格保持实际浮点时间,不吸附到附近事件。内部时间事件额外保存 `nextafter(event, -INFINITY)` 和事件时刻:前者是积分段的实际左侧终点,后者沿用连续状态、使用事件后的信号。状态重置事件在可用的积分区间内额外保存左邻时刻的插值状态,再保存事件后的接受状态。同一实际时间仍以后一次接受状态为准;时间序列严格递增,采样总数可能超过规则网格点数。 + +输出根据保存时间及完整状态重放;已有摩擦离散模式随状态保存。事件左邻样本是有限精度下的左侧样本,不声称保存了同一数学时刻的精确左极限。初始时刻或当前步起点的即时状态重置没有新的左侧区间,不倒填样本。仿真终点不额外制造一个时间事件。纯求解模式不增加采样或模型输出求值;此改动不注册新的机械事件。 + +与外部结果对照时,必须核验事件侧别。八路评估脚本使用全部 STEP/UD00 输出核验驱动阶段,所有变量共用同一对原始样本行;仅允许在输出间隔的 `1e-7` 范围内匹配时间舍入误差。无法匹配的点明确报告,不跨跳变插值、不根据力或压力误差选择样本。这个规则不代表已验证未注册的接触模式。 + 原生运行库已在本机 GCC 8.1 / SUNDIALS 7.4.0 验证。构建需要对应 C 头文件、导入库和 DLL,单纯安装 Python 包不能代替这些文件。构建器根据环境变量或当前 Python 基础环境查找 SUNDIALS,根据环境变量或 PATH 查找 GCC。 ## 仿真结果分块存储 @@ -103,7 +111,8 @@ Jacobian 构建内现已按完整输入的浮点位复用储气物性、PH 温 - 扩展编译器上限 1024 状态、16384 输出;无连续状态的信号系统使用隐藏常量状态驱动输出。气动网络必须有压力状态锚点;独立气腔之间不能无阻力直接相连。兼容固定管路容腔是已实现的合并例外。闭合未收敛或方程欠定时明确失败,不静默回退。 - 支持原生 RK45 与 CVODE BDF。CVODE 默认按可证明的结构启用着色差分,使用稠密线性求解;不支持分组的模型自动保留逐列差分。 - 网页和 Python CLI 默认 `rtol=1e-8`;生成的状态绝对误差限为质量 `1e-14 kg`、内能 `1e-8 J`、速度/位移 `1e-12`(各自 SI 单位)。独立 C 程序默认 `rtol=1e-6`,对照时应显式传入。CLI 可覆盖 rtol;本版不支持自定义 atol 或 first_step。不同积分器相同局部容差不保证全局曲线误差完全相同。 -- 时间信号显式分段,塑性/反弹端挡用稠密插值定位并重启。试探 RHS 不修改已接受状态。柔性接触沿用现有分段力公式,不改变刚度或阻尼来提速。 +- 时间信号显式分段,塑性/反弹端挡用稠密插值定位并重启。LSTP00A 默认定位间隙过零;非负力模式还定位接触区内的原始力过零。两个生成路径均从机械状态索引发出 `NativeContact` 描述,检测不调用整模型 RHS,接触事件不重置位移/速度。试探 RHS 不修改已接受状态,接触力仍用原分段公式。MASS 弹性限位及连续释放的独立实验没有合入默认路径。 +- LSTP 的防重复触发记录仅在接受事件时提交,不参与 RHS、雅可比或输出重放;同一浮点时刻的接触事件合并重启,并沿用输出语义 v2 保存两侧。结果的 `contactEvents` 提供检查、密集插值、二分和接触/力截断计数。相对速度换向时分段检测同号间隙的中间过零;这依赖已解析的积分步,不能保证捕获一步内任意多次未解析振荡。专项见 `tests/test_native_contact_events.py`。 - 每任务独立进程,支持进度、取消及超时。进程崩溃不会作为成功返回,受控失败保留最后接受状态。 - 编译缓存位于 `app/data/native-builds/`:`models//` 保存完整模型,`objects//` 保存可跨模型复用的模块目标文件。按当前模型使用的元件函数及其依赖选择模块,最多并行编译 4 个缺失单元。预处理后的实际 C 内容、工具链和编译选项组成对象键;完整模型键另含生成源码、组件合同及链接依赖。模型数值参数仍特化入 C,但仅重编受到影响的单元;时间、步长、rtol、采样选项仍是运行参数。 - 完整模型默认预算 256 MiB、对象预算 128 MiB,可分别设置非负整数环境变量 `SIMULATION_NATIVE_MODEL_CACHE_MB`、`SIMULATION_NATIVE_OBJECT_CACHE_MB`。按目录最近使用时间执行 LRU;活跃构建/运行及最后一个单独超额的条目保留并报告超额,因此是安全软上限。预算计算受管理文件的逻辑字节,不含旧版根级缓存、锁及文件系统元数据。 diff --git a/native/components/modules/signal.c b/native/components/modules/signal.c index 261aa8a..39978bf 100644 --- a/native/components/modules/signal.c +++ b/native/components/modules/signal.c @@ -1,37 +1,81 @@ #include "kernels.h" #include -double native_signal(double t, double start, int stages, int cyclic, const double *data) { - if (t < start) return data[0]; - double elapsed = t-start, duration = 0, offset = 0; - for (int i=0;i 0) elapsed = fmod(elapsed,duration); - else if (elapsed >= duration) return data[8+stages-1]; - for (int i=0;it) { + hi=lo; lo=fmax(0,hi-1); + if (signal_boundary(start,0,lo,period)>t) lo=0; + } else { + /* Usually hi=lo+1 already brackets t. At coarse time resolution, + * several cycles can round to the same double: bracket/bisect them + * rather than advancing one cycle at a time or shifting t. */ + while (signal_boundary(start,0,hi,period)<=t) { + lo=hi; hi=fmax(2*hi,nextafter(hi,INFINITY)); + if (!isfinite(hi)) return 0; + } } - return data[8+stages-1]; + for (;;) { + double mid=floor(lo+(hi-lo)/2); + if (mid<=lo || mid>=hi) break; + if (signal_boundary(start,0,mid,period)<=t) lo=mid; + else hi=mid; + } + *current=lo; *next=hi; + return 1; +} + +static SignalInterval signal_interval(double t, double start, int stages, int cyclic, + const double *data) { + if (t0 && isfinite(period); + if (cyclic && !signal_cycle(t,start,period,&cycle,&next_cycle)) + return (SignalInterval){-2,NAN,NAN}; + for (int i=0;i 0 && event <= t) { - double cycle=fmax(0,floor((t-event)/duration)+1); - event += cycle*duration; - if (event <= t) event += duration; - } - if (event > t) result=fmin(result,event); - if (i < stages) offset += data[16+i]; - } - return result; + SignalInterval interval=signal_interval(t,start,stages,cyclic,data); + return interval.stage==-2 ? NAN : fmin(end,interval.next); } diff --git a/native/include/runtime.h b/native/include/runtime.h index d20c0e9..b82091d 100644 --- a/native/include/runtime.h +++ b/native/include/runtime.h @@ -7,6 +7,9 @@ #ifndef NFRICTIONS #define NFRICTIONS 0 #endif +#ifndef NCONTACTS +#define NCONTACTS 0 +#endif #define NATIVE_JACOBIAN_STATS_COUNT 3 typedef struct { double start, stop, sample_step, max_step, rtol, timeout; @@ -44,6 +47,13 @@ typedef struct { char message_buffer[384]; int status; /* 0 completed, 1 cancelled, 2 failed */ const char *message; + /* Accepted-event latch only; never used by RHS or output force replay. */ +#if NCONTACTS + double contact_last[2*NCONTACTS]; + int contact_direction[2*NCONTACTS], contact_pending[2*NCONTACTS]; +#endif + unsigned long contact_checks, contact_dense, contact_roots; + unsigned long contact_transitions, contact_clipping; NativePropertyWarning property_warnings[6]; } NativeRun; typedef int (*NativeDense)(void *context, double t, double *state); @@ -55,6 +65,7 @@ int native_fail(NativeRun *run, const char *reason, const char *operation, const int native_rhs(NativeRun *run, double t, const double *y, double *dy); int native_jacobian_rhs(NativeRun *run, double t, const double *y, double *dy); int native_append(NativeRun *run, double t, const double *y); +int native_time_boundary_samples(NativeRun *run, double boundary, const double *state); int native_accept(NativeRun *run, double t, double next, const double *old, const double *trial, NativeDense dense, void *context, double *accepted_time, double *accepted_state); diff --git a/native/runtime/common.c b/native/runtime/common.c index 8d3200c..4f13195 100644 --- a/native/runtime/common.c +++ b/native/runtime/common.c @@ -86,6 +86,16 @@ int native_append(NativeRun *r, double t, const double *y) { return native_samples_append(r,t,y); } +/* The integrators already stop at the left-adjacent double of an internal + * time event. Preserve that actual endpoint and the continuous state at the + * event, so output replay can evaluate BOTH forcing phases without snapping + * a regular-grid timestamp or changing integration history. */ +int native_time_boundary_samples(NativeRun *r,double boundary,const double *state) { + if (!r->options.record_samples || boundary>=r->options.stop) return 1; + return native_samples_append(r,nextafter(boundary,-INFINITY),state) && + native_samples_append(r,boundary,state); +} + /* Amesim helium data domains: EOS, ideal Cp/h, viscosity. Warnings describe * the property use, not a component. Trial/Newton/Jacobian evaluations never * publish warnings; replay of output samples is also side-effect free. */ @@ -166,12 +176,14 @@ static double locate_breakaway(NativeRun *r,int index,double left,double right, } #endif +#include "contact_events.h" + int native_accept(NativeRun *r, double t, double next, const double *old, const double *trial, NativeDense dense, void *context, double *accepted_time, double *accepted_state) { - double when[2*(NSTOPS+NFRICTIONS+1)], bounds[2*(NSTOPS+NFRICTIONS+1)]; - double restitution[2*(NSTOPS+NFRICTIONS+1)], thresholds[2*(NSTOPS+NFRICTIONS+1)]; - int indices[2*(NSTOPS+NFRICTIONS+1)], friction[2*(NSTOPS+NFRICTIONS+1)], count=0; + double when[2*(NSTOPS+NFRICTIONS+NCONTACTS+1)], bounds[2*(NSTOPS+NFRICTIONS+NCONTACTS+1)]={0}; + double restitution[2*(NSTOPS+NFRICTIONS+NCONTACTS+1)]={0}, thresholds[2*(NSTOPS+NFRICTIONS+NCONTACTS+1)]={0}; + int indices[2*(NSTOPS+NFRICTIONS+NCONTACTS+1)], friction[2*(NSTOPS+NFRICTIONS+NCONTACTS+1)], count=0; for (int j=0;joptions.record_samples && before>=t && before=0) continue; + if(friction[i]!=-1) continue; double incoming=accepted_state[indices[i]]; accepted_state[indices[i]]=fabs(incoming)<=thresholds[i]?0:-restitution[i]*incoming; accepted_state[indices[i]+1]=bounds[i]; @@ -242,7 +266,7 @@ int native_accept(NativeRun *r, double t, double next, const double *old, /* Stop/reverse only at an accepted event. The discrete mode travels * with saved states so replayed results and Jacobian trials are pure. */ double drives[NFRICTIONS]; - for(int i=0;i0?1:(velocity<0?-1:0); @@ -254,6 +278,9 @@ int native_accept(NativeRun *r, double t, double next, const double *old, accepted_state[f.velocity_index]=0; accepted_state[f.mode_index]=fabs(drives[j])>f.breakaway_force ? (drives[j]>0?1:-1) : 0; } +#endif +#if NCONTACTS + contact_commit(r,stop,accepted_state,when,indices,friction,count); #endif if (!native_append(r,stop,accepted_state)) return -1; while (r->options.start+r->sample_index*r->options.sample_step<=stop) r->sample_index++; diff --git a/native/runtime/contact_events.h b/native/runtime/contact_events.h new file mode 100644 index 0000000..f09a86d --- /dev/null +++ b/native/runtime/contact_events.h @@ -0,0 +1,105 @@ +/* Private implementation included by common.c. All probes use state-only + * descriptors: no model RHS, property evaluation, mode mutation or reset. */ +#if NCONTACTS +#include + +static double contact_penetration(int j,const double *y) { + NativeContact c=model_contacts[j]; + double x1=y[c.velocity1+1],x2=y[c.velocity2+1]; + /* Match each lowering's existing force formula, including association. */ + return c.subtract_first ? -(c.gap0+(x2-x1)) : -(c.gap0+x2-x1); +} +static double contact_velocity(int j,const double *y) { + NativeContact c=model_contacts[j]; + return y[c.velocity1]-y[c.velocity2]; +} +static double contact_value(int j,int kind,const double *y) { + if(kind==2) return contact_velocity(j,y); + double p=contact_penetration(j,y); + if(!kind) return p; + NativeContact c=model_contacts[j]; + double fraction=c.depth>0 ? -expm1(-fmax(p,0)/c.depth) : 1; + return c.stiffness*p+fraction*c.damping*contact_velocity(j,y); +} +static double contact_locate(NativeRun *r,int j,int kind,double left,double right, + double sign,NativeDense dense,void *context) { + double y[NSTATES]; + for(int k=0;k<60;k++) { + double mid=left+.5*(right-left); + if(mid<=left || mid>=right) break; + r->contact_dense++;r->contact_roots++; + if(!dense(context,mid,y)) return NAN; + double g=contact_value(j,kind,y); + if(!isfinite(g)) return NAN; + if(sign<0 ? g>=0 : g<=0) right=mid; else left=mid; + } + return right; +} +static double contact_bracket(NativeRun *r,int j,int force,double left,double right, + double a,double b,NativeDense dense,void *context,int *direction) { + int slot=2*j+force; + if(r->contact_direction[slot] && r->contact_last[slot]==left) + a=r->contact_direction[slot]*DBL_MIN; + if(a==0 || (a>0 ? b>0 : b<0)) return INFINITY; + *direction=a<0 ? 1 : -1; + return contact_locate(r,j,force,left,right,a,dense,context); +} +static double contact_candidate(NativeRun *r,int j,int force,double t,double next, + const double *old,const double *trial, + NativeDense dense,void *context,int *direction) { + double a=contact_value(j,force,old),b=contact_value(j,force,trial); + if(!isfinite(a) || !isfinite(b)) return NAN; + /* Resolve a same-sign endpoint excursion by splitting at velocity reversal. + * This assumes resolved steps, not arbitrarily many oscillations per step. */ + if(!force) { + double va=contact_velocity(j,old),vb=contact_velocity(j,trial); + if((va<0 && vb>0) || (va>0 && vb<0)) { + double turn=contact_locate(r,j,2,t,next,va,dense,context),y[NSTATES]; + if(!isfinite(turn)) return NAN; + r->contact_dense++; + if(!dense(context,turn,y)) return NAN; + double g=contact_value(j,force,y); + if(g==0 && ((a<0 && b<0) || (a>0 && b>0))) return INFINITY; + double found=contact_bracket(r,j,force,t,turn,a,g,dense,context,direction); + if(isfinite(found) || isnan(found)) return found; + return contact_bracket(r,j,force,turn,next,g,b,dense,context,direction); + } + } + return contact_bracket(r,j,force,t,next,a,b,dense,context,direction); +} +static int contact_candidates(NativeRun *r,double t,double next,const double *old,const double *trial, + NativeDense dense,void *context,double *when,int *indices,int *kinds,int *count) { + for(int j=0;jcontact_checks++; + for(int force=0;force<2;force++) { + if(force && (model_contacts[j].signed_force==1 || contact_penetration(j,old)<=0)) continue; + int direction=0; + double at=contact_candidate(r,j,force,t,next,old,trial,dense,context,&direction); + if(isnan(at)) return 0; + if(isfinite(at)) { + if(force) { + double y[NSTATES];r->contact_dense++; + if(!dense(context,at,y)) return 0; + if(contact_penetration(j,y)<=0) continue; + } + int n=(*count)++; + when[n]=at;indices[n]=j;kinds[n]=force?-3:-2; + r->contact_pending[2*j+force]=direction; + } + } + } + return 1; +} +static void contact_commit(NativeRun *r,double t,const double *y, + const double *when,const int *indices,const int *kinds,int count) { + for(int i=0;i-2 || when[i]!=t) continue; + int j=indices[i],force=kinds[i]==-3,slot=2*j+force; + r->contact_last[slot]=t;r->contact_direction[slot]=r->contact_pending[slot]; + if(force) r->contact_clipping++; else r->contact_transitions++; + fprintf(stderr,"{\"phase\":\"mechanical-event\",\"kind\":\"%s\",\"index\":%d,\"time\":%.17g,\"direction\":%d,\"penetration\":%.17g,\"relativeVelocity\":%.17g}\n", + force?"force-clip":"lstp-contact",j,t,r->contact_direction[slot], + contact_penetration(j,y),contact_velocity(j,y)); + } +} +#endif diff --git a/native/runtime/cvode_solver.c b/native/runtime/cvode_solver.c index 70e49a9..5597d0e 100644 --- a/native/runtime/cvode_solver.c +++ b/native/runtime/cvode_solver.c @@ -366,6 +366,10 @@ int native_bdf(NativeRun *r) { r->starts++; } } + if (!native_time_boundary_samples(r,boundary,N_VGetArrayPointer(y))) { + native_fail(r,"sample-storage-failure","native_time_boundary_samples","Cannot store time-event samples."); + goto cleanup; + } t=boundary; r->final_time=t; memcpy(r->final_state,N_VGetArrayPointer(y),NSTATES*sizeof(double)); double sample=r->options.start+r->sample_index*r->options.sample_step; diff --git a/native/runtime/main.c b/native/runtime/main.c index 8092aea..f9d3131 100644 --- a/native/runtime/main.c +++ b/native/runtime/main.c @@ -51,7 +51,15 @@ static int write_result(NativeRun *r, const char *path, const char *index_path) fprintf(f,"{\"success\":%s,\"status\":",!r->status?"true":"false"); json_string(f,r->status==0?"completed":r->status==1?"cancelled":"failed"); fprintf(f,",\"message\":"); json_string(f,r->message); + fprintf(f,",\"contactEvents\":{\"checks\":%lu,\"denseCalls\":%lu,\"rootIterations\":%lu,\"transitions\":%lu,\"forceClipTransitions\":%lu}", + r->contact_checks,r->contact_dense,r->contact_roots,r->contact_transitions,r->contact_clipping); fprintf(f,",\"propertyWarnings\":"); native_property_warnings_json(r,f); + fputs(",\"outputSemantics\":{\"version\":2," + "\"regularSamples\":\"actual-time-no-snap\"," + "\"internalTimeEvents\":\"left-adjacent-and-at-event\"," + "\"stateEvents\":\"left-adjacent-when-available-and-post-reset\"," + "\"sameTime\":\"last-accepted-state\"," + "\"replay\":\"saved-time-and-state-including-discrete-modes\"}",f); fprintf(f,",\"jacobianReuse\":{\"gasEvaluations\":%lu,\"gasReuses\":%lu", r->jacobian_gas_evaluations,r->jacobian_gas_reuses); const char *reuse_names[]={"ph","density","pipe"}; diff --git a/native/runtime/rk45.c b/native/runtime/rk45.c index ae682a6..817d95b 100644 --- a/native/runtime/rk45.c +++ b/native/runtime/rk45.c @@ -108,6 +108,7 @@ int native_rk45(NativeRun *r) { if (impact) restart=1; else memcpy(f,K[6],sizeof(f)); } + if (!native_time_boundary_samples(r,boundary,y)) return 0; t=boundary; r->final_time=t; memcpy(r->final_state,y,sizeof(y)); double sample=r->options.start+r->sample_index*r->options.sample_step; if (r->options.record_samples && sample<=t && sample<=r->options.stop) { diff --git a/tests/manual/analyze_context_access.py b/tests/manual/analyze_context_access.py new file mode 100644 index 0000000..4884d22 --- /dev/null +++ b/tests/manual/analyze_context_access.py @@ -0,0 +1,256 @@ +"""Verify each actual read and ordered write, then summarize the two cases.""" +from collections import Counter +from pathlib import Path +import ctypes as c +import hashlib +import json +import struct + +ROOT = Path(__file__).resolve().parents[2] +OUT = ROOT/'test/context-access-20260917' + + +class Medium(c.Structure): + _fields_ = [('real_helium', c.c_int)] + [(k, c.c_double) for k in ['R','cp','Tref','slope','mu','muT','S']] + + +class State(c.Structure): + _fields_ = [('medium', Medium)] + [(k, c.c_double) for k in ['p','T','h','rho','mu','isentropic_factor','isentropic_exponent']] + [('valid', c.c_uint),('temperatures',c.c_void_p),('jacobian',c.c_void_p)] + + +class Pipe(c.Structure): + _fields_ = [('medium',Medium)] + [(k,c.c_double) for k in ['p1','p2','T','diameter','length','roughness','flow']] + [('kind',c.c_int),('valid',c.c_int)] + + +class Context(c.Structure): + _fields_ = [('states',c.c_void_p),('count',c.c_size_t),('capacity',c.c_size_t),('temperatures',c.c_void_p),('jacobian',c.c_void_p)] + + +def doubles(value): + data = bytes.fromhex(value) + return list(struct.unpack('<'+'d'*(len(data)//8),data)) + + +def value(data, typ): + if typ == c.c_double: + return struct.unpack('74 else None + if st74: summary['state74']={'p':st74.p,'T':st74.T,'valid':st74.valid} + pending=None + for event in events[2:]: + kind=event['event'] + if kind=='query': + assert pending is None + pending=dict(event, allMatches=first_match(memory,event), tested=[]) + elif kind=='match': + assert pending and pending['kind']==event['kind'] + matches=pending['allMatches']; expected=matches[0] if matches else -1 + assert event['slot']==expected and event['hit']==bool(matches) + expected_scans=list(range(expected+1 if expected>=0 else pending['count'])) + assert pending['tested']==expected_scans,(begin,pending) + summary['queries'].append(dict(kind=event['kind'],key=pending['key'],keyValues=doubles(pending['key']),mediumKind=pending['mediumKind'],hit=event['hit'],slot=event['slot'],allMatches=matches,scanCount=len(expected_scans))) + pending=None + elif kind in ['read','write']: + domain=event['domain']; offset=event['offset']; data=bytes.fromhex(event['value']) + previous=memory[domain][offset:offset+len(data)] + slot,field,typ=named(domain,offset,len(data)) + if kind=='read': + assert previous==data,(begin,event,'read mismatch') + summary['accessReads']+=1 + # Lookup reads are recorded in raw events; this list isolates + # fields consumed after selection, including observers/keys. + if domain!='context' and pending is None and event['function']!='same_medium': + summary['consumed'].append(dict(domain=domain,slot=slot,field=field,value=event['value'],function=event['function'])) + else: + summary['accessWrites']+=1 + equal=all(known[domain][offset:offset+len(data)]) and previous==data + summary['sameValueWrites']+=equal + memory[domain][offset:offset+len(data)]=data + known[domain][offset:offset+len(data)]=b'\1'*len(data) + summary['writes'].append(dict(domain=domain,slot=slot,field=field,value=event['value'],sameValue=equal,function=event['function'])) + elif kind=='valid_test': + st=State.from_buffer_copy(memory['states'],event['slot']*c.sizeof(State)) + assert st.valid & event['mask']==event['value'] + if pending is not None: + pending['tested'].append(event['slot']) + else: + summary['validTests'].append(event) + elif kind=='allocate': + assert event['branch']=='append', 'Scratch requires additional object registration; fail closed.' + assert event['slot']==event['countAfter']-1 + assert event['countAfter']==Context.from_buffer_copy(memory['context']).count + assert event['countAfter']<=event['capacity'] + summary['allocations'].append(event) + elif kind=='snapshot': + assert event['phase']=='exit' + for domain in memory: + expected=bytes.fromhex(event[domain]) + assert memory[domain][:len(expected)]==expected,(begin,domain,'write replay mismatch') + summary['exitCount']=event['count'] + # Everything not written remains the live probe entry, checked + # across every byte of every existing slot and every pipe slot. + summary['replayExact']=True + elif kind=='end': + summary['outputs']=event['outputs'] + summary['outputValues']=doubles(event['outputs']) + elif kind.startswith('scalar_'): + summary['scalar'].append(event) + else: + raise AssertionError(event) + assert pending is None and summary['replayExact'] + # Snapshot replay alone cannot detect an omitted equal-valued write. + # Check the reviewed source's mandatory store sequence independently. + pipe_writes=[w for w in summary['writes'] if w['domain']=='pipes'] + assert [w['field'] for w in pipe_writes]==['valid','medium','p1','p2','T','diameter','length','roughness','kind','flow','valid'] + assert int.from_bytes(bytes.fromhex(pipe_writes[0]['value']),'little')==0 + assert int.from_bytes(bytes.fromhex(pipe_writes[-1]['value']),'little')==1 + assert pipe_writes[-2]['value']==summary['outputs'] + assert all(w['slot']==(28 if begin['position']==52 else 0) for w in pipe_writes) + for allocation in summary['allocations']: + writes=[w for w in summary['writes'] if w['domain']=='states' and w['slot']==allocation['slot']] + assert [w['field'] for w in writes[:7]]==['*','medium','p','T','valid','temperatures','jacobian'] + assert not [w for w in summary['writes'] if w['domain']=='states' and w['slot']=0 else 'padding' + full_field=field if kind==1 else f'{"states" if kind==2 else "pipes"}[{x["index"]}].{field}' + key=(g,r,kind,x['index'],fld,x['persistentOrigin'],x['firstContextOrigin']);origins[key]+=1 + first_context_origins[g,x['firstContextOrigin']]+=1 + field_totals[field]+=1;regions[r]['fields'][field]+=1;pairs[g,r]['fields'][field]+=1 + pos=x['persistentOrigin'];first=x['firstContextOrigin'] + if pos<0 or first<0:missing.append(x) + deps=set(plan['operations'][pos]['stateIndices'])&set(plan['groups'][g]['stateIndices']) if pos>=0 else set() + causal['directFieldOrigin' if deps else 'cacheSideEffectOrigin']+=1 + rootdeps=set(plan['operations'][first]['stateIndices'])&set(plan['groups'][g]['stateIndices']) if first>=0 else set() + causal['firstContextHasStateDependency' if rootdeps else 'firstContextNoStateDependency']+=1 + if (g,r) not in samples or x['jac']==200: + floating=kind>1 and fld>=0 and trace['fields'][str(kind)][fld]['floating'] + samples[g,r]={**x,'fieldName':full_field,'baselineValue':decode(x['baseline'],floating),'trialValue':decode(x['trial'],floating), + 'originName':plan['operations'][pos]['key'] if pos>=0 else str(pos),'originOperationId':plan['operations'][pos]['id'] if pos>=0 else pos, + 'originPerturbedStates':[plan['stateKeys'][i] for i in sorted(deps)],'rootPerturbedStates':[plan['stateKeys'][i] for i in sorted(rootdeps)]} + assert trace_count==139776 and not missing,(trace_count,missing[:2]) + assert field_totals==Counter(p=105741,count=34035) + assert sum(origins.values())==139776 + # The census and provenance modes must have identical region-level counts. + for x in trace['regions']: + for k in ('attempts','compares','failures','ops','inputDiffOps','outputDiffOps','exitOutputDiff','exitContextDiff'):assert x[k]==pairs[x['group'],x['region']][k] + operation_totals={} + for (g,pos),x in ops.items(): + if pos not in operation_totals:operation_totals[pos]=dict(position=pos,operationId=x['operationId'],name=plan['operations'][pos]['key'],count=0,seconds=0,groups=[],regions=[]) + a=operation_totals[pos];a['count']+=x['count'];a['seconds']+=x['seconds'];a['groups'].append(g) + if x['region'] not in a['regions']:a['regions'].append(x['region']) + group_totals=[] + for g in range(27): + rr=[x for (gg,r),x in pairs.items() if gg==g];group_totals.append(dict(group=g,states=plan['groups'][g]['states'], + compares=sum(x['compares'] for x in rr),failures=sum(x['failures'] for x in rr),seconds=sum(x['seconds'] for x in rr), + failedRegions=[x['region'] for x in rr if x['failures']],ops=sum(x['ops'] for x in rr))) + hot=sorted((r for r in regions.values() if r['failures']),key=lambda r:-r['seconds']);hotops=sorted(operation_totals.values(),key=lambda o:-o['seconds']) + total=sum(r['seconds'] for r in hot);op_total=sum(o['seconds'] for o in hotops) + native_ops=[o for o in hotops if plan['versions'][o['position']]!=plan['versions'][o['position']+1]] + native_share=sum(o['seconds'] for o in native_ops)/op_total + for r in regions.values(): + r['failedGroups']=[g for g in r['groups'] if pairs[g,r['id']]['failures']] + r['failureRate']=r['failures']/r['compares'] if r['compares'] else None + r['meanUs']=r['seconds']/r['failures']*1e6 if r['failures'] else 0 + r['nativeOperationPositions']=[pos for pos in range(r['start'],r['end']) if plan['versions'][pos]!=plan['versions'][pos+1]] + # Production and pre-existing experimental source bytes are unchanged. + for directory in ('worker','kernels','trace-worker'): + b=read(directory,'build.json') + for name,h in b['sourceHashes'].items():assert hashlib.sha256((SOURCE/name).read_text(encoding='utf-8').encode()).hexdigest()==h + production=json.loads((SOURCE/'build-metadata.json').read_text(encoding='utf-8')) + for name,h in production['sourceHashes'].items(): + assert hashlib.sha256((ex.builder.NATIVE/name).read_text(encoding='utf-8').encode()).hexdigest()==h + assert hashlib.sha256((ROOT/'tests/data/test-mql-8-corrected.json').read_bytes()).hexdigest()==production['inputSha256'] + assert not subprocess.check_output(['git','diff','HEAD','--name-only'],cwd=ROOT,text=True).strip() + checked=[] + for path in sorted(OUT.glob('*/measurement.json')): + m=json.loads(path.read_text());checked.append(m) + result=dict(regions=list(regions.values()),groupRegions=list(pairs.values()),groups=group_totals,operations=list(operation_totals.values()), + groupOperations=list(ops.values()),kernels=list(kernels.values()),regionKernels=[dict(region=r,kernel=k,**v) for (r,k),v in region_kernels.items()], + provenance=[dict(group=k[0],region=k[1],kind=k[2],index=k[3],field=k[4],origin=k[5],firstContextOrigin=k[6],count=v) for k,v in origins.items()], + fieldCounts=field_totals,causalCounts=causal,samples=list(samples.values()),measurements=checked, + totals=dict(compares=sum(r['compares'] for r in regions.values()),failures=139776,regionSeconds=total,operationSeconds=op_total,nativeOperationShare=native_share)) + ex.write(OUT/'analysis.json',result) + report=['**context fallback 定位报告**\n', + '本轮只新增独立诊断工具及worker副本,原lp_reuse比较/恢复实现、dependency graph、ordinary residual、物性算法、accepted-step check及线性求解器未改动。八路模型0–10s,BDF、rtol=1e-8,其他设置沿用前轮。以下区间ID与schedule位置均从0开始,范围使用[start,end),position与operation原始ID不同,映射保存在plan.json。\n'] + report.append('**主要发现**\n') + report.append('139,776次回退全部具有相同特征:区间各operation的显式输入、输出均与baseline逐位一致,区间出口输出也一致;但出口context/cache全部仍不一致。首次失败原因仅为states[i].p(105,741次)或count(34,035次)。这支持“全context比较/整体snapshot恢复使局部无关区间回退”的判断,不支持“回退区间方程本身受到这些状态扰动”的判断。\n') + report.append('差异也不是假数据:前置受扰动operation确实会产生不同压力/温度的缓存条目;还有“前一operation改了缓存键→后一个物理输入不变的operation由命中变为新增条目→count变化”的间接链路。不能直接忽略这些差异并恢复整个baseline出口context,因为这会覆盖已有的真实扰动记录;本轮并未测试或声称该覆盖一定会改变最终解,也未证明放宽保护在任意模型上安全。\n') + report.append('**计数、覆盖范围与耗时口径**\n') + report.append(f'全量记录896个Jacobian、24,192次probe;495个可复用区间中127个包含context操作,发生160,384次context比较,139,776次失败({139776/160384:.2%}),涉及115个不同区间、156个group→区间组合。另有521,472次纯代数复用尝试不执行context比较,未混入比较分母。所有失败组合在896个Jacobian中均失败896次;其他组合全部成功。\n') + report.append('计数与字段来自全量census/provenance。所有139,776次失败均回溯到具体operation,无未定位记录。完整属性条目新建调用链、实际变化输入及数值样例在第0、200、450、700、895个Jacobian详细记录,覆盖约0、0.373、2.458、4.721、9.958s。字段原值同时保存十六进制浮点位,不以相对误差判定相同。\n') + report.append('耗时分三种独立模式,各3轮,每8个Jacobian分层抽1个,每轮112个,均完成完整仿真。区间计时从guard失败后的原计算开始,到区间执行结束为止,不包含context比较或字段日志;包含该区间原调度及诊断hook成本。operation计时包围原code语句;函数计时同时记录inclusive与扣除已插桩子函数后的exclusive。三种时间不能互相叠加。时间为原始插桩值,未扣空标记或强制缩放,极短操作和函数均值会高估;沿用前轮结论,不把细分时间当作精确优化收益。\n') + controls=[read(f'control-{i}','measurement.json') for i in range(3)] + report.append(table(['中位数','未插桩','区间抽样插桩','变化'],[(k,f'{statistics.median(x[k] for x in controls):.6f}',f'{statistics.median(x[k] for x in times["regions"]):.6f}',f'{statistics.median(x[k] for x in times["regions"])/statistics.median(x[k] for x in controls)-1:+.2%}') for k in ('jacobianSeconds','solveSeconds','solveCpuSeconds','processSeconds')])) + report.append(table(['模式,每种3轮','Jacobian中位数s','Jacobian最小–最大s','相对未插桩中位数','积分中位数s'],[(name,f'{statistics.median(x["jacobianSeconds"] for x in tt):.6f}',f'{min(x["jacobianSeconds"] for x in tt):.6f}–{max(x["jacobianSeconds"] for x in tt):.6f}',f'{statistics.median(x["jacobianSeconds"] for x in tt)/statistics.median(x["jacobianSeconds"] for x in controls)-1:+.2%}',f'{statistics.median(x["solveSeconds"] for x in tt):.6f}') for name,tt in [('未插桩',controls),('区间',times['regions']),('operation',times['ops']),('函数',times['functions'])]])) + report.append('这些小幅下降属于运行波动/编译布局差异,不能解释为插桩加速。函数模式中位Jacobian约增加4.84%,对抽中的短函数影响更大;其exclusive累计约0.879s,高于独立operation计时约0.660s。因此函数时间只用于热点排序和数量级判断,不作为无插桩下可节省时间的精确值。全量provenance为了追踪字段有意增加复制和日志,其Jacobian为4.801s、积分8.894s,完全不用于性能估计。\n') + report.append(f'区间回退累计时间折算约**{total:.6f}s**;operation模式独立测得约**{op_total:.6f}s**。两者来自不同插桩和样本,差值不能直接当作调度开销。\n') + report.append('**耗时最高的回退区间**\n') + report.append(table(['区间','schedule范围','group','比较/失败','失败率','累计ms','每次µs','占全部回退'],[(r['id'],f'[{r["start"]},{r["end"]})',','.join(map(str,r['failedGroups'])),f'{r["compares"]}/{r["failures"]}',f'{r["failureRate"]:.1%}',f'{r["seconds"]*1e3:.3f}',f'{r["meanUs"]:.3f}',f'{r["seconds"]/total:.2%}') for r in hot[:20]])) + report.append('全部127个context区间(包含成功区间)的比较次数、失败率、时间与组映射见intervals.md;纯代数区间也保存在analysis.json,但比较次数为0。\n') + report.append('**group分布与扰动变量**\n') + report.append(table(['group','比较/失败','累计回退ms','失败区间','扰动状态'],[(g['group'],f'{g["compares"]}/{g["failures"]}',f'{g["seconds"]*1e3:.3f}',','.join(map(str,g['failedRegions'])),', '.join(g['states'])) for g in group_totals])) + report.append(f'group0–9占失败次数{sum(g["failures"] for g in group_totals[:10])/139776:.2%},占回退时间{sum(g["seconds"] for g in group_totals[:10])/total:.2%};group10–25只有两个失败区间/组,但其区间较长,合计占时间{sum(g["seconds"] for g in group_totals[10:26])/total:.2%}。group26没有fallback。group10–25的对应缓存分叉由PNCH012的m/U扰动引起;同组若还含机械速度状态,不能仅凭共组就把cache差异归给该速度。详见originPerturbedStates。\n') + report.append('**完整证据链:直接压力变化**\n') + report.append('Jacobian 200,t≈0.372854446s,group18仅扰动amesim_pnch012_12.m。它改变p[43]和h[184];schedule position51(operation原始ID59,flow:amesim_pnl0001_16.port_1)的实际输入日志确认这两个值变化。调用链为native_pipe_flow_cached_context → native_pipe_flow_context → state_valve → isentropic → property_pt → property_new。详细trace覆盖上述被包装函数;isentropic/property_new的位置由源码补全,property_new日志直接记录新建条目。\n') + report.append('该operation新建的下游等熵条目states[74].p,baseline=15019640.749374540、probe=15019641.148960622 Pa;T也由460.10768147887495变为460.1076863719882 K。随后R475=[52,175)、R477=[313,452)入口首先在这个p字段不一致而失败。两区间实际执行的所有显式输入和输出均与baseline逐位一致。前置管路及其cache记录存在真实扰动依赖,但这两个候选复用区间的显式输入没有继续分叉;当前全context保护把前置变化传播成了这些区间的回退。\n') + report.append('对应R475累计/均值及R477累计/均值见上表和intervals.md;两者均每组896次,合并group18/19后各1792次。R490的对应链为group24/25 → amesim_pnch012_15.m/U → position48(id56,amesim_pnl0001_13.port_1)→ states[68].p → R490/R492;group24在同一时刻的p为15019642.069348963→15019642.468935065 Pa。\n') + report.append('**完整证据链:缓存新增导致count变化**\n') + report.append('同一Jacobian 200,group6包含amesim_pnl0001_9.m。position8(flow:amesim_pnor001_5.port_1)受其影响,baseline缓存slot6的键(p,T)=(15019652.421499353,290.99609647580155),probe变为(15019652.526950026,291.859408827332)。\n') + report.append('接着position10(flow:amesim_pnor001_6.port_1)的显式输入没有变化。它仍需要旧键(15019652.421499353,290.99609647580155):baseline在slot6命中,而probe该键已不存在,于native_temperature_ph_context → property_pt → property_new中新建slot8;其后另一个等熵条目也顺延。position10结束时count为9/10,后续R288入口为12/13,首次失败谓词是count。这里“首次写出count差异的operation10”不直接依赖扰动变量,真正上游原因是operation8改变了缓存内容。trace-created.jsonl保留两个调用链与键值,trace-inputs.jsonl证实operation10没有显式输入差异。\n') + report.append('group6一次同时扰动多个状态:该probe最早的任意context差异发生在position2,而上述特定旧键消失发生在position8,count开始持续不等发生在position10。这三个“首次”不能混同;firstContextOrigin只记录最早的任意context分叉,不自动证明它就是每一个后续字段的原因。特定键的因果链由上述新建条目日志另行确认。\n') + report.append(f'全量归因中,字段持续分叉来源直接含本group状态依赖的记录为{causal["directFieldOrigin"]:,},来源operation不含该group显式状态依赖的记录为{causal["cacheSideEffectOrigin"]:,};后者属于context/cache隐式影响,具体旧键→命中/新增→count链路以上述详细trace为例。所有139,776条记录所在probe的最早context分叉均出自图上受扰动影响的operation。这里只对五个时刻记录全部新建键调用链,未声称对每一条间接记录都完成了逐键因果回放。\n') + report.append('**fallback到底重算了什么**\n') + report.append(table(['position / 原始ID','operation','全量次数','累计ms','每次µs'],[(f'{o["position"]}/{o["operationId"]}',o['name'],o['count'],f'{o["seconds"]*1e3:.3f}',f'{o["seconds"]/o["count"]*1e6:.3f}') for o in hotops[:25]])) + report.append(f'包含native调用的{len(native_ops)}个不同operation贡献operation计时的{native_share:.2%},执行次数占{sum(o["count"] for o in native_ops)/4620672:.2%};其余大量线性、alias、stream等操作虽然次数多,但单次较便宜。最耗时的前20个operation占{sum(o["seconds"] for o in hotops[:20])/op_total:.2%},前40个占{sum(o["seconds"] for o in hotops[:40])/op_total:.2%}。完整operation逐组、逐区间归属保存在analysis.json。\n') + report.append('R490每次重跑135个operation,R457每次重跑147个operation;不是每个operation都需要context。最大连续“图上无关”区间只要跨过context操作,就采用一次入口整体比较,失败后连同纯代数部分一起执行。这是区间粒度带来的额外工作;是否值得进一步拆分仍需考虑比较、调度、恢复开销,本轮不据此修改。\n') + report.append('区间长也不必然更贵:R3仅18个operation,每次约23.876µs,高于135个operation的R490(18.865µs);R3集中执行管路流量。相反,很多区间的operation只是别名传播或代数赋值。因此需要同时看回退次数、操作构成和单次成本,不能只按区间长度判断。主要热点的逐区间字段、来源operation、回退operation及函数时间在hotspots.md中串联展示。\n') + report.append('**函数层调用次数与时间**\n') + report.append('exclusive已减去本表中被插桩的子调用;仍包含未插桩子函数、计时和包装开销。inclusive存在嵌套,禁止求和当作总时间。函数与operation/区间来自独立运行,不能叠加。\n') + report.append(table(['函数','全量fallback调用次数','inclusive ms','exclusive ms','exclusive µs/次'],[(k['name'],k['count'],f'{k["inclusiveSeconds"]*1e3:.3f}',f'{k["exclusiveSeconds"]*1e3:.3f}',f'{k["exclusiveSeconds"]/k["count"]*1e6:.3f}' if k['count'] else '—') for k in sorted(kernels.values(),key=lambda v:-v['exclusiveSeconds'])])) + report.append('state_valve的单次成本明显高于property_pt等查找函数;后者主要靠次数累积。native_temperature_ph、native_density和native_pipe_resistance在fallback内的实际调用均为0:已有Jacobian memo覆盖了这些求解,不能把本次fallback热点归因于重复PH反算、密度求解或管阻求根。仍发生的主要计算包括阀流量/等熵计算、黏度计算、物性上下文查找和缓存键查询。\n') + report.append('**正确性判断与边界**\n') + report.append('1. 对本模型和完整本次轨迹,未发现fallback区间存在未被dependency graph捕获的数值输出依赖;4,620,672次回退operation的显式输入及输出全部逐位一致。不能据此证明所有模型和输入都可忽略context。\n') + report.append('2. 已证实完整context比较覆盖了来自区间外的压力/缓存分配变化;已证实大区间失败会带动纯代数operation重算。这属于比较/区间粒度较粗以及cache副作用的保守传播。没有证据把它归为物性算法错误,或把fallback本身归为状态dependency graph过度保守。\n') + report.append('3. 原lp_reuse成功后恢复整个出口property states和全部pipe cache;本轮139,776次回退出口context均不等于baseline。直接放宽比较并调用原restore会覆盖这些差异,不能由“区间输出相同”推出“完整上下文恢复安全”。保持现有context逐位语义时,当前保护的回退符合实现约定;它不等价于这些方程数学上必须全部重算。\n') + report.append('4. 首次失败谓词只有count和states[i].p;由于原代码短路比较,这不代表其他property字段或pipe cache没有差异。本报告没有把“未成为首次失败原因”当作“始终相同”。\n') + report.append('**数值与原实现保护**\n') + report.append(f'本轮保存{len(checked)}份成功运行记录,均验证完整states/outputs/events二进制、warning、最终状态、步数和求解器计数,以及原context保护命中/回退计数不变。census、函数插桩验证轮与全量provenance轮另比较896个132×132矩阵及其(t,y),共15,611,904元素逐位一致。accepted/rejected=10840/918,Newton iterations=19371,nfev/njev/nlu=44467/896/3106。原实验和生产实现未修改。\n') + report.append('工具:tests/manual/diagnose_context_fallback.py、context_fallback_diag.h/.c、analyze_context_fallback.py。plan.json含每个operation原代码及inputs/outputs/stateIndices;analysis.json含完整区间/group/operation/函数计数与时间;provenance/failures.jsonl含每次失败字段、十六进制原值和来源operation;provenance/trace-created.jsonl及trace-inputs.jsonl给出五个时刻的新建条目调用链和变化输入。\n') + report.append('**代码证据与复现**\n') + report.append('[lp_reuse入口比较和整体restore](F:/Master/SystemSimulationApp/tests/manual/local_probe_support.c:69);[property_new/count与property_pt精确键查询](F:/Master/SystemSimulationApp/native/components/modules/properties.c:40);[PH context与Jacobian memo](F:/Master/SystemSimulationApp/native/components/modules/properties.c:93);[等熵及state_valve](F:/Master/SystemSimulationApp/native/components/modules/properties.c:244);[pipe flow/cache链路](F:/Master/SystemSimulationApp/native/components/modules/pipe.c:114)。\n') + report.append(f'已核验当前生产源文件与前轮编译元数据中的SHA-256一致,输入工程SHA-256一致;当前git diff HEAD为空。前轮编译提交为{production["commit"]}。新增文件仅为本轮独立诊断工具和产物。\n') + report.append('从仓库根目录使用.venv-win/Scripts/python.exe -B运行tests/manual/diagnose_context_fallback.py。prepare分别不带参数、带--kernels、带--kernels --trace,生成三个独立worker;run --label census --mode 1 --matrices进行全量计数;run --label provenance --mode 5 --kernels --trace --matrices进行全量字段追踪。计时分别为mode 2(区间)、mode 3(operation)、mode 4 --kernels(函数),均--stride 8;三轮seed分别为17–19、43–45、67–69。control使用--control --mode 0;三类计时独立串行运行,避免与编译重叠。最后运行tests/manual/analyze_context_fallback.py重建报告。\n') + (OUT/'report.md').write_text('\n'.join(report),encoding='utf-8') + detail=['**全部context区间统计**\n','编号为0基,schedule范围为[start,end)。时间来自三轮1/8抽样的原始区间计时。\n',table(['ID','范围','比较','失败','失败率','累计ms','µs/失败','失败group','首次字段计数'],[(r['id'],f'[{r["start"]},{r["end"]})',r['compares'],r['failures'],f'{r["failureRate"]:.2%}' if r['failureRate'] is not None else '—',f'{r["seconds"]*1e3:.4f}',f'{r["meanUs"]:.3f}',','.join(map(str,r['failedGroups'])),dict(r['fields'])) for r in regions.values() if r['contextual']])] + (OUT/'intervals.md').write_text('\n'.join(detail),encoding='utf-8') + # Readable joins for hot regions; complete machine-readable joins are in analysis.json. + selected=list(dict.fromkeys([r['id'] for r in hot[:10]]+[477,492,288])) + details=['**热点区间的完整对应关系**\n','以下计数全量,时间来自独立三轮抽样折算;字段值为Jacobian 200的样例,槽号和首次字段随时刻可能变化。各区间包含的所有operation连续位于[start,end),完整原代码在plan.json。字段来源与函数时间来自不同诊断模式,不能相加。\n'] + for rid in selected: + r=regions[rid] + details.append(f'**R{rid},[{r["start"]},{r["end"]}),group {r["failedGroups"]}**\n') + details.append(f'比较{r["compares"]:,}次,失败{r["failures"]:,}次({r["failureRate"]:.1%});累计{r["seconds"]*1e3:.3f}ms,每次{r["meanUs"]:.3f}µs。全量首个失败字段分布:{dict(r["fields"])}。每次重算{r["end"]-r["start"]}个operation,其中{len(r["nativeOperationPositions"])}个包含native调用。\n') + ss=[samples[g,rid] for g in r['failedGroups']] + details.append(table(['group','本例首字段','baseline → probe','字段持续分叉position/ID','来源operation','与group交集的依赖状态'],[(s['group'],s['fieldName'],f'{s["baselineValue"]!r} → {s["trialValue"]!r}',f'{s["persistentOrigin"]}/{s["originOperationId"]}',s['originName'],', '.join(s['originPerturbedStates']) or '无;需追context/cache副作用') for s in ss])) + oo={} + for (g,pos),o in ops.items(): + if o['region']!=rid:continue + a=oo.setdefault(pos,dict(count=0,seconds=0));a['count']+=o['count'];a['seconds']+=o['seconds'] + ordered=sorted(oo.items(),key=lambda x:-x[1]['seconds']) + details.append(table(['前10个耗时operation的position/ID','operation','次数','累计ms','平均µs'],[(f'{pos}/{plan["operations"][pos]["id"]}',plan['operations'][pos]['key'],o['count'],f'{o["seconds"]*1e3:.4f}',f'{o["seconds"]/o["count"]*1e6:.4f}') for pos,o in ordered[:10]])) + kk=sorted([(k,v) for (rr,k),v in region_kernels.items() if rr==rid],key=lambda x:-x[1]['exclusiveSeconds']) + details.append(table(['该区间内函数','次数','inclusive ms','exclusive ms','exclusive µs/次'],[(kernels[k]['name'],v['count'],f'{v["inclusiveSeconds"]*1e3:.4f}',f'{v["exclusiveSeconds"]*1e3:.4f}',f'{v["exclusiveSeconds"]/v["count"]*1e6:.4f}' if v['count'] else '—') for k,v in kk])) + (OUT/'hotspots.md').write_text('\n'.join(details),encoding='utf-8') + print(json.dumps(dict(total=total,operationTotal=op_total,fields=field_totals,causes=causal,topRegions=[{k:r[k] for k in ['id','start','end','seconds','meanUs','failures']} for r in hot[:10]],nativeShare=native_share,top20Share=sum(o['seconds'] for o in hotops[:20])/op_total),ensure_ascii=False,indent=2)) + +if __name__=='__main__':main() diff --git a/tests/manual/analyze_context_shadow.py b/tests/manual/analyze_context_shadow.py new file mode 100644 index 0000000..b6a4585 --- /dev/null +++ b/tests/manual/analyze_context_shadow.py @@ -0,0 +1,109 @@ +"""Audit the two staged shadow runs and write the result document.""" +from collections import Counter +from pathlib import Path +import hashlib,json + +ROOT=Path(__file__).resolve().parents[2] +OUT=ROOT/'test/context-shadow-20260917' + + +def read(path):return json.loads(path.read_text(encoding='utf-8')) + + +def audit(label): + folder=OUT/label;s=read(folder/'shadow-summary.json') + rows=[json.loads(line) for line in (folder/'shadow-trials.jsonl').read_text(encoding='utf-8').splitlines()] + negative=[json.loads(line) for line in (folder/'shadow-negative.jsonl').read_text(encoding='utf-8').splitlines()] + assert len(rows)==s['total']==s['accepted']==896 + assert {r['jac'] for r in rows}==set(range(896)) + assert all(r['reason']=='accepted' and r['referenceCount']==r['candidateCount'] for r in rows) + assert all(r['candidateCount']==r['probeCount']+r['appends'] for r in rows) + assert sum(r['baselineCount']!=r['probeCount'] for r in rows)==s['countDifferent'] + assert sum(any(a!=b for a,b in r['mapping']) for r in rows)==s['slotRelocationTrials'] + assert sum(sum(a!=b for a,b in r['mapping']) for r in rows)==s['relocatedSlots'] + assert sum(r['appends'] for r in rows)==s['appends'] + assert [sum(r['path']==i for r in rows) for i in range(4)]==s['paths'] + assert s['mismatches']==s['liveContamination']==s['forbiddenNativeCalls']==s['rejected']==0 + for key in ['tailEqual','liveTailEqual','memoImmutable','untouchedProbeEqual','metadataImmutable']:assert s[key]==896 + assert len(negative)==s['negativePassed'] and all(n['reason']==n['expected'] and n['unchanged'] for n in negative) + assert not (folder/'first-mismatch.json').exists() + s['negativeReasonCounts']=dict(Counter(n['reason'] for n in negative)) + s['example200']=next(r for r in rows if r['jac']==200) + s['measurement']=read(folder/'measurement.json') + return s + + +def main(): + a=audit('r288');b=audit('position52') + worker_sha=hashlib.sha256((OUT/'worker/model.exe').read_bytes()).hexdigest() + assert a['validatedWorkerSha256']==b['validatedWorkerSha256']==worker_sha + assert a['measurement']['hashes']==b['measurement']['hashes'] + build=read(OUT/'worker/build.json');assert build['wholeContextGuardUnchanged'] + result=dict(r288=a,position52=b,workerSha256=worker_sha,productionPathChanged=False) + (OUT/'validation.json').write_text(json.dumps(result,ensure_ascii=False,indent=2)+'\n',encoding='utf-8') + lines=['# 最小 context shadow replay 验证结果', '', + '## 1. Candidate 能否逐位复现 Reference', '', + '**可以,在本轮两个 operation 的已覆盖路径和严格前置条件下,1,792 次 shadow replay 全部通过,出口不一致为 0。** R288 先完成全部 896 次;同一 worker 二进制通过该阶段后,才运行 position 52 的 896 次。', '', + '参考答案始终是**同一个当前 probe 入口的独立深拷贝,真实执行原 operation 后的出口**。没有把 baseline 出口当参考。baseline 只提供本 Jacobian 内的局部语义记录,不提供用于恢复的完整 context。', '', + '| 统计 | group 6 / R288 / position 16 | group 18 / R475 / position 52 |', '|---|---:|---:|'] + for title,key in [('shadow 总次数','total'),('可重放','accepted'),('自然轨迹 reject','rejected'),('Reference/Candidate 不一致','mismatches'),('入口 count 不同','countDifferent'),('发生 slot relocation 的 probe','slotRelocationTrials'),('重定位逻辑 slot 数量','relocatedSlots'),('追加条目总数','appends'),('PT hit','ptHit'),('PT miss','ptMiss'),('PH hit','phHit'),('PH miss','phMiss'),('独立后续 evaluator 返回值/dy/w/context 一致','tailEqual'),('后续 evaluator 与主仿真返回值/dy/w 一致','liveTailEqual'),('memo 条目只读验证','memoImmutable'),('未写入的 probe 数据保持不变','untouchedProbeEqual'),('局部 metadata 未被 Reference/Candidate 改写','metadataImmutable'),('Candidate 物理 native 调用','forbiddenNativeCalls'),('主 context/浮点环境污染','liveContamination')]: + lines.append(f'| {title} | {a[key]} | {b[key]} |') + lines += ['', '### 如何保证比较有意义', '', + '1. 在目标 probe 的 operation 前,深拷贝 property states、全部 pipe 槽、scalar memo 的全部 entries 和计数,分别绑定到 Reference/Candidate 私有存储。两路没有共享可写 context。', + '2. Reference 调用该位置的原始表达式。Candidate 只解释 baseline 捕获的 query/read/valid/allocate/write/OR/scalar-get 语义;物理 native 入口有运行时禁入检查。Reference 执行期间不能补写 Candidate 的记录。', + '3. Candidate 从自己的 probe 副本建立 overlay。重新扫描当前有序 entries,采用原 PT/PH 的 exact `==` 和 medium 比较语义找 first-match;用逻辑 ID 映射结果,追加使用当前 count。', + '4. 所有条件成功后进入无失败分支的 commit,仅按记录顺序更新实际写入字段、valid OR、count、指定 pipe 槽、memo 命中计数及显式输出;不复制 baseline context,也不覆盖 probe 未写字段。reject 前没有向 Candidate 或输出提交任何存储。', + '5. 逐位比较输出、所有 active property 字段、valid bits、全部 pipe 字段、memo entries/计数、warning observer、errno 和 x87/SSE 环境。私有指针按“绑定到各自当前 owner”检查;不要求两个独立 allocation 的地址相等。数值字段没有容差或近似比较。', + '6. 两个独立出口继续执行同一份原 evaluator 后续代码,实测返回状态、完整 dy/w、context 和 memo 计数一致;再与主仿真的真实 evaluator 返回值/dy/w 核对。主仿真仍然执行原 operation,没有采用 Candidate 输出,没有进入真实 skip 路径。', + '7. 两轮主仿真的状态、输出、事件及全部 896 个 132×132 Jacobian 仍与未插桩基线逐字节一致。whole-context guard 源码哈希保持一致。', '', + 'operation 本身返回一个 double,没有单独的 int 成功码;报告中的 evaluator 状态来自两条后续执行路径,不是用 `isfinite(output)` 代替。', '', + '### 两个具体入口例子(Jacobian 200)', '', '| 项目 | R288 | position 52 |', '|---|---|---|'] + for title,key in [('baseline count','baselineCount'),('当前 probe count','probeCount'),('Reference 出口 count','referenceCount'),('Candidate 出口 count','candidateCount'),('逻辑 slot → 当前 slot','mapping')]:lines.append(f'| {title} | `{a["example200"][key]}` | `{b["example200"][key]}` |') + lines += ['', 'R288 保留 probe 原 slot 12,在 13、14 追加;position 52 保留 probe slot 74 的压力/温度差异,查询重新扫描后仍 miss,再在 75、76 追加。', '', + '## 2. 哪些路径已经可以 replay', '', '| 查询路径 | R288 次数 | position 52 次数 |', '|---|---:|---:|'] + for i,name in enumerate(['近零流量,无物性查询','PT miss → PT miss','PH miss → PT miss → PT hit → PT miss','PH hit → PT hit → PT miss']):lines.append(f'| {name} | {a["paths"][i]} | {b["paths"][i]} |') + lines += ['', + '以上“可 replay”指本轮相应 operation 的完整 0–10 s 轨迹,且所有 runtime guard 同时成立。PH miss 的 h 登记与 valid OR、后续对刚追加逻辑条目的 PT hit 都保留。近零流量不追加物性条目,但仍执行 pipe 的有序写入,包括 valid=0 的同值写入。', '', + 'R288 的 638 次 count 差异中,67 次属于无查询/无追加路径,因此只有 571 次发生实际 slot relocation。position 52 同理,24 次 count 差异中有一次无追加。不能把 count 差异次数当作重定位次数。', '', + '## 3. 哪些情况仍必须 reject / fallback', '', + '自然轨迹中各 reject 分类均为 0。为防止“全成功但拒绝机制无效”,另在私有副本中执行下列负例,要求 reject 且整个 Candidate 与输出 sentinel 逐字节不变;这些不计入自然轨迹的 896 次。', '', + '| reject 分类 | R288 负例通过次数 | position 52 负例通过次数 |', '|---|---:|---:|'] + for reason in sorted(set(a['negativeReasonCounts'])|set(b['negativeReasonCounts'])):lines.append(f'| `{reason}` | {a["negativeReasonCounts"].get(reason,0)} | {b["negativeReasonCounts"].get(reason,0)} |') + lines += ['', + '严格拒绝边界:', '', + '- 记录不属于当前 Jacobian/operation、源代码不再匹配已验证 worker、记录溢出或未知语义事件。', + '- 输入改变、first-match 逻辑映射冲突、hit/miss 路径改变、已消费字段或 valid 掩码结果不一致。', + '- 容量不足或 scratch、非空 observer、未覆盖的已有 entry 原地更新、pipe 命中分支变化、非有限结果或未覆盖路径。', + '- memo owner/lifetime/recording 不符合只读约束、所需 scalar key 未命中或值不匹配。Candidate 不调用物理 fallback 来弥补 memo miss。', + '- 舍入模式、SSE 控制模式或所需异常状态不满足已验证条件;已记录的非零 errno 前置条件不成立。', '', + '负例包括第一次追加完成后第二次容量检查失败、末尾未知事件、末尾非有限输出,因此覆盖了 overlay 已发生大量修改后的回滚,不只是入口早退。position 52 的 PH-hit 样本还直接改变 probe 已有条目的 rho 和 MU 位,确认消费值/valid guard 生效。', '', + 'Jacobian memo 的 entries 始终只读,但 Reference 的 scalar get 会增加 reuses 计数。Candidate 在 overlay 中验证同一 bit-key 查找结果,并在 commit 中重放对应计数增量。没有将 baseline 的 recording/put 副作用照搬到 probe。', '', + '验证期间发现并修复了 Windows 诊断隔离问题:该工具链的 `fesetenv` 不完整恢复 SSE 控制寄存器。负例现在保存/恢复完整 x87/SSE 环境,并验证 SSE 单独改变时会拒绝。修复前失败证据保存在 `test/context-shadow-20260917/env-restore-investigation/`;本报告仅使用修复后的同一构建全量重跑结果。', '', + '## 4. Runtime metadata 需要多少', '', '| 当前实现 | R288 | position 52 |', '|---|---:|---:|', + f'| 单条语义事件 | {a["eventBytes"]} B | {b["eventBytes"]} B |', + f'| 最大事件数 | {a["eventMax"]} | {b["eventMax"]} |', + f'| 每条 operation 记录实际使用范围 | {a["metadataMin"]}–{a["metadataMax"]} B | {b["metadataMin"]}–{b["metadataMax"]} B |', + f'| 每阶段为 512 个事件预留 | {a["metadataReserved"]} B | {b["metadataReserved"]} B |', '', + '记录包括:Jacobian/operation 身份、四个显式输入、出口值、query 完整 key/逻辑命中关系、实际消费的字节值与 valid 掩码、创建顺序、字段更新/OR、scalar key/value、memo 绑定及环境前置条件。**不保存 baseline 完整 property/pipe context 用于 replay。** 只保留当前 Jacobian 的一条目标 operation 记录。', '', + f'事务 scratch 另需一个 {a["contextCopyBytes"]} B 的 overlay;本诊断为入口、Reference、Candidate、回滚检查及双路后续比较共保留 7 个 context 副本、2 个 {a["frameCopyBytes"]} B frame。它们是 shadow 验证工作内存,不能算作未来 skip 每条记录都必须长期保存的 metadata。slot 映射临时表为 256 个 int(当前 ABI 1,024 B)。', '', + '这是当前保守事件表示的实测大小,未做去重或压缩,也没有据此评价性能。', '', + '## 5. 是否具备最小真实 skip 实验的条件', '', + '**已具备针对这两个单独 operation、上述四种已验证路径的下一阶段实验条件。** 下一步必须继续保持严格 guard、事务 commit、reject 后原执行以及独立 Reference 抽查/全量对比;先对 R288 单点实验,再单独考虑 position 52。', '', + '本轮没有实现真实 skip,也没有移除或放宽 whole-context guard。结论不适用于整个 R475、全部 reuse interval、新模型、其他 native kernel、容量耗尽/observer 非空/已有 entry 原地更新等未验证分支;Linux 尚未运行本实验。双路执行和详细检查的时长不作性能证据。', '', + '## 复现与证据', '', + '依赖上一轮保留的 access worker 和未插桩 baseline 文件。准备阶段核对 access worker 源码哈希;第二阶段核对第一阶段通过的 worker 二进制 SHA-256。', '', + '```powershell', + r'.venv-win\Scripts\python.exe tests/manual/diagnose_context_shadow.py prepare', + r'.venv-win\Scripts\python.exe tests/manual/diagnose_context_shadow.py run --position 16', + r'.venv-win\Scripts\python.exe tests/manual/diagnose_context_shadow.py run --position 52', + r'.venv-win\Scripts\python.exe tests/manual/analyze_context_shadow.py', + '```', '', + '生成文件位于 `test/context-shadow-20260917/`:`worker/build.json`、各阶段 `shadow-trials.jsonl`、`shadow-summary.json`、`shadow-negative.jsonl`、`measurement.json`,以及汇总的 `validation.json`。若出现不一致,会输出首个不同字段和完整入口/两路 context、frame、metadata 到 `first-mismatch.json`。最终两阶段均未生成该文件。', '', + f'最终 worker SHA-256:`{worker_sha}`。', '', + '全量主轨迹哈希(两阶段相同):', ''] + for name,digest in a['measurement']['hashes'].items():lines.append(f'- {name}: `{digest}`') + target=ROOT/'tests/manual/context_shadow_report.md';target.write_text('\n'.join(lines)+'\n',encoding='utf-8') + print(json.dumps(dict(total=a['total']+b['total'],accepted=a['accepted']+b['accepted'],mismatches=0,negativePassed=a['negativePassed']+b['negativePassed'],report=str(target)),ensure_ascii=False,indent=2)) + + +if __name__=='__main__':main() diff --git a/tests/manual/analyze_fallback_profitability.py b/tests/manual/analyze_fallback_profitability.py new file mode 100644 index 0000000..6c71754 --- /dev/null +++ b/tests/manual/analyze_fallback_profitability.py @@ -0,0 +1,244 @@ +"""Read archived profiling only; write a reproducible fallback cost inventory. + +No worker build, simulation, native code edit, or extrapolated speedup claim. +Run from any directory with the repository's Python interpreter. +""" +from collections import defaultdict +import hashlib +import json +from pathlib import Path +import re +import statistics + +ROOT = Path(__file__).resolve().parents[2] +SOURCE = ROOT / "test/context-fallback-20260917" +OUT = ROOT / "test/fallback-profitability-20260917" +DOCS = ROOT / "tests/manual" +J = 896 +MANIFEST = {} + + +def read(path): + raw = path.read_bytes() + MANIFEST[path.relative_to(ROOT).as_posix()] = hashlib.sha256(raw).hexdigest() + return json.loads(raw) + + +def close(a, b): + assert abs(a - b) <= 1e-12 * max(1, abs(a), abs(b)), (a, b) + + +def table(headers, rows): + def cell(x): + return str(x).replace("|", "\\|").replace("\n", " ") + return "\n| " + " | ".join(headers) + " |\n| " + " | ".join(["---"] * len(headers)) + " |\n" + "\n".join("| " + " | ".join(map(cell, r)) + " |" for r in rows) + "\n" + + +def labels(xs, prefix=""): + return ",".join(prefix + str(x) for x in xs) + + +def span_stats(xs): + return dict(rounds=xs, mean=statistics.mean(xs), median=statistics.median(xs), min=min(xs), max=max(xs)) + + +def main(): + a = read(SOURCE / "analysis.json") + plan = read(SOURCE / "plan.json") + typed = read(ROOT / "test/r288-typed-replay-20260917/performance-summary.json") + layered = read(ROOT / "test/local-probe-profile-direct-20260917/comparison.json") + P = typed["metrics"]["C"]["stages"]["median"] + H = typed["paired"]["C"]["metadataIncrementUs"]["median"] + + def budgets(c, m): + # Strict inequality is required for a positive saving. Negative budgets + # are retained: they mean no nonnegative implementation cost can fit. + return dict(originalUs=c, structuralReusePerJacobian=m, + captureMaxAtZeroProbeUs=m*c, + captureMaxAtProbeUs={str(p): m*(c-p) for p in (0.1, 0.25, 0.5, 2, 5, 10, P)}, + probeMaxAtTypedCaptureUs=c-H/m, + typedZeroCapturePossible=c>P, + typedWithSharedCapturePossible=c>P+H/m, + typedScenarioNetMs=J*(m*(c-P)-H)/1000) + + native = {} + for pos, lines in enumerate(plan["code"]): + native[pos] = sorted(set(re.findall(r"\b(native_\w+)\s*\(", "\n".join(lines)))) + for op in a["operations"]: + pos = op["position"] + assert bool(native[pos]) == (plan["versions"][pos] != plan["versions"][pos+1]) + + pairs = {(x["group"], x["region"]): x for x in a["groupRegions"] if x["failures"]} + gos = {(x["group"], x["position"]): x for x in a["groupOperations"]} + assert len(pairs) == 156 and len(gos) == len(a["groupOperations"]) == 5157 + assert sum(x["failures"] for x in pairs.values()) == 139776 + assert sum(x["count"] for x in gos.values()) == 4620672 + assert all(x["failures"] == J for x in pairs.values()) + assert all(x["inputDiffOps"] == x["outputDiffOps"] == x["exitOutputDiff"] == 0 and x["exitContextDiff"] == J for x in pairs.values()) + region_runs, op_runs = [], [] + for i in range(3): + r = read(SOURCE / f"regions-{i}/context.json") + o = read(SOURCE / f"ops-{i}/context.json") + assert r["sampled"] == o["sampled"] == 112 + region_runs.append({(v["group"], v["region"]): v["regionTicks"] * J/r["sampled"]/r["frequency"] for v in r["regions"]}) + op_runs.append({(g, pos): ticks * J/o["sampled"]/o["frequency"] for g, pos, n, ticks in o["operations"]}) + for key, x in pairs.items(): + close(x["seconds"], statistics.mean(r[key] for r in region_runs)) + for key, x in gos.items(): + close(x["seconds"], statistics.mean(o[key] for o in op_runs)) + + def composition(ops): + ns = sum(x["seconds"] for x in ops if native[x["position"]]) + alg = sum(x["seconds"] for x in ops if not native[x["position"]]) + return dict(nativeContainingOperationSeconds=ns, pureAlgebraAliasOperationSeconds=alg) + + operations = [] + for x in sorted(a["operations"], key=lambda x: -x["seconds"]): + pos = x["position"] + members = [o for (g, p), o in gos.items() if p == pos] + close(x["seconds"], sum(o["seconds"] for o in members)) + assert x["count"] == len(x["groups"])*J + calls = native[pos] + kernel_family = any(c in calls for c in ("native_pipe_flow_cached_context", "native_medium_orifice_context")) + operation = dict(**x, nativeCalls=calls, code=plan["code"][pos], + nativeOperationCount=int(bool(calls)), operationCount=1, + contextFreeAlgebraAlias=not calls, + decision="A;停止R288后续优化" if pos == 16 else "A;E只测纯数值尾部" if kernel_family else "A" if calls else "A;C合并代数段", + baselineSharing="显式输入/输出同值;跨组记录共享仅为结构上限,消费字段/分支未普遍验证", + budget=budgets(x["seconds"]*1e6/x["count"], len(x["groups"])), + roundSeconds=span_stats([sum(run[g, pos] for g in x["groups"]) for run in op_runs]), + groupMeanUsRange=[min(o["seconds"]*1e6/o["count"] for o in members), max(o["seconds"]*1e6/o["count"] for o in members)], + functionTimingScope="仅有 regionKernels 的区间函数归因,无本 position 专属函数计时", + **composition(members)) + operations.append(operation) + regions, group_regions = [], [] + for r in sorted((r for r in a["regions"] if r["failures"]), key=lambda r: -r["seconds"]): + rid = r["id"] + members = [x for x in gos.values() if x["region"] == rid] + comp = composition(members) + close(r["operationSeconds"], sum(comp.values())) + native_positions = [p for p in range(r["start"], r["end"]) if native[p]] + assert native_positions == r["nativeOperationPositions"] + count = r["end"]-r["start"] + kernels = sorted((dict(name=plan["kernels"][k["kernel"]][1], **k) for k in a["regionKernels"] if k["region"] == rid), key=lambda k: -k["exclusiveSeconds"]) + m = len(r["failedGroups"]) + op_us = r["operationSeconds"]*1e6/r["failures"] + decision = "A" if count == 1 else "C/E预算筛选;native默认A" + if rid in (3, 480, 485, 490): + decision += ";D仅融合区间的待证假设" + region = dict(**r, operationCount=count, nativeOperationCount=len(native_positions), + nativeCalls=sorted(set(c for p in native_positions for c in native[p])), + kernels=kernels, decision=decision, **comp, + baselineSharing="同一 baseline 区间结构可供多个组引用;语义兼容未证" if m>1 else "仅一个失败组,无跨失败组摊销", + budget=budgets(r["meanUs"], m), operationModeBudget=budgets(op_us, m), + bothModesTypedWithSharedCapturePossible=min(r["meanUs"], op_us)>P+H/m, + roundMeanUs=span_stats([sum(run[g, rid] for g in r["failedGroups"])*1e6/r["failures"] for run in region_runs])) + regions.append(region) + for g in r["failedGroups"]: + x = pairs[g, rid] + group_regions.append(dict(**x, operationCount=count, nativeOperationCount=len(native_positions), + nativeCalls=region["nativeCalls"], + functionTimingScope=f"R{rid}汇总,无group级函数拆分", decision=decision, + **composition([v for v in members if v["group"] == g]), + budget=budgets(x["seconds"]*1e6/J, m), + # The net over J*m applies to a homogeneous-cost + # scenario, not the measured total for this group. + sharedBudgetNote="m为整个interval共享上限;本group成本的预算是假设同成本的情景,不是全interval实测净收益", + roundMeanUs=span_stats([run[g, rid]*1e6/J for run in region_runs]))) + assert len(regions) == 115 and len(operations) == 343 + close(sum(r["seconds"] for r in regions), a["totals"]["regionSeconds"]) + close(sum(o["seconds"] for o in operations), a["totals"]["operationSeconds"]) + close(sum(r["pureAlgebraAliasOperationSeconds"] for r in regions), sum(o["pureAlgebraAliasOperationSeconds"] for o in operations)) + + cohorts = [] + for p in (2, 5, P, 10): + for capture in (0, H): + eligible = [r for r in regions if r["meanUs"] > p+capture/r["budget"]["structuralReusePerJacobian"]] + cohorts.append(dict(probeCostUs=p, captureUs=capture, regionIds=[r["id"] for r in eligible], + count=len(eligible), originalSeconds=sum(r["seconds"] for r in eligible), + originalShare=sum(r["seconds"] for r in eligible)/a["totals"]["regionSeconds"], + hypotheticalNetSeconds=sum(r["seconds"]-r["failures"]*p/1e6-J*capture/1e6 for r in eligible), + assumption="每个完整interval只付一次固定P,baseline捕获H跨所有失败组共享,100%成功;不等于逐operation replay,也不是预测")) + total_comp = composition(list(gos.values())) + pure_spans = [] + for r in regions: + pos = r["start"] + while pos < r["end"]: + if native[pos]: + pos += 1 + continue + start = pos + while pos < r["end"] and not native[pos]: + pos += 1 + members = [gos[g, p] for g in r["failedGroups"] for p in range(start, pos)] + seconds = sum(x["seconds"] for x in members) + pure_spans.append(dict(region=r["id"], groups=r["failedGroups"], start=start, end=pos, + operationCount=pos-start, count=r["failures"], seconds=seconds, + meanUs=seconds*1e6/r["failures"])) + close(sum(s["seconds"] for s in pure_spans), total_comp["pureAlgebraAliasOperationSeconds"]) + pure_spans.sort(key=lambda s: -s["seconds"]) + kernel_cohort = [o for o in operations if any(c in o["nativeCalls"] for c in ("native_pipe_flow_cached_context", "native_medium_orifice_context"))] + summary = dict(**a["totals"], **total_comp, intervals=len(regions), groupIntervals=len(pairs), + operations=len(operations), groupOperations=len(gos), + maxOperationMeanUs=max(o["budget"]["originalUs"] for o in operations), + maxGroupOperationMeanUs=max(o["seconds"]*1e6/o["count"] for o in gos.values()), + pureSpanCount=len(pure_spans), + pureSpansAbove2UsSeconds=sum(s["seconds"] for s in pure_spans if s["meanUs"]>2), + kernelContainingPositions=[o["position"] for o in kernel_cohort], + kernelContainingOriginalSeconds=sum(o["seconds"] for o in kernel_cohort), + layeredFallbackSeconds=layered["main"]["totals"]["correctedSeconds"]["schedule_context_fallback"]*J/layered["main"]["samples"], + wholeContextSuccesses=a["totals"]["compares"]-a["totals"]["failures"]) + group_ops = [] + for x in a["groupOperations"]: + op = next(o for o in operations if o["position"] == x["position"]) + group_ops.append(dict(**x, nativeCalls=op["nativeCalls"], decision=op["decision"], + meanUs=x["seconds"]*1e6/x["count"], + structuralReusePerJacobian=op["budget"]["structuralReusePerJacobian"])) + out = dict(scope="archived 0–10 s; diagnostics only", summary=summary, + referenceCosts=dict(typedProbeUs=P, typedCaptureIncrementUs=H, metrics=typed["metrics"], paired=typed["paired"]), + assumptions=dict(sharing="structural upper bound, not established semantic reuse", successRate=1, + originalCost="sampled instrumented mean; not an uninstrumented lower bound", + budgets="strictly less for profit; no incremental failed-guard/fallback-dispatch cost assumed"), + regions=regions, groupRegions=group_regions, operations=operations, groupOperations=group_ops, + pureSpans=pure_spans, groups=a["groups"], kernels=a["kernels"], scenarios=cohorts, + successfulContextRegions=[r for r in a["regions"] if r["contextual"] and not r["failures"]], + inputSha256=MANIFEST, + checks=["139776 interval failures", "4620672 fallback operation executions", "no duplicate (group,position)", + "156 failing group/interval pairs; 115 intervals; 343 positions; 5157 group/position pairs", + "raw three-run timing reconstruction matches archived analysis", "native-call and context-version classification agree", + "region, group and operation partitions conserve totals", "all pure spans conserve algebra time"]) + OUT.mkdir(parents=True, exist_ok=True) + (OUT / "analysis.json").write_text(json.dumps(out, ensure_ascii=False, indent=2)+"\n", encoding="utf-8") + + preamble = "# 全部 fallback 区间及预算\n\n由 `analyze_fallback_profitability.py` 从历史数据生成。ID、group、position 为0基,范围为[start,end)。ms为完整896个Jacobian折算累计;µs为每次fallback。按累计原计算时间排序。\n\nP=probe总开销,H=每个Jacobian新增baseline捕获,m=结构上最多共享的失败组数。盈利要求 P+H/m=0,(label,r['group'],'calibration below resolution',corrected) + rows.append({**r,'seconds':raw,'correctedSeconds':corrected}) + boundary=p['sampledCallbackQpcTicks']/p['qpcFrequency']-p['sampledCallbackTicks']/f + # The two pairs of enclosing clocks delimit a real wrapper interval. Keep + # it visible, rather than rescale the TSC buckets to match QPC. + assert abs(boundary)<.01,(label,'clock cross-check',boundary) + rows[0]['seconds']['other']+=boundary + rows[0]['correctedSeconds']['other']+=boundary + return {**p,'rows':rows,'calibrationNs':cal/f*1e9,'timerSeconds':cal*p['markerCount']/f} + +def aggregate(labels): + ps=[profile(n) for n in labels];categories=ps[0]['categories'];samples=sum(p['sampledCallbacks'] for p in ps) + rows=[] + for i in range(len(ps[0]['rows'])): + totals={key:{c:sum(p['rows'][i][key][c] for p in ps) for c in categories} for key in ('seconds','correctedSeconds')} + rows.append(dict(group=i-2,evaluations=sum(p['rows'][i]['evaluations'] for p in ps),**totals, + operations=[sum(p['rows'][i].get('operations',[0,0])[k] for p in ps) for k in range(2)])) + totals={key:{c:sum(r[key][c] for r in rows) for c in categories} for key in ('seconds','correctedSeconds')} + return dict(labels=labels,samples=samples,rows=rows,totals=totals, + callbackSeconds=sum(p['sampledCallbackQpcTicks']/p['qpcFrequency'] for p in ps), + timerSeconds=sum(p['timerSeconds'] for p in ps), + calibrationNs=[p['calibrationNs'] for p in ps]) + +def main(): + # Compare every saved run, not only the runs chosen for timing summaries. + reference=json.loads((SOURCE.parent/'all-run-0/measurement.json').read_text(encoding='utf-8')) + keys=['statesSha256','outputsSha256','eventsSha256','finalState','final','propertyWarnings','acceptedSteps','rejectedSteps','stateTransitions','solverStarts','nfev','njev','nlu'] + counter_keys=['newtonIterations','newtonConvergenceFailures','contextComparedBytes','contextCopiedBytes','modelCalls','groups'] + checks=[] + for path in sorted(OUT.glob('*/measurement.json')): + r=json.loads(path.read_text(encoding='utf-8')) + diff=[k for k in keys if r[k]!=reference[k]]+[k for k in counter_keys if r['diagnostic'][k]!=reference['diagnostic'][k]] + assert not diff,(path,diff);checks.append(dict(run=path.parent.name,differences=diff)) + full=profile('validate-all');plan=json.loads((SOURCE.parent/'plan.json').read_text(encoding='utf-8')) + audit=json.loads((SOURCE.parent/'all-audit/probe.json').read_text(encoding='utf-8')) + for g,row in enumerate(full['rows'][2:]): + retained=len(plan['groups'][g]['affectedOperations'])*896 + total=sum(audit['groups'][g]['executed']) + assert row['operations']==[retained,total-retained] + assert full['rows'][28]['operations']==[0,0] + for name in ('jacobians','states','outputs','events'): + def digest(p): + with p.open('rb') as f:return hashlib.file_digest(f,'sha256').hexdigest() + assert digest(OUT/f'validate-all/{name}.bin')==digest(SOURCE.parent/f'all-audit/{name}.bin') + # Check the original sources, and the original numerical evaluator in the + # copied translation unit, have not been rewritten by this measurement. + build=json.loads((OUT/'build.json').read_text(encoding='utf-8')) + for name,expected in build['sourceHashes'].items(): + assert hashlib.sha256((SOURCE/name).read_text(encoding='utf-8').encode()).hexdigest()==expected + old_model=(SOURCE/'model.c').read_text(encoding='utf-8') + assert old_model in (OUT/'worker/model.c').read_text(encoding='utf-8') + native=json.loads((SOURCE/'build-metadata.json').read_text(encoding='utf-8'))['sourceHashes'] + for name,expected in native.items():assert hashlib.sha256((ROOT/'native'/name).read_text(encoding='utf-8').encode()).hexdigest()==expected + + main_labels=[f'profile-{i}' for i in range(5)];data=aggregate(main_labels);scale=896/data['samples'] + metrics=('jacobianSeconds','solveCpuSeconds','solveSeconds','processSeconds') + def metric(r,k):return r['diagnostic'][k] if k=='jacobianSeconds' else r[k] + controls=[read(f'control-{i}') for i in range(5)];runs=[read(n) for n in main_labels] + medians={mode:{k:statistics.median(metric(r,k) for r in rs) for k in metrics} for mode,rs in [('control',controls),('profile',runs)]} + deltas={k:medians['profile'][k]/medians['control'][k]-1 for k in metrics} + paired={k:[metric(b,k)/metric(a,k)-1 for a,b in zip(controls,runs)] for k in metrics} + densities={str(s):aggregate([f'density{s}-{i}' for i in range(2)]) for s in (8,32)} + coarse=aggregate([f'coarse-{i}' for i in range(3)]) + result=dict(numericalChecks=checks,allMatrices=dict(count=896,entries=896*132*132,differentEntries=0), + originalSourcesUnchanged=True,main=data,medians=medians,deltas=deltas,pairedDeltas=paired,densityChecks=densities, + fullCoverageOperationCounts=[r['operations'] for r in full['rows'][2:]],allInstrumented=full,coarse=coarse) + write(OUT/'comparison.json',result) + + report=['**局部 probe performance worker:互斥耗时分解(2026-09-17)**\n'] + report.append('仅增加独立诊断构建/分析工具及计时副本;原局部优化脚本、生产实现、普通 residual、物性算法、accepted-step check、线性求解器均未修改。模型为 `tests/data/test-mql-8-corrected.json`,0–10 s、BDF、rtol=1e-8,原 atol/步长设置不变。\n') + estimate=sum(data['totals']['correctedSeconds'].values())*scale + cg=coarse['rows'][28];cg_us=sum(cg['seconds'].values())/cg['evaluations']*1e6 + report.append(f'**先说明精度边界:** 低密度插桩相对未插桩的callback中位数变化{deltas["jacobianSeconds"]:+.2%}、积分CPU变化{deltas["solveCpuSeconds"]:+.2%};但细分数据扣空标记后折算{estimate:.6f}s,仍比未插桩{medians["control"]["jacobianSeconds"]:.6f}s高{estimate/medians["control"]["jacobianSeconds"]-1:.2%}。所以本轮完成了互斥分类、账本闭合和数值核验,但细分值尚未达到可直接当作未插桩精确耗时的精度。下面使用它判断热点量级与排序,不以这些百分比承诺优化收益。粗粒度独立对照中group26约{cg_us:.3f}µs,是固定底座总量的更可靠参考。\n') + report.append('**测量方法与互斥口径**\n') + report.append('主测量为5对交替串行运行,预热不计入。每连续16次 callback 分层随机抽1次,种子固定可复现;每轮56次,5轮共280次,baseline和每组probe各280次。未抽中callback走保留的原数值函数;抽中才进入计时副本,不开启shadow求值、物性内核入口计数或矩阵落盘。全量插桩+矩阵落盘仅用于数值核验。\n') + report.append('外层总时间仍用QPC;内部用带lfence的TSC读取及约10 KB互斥计数桶,避免每次记录事件数组。启动时要求CPU支持invariant TSC;用覆盖整个积分的QPC/TSC成对读数校准TSC频率。每个相邻区间只归属一个分类、一个row(外层/baseline/group),桶的TSC tick总和必须精确等于抽中callback内部总tick。外层QPC包络与内部TSC包络的差单列计入outer other,是时钟边界间的包装开销;不缩放内部分类。归并后的原始分类总和与抽中callback的QPC时间在浮点精度内相等。这只证明互斥账本闭合,不证明没有插桩扰动。\n') + report.append('在积分完成后,以相同的带屏障读时钟和计数桶更新测9批空标记,取每标记平均成本的中位数。保留原始时间,并另外给出“原始时间−标记数×空标记成本”的估计;未以未插桩总时间强制归一化,也不把任何负数截成0(出现负分类即报告失败)。校正不能消除屏障引起的执行串行化、编译布局、缓存、额外分支和操作分类的间接扰动;极短分类只作数量级参考。原QPC事件记录版保存在test/local-probe-profile-20260917,其校正后总量高估约10.8%,因此本报告继续展示残余偏差,而不宣称空标记校准能消除它。\n') + report.append(f'主测量标记成本:{min(data["calibrationNs"]):.2f}–{max(data["calibrationNs"]):.2f} ns/次。5轮被抽中callback合计 **{data["callbackSeconds"]:.9f} s**,原始分类加和完全相等;估计计时标记成本 **{data["timerSeconds"]:.9f} s**。\n') + report.append('baseline原求值列排除snapshot捕获/保存,baseline完整求值小计包含两者,只作小计不重复相加。snapshot包含入口t/y保存、选定property/pipe检查点复制、schedule结果数组保存。context比较包含元数据和memcmp;restore包含出口context/cache和区间结果恢复。schedule原计算在case原语句两侧划界,switch/循环/区间分派及测量分类开销另计schedule dispatch。\n') + report.append('initialization包括生成模型开头的数组/cache初始化及第一处gas求值前的准备赋值;gas state preparation从第一处gas求值到schedule入口,包括所有gas memo查找、必要物性计算、gas输出赋值及property context seed。schedule外的端口列仅为端口输出赋值。pipe diagnostics包括调用参数中的PH反算、诊断内核和acc累加;将acc写入w及ff的fmin限幅算remaining outputs。后者不是单纯memcpy,不能据名称假定廉价。\n') + report.append('**未插桩/插桩总耗时对照**\n') + labels={'jacobianSeconds':'Jacobian callback累计墙钟','solveCpuSeconds':'积分CPU','solveSeconds':'积分墙钟','processSeconds':'完整native进程墙钟'} + report.append(table(['5轮中位数','未插桩 / s','1/16插桩 / s','变化'],[(labels[k],f'{medians["control"][k]:.6f}',f'{medians["profile"][k]:.6f}',f'{deltas[k]:+.2%}') for k in metrics])) + report.append(table(['配对','control Jacobian','profile Jacobian','control积分','profile积分','control进程','profile进程'],[(i,*[f'{metric(r,k):.6f}' for k in ('jacobianSeconds','solveSeconds','processSeconds') for r in (controls[i],runs[i])]) for i in range(5)])) + report.append('所有样本保留,没有因较慢而删除。整进程包含启动、输出重放/落盘、诊断文件写入、标记校准和退出,不含预先完成的编译和浏览器/API流程。机器频率与调度噪声仍存在,不能把很小的负变化当作计时插桩带来的加速。\n') + report.append('**互斥分类:按抽样折算到一轮896次Jacobian**\n') + report.append('下面为5轮采样累计分类时间×896/280,未使用目标总时间做比例缩放。原始列含时间标记成本;估计列仅扣除空标记成本。\n') + raw=data['totals']['seconds'];adj=data['totals']['correctedSeconds'];total=sum(adj.values()) + report.append(table(['分类','原始折算 / s','扣标记估计 / s','估计占比'],[(LABELS[c],f'{raw[c]*scale:.6f}',f'{adj[c]*scale:.6f}',f'{adj[c]/total:.2%}') for c in raw])) + report.append(f'原始折算合计 **{sum(raw.values())*scale:.6f} s**;扣标记估计合计 **{total*scale:.6f} s**;未插桩callback中位数 **{medians["control"]["jacobianSeconds"]:.6f} s**。估计合计与未插桩相差 **{(total*scale/medians["control"]["jacobianSeconds"]-1):+.2%}**,这部分不强行塞进其他分类。\n') + report.append(table(['范围(小计,不再相加)','原始每次 / µs','扣标记每次 / µs','一轮扣标记 / s'],[(name,f'{sum(data["rows"][i]["seconds"].values())/data["rows"][i]["evaluations"]*1e6:.3f}',f'{sum(data["rows"][i]["correctedSeconds"].values())/data["rows"][i]["evaluations"]*1e6:.3f}',f'{sum(data["rows"][i]["correctedSeconds"].values())*scale:.6f}') for name,i in [('baseline完整求值',1)]])) + report.append('**probe总体及逐group平均**\n') + probe_raw=sum(sum(r['seconds'].values()) for r in data['rows'][2:]);probe_adj=sum(sum(r['correctedSeconds'].values()) for r in data['rows'][2:]) + report.append(f'共测量{data["samples"]*27:,}次probe,平均原始 **{probe_raw/(data["samples"]*27)*1e6:.3f} µs/probe**,扣标记估计 **{probe_adj/(data["samples"]*27)*1e6:.3f} µs/probe**。组平均范围为lp_eval入口至jac_rhs_reuse返回前,包含数值入口管理及后置统计;外层扰动/矩阵写回不归入probe。\n') + report.append(table(['group','次数','原始总均值 µs','扣标记总均值 µs','保留原计算 µs','fallback原计算 µs','全覆盖实际保留/回退操作'],[(r['group'],r['evaluations'],f'{sum(r["seconds"].values())/r["evaluations"]*1e6:.3f}',f'{sum(r["correctedSeconds"].values())/r["evaluations"]*1e6:.3f}',f'{r["correctedSeconds"]["schedule_retained"]/r["evaluations"]*1e6:.3f}',f'{r["correctedSeconds"]["schedule_context_fallback"]/r["evaluations"]*1e6:.3f}',str(full['rows'][r['group']+2]['operations'])) for r in data['rows'][2:]])) + report.append('各group完整分类数据(不只保留/回退)见comparison.json中main.rows[group+2],seconds与correctedSeconds除以evaluations即为单次均值。\n') + report.append('**低扰动粗粒度交叉核验**\n') + report.append('另构建coarse-worker:只有callback外层以及baseline/完整probe入口和出口的标记,数值调用直接进入原model_eval_local_internal,内部没有细分标记。3轮覆盖全部896次callback(非抽样)。以下原始均值不扣空标记,额外的每probe边界成本很小;它用于检查细分计时对每组总量的偏差,不用于强制缩放细分分类。\n') + report.append(table(['group','粗粒度完整probe µs','细分扣标记 µs','细分相对偏差'],[(g,f'{sum(coarse["rows"][g+2]["seconds"].values())/coarse["rows"][g+2]["evaluations"]*1e6:.3f}',f'{sum(data["rows"][g+2]["correctedSeconds"].values())/data["rows"][g+2]["evaluations"]*1e6:.3f}',f'{(sum(data["rows"][g+2]["correctedSeconds"].values())/data["rows"][g+2]["evaluations"])/(sum(coarse["rows"][g+2]["seconds"].values())/coarse["rows"][g+2]["evaluations"])-1:+.2%}') for g in range(27)])) + report.append(table(['粗粒度轮次','callback s','积分 s','group26 µs'],[(f'coarse-{i}',f'{read(f"coarse-{i}")["diagnostic"]["jacobianSeconds"]:.6f}',f'{read(f"coarse-{i}")["solveSeconds"]:.6f}',f'{sum(profile(f"coarse-{i}")["rows"][28]["seconds"].values())/896*1e6:.3f}') for i in range(3)])) + report.append('**group 26:schedule全跳过后的固定成本**\n') + g=data['rows'][28];n=g['evaluations'];ga=sum(g['correctedSeconds'].values()) + cg=coarse['rows'][28];coarse_g26=sum(cg['seconds'].values())/cg['evaluations']*1e6 + report.append(f'全量896次核验中,保留原操作=0、fallback原操作=0,与原audit逐操作计数相符。主测量均值:原始 **{sum(g["seconds"].values())/n*1e6:.3f} µs**,扣标记估计 **{ga/n*1e6:.3f} µs**;更低扰动的粗粒度完整probe均值 **{coarse_g26:.3f} µs**。这是本模型、本轨迹、group 26下的固定底座,不是所有group通用的固定常数,也不包含外层差分写回。\n') + report.append(table(['分类','原始均值 / µs','扣标记均值 / µs','估计占比'],[(LABELS[c],f'{g["seconds"][c]/n*1e6:.3f}',f'{v/n*1e6:.3f}',f'{v/ga:.2%}') for c,v in g['correctedSeconds'].items() if g['seconds'][c]])) + report.append('**采样密度与数值校验**\n') + density_rows=[] + for name,d in [('1/8',densities['8']),('1/16',data),('1/32',densities['32'])]: + factor=896/d['samples'];g=d['rows'][28] + density_rows.append((name,d['samples'],f'{sum(d["totals"]["correctedSeconds"].values())*factor:.6f}',f'{d["totals"]["correctedSeconds"]["schedule_retained"]*factor:.6f}',f'{d["totals"]["correctedSeconds"]["schedule_context_fallback"]*factor:.6f}',f'{sum(g["correctedSeconds"].values())/g["evaluations"]*1e6:.3f}')) + report.append(table(['密度','抽中callback','一轮扣标记估计 s','保留计算 s','fallback计算 s','group26 µs'],density_rows)) + report.append(table(['只编入插桩、采样关闭','Jacobian s','积分 s','完整进程 s'],[(f'disabled-{i}',*[f'{metric(read(f"disabled-{i}"),k):.6f}' for k in ('jacobianSeconds','solveSeconds','processSeconds')]) for i in range(2)])) + report.append(f'共{len(checks)}次完整运行的states/outputs/event二进制、全部warning字段、最终状态、accepted/rejected steps、Newton iterations/failures、nfev/njev/nlu、context复制/比较字节数、各group保护命中/回退计数均与既有performance worker一致。全量计时副本另核对896个132×132矩阵,共15,611,904元素及各矩阵输入t/y,逐字节一致。内部互斥计时桶的闭合误差为0 TSC tick;加上外层边界差后,分类与QPC总量在浮点精度内闭合。\n') + report.append('全量插桩的每组“保守保留”和“context回退”操作数分别等于依赖计划应执行数、原audit实际执行数减去应执行数;group26两者均为0。整probe fallback未发生。生产native源文件hash、旧performance worker所有源文件hash以及原生成模型函数完整文本均验证未变。\n') + report.append(table(['求解器项目','所有本轮运行保持一致'],[(k,reference[k]) for k in ['acceptedSteps','rejectedSteps','stateTransitions','solverStarts','nfev','njev','nlu']]+[(k,reference['diagnostic'][k]) for k in ['newtonIterations','newtonConvergenceFailures']])) + report.append('**可以支持的诊断结论**\n') + report.append('1. schedule实际保留计算1,390,592次,context保护失败后的回退计算4,620,672次;139,776次区间context校验失败。细分估计回退原计算约0.61s,保守保留原计算约0.42s,1/8和1/32密度下排序相同。保护的主要时间影响体现在失败后的原计算,并非memcmp本身;不能据此推断去掉保护是安全的。\n') + report.append('2. 比较约0.032s,baseline快照/结果保存约0.053s,恢复约0.030s。三者不是零成本,但合计量级显著小于回退原计算。所有这些时间均与计算阶段互斥,baseline小计没有再次加入总和。\n') + report.append('3. 未裁剪的初始化、gas准备和schedule外方程/输出/诊断合计估计约0.58s;其中remaining outputs约0.24s、pipe diagnostics约0.19s,而gas准备约0.056s。remaining outputs包含各管道ff的fmin限幅,pipe diagnostics包含参数PH反算,不能把前者等同简单写内存、后者等同单一内核调用。此处未对某个具体函数做内部剖析,不宣称fmin就是已证实的单函数热点。\n') + report.append('4. group26的schedule保留/回退均为0,依然要执行未裁剪阶段和恢复操作。粗粒度三轮均值21.109–21.263µs;细分扣标记得到24.749µs,高约16.8%,提示小probe的细分扰动占比更高。其细分热点主要是remaining outputs、pipe diagnostics与finite check,但精确占比应保留上述测量误差。\n') + report.append('5. 随机分层采样减少了整轮测量干扰,不能消除被抽中callback自身的串行化/缓存/编译布局影响。细分校正总量仍有6.98%残差;本报告没有把残差摊进other或按比例缩放各分类来制造闭合。跨轮机器负载差异也会影响粗/细计时对比。本轮没有据此修改任何优化实现。\n') + report.append('**证据与复现**\n') + report.append('脚本:tests/manual/profile_local_probe.py、local_probe_profile.h/.c、analyze_local_probe_profile.py。prepare只在本目录生成计时worker;run --stride 1 --matrices做完整数值核验;batch做预热、5对主测量和密度对照;分析脚本生成本报告。各运行目录保留profile.json、measurement.json、probe.json、result.json和二进制结果;build.json保存原文件hash和阶段边界。\n') + (OUT/'report.md').write_text('\n'.join(report),encoding='utf-8') + print(json.dumps(dict(medians=medians,deltas=deltas,rawEstimated=sum(raw.values())*scale,correctedEstimated=total*scale,group26Us=ga/n*1e6,densityRows=density_rows,numericalRuns=len(checks)),ensure_ascii=False,indent=2)) + +if __name__=='__main__':main() diff --git a/tests/manual/analyze_mql8_curve_coverage.py b/tests/manual/analyze_mql8_curve_coverage.py new file mode 100644 index 0000000..6777474 --- /dev/null +++ b/tests/manual/analyze_mql8_curve_coverage.py @@ -0,0 +1,236 @@ +"""Read-only coverage and amplitude analysis of saved, phase-paired MQL8 curves. + +No simulation, re-pairing, resampling, filtering, or production changes. Integral +metrics are trapezoidal estimates on the saved common grid, not bounds on any +unsampled transient. Occupancy durations use sample-cell weights and are not +located threshold-crossing times. Run with --plots in a matplotlib environment. +""" +from __future__ import annotations + +import argparse +import hashlib +import json +from pathlib import Path + +import numpy as np + +ROOT = Path(__file__).resolve().parents[2] +BASE = ROOT / 'test/lstp-mainline-20260917' + + +def digest(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def ratio(numerator, denominator): + return float(100 * numerator / denominator) if denominator else None + + +def intervals(mask, time, weights): + indices = np.flatnonzero(mask) + blocks = np.split(indices, np.flatnonzero(np.diff(indices) > 1) + 1) + return [dict(firstSample=float(time[b[0]]), lastSample=float(time[b[-1]]), + sampleCount=len(b), cellDurationEstimate=float(weights[b].sum())) + for b in blocks if len(b)] + + +def mask_summary(mask, time, weights): + return dict(sampleCount=int(mask.sum()), samplePercent=ratio(mask.sum(), len(mask)), + cellDurationEstimate=float(weights[mask].sum()), + timePercentEstimate=ratio(weights[mask].sum(), weights.sum()), + intervals=intervals(mask, time, weights)) + + +def curve_stats(row, actual, reference, time, weights): + error = actual - reference + absolute = np.abs(error) + magnitude = np.abs(reference) + epsilon = row['epsilon'] + active = magnitude > epsilon + near = ~active + relative = np.zeros(len(time)) + relative[active] = absolute[active] / magnitude[active] * 100 + peak = float(magnitude.max()) + significant = magnitude > max(epsilon, .01 * peak) + near_bad = near & (absolute > epsilon) + signed_integral = np.concatenate(([0.], np.cumsum( + .5 * (error[1:] + error[:-1]) * np.diff(time)))) + result = dict(key=row['key'], quantity=row['quantity'], unit=row['unit'], + epsilon=epsilon, sampleCount=len(time), referencePeak=peak, + activeCount=int(active.sum()), significantCount=int(significant.sum()), + maximumAbsolute=float(absolute.max()), + worstAbsoluteTime=float(time[np.argmax(absolute)]), + maximumAbsolutePercentOfPeak=ratio(absolute.max(), peak), + rmse=float(np.sqrt(np.dot(weights, error**2) / weights.sum())), + relativeL2Percent=ratio(np.sqrt(np.dot(weights, error**2)), + np.sqrt(np.dot(weights, reference**2))), + integratedAbsoluteError=float(np.dot(weights, absolute)), + integratedReferenceMagnitude=float(np.dot(weights, magnitude)), + relativeL1Percent=ratio(np.dot(weights, absolute), np.dot(weights, magnitude)), + signedIntegralError=float(signed_integral[-1]), + maxCumulativeSignedError=float(np.abs(signed_integral).max()), + activeRelativePercentiles={str(p): float(np.percentile(relative[active], p)) + if active.any() else None for p in (50, 95, 99, 100)}, + significantMaxRelativePercent=float(relative[significant].max()) + if significant.any() else None, + nearZeroCount=int(near.sum()), + nearZeroMaxAbsolute=float(absolute[near].max()) if near.any() else None, + nearZeroAboveEpsilon=mask_summary(near_bad, time, weights), + sensitivity={str(factor): int(((magnitude > epsilon * factor) + & (absolute > .05 * magnitude)).sum()) for factor in (.1, 1., 10.)}) + masks = {} + for threshold in (1, 5): + mask = active & (relative > threshold) + masks[str(threshold)] = mask + result['above' + str(threshold)] = mask_summary(mask, time, weights) | dict( + activePercent=ratio(mask.sum(), active.sum())) + result['relative5OrNearZeroAbsolute'] = mask_summary(masks['5'] | near_bad, time, weights) + result['examplesAbove5'] = [dict(time=float(time[i]), platform=float(actual[i]), + amesim=float(reference[i]), absoluteError=float(absolute[i]), + relativePercent=float(relative[i])) for i in np.flatnonzero(masks['5'])] + result['nearZeroExamples'] = [dict(time=float(time[i]), platform=float(actual[i]), + amesim=float(reference[i]), absoluteError=float(absolute[i])) + for i in np.flatnonzero(near_bad)] + # Cross-check the earlier diagnostic without changing its epsilon or pairing. + assert result['above5']['sampleCount'] == row['above5PercentCount'] + assert int(near_bad.sum()) == row['nearZeroBeyondEpsilon'] + return result, masks['5'], near_bad + + +def analyze(source): + paths = [source / name for name in ('comparison.json', 'curves.npz')] + before = {str(path): digest(path) for path in paths} + comparison = json.loads(paths[0].read_bytes()) + arrays = np.load(paths[1], allow_pickle=False) + time = arrays['time'] + assert arrays['phaseMatched'].all() and np.all(np.diff(time) > 0) + dt = np.diff(time) + weights = np.r_[dt[0] / 2, (dt[:-1] + dt[1:]) / 2, dt[-1] / 2] + assert np.isclose(weights.sum(), time[-1] - time[0]) + rows, masks, near_masks = [], {}, {} + for row in comparison['curves']: + actual, reference = (arrays[s + '|' + row['key']] for s in ('platform', 'amesim')) + assert np.isfinite(actual).all() and np.isfinite(reference).all() + stats, mask, near = curve_stats(row, actual, reference, time, weights) + rows.append(stats) + masks[row['key']], near_masks[row['key']] = mask, near + groups = {} + for quantity in sorted({r['quantity'] for r in rows}): + selected = [r for r in rows if r['quantity'] == quantity] + all_count = len(selected) * len(time) + active_count = sum(r['activeCount'] for r in selected) + union = np.any([masks[r['key']] for r in selected], axis=0) + near_union = np.any([near_masks[r['key']] for r in selected], axis=0) + def worst(field): + candidates = [r for r in selected if r[field] is not None] + if not candidates: + return None + r = max(candidates, key=lambda r: r[field]) + return dict(key=r['key'], value=r[field]) + groups[quantity] = dict(curveCount=len(selected), sampleCount=all_count, + activeCount=active_count, + above5Count=sum(r['above5']['sampleCount'] for r in selected), + above5SamplePercent=ratio(sum(r['above5']['sampleCount'] for r in selected), all_count), + above5ActivePercent=ratio(sum(r['above5']['sampleCount'] for r in selected), active_count), + anyCurveAbove5=mask_summary(union, time, weights), + nearZeroAboveEpsilonCount=sum(r['nearZeroAboveEpsilon']['sampleCount'] for r in selected), + nearZeroAboveEpsilonSamplePercent=ratio(sum(r['nearZeroAboveEpsilon']['sampleCount'] for r in selected), all_count), + anyCurveNearZeroAboveEpsilon=mask_summary(near_union, time, weights), + anyCurveRelative5OrNearZeroAbsolute=mask_summary(union | near_union, time, weights), + worst={field: worst(field) for field in ('maximumAbsolute', + 'maximumAbsolutePercentOfPeak', 'relativeL2Percent', 'relativeL1Percent', + 'significantMaxRelativePercent', 'integratedAbsoluteError', + 'maxCumulativeSignedError')}) + union = np.any(list(masks.values()), axis=0) + near_union = np.any(list(near_masks.values()), axis=0) + windows = [] + for start, end in [(0., 1.), (1., 10.8), (10.8, 21.6), (21.6, 32.4), (32.4, 43.2), (43.2, 50.)]: + include = (time >= start - 1e-12) & (time < end - 1e-12 if end < 50 else time <= end) + details = {} + for quantity in ('mass_flow', 'enthalpy_flow'): + selected = [r for r in rows if r['quantity'] == quantity] + details[quantity] = dict( + above5Count=sum(int((masks[r['key']] & include).sum()) for r in selected), + nearZeroAboveEpsilonCount=sum(int((near_masks[r['key']] & include).sum()) for r in selected), + maximumAbsolute=max(float(np.abs(arrays['platform|' + r['key']] + - arrays['amesim|' + r['key']])[include].max()) for r in selected)) + windows.append(dict(start=start, end=end, sampleCount=int(include.sum()), groups=details)) + assert sum(r['above5']['sampleCount'] for r in rows) == comparison['above5PercentCount'] + assert all(digest(path) == before[str(path)] for path in paths) + return dict(source=str(source), inputHashes=before, sourceUnchanged=True, + grid=dict(start=float(time[0]), end=float(time[-1]), count=len(time), + interval=float(np.median(dt))), curveCount=len(rows), + affectedCurveCount=sum(r['above5']['sampleCount'] > 0 for r in rows), + above5Count=sum(r['above5']['sampleCount'] for r in rows), + anyCurveAbove5=mask_summary(union, time, weights), + anyCurveNearZeroAboveEpsilon=mask_summary(near_union, time, weights), + anyCurveRelative5OrNearZeroAbsolute=mask_summary(union | near_union, time, weights), + groups=groups, windows=windows, curves=rows) + + +def plots(source, out): + import matplotlib + matplotlib.use('Agg') + import matplotlib.pyplot as plt + from matplotlib import font_manager + font = Path('C:/Windows/Fonts/msyh.ttc') + if font.exists(): + font_manager.fontManager.addfont(str(font)) + plt.rcParams['font.family'] = font_manager.FontProperties(fname=str(font)).get_name() + plt.rcParams.update({'font.size': 10, 'axes.unicode_minus': False, + 'axes.spines.top': False, 'axes.spines.right': False}) + arrays = np.load(source / 'curves.npz', allow_pickle=False) + t = arrays['time'] + fig, axes = plt.subplots(2, 3, figsize=(15, 8), constrained_layout=True) + selected = [('amesim_pn3node2_3.reference_mass_flow', '质量流量', 'kg/s', 1e6, 'mg/s'), + ('amesim_p4node2_4.reference_enthalpy_flow', '焓流', 'W', 1., 'W')] + for row, (key, label, unit, scale, small_unit) in enumerate(selected): + y, ref = arrays['platform|' + key], arrays['amesim|' + key] + for col, bounds in enumerate(((0, 50), (.27, .35))): + ax = axes[row, col] + mask = (t >= bounds[0] - 1e-12) & (t <= bounds[1] + 1e-12) + factor = 1. if col == 0 else scale + ax.plot(t[mask], ref[mask] * factor, color='#dd863b', lw=2, label='Amesim') + ax.plot(t[mask], y[mask] * factor, color='#126ca6', lw=1, ls='--', label='平台') + ax.set(xlabel='时间 / s', ylabel=unit if col == 0 else small_unit, + title=label + (':50 s 全程' if col == 0 else ':接近零的衰减尾部放大')) + if col == 1: + ax.axvspan(.295, .325, color='#d84b43', alpha=.12, label='差异集中区') + ax.legend(fontsize=8) + ax = axes[row, 2] + group_keys = [k for k in arrays.files if k.startswith('platform|') + and k.endswith('.reference_' + ('mass_flow' if row == 0 else 'enthalpy_flow'))] + envelope = np.max([np.abs(arrays[k] - arrays[k.replace('platform|', 'amesim|', 1)]) + for k in group_keys], axis=0) + ax.plot(t, envelope * scale, color='#8b3d50', lw=.9) + ax.set(xlabel='时间 / s', ylabel=small_unit, title=label + ':16 条曲线最大绝对差包络') + for ax in axes[row]: + ax.grid(alpha=.2) + fig.suptitle('八路基线剩余差异:全程、初始衰减段与绝对差\n0–50 s,共同网格 10 ms;事件侧已配对;不代表网格间瞬态的误差上界', fontsize=14) + fig.savefig(out / 'coverage.png', dpi=150) + plt.close(fig) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--output', type=Path, default=ROOT / 'test/mql8-curve-coverage-20260917') + parser.add_argument('--plots', action='store_true') + args = parser.parse_args() + args.output.mkdir(parents=True, exist_ok=True) + sources = dict(cyclic=BASE / 'event-output-comparison', noncyclic=BASE / 'baseline/noncyclic') + result = dict(method='Saved-grid statistics; original phase pairing and epsilon preserved. ' + 'L1/L2 normalized by each reference curve; no time interpolation. ' + 'Integrals and durations are grid estimates only. Noncyclic reference uses ordinary output.', + profiles={name: analyze(path) for name, path in sources.items()}) + (args.output / 'coverage.json').write_text(json.dumps(result, ensure_ascii=False, + indent=2, allow_nan=False) + '\n', encoding='utf-8') + if args.plots: + plots(sources['cyclic'], args.output) + for name, profile in result['profiles'].items(): + print(name, 'above5:', profile['above5Count'], 'any time:', profile['anyCurveAbove5']) + for quantity in ('force', 'mass_flow', 'enthalpy_flow', 'pressure', 'temperature', 'gap', 'velocity'): + print(quantity, json.dumps(profile['groups'][quantity], ensure_ascii=False)) + + +if __name__ == '__main__': + main() diff --git a/tests/manual/analyze_real_skip.py b/tests/manual/analyze_real_skip.py new file mode 100644 index 0000000..f78530f --- /dev/null +++ b/tests/manual/analyze_real_skip.py @@ -0,0 +1,144 @@ +"""Summarize independent serial measurements; audit times are never performance data.""" +from pathlib import Path +import json,statistics +import real_skip_experiment as experiment + +OUT=experiment.OUT +def load(path):return json.loads(path.read_text(encoding='utf-8')) +def write(path,value):experiment.write(path,value) + + +def compare_bytes(actual,reference): + offset=0 + with actual.open('rb') as a,reference.open('rb') as b: + while True: + x=a.read(1024*1024);y=b.read(1024*1024) + if x!=y: + local=next((i for i,(u,v) in enumerate(zip(x,y)) if u!=v),min(len(x),len(y))) + different=offset+local + record={'file':str(actual),'reference':str(reference),'firstByte':different} + if actual.name=='jacobians.bin': + width=(1+132+132*132)*8;j,within=divmod(different,width);element=within//8 + record.update(jacobian=j+1,field='t' if element==0 else 'y' if element<=132 else 'Jacobian',element=element) + if element>132:record.update(row=(element-133)%132,column=(element-133)//132) + write(OUT/'first-file-mismatch.json',record) + raise AssertionError(record) + if not x:break + offset+=len(x) + return offset + + +def main(): + checks=[] + for label in ['audit-control-run','audit-skip-run','audit-forced-reject']: + for name in ['jacobians','states','outputs','events']: + ref=experiment.BASE.parent/('all-audit' if name=='jacobians' else 'all-run-0')/(name+'.bin') + n=compare_bytes(OUT/label/(name+'.bin'),ref) + checks.append(dict(run=label,field=name,bytes=n,exact=True)) + write(OUT/'byte-comparison.json',checks) + rows=load(OUT/'performance.json');audit=load(OUT/'audit-skip-run/validation.json');forced=load(OUT/'audit-forced-reject/validation.json') + assert len(rows)==14 and all(r['exact'] for r in rows) + a=[r for r in rows if not r['skip']];b=[r for r in rows if r['skip']] + assert all(r['counters']['realSkips']==896 and r['counters']['nativeOriginalExecutions']==0 and r['counters']['rejects']==0 for r in b) + def timing(row,key):return row[key] if key in row else row['counters'][key] + keys=['originalSeconds','validationSeconds','overlayPatchSeconds','commitSeconds','fallbackSeconds','pathSeconds','baselineRecordSeconds','jacobianSeconds','solveCpuSeconds','solveSeconds'] + def stat(values):return dict(median=statistics.median(values),min=min(values),max=max(values)) + summary={side:{k:stat([timing(r,k) for r in rs]) for k in keys} for side,rs in [('control',a),('skip',b)]} + pairs=[] + for i in range(1,8): + c=next(r for r in a if r['label']==f'pair-{i}-control');s=next(r for r in b if r['label']==f'pair-{i}-skip') + stage=sum(s['counters'][k] for k in ['validationSeconds','overlayPatchSeconds','commitSeconds']) + pairs.append(dict(pair=i,originalUs=c['counters']['originalSeconds']/896*1e6,replayStagesUs=stage/896*1e6, + targetNetSavingSeconds=c['counters']['pathSeconds']-s['counters']['pathSeconds'], + originalMinusReplaySeconds=c['counters']['originalSeconds']-stage, + incrementalMetadataSeconds=s['counters']['baselineRecordSeconds']-c['counters']['baselineRecordSeconds'], + jacobianDeltaSeconds=s['jacobianSeconds']-c['jacobianSeconds'], + integrationCpuDeltaSeconds=s['solveCpuSeconds']-c['solveCpuSeconds'],integrationWallDeltaSeconds=s['solveSeconds']-c['solveSeconds'])) + summary['paired']={k:stat([r[k] for r in pairs]) for k in pairs[0] if k!='pair'} + summary['pairs']=pairs;write(OUT/'performance-summary.json',summary) + med=lambda side,k:summary[side][k]['median'] + orig=summary['paired']['originalUs']['median'];replay=summary['paired']['replayStagesUs']['median'] + lines=['# R288 / position16 最小 real skip 实验','', + '## 结论','', + '1. **完整求解结果逐位一致。** 0–10 s 全轨迹,896 个 132×132 Jacobian(15,611,904 个元素)及每个 t/y、states、outputs、事件、最终状态与既有基线一致;没有使用数值容差。', + '2. **896/896 次真实 skip;自然 reject/fallback 为 0。** commit 896 次,原目标 native operation 实际执行 0 次,Reference 双路执行 0 次。', + f'3. **当前 replay 更贵。** 7 轮中位数:原 operation {orig:.3f} µs/次,validation + overlay/patch + commit {replay:.3f} µs/次({replay/orig:.2f} 倍)。目标完整路径 control {med("control","pathSeconds")/896*1e6:.3f} µs/次,real skip {med("skip","pathSeconds")/896*1e6:.3f} µs/次。', + '4. **暂不把这一实现直接扩展到 position52。** 正确性门槛已满足,净收益门槛未满足。应先降低 metadata 采集、事务复制和解释执行成本;本轮不能据此推断 position52 或其他 interval 的收益。','', + '## 范围与实现','', + '- 全部改动仅在独立生成的实验 worker 及 `tests/manual`;生产路径、默认开关、property cache 语义、原 whole-context guard 未改动。', + '- 入口仅为 `lp_color == 6 && region == 288` 的 `case 16`,位于原 `lp_reuse` 失败之后。其他位置继续原执行。', + '- baseline position16 用单独命名空间的 kernels 采集必需的有序事件 metadata。其他 operation 使用原 kernels,没有全局访问插桩。', + '- guard 来自已验证的 shadow 源码,构建时校验其 SHA-256,并断言 guard 函数保持一致;唯一变量替换是将原事务入口 count 改为当前真实 probe 入口 count。', + '- 复制当前 probe 到私有 overlay;按当前 entries 验证 first-match/miss,重定位逻辑 slot,从当前 count 追加。通过后构造地址已转换的有序 write set,再 commit。已有 entries、未写字段和其他 pipe slots 保持原值。', + '- Observer、capacity/scratch、memo lifetime/value、消费字段、valid、pipe branch、未知副作用/非有限值等原保护条件保留;失败只回退原 operation。', + '- 性能版不执行 Reference、不导出访问日志或 Jacobian、不逐次比较完整 context。保留运行所需 metadata、严格 guard、overlay/patch、计时和累计计数。','', + '## 正确性证据','', + '| 检查 | 结果 |','|---|---:|', + '| Jacobian、t/y 与既有 all-audit 文件逐字节比较 | 896/896 一致 |', + '| 每个 Jacobian 的 baseline + 27 group evaluator 出口 | 25,088/25,088 一致 |', + '| 目标 operation 入口 / 出口 | 896 / 896 一致 |', + '| 返回状态、dy/w、active property entries 有序字段/valid、全部 pipe slots | 逐位一致 |', + '| memo 每次 Jacobian 的表内容、owner 绑定、recording 生命周期 | 一致 |', + '| 所有 probe 的 memo entries 只读检查 | 通过 |', + '| gas memo 内容、kernel 绑定及计数 | 一致 |', + '| errno、x87/SSE flags/rounding、warning/observer | 一致 |', + '| count 差异 / slot relocation 的目标 probe | 638 / 571 |', + '| 顺序 append | 1,658 |', + '| PT miss → PT miss / 近零流量无查询 | 829 / 67 |','', + 'Audit 对照来自**独立 control 进程真实执行当前 probe**的出口,约 1.88 GB 二进制数据。跨进程地址比较采用 owner/function 绑定身份,数值字段保持原始位模式;不把 C padding 当成数值。memo 表逐 Jacobian 比较,后续每个 probe 同时检查整表未变。目标入口、出口与所有 group 的完整 evaluator 出口均参与比较。audit 的 I/O 保留并恢复 errno、x87 和 SSE 环境。','', + '| 求解器计数 | control / real skip |','|---|---:|', + '| accepted / rejected | 10840 / 918 |','| Newton iterations / convergence failures | 19371 / 798 |', + '| nfev / njev / nlu | 44467 / 896 / 3106 |','| solverStarts / stateTransitions | 4 / 1 |','', + '额外负例:每 128 个目标 probe 注入一次晚期 nonfinite-output reject,发生在 overlay 已执行有序更新之后。7 次 reject 均完成无污染检查,fallback/native 原执行各 7 次,commit/skip 各 889 次;全轨迹仍与同一 control 和既有基线逐位一致。没有放宽 guard,也未改变容差。','', + '## 性能方法与原始数据','', + 'Windows / MinGW GCC,原构建优化选项(`-O3 -ffp-contract=off -fno-fast-math`)。完成正确性和回退测试后单独编译性能 worker;control、skip 各预热一次,再交替串行运行 7 组。表中顺序就是实际顺序。每轮 states/outputs/events 指纹和全部指定求解器计数保持一致,skip 每轮均为 896、reject 0、原执行 0。','', + 'QPC 粗粒度计时;累计整数 tick,积分过程中不做浮点时间换算。validation 包括语义校验所必需的临时有序更新;overlay/patch 包括当前 context 复制及提交 write set 构造;commit 是真实字段写回。完整 target 路径另计,包含调度、计数和额外计时开销。未减去计时器自身成本,未使用 shadow/audit 时间推断性能。','', + '| 运行顺序 | 原执行 µs/次 | validation µs/次 | overlay/patch µs/次 | commit µs/次 | fallback ms | target 总 ms | baseline pos16 总 ms | Jacobian s | 积分 CPU s | 积分 wall s | skip/reject |', + '|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|'] + for r in rows: + c=r['counters'];us=lambda k:c[k]/896*1e6 + lines.append(f'| {r["label"]} | {us("originalSeconds"):.3f} | {us("validationSeconds"):.3f} | {us("overlayPatchSeconds"):.3f} | {us("commitSeconds"):.3f} | {c["fallbackSeconds"]*1e3:.3f} | {c["pathSeconds"]*1e3:.3f} | {c["baselineRecordSeconds"]*1e3:.3f} | {r["jacobianSeconds"]:.6f} | {r["solveCpuSeconds"]:.6f} | {r["solveSeconds"]:.6f} | {c["realSkips"]}/{c["rejects"]} |') + lines+=['','baseline pos16:control 为原 baseline operation;skip 包含原 baseline operation + 本次 replay 必需 metadata 采集,不能漏算这部分成本。性能各轮没有自然 reject,因此 fallback 总时间为 0;这不代表一次 fallback 的成本为零,本轮未估计该分支的单次性能。','', + '### 中位数及 min/max','', '| 项目 | control 中位数 [min, max] | real skip 中位数 [min, max] |','|---|---:|---:|'] + for k in keys: + def cell(side): + v=summary[side][k];return f'{v["median"]:.9f} [{v["min"]:.9f}, {v["max"]:.9f}]' + lines.append(f'| {k}(s,896 次累计) | {cell("control")} | {cell("skip")} |') + lines+=['','### 配对差值','', + '| 组 | 原计算 − replay 三阶段 ms | target 完整路径净节省 ms | 新增 metadata 采集 ms | Jacobian Δ s | 积分 CPU Δ s | 积分 wall Δ s |','|---|---:|---:|---:|---:|---:|---:|'] + for r in pairs:lines.append(f'| {r["pair"]} | {r["originalMinusReplaySeconds"]*1e3:.3f} | {r["targetNetSavingSeconds"]*1e3:.3f} | {r["incrementalMetadataSeconds"]*1e3:.3f} | {r["jacobianDeltaSeconds"]:.6f} | {r["integrationCpuDeltaSeconds"]:.6f} | {r["integrationWallDeltaSeconds"]:.6f} |') + paired=summary['paired'] + lines+=['','净节省为正表示节省,Δ = skip − control。','', + f'- 原计算成本 − replay 三阶段成本:配对中位数 **{paired["originalMinusReplaySeconds"]["median"]*1e3:.3f} ms / 896 次**。', + f'- target 完整路径净节省:配对中位数 **{paired["targetNetSavingSeconds"]["median"]*1e3:.3f} ms / 896 次**;范围 [{paired["targetNetSavingSeconds"]["min"]*1e3:.3f}, {paired["targetNetSavingSeconds"]["max"]*1e3:.3f}] ms。', + f'- 此外 baseline metadata 采集增加:配对中位数 **{paired["incrementalMetadataSeconds"]["median"]*1e3:.3f} ms**。', + f'- Jacobian callback 配对 Δ:中位数 {paired["jacobianDeltaSeconds"]["median"]:.6f} s,范围 [{paired["jacobianDeltaSeconds"]["min"]:.6f}, {paired["jacobianDeltaSeconds"]["max"]:.6f}] s。', + f'- 积分 wall 配对 Δ:中位数 {paired["integrationWallDeltaSeconds"]["median"]:.6f} s,范围 [{paired["integrationWallDeltaSeconds"]["min"]:.6f}, {paired["integrationWallDeltaSeconds"]["max"]:.6f}] s。', + '','全局时间受调频、调度和系统负载影响,不能把某轮 Jacobian/积分变快归因于这个 operation。目标路径在全部配对中均更慢,已经足以否定当前实现的局部净收益;本轮不预测整个 local probe 的最终加速比例。','', + '## 成本解释及下一阶段条件','', + '原 operation 在现有 Jacobian memo 的只读复用环境下已经很便宜;当前严格 replay 仍要初始化/复制 156,816 B 的 overlay(含完整 memo),解释事件并构造 write set。运行所需有效 metadata 为 2,224–23,344 B,Plan 固定预留 90,224 B,patch 预留 16,384 B。这些是当前隔离实现的实际成本,不是机制理论上的下限。', + '','因此,本轮证明了目标路径可以安全真实跳过,但没有证明性能优化成立。下一步应先针对事务存储和 metadata 表达做最小化,再重复同样的正确性与交替性能验收;不因 position16 正确就直接扩大到 position52/整个 R475/全部 reuse interval。','', + '## 复现与证据位置','', + '运行目录:`test/r288-real-skip-20260917/`。依赖上一轮生成的 local-probe worker、context-access worker 和 shadow-certified 源码;工具链复用原实验配置,不安装依赖。','', + '```powershell', + '.venv-win/Scripts/python.exe tests/manual/real_skip_experiment.py prepare --audit', + '.venv-win/Scripts/python.exe tests/manual/real_skip_experiment.py prepare --audit --skip', + '.venv-win/Scripts/python.exe tests/manual/real_skip_experiment.py run --audit', + '.venv-win/Scripts/python.exe tests/manual/real_skip_experiment.py run --audit --skip', + '.venv-win/Scripts/python.exe tests/manual/real_skip_experiment.py run --audit --skip --label audit-forced-reject --force 128', + '.venv-win/Scripts/python.exe tests/manual/real_skip_experiment.py prepare', + '.venv-win/Scripts/python.exe tests/manual/real_skip_experiment.py prepare --skip', + '.venv-win/Scripts/python.exe tests/manual/real_skip_experiment.py benchmark --pairs 7', + '.venv-win/Scripts/python.exe tests/manual/analyze_real_skip.py','```','', + '- `audit-{control,skip}-run/validation.json`:正确性、独立执行计数、完整求解器计数及文件指纹。', + '- `audit-forced-reject/validation.json`:真实回退及 rollback 检查。', + '- `byte-comparison.json`:全部 Jacobian/t/y、states、outputs、events 与原始基线的逐字节比较。', + '- `audit-control-run/audit.bin`:所有 group 和目标 operation 的实际执行对照出口。', + '- `performance.json`:14 次按实际顺序记录的原始数据;各轮目录保留独立日志和结果。', + '- `performance-summary.json`:中位数、min/max 和逐对差值。', + '- `{audit,perf}-{control,skip}/build.json`:源文件哈希、guard 一致性和构建记录。',''] + report=Path(__file__).with_name('r288_real_skip_report.md');report.write_text('\n'.join(lines),encoding='utf-8') + print(json.dumps(summary['paired'],ensure_ascii=False,indent=2));print(report) + + +if __name__=='__main__':main() diff --git a/tests/manual/analyze_typed_replay.py b/tests/manual/analyze_typed_replay.py new file mode 100644 index 0000000..0083421 --- /dev/null +++ b/tests/manual/analyze_typed_replay.py @@ -0,0 +1,178 @@ +"""Write the R288 cost-floor report from raw audit and serial A/B/C results.""" +from pathlib import Path +import json,statistics,subprocess,re,hashlib +import specialized_replay_experiment as experiment +import analyze_real_skip as bytecheck + +ROOT=experiment.ROOT;OUT=experiment.OUT;HERE=Path(__file__).parent +def read(p):return json.loads(p.read_text(encoding='utf-8')) +def write(p,o):experiment.base.write(p,o) +def stat(v):return dict(median=statistics.median(v),min=min(v),max=max(v)) +def fmt(s,d=3):return f'{s["median"]:.{d}f} [{s["min"]:.{d}f}, {s["max"]:.{d}f}]' + + +def main(): + bytecheck.OUT=OUT;checks=[] + for mode in 'CD': + for label in ['audit-skip-run','forced-reject']: + r=read(OUT/mode/label/'validation.json');t=read(OUT/mode/label/'typed-summary.json') + assert r['exact'] and t['contractChecks']==t['oracleChecks']==896 + assert t['negativeChecks']==(20 if label=='audit-skip-run' else 0) + for name in ['jacobians','states','outputs','events']: + ref=experiment.base.BASE.parent/('all-audit' if name=='jacobians' else 'all-run-0')/(name+'.bin') + size=bytecheck.compare_bytes(OUT/mode/label/(name+'.bin'),ref) + checks.append(dict(mode=mode,run=label,name=name,bytes=size,exact=True)) + write(OUT/'byte-comparison.json',checks) + cc,_,_=experiment.base.ex.builder.toolchain();nm=Path(cc).with_name('nm.exe') + symbols=subprocess.check_output([str(nm),'--defined-only',str(OUT/'C/perf-skip/model.exe')],text=True) + assert not re.search(r'(?m)\b(?:plan|overlay|before_reject|after_reject|oracle_generic|oracle_typed|replay_overlay|ax_access)$',symbols) + write(OUT/'C/perf-skip/stripped-audit-proof.json',dict(noGenericInterpreter=True,noFullOverlay=True,noAuditOracle=True)) + for mode in 'CD': + for build in (OUT/mode).glob('*/build.json'): + v=read(build);v['guardMatchesShadow']=False;v['wholeContextGuardUnchanged']=True + v['typedGuardValidation']='C and D audit: 896 dynamic contracts, 896 generic decisions/patches, 20 negative cases, full exit comparison' + write(build,v) + rows=read(OUT/'performance.json');assert len(rows)==27 + by={m:[r for r in rows if r['mode']==m] for m in 'ABC'} + metrics={} + for m,rs in by.items(): + transformed=[] + for r in rs: + assert r['exact'];c=r['counters'];u=lambda k:c[k]/896*1e6 + if m!='A':assert c['realSkips']==896 and c['nativeOriginalExecutions']==c['rejects']==0 + transformed.append(dict(original=u('originalSeconds'),validation=u('validationSeconds'),patch=u('overlayPatchSeconds'),commit=u('commitSeconds'),outsideStages=u('pathSeconds')-(u('originalSeconds') if m=='A' else u('validationSeconds')+u('overlayPatchSeconds')+u('commitSeconds')), + stages=u('validationSeconds')+u('overlayPatchSeconds')+u('commitSeconds'),path=u('pathSeconds'),baseline=u('baselineRecordSeconds'),jacobian=r['jacobianSeconds'],cpu=r['solveCpuSeconds'],wall=r['solveSeconds'])) + metrics[m]={k:stat([v[k] for v in transformed]) for k in transformed[0]} + pairs=[] + for i in range(1,10): + a=next(r for r in by['A'] if r['label']==f'round-{i}');ac=a['counters'] + for m in 'BC': + r=next(r for r in by[m] if r['label']==f'round-{i}');c=r['counters'] + stages=c['validationSeconds']+c['overlayPatchSeconds']+c['commitSeconds'] + metadata=c['baselineRecordSeconds']-ac['baselineRecordSeconds'] + local=(ac['originalSeconds']-stages)*1e6/896 + pairs.append(dict(round=i,mode=m,probeSavingUs=local,metadataIncrementUs=metadata*1e6/896, + mechanismSavingMs=((ac['originalSeconds']-stages)-metadata)*1e3, + completePathSavingMs=((ac['pathSeconds']-c['pathSeconds'])-metadata)*1e3, + jacobianDelta=r['jacobianSeconds']-a['jacobianSeconds'],cpuDelta=r['solveCpuSeconds']-a['solveCpuSeconds'],wallDelta=r['solveSeconds']-a['solveSeconds'])) + pair_stats={m:{k:stat([p[k] for p in pairs if p['mode']==m]) for k in pairs[0] if k not in ['mode','round']} for m in 'BC'} + summary=dict(metrics=metrics,paired=pair_stats,rounds=pairs) + write(OUT/'performance-summary.json',summary) + profile_folder=OUT/'P/detailed' + if not (profile_folder/'attribution.json').exists():profile_folder=OUT/'P/perf-skip-run' + profile=read(profile_folder/'attribution.json');pc=read(profile_folder/'real-skip.json');frequency=profile['frequency'] + cat={v['id']:dict(microsecondsPerProbe=v['ticks']/frequency/896*1e6,calls=v['calls']) for v in profile['categories']} + empty=profile['emptyTimerTicks']/frequency/profile['emptyTimerCalls']*1e6 + write(OUT/'attribution-summary.json',dict(emptyBracketUs=empty,categories=cat,dispatchOnlyUs=profile['dispatchTicks']/frequency/profile['dispatchRepeats']/896*1e6,source=str(profile_folder))) + a=metrics['A'];b=metrics['B'];c=metrics['C'];paired=pair_stats['C'] + below=c['stages']['median']0 + conclusion='当前实现对 R288 没有净收益;这种低成本 operation 应直接原计算。' if not gain else '当前测量显示 R288 有净收益;仍只授权本案例,下一步应独立验证 codegen contract 和更昂贵 operation。' + lines=['# R288 / position16:typed semantic replay 成本下限实验','', + '## 三个问题的答案','', + f'1. 去掉完整 overlay 与通用事件解释器后,三阶段 replay 的每轮均值中位数为 **{c["stages"]["median"]:.3f} µs/次**,各轮范围 **{c["stages"]["min"]:.3f}–{c["stages"]["max"]:.3f} µs/次**。包含调度、计时及计数的完整目标路径中位数为 **{c["path"]["median"]:.3f} µs/次**。这是本实现、工具链和机器的实测结果,不是理论最低成本。', + f'2. 同批 A 的原 operation 为 **{a["original"]["median"]:.3f} µs/次**;专用 replay **{"低于" if below else "仍高于"}原计算**。与历史 1.143 µs 不直接跨批比较。', + f'3. 计入 baseline 捕获新增成本,配对完整机制净节省中位数为 **{paired["mechanismSavingMs"]["median"]:.3f} ms / 896 次**;含完整路径计时/统计开销的口径为 **{paired["completePathSavingMs"]["median"]:.3f} ms**。**{conclusion}**','', + '## 范围与事务语义','', + '所有实验仅针对 group6 / R288 / position16,在独立 worker 中、原 whole-context guard 失败之后启用。没有扩展 position52、R475 或其他 interval,没有接入生产默认路径。已有工作区改动不属于本实验的修改范围。','', + '- `fp_validate` 只读当前真实 probe;保留输入位比较、有限值、FP/errno、observer、count/capacity、memo owner/recording、全部 active entry 绑定、pipe branch 等保护。', + '- 两次 PT 查询依次按当前有序 entries 验证 miss;第二次查询还显式检查第一个 pending entry 的虚拟匹配。append 位置由当前 count 决定,不使用 baseline slot。', + '- memo key 的 hash 在 baseline 捕获时计算;probe 仍按相同容量、同一 bounded linear-probe 顺序查找,并逐位比较完整 key 和 value。没有复制 memo,也没有绕过 memo 验证。', + '- 只支持已验证的两个固定 schema:67 次近零流量无查询、829 次 PT miss → PT miss。不支持的路径直接 reject。', + '- 这两个 schema 中,物性计算消费的条目均是本 operation 新建的条目。初始化、字段写入、valid 测试和读取之间的条件由固定 schema 保证,并在正确性版对每个 baseline 的所有原始访问逐条证明;不是把这些 guard 删除。', + '- `fp_prepare` 最多构造两个 pending property entries、一个 pipe[0]、新 count、q[45] 和 density/pipe memo reuse 增量。所有真实写入仅在 `fp_commit` 发生。未被写入的 probe 数据保持原值。','', + '专用版是固定 R288 schema 的 typed 原型,不是通用生产 codegen。静态 schema 的核对表从全轨迹真实 generic 记录自动生成;原 native 源文件受既有 SHA-256 约束,未知源代码变化会停止构建。','', + '## Metadata 与临时存储','', + '| 项目 | generic | specialized |','|---|---:|---:|', + '| 每次 baseline 有效 metadata | 2,224–23,344 B | 240 B |', + '| 持久 metadata 固定预留 | 90,224 B | 240 B |', + '| 捕获临时区 | 通用事件记录器 | 96 B |', + '| context/memo/pipe 完整 overlay | 156,816 B | 0 B |', + '| pending patch | 16,384 B write-set,另有 overlay | 456 B,总计且不重复计算 |','', + '- **静态可确定**:medium 常量、字段布局、读取/写入顺序、valid 位演变、最多两个追加条目、memo 类型、pipe[0]、q[45]。', + '- **每个 baseline 动态捕获**:输入、输出、U/D 物性字段、pipe memo key/value、三个 memo hash、入口 count、owner/Jacobian 生命周期及 FP/errno 条件,共 240 B。', + '- **probe 才解析**:当前有序 entries、query miss、当前 count/capacity、memo 实际位置和值、owner/observer/pipe 条件;验证成功后构造 456 B pending。','', + '## 正确性验收','', + 'C 验收版:同一次 baseline 真实执行同时生成 generic 事件与小型 metadata;核对 896 份完整访问 schema、896 次 generic/typed accept/reject 和最终 patch,包括逻辑 slot 映射。', + 'D 验收版:使用性能版相同的最小 native 捕获 hooks,逐位核对先前真实 generic baseline 记录(跨进程 memo 地址转换为 owner 绑定身份),然后重复全部求解验收。该离线记录只用于 audit;性能 worker 不读取这些记录。','', + '| 检查 | 结果 |','|---|---:|', + '| Jacobian 全元素与 t/y | 896/896 逐字节一致,15,611,904 个矩阵元素 |', + '| evaluator 返回状态、dy/w 和完整 context 出口 | 25,088/25,088 一致 |', + '| 目标 operation 入口/出口、property count/有序字段/valid、pipe cache | 全部一致 |', + '| memo entries、只读生命周期、owner/kernel 绑定及计数 | 全部一致 |', + '| states/outputs/events、最终状态、warning、FP/errno | 全部一致 |', + '| 正常目标 attempts / skip / reject / 原执行 | 896 / 896 / 0 / 0 |', + '| 负例 guard 判定及无写入检查 | C、D 各 20 项通过 |', + '| 晚期 forced reject | C、D 各 7 次,无污染;889 skip、7 原执行 |', + '| count 差异 / relocation / append | 638 / 571 / 1,658 |','', + '负例包含 observer、无容量/第二次追加不足、pipe hit、memo recording/miss/value、输入改变、晚期非有限输出、过期生命周期、FP flags/rounding、entry owner、U/D 查询转 hit,以及已拒绝 metadata 的 consumed/valid/已有条目更新/未知副作用标记。后四项是错误记录传播检查;逐字段读写与 valid 演变的实质检查来自 896 份完整访问 contract 对照。','', + '| accepted | rejected | Newton iterations | convergence failures | nfev | njev | nlu | solverStarts | stateTransitions |', + '|---:|---:|---:|---:|---:|---:|---:|---:|---:|', + '|10840|918|19371|798|44467|896|3106|4|1|','', + '全部使用位比较和逐字节比较,没有更改容差。性能版二进制符号检查确认无 generic interpreter、完整 overlay 或 audit oracle。','', + '## Generic 成本归因','', + f'单独归因 worker 的总 replay 阶段为 {(pc["validationSeconds"]+pc["overlayPatchSeconds"]+pc["commitSeconds"])/896*1e6:.3f} µs/次;该数值含细粒度插桩,不与历史 22.521 µs 强行逐项相加。空 QPC bracket 均值 {empty:.5f} µs,细粒度嵌套计时会额外放大短事件成本。', + '以下分类均为每个目标 probe 的平均原始计时;父项含子项,明确标记重叠,不能相加当作净成本。','', + '| 分类 | µs/次 | 口径 |','|---|---:|---|'] + def row(name,k,note):lines.append(f'| {name} | {cat[k]["microsecondsPerProbe"]:.4f} | {note} |') + lines.append(f'| baseline 原执行 + metadata 捕获 | {pc["baselineRecordSeconds"]/896*1e6:.4f} | 新增成本由后面的同批 A/B 配对得出 |') + row('PT first-match / miss',10,'真实顺序扫描;包含一组自身计时') + row('consumed-field 读取事件',1,'含 metadata 分派和嵌套字段比较') + row('其中:字段值/绑定比较',13,'是上一行的子项') + row('valid 测试事件',8,'含事件分派') + row('memo 验证',12,'完整 key/value 与 bounded lookup;SCALAR 事件的子项') + row('完整 overlay 初始化/复制/绑定',14,'包含 context、memo、全部 pipe') + row('logical slot relocation',11,'包含映射冲突检查;属于 ALLOCATE/QUERY 子项') + for name,k in [('pending entry 字段写入',2),('pending valid 更新',3),('追加与 count 更新',7),('pipe patch 写入',5),('write-set 构造',15),('commit',16)]:row(name,k,'原始 generic 实测,含自身计时') + for name,k in [('公共 guard(映射初始化之前)',17),('overlay 清零/poison 初始化',18),('property context/entries 复制',19),('全部 pipe 复制',20),('memo header/entries 复制',21),('overlay 指针绑定',22)]: + if cat[k]['calls']:row(name,k,'细分项;已经包含在对应父项内') + dispatch=profile['dispatchTicks']/frequency/profile['dispatchRepeats']/896*1e6 + lines+=[f'| 仅 event metadata 遍历/类型分派微基准 | {dispatch:.4f} | 每份真实 plan 重复 100 次;不做 guard/write,不能当作真实解释器的独立可加项 |', + f'| 空计时 bracket | {empty:.5f} | 每组含首尾 QPC;累计统计另有开销,最终以低扰动 A/B/C 为准 |','', + '通用解释器的实际成本分布在 metadata 访问、字段比较、slot 映射、写入分派和循环控制中,没有一个能独立相减的精确“dispatch 时间”。归因版保留原始嵌套数据,最终性能版移除这些细粒度计时。','', + '## 低扰动 A/B/C 性能','', + 'A=原 operation,B=原 generic replay,C=typed/minimal replay。各预热一次,9 组按 ABC → BCA → CAB 循环,全部串行。无 Reference、完整 context 比较或详细访问日志;保留机制所需 metadata、guard、patch 和少量累计计时。每轮 B/C 均 skip 896、reject 0、原执行 0,并核对采样结果和全部指定计数。','', + '### 中位数 [min, max]','', + '| 项目 | A | B | C |','|---|---:|---:|---:|'] + for k in ['original','validation','patch','commit','stages','path','outsideStages','baseline','jacobian','cpu','wall']: + unit='s' if k in ['jacobian','cpu','wall'] else 'µs/次' + lines.append(f'| {k} ({unit}) | {fmt(a[k])} | {fmt(b[k])} | {fmt(c[k])} |') + lines+=['','replay 三阶段先在每一轮内求和再取中位数,因此不必等于三个分项中位数之和。baseline 项包括该次 baseline operation 本身;捕获新增成本应减去 A 的 baseline 项。B 的 patch 项含全 overlay copy + write-set;C 的 patch 项仅小型 pending 构造。outsideStages 是完整路径减去被包围的阶段时间,包含外层计时、统计、调度与元数据 bookkeeping;它不是纯计时器成本。无自然 reject,fallback 总时间为 0,不代表单次 fallback 免费。','', + '### 每轮原始数据(实际执行顺序)','', + '| 轮 / 模式 | 原 op µs | validation µs | patch µs | commit µs | replay 三阶段 µs | 完整路径 µs | baseline µs | Jacobian s | CPU s | wall s |','|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|'] + for r in rows: + v=r['counters'];u=lambda k:v[k]/896*1e6 + lines.append(f'| {r["label"]} / {r["mode"]} | {u("originalSeconds"):.3f} | {u("validationSeconds"):.3f} | {u("overlayPatchSeconds"):.3f} | {u("commitSeconds"):.3f} | {u("validationSeconds")+u("overlayPatchSeconds")+u("commitSeconds"):.3f} | {u("pathSeconds"):.3f} | {u("baselineRecordSeconds"):.3f} | {r["jacobianSeconds"]:.6f} | {r["solveCpuSeconds"]:.6f} | {r["solveSeconds"]:.6f} |') + lines+=['','### 两种收益口径','', + '- probe 局部净节省 = A 原 operation − validation − patch − commit。', + '- 完整机制净节省 = 896 × probe 局部净节省 − (baseline 捕获总耗时 − A baseline 原执行耗时)。', + '- 再给出包含外层调度、计时与统计的保守口径:A 完整目标路径 − replay 完整目标路径 − baseline 新增成本。', + '- 正数为节省,负数为额外成本。时间均使用同组配对,不拿历史 1.143 µs 作分母。','', + '| 轮 / 模式 | probe 净节省 µs/次 | baseline 新增 µs/次 | 完整机制净节省 ms/896 次 | 含外层开销净节省 ms | Jacobian Δ s | CPU Δ s | wall Δ s |','|---|---:|---:|---:|---:|---:|---:|---:|'] + for p in pairs:lines.append(f'| {p["round"]} / {p["mode"]} | {p["probeSavingUs"]:.3f} | {p["metadataIncrementUs"]:.3f} | {p["mechanismSavingMs"]:.3f} | {p["completePathSavingMs"]:.3f} | {p["jacobianDelta"]:.6f} | {p["cpuDelta"]:.6f} | {p["wallDelta"]:.6f} |') + lines+=['','| 配对统计:中位数 [min, max] | B | C |','|---|---:|---:|'] + for k in pair_stats['B']:lines.append(f'| {k} | {fmt(pair_stats["B"][k],6)} | {fmt(pair_stats["C"][k],6)} |') + lines+=['','所有 operation 阶段使用 QPC 墙钟计时,包含调度长尾;积分 CPU 时间单独来自求解器统计。系统负载和频率使本批数据存在较大波动(包括 commit 分项的极端值),因此报告同时保留中位数、min/max 和每组配对结果,不将归因插桩或调度延迟声称为精确的算法 CPU 成本。9 组局部与完整机制净收益均为负;不从总 Jacobian/积分墙钟的正负波动推断整个 local probe 的收益,也不声称已证明理论成本下限。计时未做不可靠的逐事件扣减。','', + '## 停止条件与交付','',conclusion, + '本轮到此停止,不继续为 R288 寻找更多微优化,也不扩展到 position52。保留生产原计算路径和原 whole-context guard。','', + '代码入口:`tests/manual/specialized_replay_experiment.py`。核心为 `typed_replay_core.inc`、`typed_replay_runtime.inc`;`typed_contract_check.inc` 与 `typed_oracle.inc` 只用于 audit。报告生成器为 `analyze_typed_replay.py`。','', + '复现顺序(项目根目录,使用既有工具链,不安装依赖):','', + '```powershell', + '.venv-win/Scripts/python.exe tests/manual/specialized_replay_experiment.py prepare --mode P', + '.venv-win/Scripts/python.exe tests/manual/specialized_replay_experiment.py run --mode P', + '.venv-win/Scripts/python.exe tests/manual/specialized_replay_experiment.py prepare --mode C --audit', + '.venv-win/Scripts/python.exe tests/manual/specialized_replay_experiment.py run --mode C --audit', + '.venv-win/Scripts/python.exe tests/manual/specialized_replay_experiment.py run --mode C --audit --label forced-reject --force 128', + '.venv-win/Scripts/python.exe tests/manual/specialized_replay_experiment.py prepare --mode D --audit', + '.venv-win/Scripts/python.exe tests/manual/specialized_replay_experiment.py run --mode D --audit', + '.venv-win/Scripts/python.exe tests/manual/specialized_replay_experiment.py run --mode D --audit --label forced-reject --force 128', + '.venv-win/Scripts/python.exe tests/manual/specialized_replay_experiment.py prepare --mode A', + '.venv-win/Scripts/python.exe tests/manual/specialized_replay_experiment.py prepare --mode B', + '.venv-win/Scripts/python.exe tests/manual/specialized_replay_experiment.py prepare --mode C', + '.venv-win/Scripts/python.exe tests/manual/specialized_replay_experiment.py benchmark --rounds 9', + '.venv-win/Scripts/python.exe tests/manual/analyze_typed_replay.py','```','', + '证据目录:`test/r288-typed-replay-20260917/`。`performance.json` 保存 27 次原始记录;`performance-summary.json` 保存统计和逐组净收益;`byte-comparison.json` 保存逐字节结果;C/D 的 `validation.json` 和 `typed-summary.json` 保存全部正确性计数。P 的 `attribution.json` 保存原始 tick、调用次数和计时器测量。',''] + (HERE/'r288_typed_replay_report.md').write_text('\n'.join(lines),encoding='utf-8') + print(json.dumps(dict(A=a['original'],B=b['stages'],C=c['stages'],C_path=c['path'],net=paired),ensure_ascii=False,indent=2)) + + +if __name__=='__main__':main() diff --git a/tests/manual/analyze_valve_tail.py b/tests/manual/analyze_valve_tail.py new file mode 100644 index 0000000..cccb66f --- /dev/null +++ b/tests/manual/analyze_valve_tail.py @@ -0,0 +1,102 @@ +"""Summarize full-trajectory position379 keys and directly timed tail work.""" +from collections import Counter,defaultdict +import hashlib,json,statistics,struct +from pathlib import Path + +ROOT=Path(__file__).resolve().parents[2] +OUT=ROOT/'test/position379-tail-20260917' +RECORD=struct.Struct('=0: + if b[10:14]!=b[14:18]:rejects['baseline_changes_x87_status']+=1 + elif r[10:14]!=b[10:14]:rejects['environment_differs']+=1 + elif key!=b[3:8]:rejects['key_differs']+=1 + else:rejects['other']+=1 + if r[1]>=0 and key==b[3:8]: + key_hits+=1;group_hits[r[1]]+=1 + if struct.pack('=0 and r[3:8]==b[3:8]], + groups=[dict(key=[f'{x:016x}' for x in key],groups=colors) for key,colors in buckets.items()], + keyHits=sum(r[1]>=0 and r[3:8]==b[3:8] for r in rr), + guardedHits=sum(r[2] for r in rr))) + assert len(groups)==896 and output_mismatches==0 + baseline=[r for r in records if r[1]<0];probes=[r for r in records if r[1]>=0];hits=[r for r in records if r[2]];executed=[r for r in records if r[21]] + assert sum(r[18] for r in records)==info['tailTicks'] + # Conservative gate: charge raw lookup/capture/reset (including their timer + # overhead), save tail work after subtracting one empty timer per hit. + saved=sum(max(0,r[18]-empty) for r in hits)*scale + lookup=sum(r[19] for r in probes)*scale + baseline_cost=(info['baseLookupTicks']+info['captureTicks']+info['resetTicks'])*scale + guarded_hit_rate=len(hits)/len(probes) + result=dict(label=folder.name,clockHz=clock,emptyClockNs=empty/clock*1e9,entries=len(records),tailExecutions=len(executed),baselineEntries=len(baseline),probeEntries=len(probes), + tailRawUs=stats([r[18]*scale for r in executed]),tailCorrectedUs=stats([max(0,r[18]-empty)*scale for r in executed]), + hitTailCorrectedUs=stats([max(0,r[18]-empty)*scale for r in hits if r[21]]) if hits and any(r[21] for r in hits) else None, + lookupHitUs=stats([r[19]*scale for r in hits]),lookupMissUs=stats([r[19]*scale for r in probes if not r[2]]), + baselineAddedUsPerJacobian=baseline_cost/896,keyHits=key_hits,keyHitRate=key_hits/len(probes), + baselineCaptureWithoutResetUs=stats([(r[19]+r[20])*scale for r in baseline]), + unlimitedMemoTheoreticalHits=general_hits,unlimitedMemoHitRate=general_hits/len(probes), + guardedHits=len(hits),guardedHitRate=guarded_hit_rate,envChanges=env_changes,bitKeyOutputMismatches=output_mismatches, + baselineEnvChanges=baseline_env_changes,rejects=dict(rejects), + environmentChanges={name:[dict(before=f'0x{a:x}',after=f'0x{b:x}',count=n) for (a,b),n in c.items()] for name,c in environment_changes.items()}, + uniqueKeysHistogram=dict(Counter(j['uniqueKeys'] for j in jacobians)),groupHits=dict(group_hits), + rawTailSavedMs=sum(r[18] for r in hits)*scale/1000,correctedTailSavedMs=saved/1000, + allProbeLookupMs=lookup/1000,baselineAddedMs=baseline_cost/1000, + conservativeDiagnosticNetMs=(saved-lookup-baseline_cost)/1000, + hitBudgetUsAfterBaselineAndMisses=(saved-baseline_cost-sum(r[19] for r in probes if not r[2])*scale)/len(hits) if hits else 0, + inputSha256=hashlib.sha256((folder/'tail-records.bin').read_bytes()).hexdigest()) + (folder/'key-groups.json').write_text(json.dumps(jacobians,indent=2)+'\n',encoding='utf-8') + (folder/'analysis.json').write_text(json.dumps(result,indent=2)+'\n',encoding='utf-8') + return result + +def main(): + rows=[analyze(p) for p in sorted(OUT.glob('diagnostic-*')) if (p/'tail-records.bin').exists()] + assert rows + # An original-execution miss may legitimately change FP status. Only the + # proposed hit subset must have zero effect (asserted above). + gate=all(r['conservativeDiagnosticNetMs']>0 and r['lookupHitUs']['mean'].8 for r in rows) + timing=[] + for folder in sorted(OUT.glob('tail-only-*')): + if not (folder/'tail-records.bin').exists():continue + info=json.loads((folder/'tail.json').read_text());records=list(RECORD.iter_unpack((folder/'tail-records.bin').read_bytes())) + assert len(records)==info['entries']==info['executions']==24192 + empty=statistics.median(x[0] for x in struct.iter_unpack(' +#include +#include +static FILE *ax_file; +static NativePropertyCache *ax_cache; +static NativePipeCache *ax_pipes; +static int ax_active,ax_pending; +static unsigned long long ax_id; +static void hex(const void *p,size_t n){const unsigned char *b=p;for(size_t i=0;icount); + blob("context",ax_cache,sizeof(*ax_cache));fputc(',',ax_file); + blob("states",ax_cache->states,ax_cache->count*sizeof(*ax_cache->states));fputc(',',ax_file); + blob("pipes",ax_pipes,40*sizeof(*ax_pipes));fputs("}\n",ax_file); +} +static int locate(const void *ptr,size_t n,const char **domain,size_t *offset){ + uintptr_t p=(uintptr_t)ptr; + const void *bases[]={ax_cache,ax_cache?ax_cache->states:NULL,ax_pipes}; + size_t sizes[]={sizeof(*ax_cache),ax_cache?ax_cache->capacity*sizeof(*ax_cache->states):0,40*sizeof(*ax_pipes)}; + const char *names[]={"context","states","pipes"}; + for(int i=0;i<3;i++)if(bases[i] && p>=(uintptr_t)bases[i] && p-(uintptr_t)bases[i]+n<=sizes[i]){ + *domain=names[i];*offset=p-(uintptr_t)bases[i];return 1; + } + return 0; +} +void ax_access(const char *action,const void *p,size_t n,const char *field,const char *fn){ + if(!ax_active){return;}const char *domain;size_t offset; + if(!locate(p,n,&domain,&offset))return; + fprintf(ax_file,"{\"event\":\"%s\",\"domain\":\"%s\",\"offset\":%llu,\"field\":\"%s\",\"function\":\"%s\",",action,domain,(unsigned long long)offset,field,fn); + blob("value",p,n);fputs("}\n",ax_file); +} +void ax_bind(NativePropertyCache *p,NativePipeCache *pipes){ax_cache=p;ax_pipes=pipes;} +void ax_begin(unsigned long long jac,int group,int pos,double t,const double *inputs,int n){ + ax_active=(pos==52 && (group==-1 || group==18)) || (pos==16 && (group==-1 || group==6)); + if(!ax_active){return;} + if(!ax_file){ax_file=fopen("access.jsonl","wb");if(!ax_file)abort();setvbuf(ax_file,NULL,_IOFBF,1024*1024);} + ax_pending=1; + fprintf(ax_file,"{\"event\":\"begin\",\"id\":%llu,\"jac\":%llu,\"group\":%d,\"position\":%d,\"t\":%.17g,\"stateSize\":%llu,\"pipeSize\":%llu,",++ax_id,jac,group,pos,t,(unsigned long long)sizeof(NativePropertyState),(unsigned long long)sizeof(NativePipeCache)); + blob("inputs",inputs,n*sizeof(double));fputs("}\n",ax_file);snapshot("entry"); +} +void ax_end(const double *outputs,int n){if(!ax_active){return;}snapshot("exit");fputs("{\"event\":\"end\",\"completed\":true,",ax_file);blob("outputs",outputs,n*sizeof(double));fputs("}\n",ax_file);ax_active=0;} +void ax_result(int result){if(ax_pending){fprintf(ax_file,"{\"event\":\"eval_return\",\"result\":%d}\n",result);ax_pending=0;}} +void ax_finish(void){if(ax_file){fclose(ax_file);ax_file=NULL;}} +void ax_query(const char *kind,const NativeMedium *m,double p,double second){ + if(!ax_active){return;}double key[]={p,second,m->R,m->cp,m->Tref,m->slope,m->mu,m->muT,m->S}; + fprintf(ax_file,"{\"event\":\"query\",\"kind\":\"%s\",\"mediumKind\":%d,\"count\":%llu,",kind,m->real_helium,(unsigned long long)ax_cache->count); + blob("key",key,sizeof(key));fputs("}\n",ax_file); +} +void ax_match(const char *kind,NativePropertyState *s){if(!ax_active){return;}fprintf(ax_file,"{\"event\":\"match\",\"kind\":\"%s\",\"hit\":%s,\"slot\":%lld}\n",kind,s?"true":"false",s?(long long)(s-ax_cache->states):-1LL);} +void ax_new(NativePropertyCache *cache,NativePropertyState *s,int valid){ + if(!ax_active){return;}const char *domain;size_t offset;int stored=locate(s,sizeof(*s),&domain,&offset) && !strcmp(domain,"states"); + fprintf(ax_file,"{\"event\":\"allocate\",\"validInput\":%s,\"slot\":%lld,\"countAfter\":%llu,\"capacity\":%llu,\"branch\":\"%s\"}\n",valid?"true":"false",stored?(long long)(offset/sizeof(*s)):-1LL,(unsigned long long)(cache?cache->count:0),(unsigned long long)(cache?cache->capacity:0),stored?"append":"scratch"); +} +unsigned ax_test(NativePropertyState *s,unsigned mask,const char *fn){ + unsigned value=s->valid&mask; + if(ax_active){ + const char *domain;size_t offset; + int stored=locate(s,sizeof(*s),&domain,&offset) && !strcmp(domain,"states"); + fprintf(ax_file,"{\"event\":\"valid_test\",\"slot\":%lld,\"mask\":%u,\"value\":%u,\"function\":\"%s\"}\n",stored?(long long)(offset/sizeof(*s)):-1LL,mask,value,fn); + } + return value; +} +void ax_scalar(const char *action,NativeJacobianScalars *cache,int kind,int medium,const double *inputs,size_t n,int hit,const double *value){ + if(!ax_active){return;}fprintf(ax_file,"{\"event\":\"scalar_%s\",\"kind\":%d,\"mediumKind\":%d,\"hit\":%d,\"recording\":%d,\"capacity\":%llu,",action,kind,medium,hit,cache?cache->recording:-1,(unsigned long long)(cache?cache->capacity:0)); + blob("key",inputs,n*sizeof(double));if(value){fputc(',',ax_file);blob("value",value,sizeof(double));}fputs("}\n",ax_file); +} diff --git a/tests/manual/context_access_diag.h b/tests/manual/context_access_diag.h new file mode 100644 index 0000000..b50f730 --- /dev/null +++ b/tests/manual/context_access_diag.h @@ -0,0 +1,21 @@ +#ifndef CONTEXT_ACCESS_DIAG_H +#define CONTEXT_ACCESS_DIAG_H +#include "kernels.h" +#include +/* GCC-only diagnostic worker: each expression is evaluated once. */ +void ax_access(const char *,const void *,size_t,const char *,const char *); +void ax_bind(NativePropertyCache *,NativePipeCache *); +void ax_begin(unsigned long long,int,int,double,const double *,int); +void ax_end(const double *,int); +void ax_result(int); +void ax_finish(void); +void ax_query(const char *,const NativeMedium *,double,double); +void ax_match(const char *,NativePropertyState *); +void ax_new(NativePropertyCache *,NativePropertyState *,int); +unsigned ax_test(NativePropertyState *,unsigned,const char *); +void ax_scalar(const char *,NativeJacobianScalars *,int,int,const double *,size_t,int,const double *); +#define AX_R(x) ({ __typeof__(x) ax_v=(x); ax_access("read",&(x),sizeof(x),#x,__func__); ax_v; }) +#define AX_W(x,v) do { (x)=(v); ax_access("write",&(x),sizeof(x),#x,__func__); } while(0) +#define AX_OR(x,v) do { (x)|=(v); ax_access("write",&(x),sizeof(x),#x,__func__); } while(0) +#define AX_INC(x) ({ __typeof__(x) ax_v=(x)++; ax_access("write",&(x),sizeof(x),#x,__func__); ax_v; }) +#endif diff --git a/tests/manual/context_access_diagnostic.md b/tests/manual/context_access_diagnostic.md new file mode 100644 index 0000000..cd64263 --- /dev/null +++ b/tests/manual/context_access_diagnostic.md @@ -0,0 +1,169 @@ +# 两个 operation 的 context 访问级诊断 + +## 结论 + +两个案例均能按“查询 key 和首次匹配关系 → 消费字段 → 有序更新与追加”建立局部清单。完整仿真中,每例各验证 896 对 baseline/probe:查询 key、命中状态、逻辑条目对应后的消费值、valid 测试及有序写入一致,operation 输出逐位一致。 + +- **group 18 / R475 / position 52**:Jacobian 200 中 `states[74].p` 确实被两次 PT 查询扫描读取,但两次都在压力比较处排除。它没有成为物性计算输入;`states[74].T/h/rho/isentropic` 均未被该 operation 读取。因此,**本次入口差异不影响查询选择和计算**,不能表述为“这个字段完全没被读取”,也不能无条件忽略任意压力变化。 +- **group 6 / R288 / position 16**:Jacobian 200 的入口 count 从 baseline 12 变为 probe 13。两次 PT 查询均 miss,逻辑新条目应从 baseline 12/13 映射到 probe 13/14;退出 count 应为 15。probe 原有 slot 12 必须保留。 + +这是独立诊断 worker 的验证结果。生产求解器、现有 local-probe guard 和整段回放策略没有修改;不能据此直接允许整个 R475 复用。 + +具体数值、每次查询的完整 key、valid 测试和逐项有序写入见 [实测清单](context_access_checklists.md)。以下解释这些清单如何用于后续设计。 + +## 验证范围与方法 + +输入 `tests/data/test-mql-8-corrected.json`,BDF,0–10 s,采样间隔 0.01 s,rtol=1e-8。位置、group、Jacobian 序号均采用已有诊断的 0 起始编号。Windows / GCC 8.1.0;本次没有执行 Linux 验收。 + +独立 worker 来源为 `test/context-fallback-20260917/worker`。生成时,仅在副本内包装实际字段读取、valid 位测试、赋值、结构体清零、count++、查询和分配点,保留原表达式、短路条件和原 guard。Jacobian scalar get 经输出指针写入 `rho` 的路径也单独记录。 + +原始事件带有执行顺序、函数、context 域、字节偏移及原始位值。PT/PH 查询记录 medium kind、全部七个 medium 浮点参数、p 和 T/h;校验器独立扫描当前重放状态,检查所有匹配项、首个匹配 slot,以及实际扫描到的 valid 测试序列。miss 后的 `allocate` 事件给出所选追加 slot。 + +观察器和 memo 指针还校验了当前 context 的归属,以及同一 Jacobian 内 baseline/probe 的绑定一致性,不只是比较“是否非空”。日志中的原始地址仅用于本次进程诊断,不作为跨进程、跨机器的逻辑 key。 + +**前后快照仅作校验终点**:逐条读取须与当时 context 一致;逐条写入须重建退出 context 的每个有效字节,包括未改动旧条目和其余 pipe 槽。另独立检查原代码规定的新条目初始化顺序及 pipe 的 11 次写入,因而删除 `pipe.valid=0` 这类同值写入也会验收失败。未初始化槽清零前的内容未知,不把它计作“已知同值写入”。 + +`operations.json` 中的 `consumed` 是选择完成后的实际字段读取集合;查询扫描的 `p/T/h/medium` 和 valid 测试在原始事件中单独保留。新条目内部的计算读取也在集合内,但这些值不是需要和入口旧条目比较的前置条件。 + +## 案例一:group 18,R475 内 position 52 + +operation 为 `flow:amesim_pnl0001_17.port_1`,输出 `q[152]`,使用 `pipe_cache[28]`。 + +### 查询及必要前置条件 + +Jacobian 200,t≈0.372854446 s,入口 count=75,capacity=256。 + +| 项目 | baseline | probe | +|---|---:|---:| +| states[74].p | 15019640.74937454 | 15019641.148960622 | +| states[74].T | 460.10768147887495 | 460.1076863719882 | +| states[74].valid | 21 | 21 | +| 上游 PT key 的 p | 15019625.056587901 | 相同 | +| 上游 PT key 的 T | 529.0977489177745 | 相同 | +| 下游 PT key 的 p | 15019613.376778807 | 相同 | +| 下游 PT key 的 T | 529.0975843945621 | 相同 | + +完整 medium key 为 `(real_helium=1, R=2077.26439404998, cp=5193.1609851249505, Tref=293.15, slope=0, mu=1.96e-5, muT=293.15, S=79.4)`。实际二进制位值保存在事件和清单来源 JSON 中,不依赖上表小数显示精度。 + +两次 PT 都 miss,分别扫描 75、76 项,追加到 slot 75、76。slot 74 的两次访问均为 `valid & 1 → p == query.p`;压力不等后短路,未读取其 T 或 medium。满足同样排除关系即可允许这项差异,不要求两次入口的 slot 74 压力相等。 + +此时 `p[44] <= g[40].p`:温度来自 `g[40].T`,没有 PH 查询,`h[178]` 没有被本次分支消费。需要保持压力方向、近零流量分支、实际温度来源、medium 和管道常量一致。 + +### 实际读取 → 必须保留的 probe 数据 + +- 上游新条目:p、T、medium、mu、rho、isentropic_factor、isentropic_exponent,以及温度观察器/Jacobian memo 指针。 +- 下游新条目:p、T、medium、rho、两个 isentropic 字段和上述指针;没有消费下游 mu/h。 +- 选中后的 valid 测试依次为:上游 MU=0、ISENTROPIC=0、RHO=0;下游 ISENTROPIC=0、RHO=0;再次上游 RHO=4。 +- `pipe_cache[28]` 入口只读 valid=0,其余旧键字段被短路跳过。 +- 保留 probe 的 `states[0:75]` 全部内容,尤其是 slot 74 的新 p/T;其他 39 个 pipe 槽保持 probe 值。这条实测路径没有写任何已有物性条目。 + +### 有序字段更新与追加动作 + +1. count 75→76;在 slot 75 整体清零,依次写 medium、p、T、valid=PT、temperatures、jacobian。 +2. 写 mu,`valid |= MU`;写 rho,`valid |= RHO`;写 factor、exponent,`valid |= ISENTROPIC`。上游最终 valid=29。 +3. count 76→77;同样初始化 slot 76;写 rho、RHO 位、factor、exponent、ISENTROPIC 位。下游最终 valid=21,未写 h/mu 的计算值,但初始化清零必须执行。 +4. pipe[28] 按顺序写 valid=0、medium、p1、p2、T、diameter、length、roughness、kind、flow、valid=1。首次 valid=0 是真实的 0→0 写入。 +5. 输出 `q[152]=-1.4080633385291488e-6`;model evaluator 返回 1。 + +### 不能省略的其他实测分支 + +| position 52 查询序列 | probe 次数 | 代表 Jacobian | +|---|---:|---:| +| PT miss → PT miss | 727 | 200 | +| PH miss → PT miss(登记 h)→ PT hit → PT miss | 167 | 2 | +| PH hit → PT hit → PT miss | 1 | 1 | +| 无物性查询,近零流量 | 1 | 0 | + +PH miss 路径需要测试 H 位并显式写 h、`valid |= H`,随后 pipe 的 PT 查询复用这个刚创建的逻辑条目;不能把后一次 hit 另当一次追加。PH hit 路径实际消费已有条目的 T,随后读取其已有效的 mu/rho/isentropic 字段;这些入口载荷必须比较。Jacobian 1 中首次匹配为 slot 7,仅新增下游条目。两条补充分支的完整 key 和写入顺序已收入实测清单。 + +全程有 24 对入口 count 不同,偏移为 +1 或 +2;其查询和逻辑字段仍一致。位置 52 也需要动态 slot 映射,不能固定为 75/76。 + +## 案例二:group 6,R288 / position 16 + +operation 为 `flow:amesim_pnl0001_1.port_1`,输出 `q[45]`,使用 `pipe_cache[0]`。 + +### 查询及必要前置条件 + +Jacobian 200 的 medium 与上例相同: + +| 逻辑条目 / 查询顺序 | p | T | baseline slot | probe slot | +|---|---:|---:|---:|---:| +| 上游 U | 15019678.822746754 | 291.8496414260662 | miss → 12 | miss → 13 | +| 下游 D | 15019662.638804033 | 291.84951601402554 | miss → 13 | miss → 14 | + +baseline 扫描 12、13 项;probe 扫描 13、14 项。probe 增加的入口项都不匹配当前查询,且两次追加时均有容量。**相同逻辑条目由查询 key 和创建次序定义,不能用“相同数组下标”定义。** + +此时 `p[3] <= g[4].p`,使用 `g[4].T`,未消费 `h[64]`。需要比较实际压力/温度输入、medium/管道常量,以及查询和分支前置条件。 + +### 实际读取 → 必须保留的 probe 数据 + +U、D 的消费字段及 valid 测试与上例的两次 PT miss 路径一致,只需映射到 probe slot 13/14。pipe[0] 只消费入口 valid=0。 + +**保留 probe `states[0:13]` 的每个字段,包括 slot 12,以及其他全部 pipe 槽。** 本 operation 没有对已有物性条目的写入。入口 count 不需要等于 baseline,但它决定下一次追加位置,不能忽略其作用。 + +### 有序字段更新与追加动作 + +1. 在 probe 当前尾部追加 U:count 13→14,slot 13 完整初始化,写 mu/rho/factor/exponent 和对应 valid 位,最终 valid=29。 +2. 重新按更新后的 probe context 查询 D;miss 后 count 14→15,slot 14 初始化,写 rho/factor/exponent 和对应 valid 位,最终 valid=21。 +3. pipe[0] 执行同样的 11 次有序写入,包括同值 valid=0。 +4. `q[45]=-3.34452871903457e-6`,model evaluator 返回 1。 + +把 baseline slot 12/13 的退出快照直接覆盖到 probe 会损坏原 slot 12;把 count 恢复为 baseline 14 会丢失 probe 的有效尾项。正确动作是“保留 probe 前缀,在 probe 当前 count 顺序追加”。 + +896 对中,638 对存在 count 差异,最大偏移 +4;829 对走两次 PT miss,67 对走近零流量路径且不增加 count。全部能建立一致的逻辑映射。 + +## 比较、保留、写回与 fallback 的边界 + +| 入口差异 / 动作 | 本次清单要求 | +|---|---| +| 未选中的条目 p/T/h/medium 不同 | 可允许数值不同,但必须验证仍被当前查询排除,不改变首次匹配关系 | +| count / slot 平移 | 可允许;用当前 probe count 检查容量,按 key/首次命中和创建顺序建立映射 | +| 选中已有条目的有效载荷不同 | 必须比较本分支实际消费的 T、mu、rho、factor、exponent 等;不一致则 fallback | +| valid 位不同 | 比较实际测试的掩码结果;未知分支不可放行。`valid |= flag` 应保留其余 probe 位,不能泛化为整字覆盖 | +| 入口未被读取的 h 或 pipe 旧键值不同 | 本分支可以忽略比较;仍须保留未被实际写入的 probe 数据 | +| 新条目 | 当前尾部追加;完整初始化再按事件次序赋值,同值写入也不能删除 | +| 已有条目更新 | 两例的完整运行未观察到。插桩已覆盖相关赋值,但不能声称已实测该分支;若后续出现,需要保留其原有有效字段、按映射定点更新 | +| 容量不足、无效 key、scratch 路径 | 本次未出现。清单要求 fallback,不能套用“必定追加两项” | +| 非空 temperature observer | 本次均为 NULL;非空会有外部观察器副作用,未验证,fallback | +| pipe 入口命中分支改变 | 本次目标槽均 valid=0;若变为有效需重新核验完整 pipe key、flow 载荷和分支,不能沿用本清单 | +| 首匹配不同 / 重复 key 改变匹配先后 | fallback;单纯“某处有同 key”不够 | + +Jacobian scalar memo 是另一个需要保留的作用域:其 key、get 命中、put 尝试、recording/capacity 已记录。Jacobian 200 baseline 的两次 density 和一次 pipe scalar get 为 miss,probe 均 hit;返回数值一致。此处不能简单回放 baseline 的 memo 写入或统计副作用。当前诊断不修改该 memo;后续局部复用必须维持其生命周期和统计语义。 + +operation 本身是一次 double 赋值,没有独立 int 状态码。记录的是实际输出/pipe flow,以及包围它的 model evaluator 返回值;没有虚构 operation 成功码。 + +## 验收结果 + +- 3,584 次 operation(2 案例 × baseline/probe × 896),全部访问校验和退出 context 重放一致。 +- 1,382,612 次字段/基址读取逐条核对通过;valid 掩码测试另外记录并校验。 +- 每例 896 对的完整查询 key、hit/miss、首次匹配关系经过独立验证;按逻辑映射后的消费字段集合、valid 测试及有序写入一致。 +- 136,606 次显式写入,其中 **10,614 次已知同值写入**;检查包括 pipe valid 的 0→0,未把未初始化槽的清零算作已知同值写入。 +- 状态、输出、事件及所有 896 个 132×132 Jacobian 的 SHA-256 与已有未插桩基线一致;矩阵元素共 15,611,904 个。 +- accepted=10,840,rejected=918,nfev=44,467,njev=896,nlu=3,106;Newton iterations=19,371,Newton convergence failures=798,均与基线一致。 +- 2,688 次包含目标 operation 的 model evaluator 调用均返回 1;baseline 每次含两个目标 operation,所以该数小于 operation 数量。 +- 证据校验器另有 6 个负例:遗漏同值写入、rho 读取错误、追加使用 baseline slot、恢复 baseline count、错误首匹配、错误 valid 位结果。它们是对证据校验器的反例测试,**不是已实现的生产 fallback 测试**。 + +插桩会引入大量日志开销,本次时长不用于评价优化收益。容量耗尽、scratch、非空 observer、已有物性条目更新及 pipe 命中分支未在这两个案例内实测,仍是未来实现时的明确边界。 + +## 复现与交付文件 + +需先保留已有 local-probe、context-fallback worker 及其 baseline 验证文件。它们的生成入口分别是 `local_probe_experiment.py`、`diagnose_context_fallback.py`;本次脚本会对缺失来源报错,不生成替代基线。 + +```powershell +.venv-win\Scripts\python.exe tests/manual/diagnose_context_access.py prepare +.venv-win\Scripts\python.exe tests/manual/diagnose_context_access.py run +.venv-win\Scripts\python.exe tests/manual/analyze_context_access.py +.venv-win\Scripts\python.exe tests/manual/verify_context_access_evidence.py +``` + +所有路径由脚本所在项目目录推导。原始证据和生成 worker 位于 `test/context-access-20260917/`,属于被 Git 忽略的运行产物: + +- `worker/build.json`:来源及插桩源码 SHA-256。 +- `audit/access.jsonl`:逐次实际查询、读取、测试、写入、分配、输出和返回事件。 +- `audit/measurement.json`:全量仿真数值一致性哈希。 +- `summary.json`:逐 Jacobian 的两路径对应关系。 +- `operations.json`:全部 3,584 次 operation 的查询、消费字段、valid 测试、有序更新与追加。 +- `jacobian-200.json`:两个主案例的原始事件和结构化清单。 +- `negative-checks.json`:六项证据负例结果。 + +本报告及 [实测数值清单](context_access_checklists.md) 保存在 `tests/manual/`,可与诊断脚本一起提交。 diff --git a/tests/manual/context_fallback_diag.c b/tests/manual/context_fallback_diag.c new file mode 100644 index 0000000..e38dbba --- /dev/null +++ b/tests/manual/context_fallback_diag.c @@ -0,0 +1,213 @@ +/* Included after the unchanged local_probe_support.c, so baseline snapshots + are observed directly. None of the original guard or restore code is edited. */ +#include "context_fallback_diag.h" +#include +/* DIAG_TABLES */ +int dx_mode,dx_selected,dx_region=-1,dx_position=-1,dx_shadow; +static uint64_t dx_jac,dx_sampled,dx_tick0,dx_q0,dx_region_tick,dx_op_tick; +static unsigned dx_stride=16,dx_seed=1,dx_pick; +static double dx_time; +typedef struct { + uint64_t attempts,compares,failures,region_ticks,op_ticks,ops; + uint64_t input_diff_ops,output_diff_ops,exit_output_diff,exit_context_diff; +} DxRegion; +static DxRegion dx_stats[LP_NC][LP_NR]; +static uint64_t dx_op_count[LP_NC][LP_NO],dx_op_ticks[LP_NC][LP_NO]; +static uint64_t dx_kernel_count[LP_NR][DX_NK],dx_kernel_inclusive[LP_NR][DX_NK],dx_kernel_exclusive[LP_NR][DX_NK]; +static double dx_inputs[DX_NIN],dx_outputs[DX_NOUT]; +static FILE *dx_fail_file,*dx_matrix_file; +static FILE *dx_input_file; +static FILE *dx_create_file; +static int dx_trace,dx_details,dx_first_context=-999,dx_seen[LP_NB],dx_source[LP_NB]; +static Snapshot dx_history[LP_NB]; +static struct {uint64_t start,child;int kind,region;} dx_stack[64]; +static int dx_depth; +typedef struct {const char *name;size_t offset,size;int floating;} DxField; +#define DX_FIELD(type,field,float_flag) {#field,offsetof(type,field),sizeof(((type*)0)->field),float_flag} +#define DX_MEDIUM(type,field,float_flag) {"medium." #field,offsetof(type,medium)+offsetof(NativeMedium,field),sizeof(((NativeMedium*)0)->field),float_flag} +#define DX_MEDIUM_FIELDS(type) DX_MEDIUM(type,real_helium,0),DX_MEDIUM(type,R,1),DX_MEDIUM(type,cp,1),DX_MEDIUM(type,Tref,1),DX_MEDIUM(type,slope,1),DX_MEDIUM(type,mu,1),DX_MEDIUM(type,muT,1),DX_MEDIUM(type,S,1) +static const DxField dx_state_fields[]={DX_MEDIUM_FIELDS(NativePropertyState), + DX_FIELD(NativePropertyState,p,1),DX_FIELD(NativePropertyState,T,1),DX_FIELD(NativePropertyState,h,1), + DX_FIELD(NativePropertyState,rho,1),DX_FIELD(NativePropertyState,mu,1),DX_FIELD(NativePropertyState,isentropic_factor,1), + DX_FIELD(NativePropertyState,isentropic_exponent,1),DX_FIELD(NativePropertyState,valid,0), + DX_FIELD(NativePropertyState,temperatures,0),DX_FIELD(NativePropertyState,jacobian,0)}; +static const DxField dx_pipe_fields[]={DX_MEDIUM_FIELDS(NativePipeCache), + DX_FIELD(NativePipeCache,p1,1),DX_FIELD(NativePipeCache,p2,1),DX_FIELD(NativePipeCache,T,1), + DX_FIELD(NativePipeCache,diameter,1),DX_FIELD(NativePipeCache,length,1),DX_FIELD(NativePipeCache,roughness,1), + DX_FIELD(NativePipeCache,flow,1),DX_FIELD(NativePipeCache,kind,0),DX_FIELD(NativePipeCache,valid,0)}; +typedef struct {int kind,index,field;size_t offset,size;uint64_t baseline,trial;} DxDifference; +static DxDifference dx_difference(NativePropertyCache*,NativePipeCache*,Snapshot*); +static void dx_boundary(int pos,int source,NativePropertyCache *p,NativePipeCache *pipes){ + if(!dx_trace || lp_color<0)return; + int v=dx_version[pos];if(dx_seen[v])return; + Snapshot *s=&dx_history[v];memset(s,0,sizeof(*s)); + s->count=p->count;s->capacity=p->capacity;s->temperatures=p->temperatures;s->jacobian=p->jacobian; + memcpy(s->states,p->states,p->count*sizeof(*p->states));memcpy(s->pipes,pipes,sizeof(s->pipes)); + dx_seen[v]=1;dx_source[v]=source; + if(dx_first_context==-999 && dx_difference(p,pipes,&saved->snapshots[v]).kind)dx_first_context=source; +} +static int dx_field_equal(Snapshot *a,Snapshot *b,DxDifference d){ + if(d.kind==1){uint64_t x[]={a->count,a->capacity,(uintptr_t)a->temperatures,(uintptr_t)a->jacobian};uint64_t y[]={b->count,b->capacity,(uintptr_t)b->temperatures,(uintptr_t)b->jacobian};return x[d.field]==y[d.field];} + if(d.kind==2 && ((size_t)d.index>=a->count || (size_t)d.index>=b->count))return (size_t)d.index>=a->count && (size_t)d.index>=b->count; + unsigned char *x=(unsigned char*)(d.kind==2?(void*)&a->states[d.index]:(void*)&a->pipes[d.index]); + unsigned char *y=(unsigned char*)(d.kind==2?(void*)&b->states[d.index]:(void*)&b->pipes[d.index]); + return memcmp(x+d.offset,y+d.offset,d.size)==0; +} +static int dx_origin(DxDifference d,int v,int *first){ + int previous_equal=1,origin=-999;*first=-999; + for(int i=0;i<=v;i++)if(dx_seen[i]){ + int equal=dx_field_equal(&dx_history[i],&saved->snapshots[i],d); + if(!equal && previous_equal){origin=dx_source[i];if(*first==-999)*first=origin;} + previous_equal=equal; + } + return origin; +} +static DxDifference dx_difference(NativePropertyCache *p,NativePipeCache *pipes,Snapshot *b){ + DxDifference d={0,-1,-1,0,0,0,0}; + uint64_t bm[]={b->count,b->capacity,(uintptr_t)b->temperatures,(uintptr_t)b->jacobian}; + uint64_t pm[]={p->count,p->capacity,(uintptr_t)p->temperatures,(uintptr_t)p->jacobian}; + for(int i=0;i<4;i++)if(bm[i]!=pm[i]){d.kind=1;d.field=i;d.baseline=bm[i];d.trial=pm[i];return d;} + for(int kind=2;kind<=3;kind++){ + size_t size=kind==2?sizeof(NativePropertyState):sizeof(NativePipeCache),count=kind==2?b->count:LP_NPC; + const unsigned char *a=(const unsigned char*)(kind==2?(void*)b->states:(void*)b->pipes),*c=(const unsigned char*)(kind==2?(void*)p->states:(void*)pipes); + const DxField *fields=kind==2?dx_state_fields:dx_pipe_fields; + int nf=kind==2?(int)(sizeof(dx_state_fields)/sizeof(*dx_state_fields)):(int)(sizeof(dx_pipe_fields)/sizeof(*dx_pipe_fields)); + for(size_t i=0;i=fields[f].offset && offsnapshots[lp_before[r]]); + if(!d.kind){fprintf(stderr,"failure without difference\n");abort();} + int first=-999,origin=dx_trace?dx_origin(d,dx_version[dx_start_pos[r]],&first):-999; + fprintf(dx_fail_file,"{\"jac\":%llu,\"t\":%.17g,\"group\":%d,\"region\":%d,\"kind\":%d,\"index\":%d,\"field\":%d,\"offset\":%llu,\"baseline\":\"%016llx\",\"trial\":\"%016llx\",\"firstOrigin\":%d,\"persistentOrigin\":%d,\"firstContextOrigin\":%d}\n", + (unsigned long long)(dx_jac-1),dx_time,lp_color,r,d.kind,d.index,d.field,(unsigned long long)d.offset,(unsigned long long)d.baseline,(unsigned long long)d.trial,first,origin,dx_first_context); +} +void dx_initialize(void){ + const char *m=getenv("CONTEXT_DIAG_MODE"),*s=getenv("CONTEXT_DIAG_STRIDE"),*seed=getenv("CONTEXT_DIAG_SEED"),*mat=getenv("CONTEXT_DIAG_MATRICES"); + dx_mode=m?atoi(m):1;dx_stride=s?(unsigned)atoi(s):16;dx_seed=seed?(unsigned)atoi(seed):1; + dx_q0=lp_tick();dx_tick0=dx_clock(); + if(dx_mode==1 || dx_mode==5){dx_fail_file=fopen("failures.jsonl","wb");if(!dx_fail_file)abort();setvbuf(dx_fail_file,NULL,_IOFBF,1024*1024);} + if(dx_mode==5){dx_input_file=fopen("trace-inputs.jsonl","wb");if(!dx_input_file)abort();} + if(dx_mode==5){dx_create_file=fopen("trace-created.jsonl","wb");if(!dx_create_file)abort();} + if(mat && atoi(mat)){dx_matrix_file=fopen("jacobians.bin","wb");if(!dx_matrix_file)abort();setvbuf(dx_matrix_file,NULL,_IOFBF,1024*1024);} +} +void dx_jacobian(void){ + unsigned pos=(unsigned)(dx_jac++%(dx_stride?dx_stride:1)); + if(!pos){dx_seed^=dx_seed<<13;dx_seed^=dx_seed>>17;dx_seed^=dx_seed<<5;dx_pick=dx_seed%(dx_stride?dx_stride:1);} + dx_selected=dx_mode==1 || dx_mode==5 || (dx_stride && pos==dx_pick); + dx_trace=dx_mode==5; + dx_details=dx_mode==5 && (dx_jac==1 || dx_jac==201 || dx_jac==451 || dx_jac==701 || dx_jac==896); + if(dx_selected)dx_sampled++; +} +void dx_eval_begin(double t,const double *y){(void)y;dx_time=t;dx_region=-1;dx_position=-1;dx_first_context=-999;memset(dx_seen,0,sizeof(dx_seen));} +void dx_eval_end(void){if(dx_region>=0){fprintf(stderr,"unclosed fallback region\n");abort();}dx_position=-1;} +void dx_schedule(NativePropertyCache *p,NativePipeCache *pipes){dx_region=-1;dx_boundary(0,-2,p,pipes);} +int dx_reuse(int r,NativePropertyCache *p,NativePipeCache *pipes,double *pv,double *h,double *q,double *w,double *fb){ + int ok=lp_reuse(r,p,pipes,pv,h,q,w,fb); + DxRegion *s=&dx_stats[lp_color][r];s->attempts++;if(dx_contextual[r])s->compares++; + if(!ok){ + if(!dx_contextual[r])abort(); + s->failures++;dx_region=r; + if(dx_fail_file)dx_failure(r,p,pipes); + if(dx_mode>=2 && dx_mode<=4)dx_region_tick=dx_clock(); + } + else dx_boundary(lp_end[r],-1000-r,p,pipes); + return ok; +} +void dx_op_begin(int pos,const double *inputs){ + dx_position=pos; + if(lp_color<0){if(dx_mode==1 || dx_mode==5)memcpy(dx_inputs+dx_in_offset[pos],inputs,(dx_in_offset[pos+1]-dx_in_offset[pos])*sizeof(double));return;} + if(dx_details){ + int any=0; + for(int i=0;i=2 && dx_mode<=4)dx_stats[lp_color][r].region_ticks+=dx_clock()-dx_region_tick; + if(dx_mode==1 || dx_mode==5){ + double *dst[]={p,h,q,w,fb};double *src[]={saved->p,saved->h,saved->q,saved->w,saved->fb};int changed=0; + for(int i=lp_first_output[r];isnapshots[lp_after[r]]).kind!=0; + } + dx_region=-1; +} +uint64_t dx_kernel_begin(int kind){ + if(dx_mode==5 && dx_details && !dx_shadow && dx_position>=0){ + if(dx_region>=0 && lp_color>=0)dx_kernel_count[dx_region][kind]++; + if(dx_depth>=64)abort(); + dx_stack[dx_depth].kind=kind;dx_depth++;return 1; + } + if(!dx_selected || dx_shadow || dx_region<0 || lp_color<0)return 0; + dx_kernel_count[dx_region][kind]++; + if(dx_mode!=4)return 0; + if(dx_depth>=64)abort(); + uint64_t t=dx_clock();dx_stack[dx_depth].start=t;dx_stack[dx_depth].child=0;dx_stack[dx_depth].kind=kind;dx_stack[dx_depth].region=dx_region;dx_depth++;return t; +} +void dx_kernel_end(int kind,uint64_t start){ + if(!start)return; + if(start==1 && dx_mode==5){if(--dx_depth<0 || dx_stack[dx_depth].kind!=kind)abort();return;} + uint64_t elapsed=dx_clock()-start;int i=--dx_depth; + if(i<0 || dx_stack[i].kind!=kind)abort(); + int r=dx_stack[i].region;dx_kernel_inclusive[r][kind]+=elapsed;dx_kernel_exclusive[r][kind]+=elapsed-dx_stack[i].child; + if(i)dx_stack[i-1].child+=elapsed; +} +void dx_property_created(NativePropertyCache *p,NativePropertyState *s){ + if(!dx_details || dx_position<0 || !p)return; + uintptr_t a=(uintptr_t)s,b=(uintptr_t)p->states; + int slot=a>=b && acapacity*sizeof(*s)?(int)((a-b)/sizeof(*s)):-1; + fprintf(dx_create_file,"{\"jac\":%llu,\"group\":%d,\"position\":%d,\"slot\":%d,\"p\":%.17g,\"T\":%.17g,\"count\":%llu,\"path\":[",(unsigned long long)(dx_jac-1),lp_color,dx_position,slot,s->p,s->T,(unsigned long long)p->count); + for(int i=0;iattempts)continue; + fprintf(f,"%s{\"group\":%d,\"region\":%d,\"attempts\":%llu,\"compares\":%llu,\"failures\":%llu,\"regionTicks\":%llu,\"opTicks\":%llu,\"ops\":%llu,\"inputDiffOps\":%llu,\"outputDiffOps\":%llu,\"exitOutputDiff\":%llu,\"exitContextDiff\":%llu}",comma++?",":"",g,r,(unsigned long long)s->attempts,(unsigned long long)s->compares,(unsigned long long)s->failures,(unsigned long long)s->region_ticks,(unsigned long long)s->op_ticks,(unsigned long long)s->ops,(unsigned long long)s->input_diff_ops,(unsigned long long)s->output_diff_ops,(unsigned long long)s->exit_output_diff,(unsigned long long)s->exit_context_diff); + } + fprintf(f,"],\"operations\":[");comma=0; + for(int g=0;g +#include +#include "local_probe.h" +extern int dx_mode,dx_selected,dx_region,dx_position,dx_shadow; +extern const int dx_start_pos[LP_NR],dx_contextual[LP_NR],dx_version[LP_NO+1],dx_mutates[LP_NO]; +extern const int dx_in_offset[LP_NO+1],dx_out_offset[LP_NO+1]; +void dx_initialize(void);void dx_finish(void);void dx_jacobian(void); +int dx_reuse(int,NativePropertyCache*,NativePipeCache*,double*,double*,double*,double*,double*); +void dx_eval_begin(double,const double*);void dx_eval_end(void); +void dx_schedule(NativePropertyCache*,NativePipeCache*); +void dx_op_begin(int,const double*);void dx_op_end(int,const double*,NativePropertyCache*,NativePipeCache*); +void dx_region_end(double*,double*,double*,double*,double*,NativePropertyCache*,NativePipeCache*); +int dx_eval(double,const double*,double*,double*,ModelJacobianWorkspace*); +uint64_t dx_kernel_begin(int);void dx_kernel_end(int,uint64_t); +void dx_validate_matrix(double,const double*,const double*); +void dx_property_created(NativePropertyCache*,NativePropertyState*); +static inline uint64_t dx_clock(void){unsigned lo,hi;__asm__ __volatile__("lfence\n\trdtsc\n\tlfence":"=a"(lo),"=d"(hi)::"memory");return ((uint64_t)hi<<32)|lo;} +#endif diff --git a/tests/manual/context_shadow_replay.c b/tests/manual/context_shadow_replay.c new file mode 100644 index 0000000..5eab015 --- /dev/null +++ b/tests/manual/context_shadow_replay.c @@ -0,0 +1,578 @@ +/* Diagnostic only. The semantic interpreter never calls a physics kernel. */ +#include "context_access_diag.h" +#include "context_shadow_replay.h" +#include +#include +#include +#include +#include +#include +#if defined(__SSE__) +#include +#endif +typedef struct {fenv_t standard;int rounding;unsigned sse;} SREnvironment; +static unsigned sse_control(void){ +#if defined(__SSE__) + return _mm_getcsr(); +#else + return 0; +#endif +} +static void save_environment(SREnvironment *env){ + fegetenv(&env->standard);env->rounding=fegetround();env->sse=0; +#if defined(__SSE__) + env->sse=_mm_getcsr(); +#endif +} +static void restore_environment(const SREnvironment *env){ + fesetenv(&env->standard);fesetround(env->rounding); +#if defined(__SSE__) + /* MinGW's fesetenv does not restore the complete SSE control register. */ + _mm_setcsr(env->sse); +#endif +} + +enum {OFF,RECORD,REFERENCE,CANDIDATE,TAIL}; +enum {READ_STATE=1,WRITE_STATE,WRITE_OR,READ_PIPE,WRITE_PIPE,QUERY,ALLOCATE,VALID,SCALAR}; +enum {OK,NO_RECORD,INPUTS,FIRST_MATCH,QUERY_PATH,CONSUMED,VALID_BITS,CAPACITY,OBSERVER, + EXISTING_UPDATE,PIPE_BRANCH,UNKNOWN_EFFECT,NONFINITE,MEMO_BINDING,MEMO_MISS,MEMO_VALUE,OVERFLOW,NREASONS}; +static const char *reasons[]={"accepted","no_current_record","inputs","first_match_relation","query_hit_miss_path", + "consumed_field","valid_test","capacity_scratch","observer_nonnull","existing_entry_update","pipe_hit_branch", + "unknown_effect_or_schema","nonfinite_or_uncovered_branch","memo_lifetime_binding","memo_miss","memo_value","metadata_overflow"}; +typedef struct { + int type,slot,offset,size,aux; + unsigned mask; + unsigned char data[sizeof(NativePropertyState)]; + double result; +} Event; +typedef struct { + Event events[SR_EVENTS]; + unsigned long long jac; + uintptr_t memo_binding; + double inputs[4],output; + size_t entry_count; + int position,n,ready,error,query_open,query_index,path; + int required_flags,baseline_errno,rounding; + unsigned required_sse_flags,sse_mode; +} Plan; +typedef struct { + unsigned long long total,accepted,rejected,mismatches,count_different,relocated,relocated_slots,appends; + unsigned long long paths[4],pt_hit,pt_miss,ph_hit,ph_miss,rejects[NREASONS]; + unsigned long long live_contamination,rollbacks,tail_equal,live_tail_equal,memo_immutable,negative_passed; + unsigned long long untouched_equal,metadata_immutable; + size_t metadata_min,metadata_max,event_max; +} Statistics; +static Plan plan; +static Statistics stats; +static int phase,position,group,configured,pending_live,pending_status; +static unsigned long long jac; +static double sim_time,op_inputs[4]; +static NativePropertyCache *bound; +static NativePipeCache *bound_pipes; +static SRContext entry,reference,candidate,overlay,before_transaction,tail_ref,tail_cand; +static SRFrame frame_ref,frame_cand; +static FILE *trials,*negative; +static unsigned long long forbidden_native_calls; + +static void fatal(const char *message){fprintf(stderr,"shadow fatal: %s\n",message);abort();} +static void initialize(void){ + if(configured)return; + const char *s=getenv("CONTEXT_SHADOW_POSITION");configured=s?atoi(s):16; + if(configured!=16 && configured!=52)fatal("invalid stage"); + trials=fopen("shadow-trials.jsonl","wb");negative=fopen("shadow-negative.jsonl","wb"); + if(!trials || !negative)fatal("open logs"); + stats.metadata_min=(size_t)-1; +} +void sr_native_enter(const char *name){ + if(phase==CANDIDATE){forbidden_native_calls++;fprintf(stderr,"Candidate called %s\n",name);fatal("physical call during replay");} +} +static int locate(const void *ptr,size_t width,int *domain,int *slot,int *offset){ + uintptr_t p=(uintptr_t)ptr; + const void *bases[]={bound,bound?bound->states:NULL,bound_pipes}; + size_t sizes[]={sizeof(NativePropertyCache),bound?bound->capacity*sizeof(NativePropertyState):0,SR_PIPES*sizeof(NativePipeCache)}; + size_t units[]={sizeof(NativePropertyCache),sizeof(NativePropertyState),sizeof(NativePipeCache)}; + for(int i=0;i<3;i++)if(bases[i] && p>=(uintptr_t)bases[i] && p-(uintptr_t)bases[i]+width<=sizes[i]){ + size_t delta=p-(uintptr_t)bases[i];*domain=i;*slot=(int)(delta/units[i]);*offset=(int)(delta%units[i]);return 1; + } + return 0; +} +static Event *event(int type){ + if(plan.n>=SR_EVENTS){plan.error=OVERFLOW;return NULL;} + Event *e=&plan.events[plan.n++];memset(e,0,sizeof(*e));e->type=type;return e; +} +static int state_pointer(int offset){ + return offset==(int)offsetof(NativePropertyState,jacobian) || offset==(int)offsetof(NativePropertyState,temperatures); +} +static int floating_field(int domain,int offset,int size){ + if(size!=8)return 0; + if(domain==1 && state_pointer(offset))return 0; + return offset>=(int)offsetof(NativeMedium,R) && offset<(domain==1?(int)offsetof(NativePropertyState,valid):(int)offsetof(NativePipeCache,kind)); +} +void ax_access(const char *action,const void *ptr,size_t width,const char *field,const char *fn){ + (void)field; + if(phase!=RECORD)return; + int domain,slot,offset; + if(!locate(ptr,width,&domain,&slot,&offset)){ + /* The only recorded store to a stack local is scalar_get's output. */ + if(!strcmp(action,"write") && strcmp(fn,"native_jacobian_scalar_get"))plan.error=UNKNOWN_EFFECT; + return; + } + if(domain==0){ + if(!strcmp(action,"write") && offset!=(int)offsetof(NativePropertyCache,count))plan.error=UNKNOWN_EFFECT; + return; /* count/capacity and pointer bindings are semantic guards. */ + } + if(plan.query_open && !strcmp(action,"read"))return; /* Re-run query, not baseline's scan slots. */ + if(width>sizeof(NativePropertyState)){plan.error=UNKNOWN_EFFECT;return;} + int write=!strcmp(action,"write"); + Event *e=event(domain==1?(write?WRITE_STATE:READ_STATE):(write?WRITE_PIPE:READ_PIPE)); + if(!e)return; + e->slot=slot;e->offset=offset;e->size=(int)width;memcpy(e->data,ptr,width); + if(floating_field(domain,offset,(int)width)){double x;memcpy(&x,ptr,8);if(!isfinite(x))plan.error=NONFINITE;} + if(domain==1 && write && (size_t)slotprevious){Event *e=&plan.events[plan.n-1];e->type=WRITE_OR;e->mask=mask;} +} +void ax_bind(NativePropertyCache *p,NativePipeCache *pipes){bound=p;bound_pipes=pipes;} +void ax_begin(unsigned long long j,int color,int pos,double t,const double *inputs,int n){ + initialize();phase=OFF;jac=j;group=color;position=pos;sim_time=t; + if(pos!=configured)return; + if(n!=4 || !inputs)fatal("operation signature"); + memcpy(op_inputs,inputs,sizeof(op_inputs)); + if(color<0){ + memset(&plan,0,sizeof(plan));plan.jac=j;plan.position=pos;plan.entry_count=bound->count; + plan.memo_binding=(uintptr_t)bound->jacobian;memcpy(plan.inputs,inputs,sizeof(plan.inputs)); + plan.rounding=fegetround(); + plan.sse_mode=sse_control()&~63u; + if(bound->temperatures)plan.error=OBSERVER; + phase=RECORD; + } +} +void ax_query(const char *kind,const NativeMedium *m,double p,double second){ + if(phase!=RECORD)return; + if(plan.query_open){plan.error=UNKNOWN_EFFECT;return;} + Event *e=event(QUERY);if(!e)return; + e->aux=!strcmp(kind,"PH");e->offset=m->real_helium;e->slot=-2; + double key[]={p,second,m->R,m->cp,m->Tref,m->slope,m->mu,m->muT,m->S}; + memcpy(e->data,key,sizeof(key));for(int i=0;i<9;i++)if(!isfinite(key[i]))plan.error=NONFINITE; + plan.query_open=1;plan.query_index=plan.n-1; +} +void ax_match(const char *kind,NativePropertyState *s){ + if(phase!=RECORD)return; + if(!plan.query_open){plan.error=UNKNOWN_EFFECT;return;} + Event *e=&plan.events[plan.query_index]; + if(e->aux!=(!strcmp(kind,"PH")))plan.error=UNKNOWN_EFFECT; + e->slot=s?(int)(s-bound->states):-1;plan.query_open=0; +} +void ax_new(NativePropertyCache *cache,NativePropertyState *s,int valid){ + if(phase!=RECORD)return; + int domain,slot,offset; + if(!valid || !cache || !locate(s,sizeof(*s),&domain,&slot,&offset) || domain!=1 || offset || (size_t)slot+1!=cache->count){plan.error=CAPACITY;return;} + Event *e=event(ALLOCATE);if(e)e->slot=slot; +} +unsigned ax_test(NativePropertyState *s,unsigned mask,const char *fn){ + (void)fn;unsigned result=s->valid&mask; + if(phase==RECORD && !plan.query_open){ + int domain,slot,offset; + if(!locate(s,sizeof(*s),&domain,&slot,&offset) || domain!=1){plan.error=CAPACITY;return result;} + Event *e=event(VALID);if(e){e->slot=slot;e->mask=mask;memcpy(e->data,&result,sizeof(result));} + } + return result; +} +void ax_scalar(const char *action,NativeJacobianScalars *memo,int kind,int medium,const double *keys,size_t n,int hit,const double *value){ + if(phase!=RECORD)return; + if(memo!=bound->jacobian || n>NATIVE_JACOBIAN_SCALAR_KEYS){plan.error=UNKNOWN_EFFECT;return;} + if(!strcmp(action,"get")){ + Event *e=event(SCALAR);if(!e)return;e->slot=kind;e->offset=medium;e->size=(int)n;memcpy(e->data,keys,n*8); + if(hit && value){e->result=*value;e->aux=1;} + }else if(!strcmp(action,"put_attempt")){ + int found=0; + for(int i=plan.n-1;i>=0;i--){Event *e=&plan.events[i];if(e->type==SCALAR && e->slot==kind && e->offset==medium && e->size==(int)n && !memcmp(e->data,keys,n*8)){ + if(!value || !isfinite(*value)){plan.error=NONFINITE;break;}e->result=*value;e->aux=1;found=1;break; + }} + if(!found)plan.error=UNKNOWN_EFFECT; + }else plan.error=UNKNOWN_EFFECT; +} +static int path_of_plan(void){ + int pattern[4],n=0; + for(int i=0;iaux*2+(e->slot>=0);} + if(n==0)return 0; + if(n==2 && pattern[0]==0 && pattern[1]==0)return 1; + if(n==4 && pattern[0]==2 && pattern[1]==0 && pattern[2]==1 && pattern[3]==0)return 2; + if(n==3 && pattern[0]==3 && pattern[1]==1 && pattern[2]==0)return 3; + return -1; +} +void ax_end(const double *outputs,int n){ + if(phase!=RECORD)return; + if(n!=1 || !isfinite(outputs[0]))plan.error=NONFINITE; + plan.output=outputs[0];plan.path=path_of_plan(); + plan.required_flags=fetestexcept(FE_ALL_EXCEPT);plan.baseline_errno=errno; + plan.required_sse_flags=sse_control()&63u; + if(plan.path<0 || plan.query_open)plan.error=NONFINITE; + for(int i=0;icontext.states=dst->states;dst->memo.entries=dst->entries; + if(src->context.jacobian==&src->memo)dst->context.jacobian=&dst->memo; + if(src->context.temperatures==&src->observer)dst->context.temperatures=&dst->observer; + for(size_t i=0;icontext.count;i++){ + if(src->states[i].jacobian==&src->memo)dst->states[i].jacobian=&dst->memo; + if(src->states[i].temperatures==&src->observer)dst->states[i].temperatures=&dst->observer; + } +} +static void from_live(SRContext *dst,NativePropertyCache *src,NativePipeCache *pipes){ + if(src->count>SR_STATES || src->capacity>SR_STATES || !src->jacobian || src->jacobian->capacity>MODEL_JACOBIAN_SCALAR_COUNT)fatal("context bounds"); + memset(dst,0,sizeof(*dst));memset(dst->states,0xa5,sizeof(dst->states)); + dst->context=*src;memcpy(dst->states,src->states,src->count*sizeof(*src->states));memcpy(dst->pipes,pipes,sizeof(dst->pipes)); + dst->memo=*src->jacobian;memcpy(dst->entries,src->jacobian->entries,src->jacobian->capacity*sizeof(*dst->entries)); + dst->memo.entries=dst->entries;dst->context.states=dst->states;dst->context.jacobian=&dst->memo; + if(src->temperatures){dst->observer=*src->temperatures;dst->context.temperatures=&dst->observer;} + for(size_t i=0;icount;i++){ + if(dst->states[i].jacobian==src->jacobian)dst->states[i].jacobian=&dst->memo; + if(src->temperatures && dst->states[i].temperatures==src->temperatures)dst->states[i].temperatures=&dst->observer; + } +} +static int same_medium_key(const NativeMedium *m,int kind,const double *key){ + return m->real_helium==kind && m->R==key[2] && m->cp==key[3] && m->Tref==key[4] && m->slope==key[5] && m->mu==key[6] && m->muT==key[7] && m->S==key[8]; +} +static int first_match(SRContext *ctx,const Event *e){ + double key[9];memcpy(key,e->data,sizeof(key)); + for(size_t i=0;icontext.count;i++){ + NativePropertyState *s=&ctx->states[i];unsigned mask=e->aux?NATIVE_PROPERTY_H:NATIVE_PROPERTY_PT; + if((s->valid&mask) && s->p==key[0] && (e->aux?s->h:s->T)==key[1] && same_medium_key(&s->medium,e->offset,key))return (int)i; + } + return -1; +} +static int map_slot(int *mapping,int logical,int actual){ + if(logical<0 || logical>=SR_STATES || actual<0 || actual>=SR_STATES)return 0; + if(mapping[logical]>=0)return mapping[logical]==actual; + for(int i=0;imemo;size_t capacity=memo->capacity; + if(!capacity || capacity>MODEL_JACOBIAN_SCALAR_COUNT || (capacity&(capacity-1)) || memo->recording)return MEMO_BINDING; + uint64_t hash=UINT64_C(14695981039346656037)^(unsigned)e->slot; + hash=(hash^(unsigned)e->offset)*UINT64_C(1099511628211); + for(int i=0;isize;i++){uint64_t bits;memcpy(&bits,e->data+i*8,8);hash=(hash^bits)*UINT64_C(1099511628211);hash^=hash>>32;} + if(!hash)hash=1; + size_t limit=capacity<32?capacity:32; + for(size_t i=0;ientries[(hash+i)&(capacity-1)]; + if(!v->hash)return MEMO_MISS; + if(v->hash==hash && v->kind==e->slot && v->medium_kind==e->offset && v->input_count==(size_t)e->size && !memcmp(v->inputs,e->data,e->size*8)){ + if(!isfinite(v->value) || memcmp(&v->value,&e->result,8))return MEMO_VALUE; + memo->reuses[e->slot]++;return OK; + } + } + return MEMO_MISS; +} +static int state_read_equal(SRContext *ctx,int slot,const Event *e){ + NativePropertyState *s=&ctx->states[slot]; + if(e->offset==(int)offsetof(NativePropertyState,jacobian))return s->jacobian==&ctx->memo; + if(e->offset==(int)offsetof(NativePropertyState,temperatures))return s->temperatures==NULL; + return !memcmp((unsigned char*)s+e->offset,e->data,e->size); +} +static int replay_overlay(SRContext *ctx,const double *inputs,int *mapping,int *appends){ + if(!plan.ready || plan.jac!=jac || plan.position!=position)return NO_RECORD; + if(plan.error)return plan.error; + if(fegetround()!=plan.rounding || (fetestexcept(FE_ALL_EXCEPT)&plan.required_flags)!=plan.required_flags || + (plan.baseline_errno && errno!=plan.baseline_errno) || (sse_control()&~63u)!=plan.sse_mode || + (sse_control()&plan.required_sse_flags)!=plan.required_sse_flags)return UNKNOWN_EFFECT; + if(memcmp(inputs,plan.inputs,sizeof(plan.inputs)))return INPUTS; + for(int i=0;i<4;i++)if(!isfinite(inputs[i]))return NONFINITE; + if(ctx->context.temperatures)return OBSERVER; + if(ctx->context.capacity>SR_STATES || ctx->context.count>ctx->context.capacity)return CAPACITY; + if(ctx->context.jacobian!=&ctx->memo || ctx->memo.entries!=ctx->entries || ctx->memo.recording || (uintptr_t)bound->jacobian!=plan.memo_binding)return MEMO_BINDING; + for(size_t i=0;icontext.count;i++)if(ctx->states[i].temperatures || ctx->states[i].jacobian!=&ctx->memo)return MEMO_BINDING; + if(ctx->pipes[position==16?0:28].valid)return PIPE_BRANCH; + for(int i=0;itypetype>SCALAR)return UNKNOWN_EFFECT; + if(e->type==QUERY){ + int actual=first_match(ctx,e); + if((e->slot<0)!=(actual<0))return QUERY_PATH; + if(e->slot>=0 && !map_slot(mapping,e->slot,actual))return FIRST_MATCH; + pending_miss=(actual<0 && !e->aux);continue; + } + if(e->type==ALLOCATE){ + if(!pending_miss)return UNKNOWN_EFFECT; + if(ctx->context.count>=ctx->context.capacity)return CAPACITY; + if(!map_slot(mapping,e->slot,(int)ctx->context.count))return FIRST_MATCH; + ctx->context.count++;(*appends)++;pending_miss=0;continue; + } + if(e->type==SCALAR){int reason=scalar_lookup(ctx,e);if(reason)return reason;continue;} + if(e->type==READ_PIPE || e->type==WRITE_PIPE){ + if(e->slot!=(position==16?0:28) || e->offset<0 || e->offset+e->size>(int)sizeof(NativePipeCache))return UNKNOWN_EFFECT; + unsigned char *ptr=(unsigned char*)&ctx->pipes[e->slot]+e->offset; + if(e->type==READ_PIPE){if(memcmp(ptr,e->data,e->size))return PIPE_BRANCH;} + else memcpy(ptr,e->data,e->size); + continue; + } + if(e->slot<0 || e->slot>=SR_STATES || (slot=mapping[e->slot])<0 || (size_t)slot>=ctx->context.count)return FIRST_MATCH; + NativePropertyState *s=&ctx->states[slot]; + if(e->type==VALID){unsigned expected;memcpy(&expected,e->data,sizeof(expected));if((s->valid&e->mask)!=expected)return VALID_BITS;continue;} + if(e->offset<0 || e->size<0 || e->offset+e->size>(int)sizeof(*s))return UNKNOWN_EFFECT; + if(e->type==READ_STATE){if(!state_read_equal(ctx,slot,e))return CONSUMED;continue;} + if(e->type!=WRITE_STATE && e->type!=WRITE_OR)return UNKNOWN_EFFECT; + if((size_t)e->slottype==WRITE_OR){if(e->offset!=(int)offsetof(NativePropertyState,valid))return UNKNOWN_EFFECT;s->valid|=e->mask;} + else if(e->offset==(int)offsetof(NativePropertyState,jacobian))s->jacobian=&ctx->memo; + else if(e->offset==(int)offsetof(NativePropertyState,temperatures))s->temperatures=NULL; + else memcpy((unsigned char*)s+e->offset,e->data,e->size); + } + if(pending_miss || !isfinite(plan.output))return NONFINITE; + return OK; +} +static void commit_patch(SRContext *dst,double *output,const int *mapping){ + /* Apply only validated stores, in recorded order. Everything not addressed + * by a store stays in the original Candidate allocation, even at commit. */ + for(int i=0;itype==ALLOCATE)dst->context.count++; + else if(e->type==SCALAR)dst->memo.reuses[e->slot]++; + else if(e->type==WRITE_PIPE)memcpy((unsigned char*)&dst->pipes[e->slot]+e->offset,e->data,e->size); + else if(e->type==WRITE_STATE || e->type==WRITE_OR){ + NativePropertyState *s=&dst->states[mapping[e->slot]]; + if(e->type==WRITE_OR)s->valid|=e->mask; + else if(e->offset==(int)offsetof(NativePropertyState,jacobian))s->jacobian=&dst->memo; + else if(e->offset==(int)offsetof(NativePropertyState,temperatures))s->temperatures=NULL; + else memcpy((unsigned char*)s+e->offset,e->data,e->size); + } + } + *output=plan.output; +} +static int transaction(SRContext *dst,const double *inputs,double *output,int *mapping,int *appends){ + copy_context(&overlay,dst);phase=CANDIDATE; + int reason=replay_overlay(&overlay,inputs,mapping,appends); + /* No stores to dst or output occur before this commit point. */ + if(!reason)commit_patch(dst,output,mapping); + phase=OFF;return reason; +} + +typedef struct {const char *name;size_t offset,size;} Field; +#define FIELD(T,n) {#n,offsetof(T,n),sizeof(((T*)0)->n)} +#define MEDIUM(T,n) {"medium." #n,offsetof(T,medium)+offsetof(NativeMedium,n),sizeof(((NativeMedium*)0)->n)} +#define MEDIUM_FIELDS(T) MEDIUM(T,real_helium),MEDIUM(T,R),MEDIUM(T,cp),MEDIUM(T,Tref),MEDIUM(T,slope),MEDIUM(T,mu),MEDIUM(T,muT),MEDIUM(T,S) +static const Field state_fields[]={MEDIUM_FIELDS(NativePropertyState),FIELD(NativePropertyState,p),FIELD(NativePropertyState,T),FIELD(NativePropertyState,h),FIELD(NativePropertyState,rho),FIELD(NativePropertyState,mu),FIELD(NativePropertyState,isentropic_factor),FIELD(NativePropertyState,isentropic_exponent),FIELD(NativePropertyState,valid)}; +static const Field pipe_fields[]={MEDIUM_FIELDS(NativePipeCache),FIELD(NativePipeCache,p1),FIELD(NativePipeCache,p2),FIELD(NativePipeCache,T),FIELD(NativePipeCache,diameter),FIELD(NativePipeCache,length),FIELD(NativePipeCache,roughness),FIELD(NativePipeCache,flow),FIELD(NativePipeCache,kind),FIELD(NativePipeCache,valid)}; +static char difference[160]; +static int differ(const char *domain,int slot,const char *field){snprintf(difference,sizeof(difference),"%s[%d].%s",domain,slot,field);return 1;} +static int compare_frame(const SRFrame *a,const SRFrame *b){ + const double *left[]={a->dy,a->w,a->p,a->h,a->q,a->fb,(const double*)a->g}; + const double *right[]={b->dy,b->w,b->p,b->h,b->q,b->fb,(const double*)b->g}; + const char *names[]={"dy","w","p","h","q","fb","gas_scalar"}; + const int counts[]={NSTATES,NOUTPUTS,48,232,232,152,56*5}; + for(int array=0;array<7;array++)for(int i=0;icontext.count!=b->context.count)return differ("property",-1,"count"); + if(a->context.capacity!=b->context.capacity)return differ("property",-1,"capacity"); + if(a->context.states!=a->states || b->context.states!=b->states)return differ("property",-1,"states_binding"); + if(a->context.jacobian!=&a->memo || b->context.jacobian!=&b->memo)return differ("property",-1,"memo_binding"); + if(!!a->context.temperatures!=!!b->context.temperatures)return differ("property",-1,"observer_binding"); + for(size_t i=0;icontext.count;i++){ + const NativePropertyState *x=&a->states[i],*y=&b->states[i]; + for(size_t f=0;fjacobian!=&a->memo || y->jacobian!=&b->memo)return differ("states",(int)i,"memo_binding"); + if(!!x->temperatures!=!!y->temperatures)return differ("states",(int)i,"observer_binding"); + } + for(int i=0;ipipes[i]+pipe_fields[f].offset,(const char*)&b->pipes[i]+pipe_fields[f].offset,pipe_fields[f].size))return differ("pipes",i,pipe_fields[f].name); + if(a->memo.entries!=a->entries || b->memo.entries!=b->entries || a->memo.capacity!=b->memo.capacity || a->memo.recording!=b->memo.recording)return differ("memo",-1,"lifecycle"); + if(memcmp(a->memo.evaluations,b->memo.evaluations,sizeof(a->memo.evaluations)))return differ("memo",-1,"evaluations"); + if(memcmp(a->memo.reuses,b->memo.reuses,sizeof(a->memo.reuses)))return differ("memo",-1,"reuses"); + for(size_t i=0;imemo.capacity;i++)if(memcmp(&a->entries[i],&b->entries[i],sizeof(*a->entries))){ + const Field fields[]={FIELD(NativeJacobianScalarEntry,hash),FIELD(NativeJacobianScalarEntry,inputs),FIELD(NativeJacobianScalarEntry,value),FIELD(NativeJacobianScalarEntry,kind),FIELD(NativeJacobianScalarEntry,medium_kind),FIELD(NativeJacobianScalarEntry,input_count)}; + for(size_t k=0;kentries[i]+fields[k].offset,(const char*)&b->entries[i]+fields[k].offset,fields[k].size))return differ("memo_entries",(int)i,fields[k].name); + return differ("memo_entries",(int)i,"padding_byte"); + } + if(memcmp(&a->observer,&b->observer,sizeof(a->observer)))return differ("warning",-1,"observer"); + return 0; +} +static void dump_blob(FILE *f,const char *name,const void *ptr,size_t n){ + fprintf(f,"\"%s\":\"",name);const unsigned char *b=ptr;for(size_t i=0;ip,p,sizeof(f->p));memcpy(f->h,h,sizeof(f->h));memcpy(f->q,q,sizeof(f->q));memcpy(f->fb,fb,sizeof(f->fb));memcpy(f->g,g,sizeof(f->g));memcpy(f->w,w,sizeof(f->w)); +} +static int live_equal(NativePropertyCache *p,NativePipeCache *pipes){ + if(p->count!=entry.context.count || p->capacity!=entry.context.capacity || p->temperatures)return 0; + for(size_t i=0;icount;i++){ + NativePropertyState s=entry.states[i];s.jacobian=p->jacobian; + if(memcmp(&s,&p->states[i],sizeof(s)))return 0; + } + return !memcmp(pipes,entry.pipes,sizeof(entry.pipes)) && !memcmp(p->jacobian->entries,entry.entries,p->jacobian->capacity*sizeof(*entry.entries)) && + !memcmp(p->jacobian->evaluations,entry.memo.evaluations,sizeof(entry.memo.evaluations)) && !memcmp(p->jacobian->reuses,entry.memo.reuses,sizeof(entry.memo.reuses)); +} +static int untouched_equal(const SRContext *ctx){ + for(size_t i=0;imemo; + if(memcmp(&ctx->states[i],&expected,sizeof(expected)))return 0; + } + for(int i=0;ipipes[i],&entry.pipes[i],sizeof(NativePipeCache)))return 0; + /* Unallocated tail is a poison-filled diagnostic canary. */ + if(memcmp(ctx->states+ctx->context.count,entry.states+ctx->context.count,(SR_STATES-ctx->context.count)*sizeof(NativePropertyState)))return 0; + return 1; +} +static uint64_t metadata_hash(void){ + const unsigned char *bytes=(const unsigned char*)&plan;uint64_t h=UINT64_C(14695981039346656037); + for(size_t i=0;icount); + int saved_errno=errno;SREnvironment saved_env;save_environment(&saved_env);int entry_flags=fetestexcept(FE_ALL_EXCEPT); + from_live(&entry,properties,pipes);copy_context(&reference,&entry);copy_context(&candidate,&entry); + if(compare_contexts(&reference,&candidate))fatal("initial clones differ"); + if(reference.states==candidate.states || reference.entries==candidate.entries)fatal("aliased clones"); + phase=REFERENCE;double ref_output=operation(&reference.context,reference.pipes,op_inputs);phase=OFF; + int ref_errno=errno,ref_flags=fetestexcept(FE_ALL_EXCEPT);unsigned ref_sse=sse_control(); + errno=saved_errno;restore_environment(&saved_env); + memcpy(&before_transaction,&candidate,sizeof(candidate));double cand_output=NAN;int mapping[SR_STATES],appends=0; + int reason=transaction(&candidate,op_inputs,&cand_output,mapping,&appends); + int cand_errno=errno,cand_flags=fetestexcept(FE_ALL_EXCEPT);unsigned cand_sse=sse_control(); + if(reason){ + stats.rejected++;stats.rejects[reason]++; + if(memcmp(&before_transaction,&candidate,sizeof(candidate)) || !isnan(cand_output))fatal("reject modified Candidate"); + stats.rollbacks++; + }else{ + stats.accepted++;stats.paths[plan.path]++;stats.appends+=appends; + int relocated=0; + for(int i=0;i=0 && mapping[i]!=i){relocated++;stats.relocated_slots++;} + stats.relocated+=(relocated>0); + for(int i=0;iaux){if(e->slot>=0)stats.ph_hit++;else stats.ph_miss++;}else{if(e->slot>=0)stats.pt_hit++;else stats.pt_miss++;}} + size_t bytes=sizeof(Plan)-sizeof(plan.events)+(size_t)plan.n*sizeof(Event); + if(bytesstats.metadata_max)stats.metadata_max=bytes; + if((size_t)plan.n>stats.event_max)stats.event_max=(size_t)plan.n; + if(memcmp(&ref_output,&cand_output,8)){differ("operation",pos,"output");mismatch("operation",ref_output,cand_output);} + else if(compare_contexts(&reference,&candidate))mismatch("operation_context",ref_output,cand_output); + if(!untouched_equal(&reference) || !untouched_equal(&candidate)){differ("probe",-1,"unwritten_data");mismatch("untouched",ref_output,cand_output);}else stats.untouched_equal++; + if(ref_errno!=cand_errno || ref_flags!=cand_flags || ref_sse!=cand_sse){differ("warning",-1,"errno_or_fenv_sse");mismatch("side_effect",ref_output,cand_output);} + if(memcmp(entry.entries,reference.entries,entry.memo.capacity*sizeof(*entry.entries)) || memcmp(entry.entries,candidate.entries,entry.memo.capacity*sizeof(*entry.entries))){differ("memo",-1,"readonly_entries");mismatch("memo",ref_output,cand_output);}else stats.memo_immutable++; + /* Both independent continuations begin at these operation exits. */ + copy_context(&tail_ref,&reference);copy_context(&tail_cand,&candidate); + fill_frame(&frame_ref,p,h,q,fb,g,w);memcpy(&frame_cand,&frame_ref,sizeof(frame_ref)); + int out_index=pos==16?45:152;frame_ref.q[out_index]=ref_output;frame_cand.q[out_index]=cand_output; + phase=TAIL;errno=saved_errno;restore_environment(&saved_env); + int ref_status=tail(t,y,&tail_ref,&frame_ref); + errno=saved_errno;restore_environment(&saved_env); + int cand_status=tail(t,y,&tail_cand,&frame_cand);phase=OFF; + if(ref_status!=cand_status){differ("evaluator",-1,"return");mismatch("continuation",ref_output,cand_output);} + else if(compare_frame(&frame_ref,&frame_cand))mismatch("continuation",ref_output,cand_output); + else if(compare_contexts(&tail_ref,&tail_cand))mismatch("continuation_context",ref_output,cand_output); + else stats.tail_equal++; + pending_live=1;pending_status=ref_status; + } + fprintf(trials,"{\"jac\":%llu,\"position\":%d,\"t\":%.17g,\"reason\":\"%s\",\"baselineCount\":%llu,\"probeCount\":%llu,\"referenceCount\":%llu,\"candidateCount\":%llu,\"appends\":%d,\"path\":%d,\"events\":%d,\"entryFlags\":%d,\"referenceFlags\":%d,\"candidateFlags\":%d,\"mapping\":[",jac,pos,t,reasons[reason],(unsigned long long)plan.entry_count,(unsigned long long)entry.context.count,(unsigned long long)reference.context.count,(unsigned long long)candidate.context.count,appends,plan.path,plan.n,entry_flags,ref_flags,cand_flags); + int comma=0;if(!reason)for(int i=0;i=0)fprintf(trials,"%s[%d,%d]",comma++?",":"",i,mapping[i]); + fputs("]}\n",trials); + if(!reason && (jac==200 || (configured==52 && jac==1)))negative_tests(op_inputs); + if(metadata_hash()!=metadata_before)fatal("Reference or Candidate modified metadata"); + stats.metadata_immutable++; + if(!live_equal(properties,pipes))stats.live_contamination++; + phase=OFF;restore_environment(&saved_env);errno=saved_errno; +#if defined(__SSE__) + if(_mm_getcsr()!=saved_env.sse)stats.live_contamination++; +#endif + if(fegetround()!=saved_env.rounding)stats.live_contamination++; +} +void sr_eval_result(int result,const double *dy,const double *w){ + if(!pending_live){return;}pending_live=0; + int different=result!=pending_status; + if(different)differ("evaluator",-1,"return"); + for(int i=0;!different && i1){copy_context(&candidate,&entry);candidate.context.capacity=candidate.context.count+1;negative_case("capacity_after_first_append",CAPACITY,inputs);} + copy_context(&candidate,&entry);candidate.pipes[position==16?0:28].valid=1;negative_case("pipe_hit",PIPE_BRANCH,inputs); + copy_context(&candidate,&entry);candidate.memo.recording=1;negative_case("memo_recording",MEMO_BINDING,inputs); + copy_context(&candidate,&entry);memset(candidate.entries,0,sizeof(candidate.entries));negative_case("memo_miss",MEMO_MISS,inputs); + copy_context(&candidate,&entry); + for(size_t i=0;i=0){ + copy_context(&candidate,&entry);Event *e=&plan.events[qi];e->slot=e->slot<0?0:-1;negative_case("query_path",QUERY_PATH,inputs);memcpy(&plan,&saved,sizeof(plan)); + } + if(ri>=0){copy_context(&candidate,&entry);plan.events[ri].data[0]^=1;negative_case("consumed_rho",CONSUMED,inputs);memcpy(&plan,&saved,sizeof(plan));} + if(vi>=0){copy_context(&candidate,&entry);plan.events[vi].data[0]^=(unsigned char)plan.events[vi].mask;negative_case("valid_test",VALID_BITS,inputs);memcpy(&plan,&saved,sizeof(plan));} + if(wi>=0){copy_context(&candidate,&entry);plan.entry_count=SR_STATES;negative_case("existing_entry_store",EXISTING_UPDATE,inputs);memcpy(&plan,&saved,sizeof(plan));} + if(allocations>1){ + int logical=-1; + for(int i=0;i=0 && plan.events[qi].slot>=0){ + int hit=plan.events[qi].slot; + copy_context(&candidate,&entry);candidate.states[hit].rho=nextafter(candidate.states[hit].rho,INFINITY);negative_case("probe_consumed_rho",CONSUMED,inputs); + copy_context(&candidate,&entry);candidate.states[hit].valid^=NATIVE_PROPERTY_MU;negative_case("probe_valid_bit",VALID_BITS,inputs); + for(int i=qi+1;i1?",":"",reasons[i],stats.rejects[i]); + fprintf(f,"},\"untouchedProbeEqual\":%llu,\"metadataImmutable\":%llu}\n",stats.untouched_equal,stats.metadata_immutable);fclose(f);fclose(trials);fclose(negative); +} diff --git a/tests/manual/context_shadow_replay.h b/tests/manual/context_shadow_replay.h new file mode 100644 index 0000000..857e47d --- /dev/null +++ b/tests/manual/context_shadow_replay.h @@ -0,0 +1,26 @@ +#ifndef CONTEXT_SHADOW_REPLAY_H +#define CONTEXT_SHADOW_REPLAY_H +#include "model.h" +#define SR_STATES 256 +#define SR_PIPES 40 +#define SR_EVENTS 512 +typedef struct { + NativePropertyCache context; + NativePropertyState states[SR_STATES]; + NativePipeCache pipes[SR_PIPES]; + NativeJacobianScalars memo; + NativeJacobianScalarEntry entries[MODEL_JACOBIAN_SCALAR_COUNT]; + NativePropertyTemperatures observer; +} SRContext; +typedef struct { + double p[48],h[232],q[232],fb[152],w[NOUTPUTS],dy[NSTATES]; + NativeGas g[56]; +} SRFrame; +typedef int (*SRTail)(double,const double*,SRContext*,SRFrame*); +typedef double (*SROperation)(NativePropertyCache*,NativePipeCache*,const double*); +void sr_shadow(int,double,const double*,NativePropertyCache*,NativePipeCache*, + const double*,const double*,const double*,const double*,const NativeGas*,const double*,SROperation,SRTail); +void sr_eval_result(int,const double*,const double*); +void sr_native_enter(const char*); +void sr_or(const void*,unsigned,const char*,const char*); +#endif diff --git a/tests/manual/context_shadow_report.md b/tests/manual/context_shadow_report.md new file mode 100644 index 0000000..5b9ecd8 --- /dev/null +++ b/tests/manual/context_shadow_report.md @@ -0,0 +1,145 @@ +# 最小 context shadow replay 验证结果 + +## 1. Candidate 能否逐位复现 Reference + +**可以,在本轮两个 operation 的已覆盖路径和严格前置条件下,1,792 次 shadow replay 全部通过,出口不一致为 0。** R288 先完成全部 896 次;同一 worker 二进制通过该阶段后,才运行 position 52 的 896 次。 + +参考答案始终是**同一个当前 probe 入口的独立深拷贝,真实执行原 operation 后的出口**。没有把 baseline 出口当参考。baseline 只提供本 Jacobian 内的局部语义记录,不提供用于恢复的完整 context。 + +| 统计 | group 6 / R288 / position 16 | group 18 / R475 / position 52 | +|---|---:|---:| +| shadow 总次数 | 896 | 896 | +| 可重放 | 896 | 896 | +| 自然轨迹 reject | 0 | 0 | +| Reference/Candidate 不一致 | 0 | 0 | +| 入口 count 不同 | 638 | 24 | +| 发生 slot relocation 的 probe | 571 | 23 | +| 重定位逻辑 slot 数量 | 1142 | 45 | +| 追加条目总数 | 1658 | 1789 | +| PT hit | 0 | 168 | +| PT miss | 1658 | 1789 | +| PH hit | 0 | 1 | +| PH miss | 0 | 167 | +| 独立后续 evaluator 返回值/dy/w/context 一致 | 896 | 896 | +| 后续 evaluator 与主仿真返回值/dy/w 一致 | 896 | 896 | +| memo 条目只读验证 | 896 | 896 | +| 未写入的 probe 数据保持不变 | 896 | 896 | +| 局部 metadata 未被 Reference/Candidate 改写 | 896 | 896 | +| Candidate 物理 native 调用 | 0 | 0 | +| 主 context/浮点环境污染 | 0 | 0 | + +### 如何保证比较有意义 + +1. 在目标 probe 的 operation 前,深拷贝 property states、全部 pipe 槽、scalar memo 的全部 entries 和计数,分别绑定到 Reference/Candidate 私有存储。两路没有共享可写 context。 +2. Reference 调用该位置的原始表达式。Candidate 只解释 baseline 捕获的 query/read/valid/allocate/write/OR/scalar-get 语义;物理 native 入口有运行时禁入检查。Reference 执行期间不能补写 Candidate 的记录。 +3. Candidate 从自己的 probe 副本建立 overlay。重新扫描当前有序 entries,采用原 PT/PH 的 exact `==` 和 medium 比较语义找 first-match;用逻辑 ID 映射结果,追加使用当前 count。 +4. 所有条件成功后进入无失败分支的 commit,仅按记录顺序更新实际写入字段、valid OR、count、指定 pipe 槽、memo 命中计数及显式输出;不复制 baseline context,也不覆盖 probe 未写字段。reject 前没有向 Candidate 或输出提交任何存储。 +5. 逐位比较输出、所有 active property 字段、valid bits、全部 pipe 字段、memo entries/计数、warning observer、errno 和 x87/SSE 环境。私有指针按“绑定到各自当前 owner”检查;不要求两个独立 allocation 的地址相等。数值字段没有容差或近似比较。 +6. 两个独立出口继续执行同一份原 evaluator 后续代码,实测返回状态、完整 dy/w、context 和 memo 计数一致;再与主仿真的真实 evaluator 返回值/dy/w 核对。主仿真仍然执行原 operation,没有采用 Candidate 输出,没有进入真实 skip 路径。 +7. 两轮主仿真的状态、输出、事件及全部 896 个 132×132 Jacobian 仍与未插桩基线逐字节一致。whole-context guard 源码哈希保持一致。 + +operation 本身返回一个 double,没有单独的 int 成功码;报告中的 evaluator 状态来自两条后续执行路径,不是用 `isfinite(output)` 代替。 + +### 两个具体入口例子(Jacobian 200) + +| 项目 | R288 | position 52 | +|---|---|---| +| baseline count | `12` | `75` | +| 当前 probe count | `13` | `75` | +| Reference 出口 count | `15` | `77` | +| Candidate 出口 count | `15` | `77` | +| 逻辑 slot → 当前 slot | `[[12, 13], [13, 14]]` | `[[75, 75], [76, 76]]` | + +R288 保留 probe 原 slot 12,在 13、14 追加;position 52 保留 probe slot 74 的压力/温度差异,查询重新扫描后仍 miss,再在 75、76 追加。 + +## 2. 哪些路径已经可以 replay + +| 查询路径 | R288 次数 | position 52 次数 | +|---|---:|---:| +| 近零流量,无物性查询 | 67 | 1 | +| PT miss → PT miss | 829 | 727 | +| PH miss → PT miss → PT hit → PT miss | 0 | 167 | +| PH hit → PT hit → PT miss | 0 | 1 | + +以上“可 replay”指本轮相应 operation 的完整 0–10 s 轨迹,且所有 runtime guard 同时成立。PH miss 的 h 登记与 valid OR、后续对刚追加逻辑条目的 PT hit 都保留。近零流量不追加物性条目,但仍执行 pipe 的有序写入,包括 valid=0 的同值写入。 + +R288 的 638 次 count 差异中,67 次属于无查询/无追加路径,因此只有 571 次发生实际 slot relocation。position 52 同理,24 次 count 差异中有一次无追加。不能把 count 差异次数当作重定位次数。 + +## 3. 哪些情况仍必须 reject / fallback + +自然轨迹中各 reject 分类均为 0。为防止“全成功但拒绝机制无效”,另在私有副本中执行下列负例,要求 reject 且整个 Candidate 与输出 sentinel 逐字节不变;这些不计入自然轨迹的 896 次。 + +| reject 分类 | R288 负例通过次数 | position 52 负例通过次数 | +|---|---:|---:| +| `capacity_scratch` | 2 | 3 | +| `consumed_field` | 1 | 3 | +| `existing_entry_update` | 1 | 2 | +| `first_match_relation` | 1 | 2 | +| `inputs` | 1 | 2 | +| `memo_lifetime_binding` | 1 | 2 | +| `memo_miss` | 1 | 2 | +| `memo_value` | 1 | 2 | +| `no_current_record` | 1 | 2 | +| `nonfinite_or_uncovered_branch` | 1 | 2 | +| `observer_nonnull` | 1 | 2 | +| `pipe_hit_branch` | 1 | 2 | +| `query_hit_miss_path` | 1 | 2 | +| `unknown_effect_or_schema` | 4 | 8 | +| `valid_test` | 1 | 3 | + +严格拒绝边界: + +- 记录不属于当前 Jacobian/operation、源代码不再匹配已验证 worker、记录溢出或未知语义事件。 +- 输入改变、first-match 逻辑映射冲突、hit/miss 路径改变、已消费字段或 valid 掩码结果不一致。 +- 容量不足或 scratch、非空 observer、未覆盖的已有 entry 原地更新、pipe 命中分支变化、非有限结果或未覆盖路径。 +- memo owner/lifetime/recording 不符合只读约束、所需 scalar key 未命中或值不匹配。Candidate 不调用物理 fallback 来弥补 memo miss。 +- 舍入模式、SSE 控制模式或所需异常状态不满足已验证条件;已记录的非零 errno 前置条件不成立。 + +负例包括第一次追加完成后第二次容量检查失败、末尾未知事件、末尾非有限输出,因此覆盖了 overlay 已发生大量修改后的回滚,不只是入口早退。position 52 的 PH-hit 样本还直接改变 probe 已有条目的 rho 和 MU 位,确认消费值/valid guard 生效。 + +Jacobian memo 的 entries 始终只读,但 Reference 的 scalar get 会增加 reuses 计数。Candidate 在 overlay 中验证同一 bit-key 查找结果,并在 commit 中重放对应计数增量。没有将 baseline 的 recording/put 副作用照搬到 probe。 + +验证期间发现并修复了 Windows 诊断隔离问题:该工具链的 `fesetenv` 不完整恢复 SSE 控制寄存器。负例现在保存/恢复完整 x87/SSE 环境,并验证 SSE 单独改变时会拒绝。修复前失败证据保存在 `test/context-shadow-20260917/env-restore-investigation/`;本报告仅使用修复后的同一构建全量重跑结果。 + +## 4. Runtime metadata 需要多少 + +| 当前实现 | R288 | position 52 | +|---|---:|---:| +| 单条语义事件 | 176 B | 176 B | +| 最大事件数 | 132 | 138 | +| 每条 operation 记录实际使用范围 | 2224–23344 B | 2224–24400 B | +| 每阶段为 512 个事件预留 | 90224 B | 90224 B | + +记录包括:Jacobian/operation 身份、四个显式输入、出口值、query 完整 key/逻辑命中关系、实际消费的字节值与 valid 掩码、创建顺序、字段更新/OR、scalar key/value、memo 绑定及环境前置条件。**不保存 baseline 完整 property/pipe context 用于 replay。** 只保留当前 Jacobian 的一条目标 operation 记录。 + +事务 scratch 另需一个 156816 B 的 overlay;本诊断为入口、Reference、Candidate、回滚检查及双路后续比较共保留 7 个 context 副本、2 个 22880 B frame。它们是 shadow 验证工作内存,不能算作未来 skip 每条记录都必须长期保存的 metadata。slot 映射临时表为 256 个 int(当前 ABI 1,024 B)。 + +这是当前保守事件表示的实测大小,未做去重或压缩,也没有据此评价性能。 + +## 5. 是否具备最小真实 skip 实验的条件 + +**已具备针对这两个单独 operation、上述四种已验证路径的下一阶段实验条件。** 下一步必须继续保持严格 guard、事务 commit、reject 后原执行以及独立 Reference 抽查/全量对比;先对 R288 单点实验,再单独考虑 position 52。 + +本轮没有实现真实 skip,也没有移除或放宽 whole-context guard。结论不适用于整个 R475、全部 reuse interval、新模型、其他 native kernel、容量耗尽/observer 非空/已有 entry 原地更新等未验证分支;Linux 尚未运行本实验。双路执行和详细检查的时长不作性能证据。 + +## 复现与证据 + +依赖上一轮保留的 access worker 和未插桩 baseline 文件。准备阶段核对 access worker 源码哈希;第二阶段核对第一阶段通过的 worker 二进制 SHA-256。 + +```powershell +.venv-win\Scripts\python.exe tests/manual/diagnose_context_shadow.py prepare +.venv-win\Scripts\python.exe tests/manual/diagnose_context_shadow.py run --position 16 +.venv-win\Scripts\python.exe tests/manual/diagnose_context_shadow.py run --position 52 +.venv-win\Scripts\python.exe tests/manual/analyze_context_shadow.py +``` + +生成文件位于 `test/context-shadow-20260917/`:`worker/build.json`、各阶段 `shadow-trials.jsonl`、`shadow-summary.json`、`shadow-negative.jsonl`、`measurement.json`,以及汇总的 `validation.json`。若出现不一致,会输出首个不同字段和完整入口/两路 context、frame、metadata 到 `first-mismatch.json`。最终两阶段均未生成该文件。 + +最终 worker SHA-256:`0491872485d3d97d6b79da556ea4de53b77e6daed9100112f1e1179a6dd3d18f`。 + +全量主轨迹哈希(两阶段相同): + +- states: `100864d208ce5cd86d4728aa9c1a343b6252b057266c8ccb7010c7fb0bc14317` +- outputs: `46fdbd10844d72e4fd23debe3439fd8eb736567f48713950b1bb5faadbdb1ff8` +- events: `4a068d192278eb91bb78782cdcc7d635deaef8dabd074ddbde3edc1d5d080b7d` +- jacobians: `74d559c39973a609884c6bc9274ce1185d23e163b33d60443108c4519fedb448` diff --git a/tests/manual/diagnose_context_access.py b/tests/manual/diagnose_context_access.py new file mode 100644 index 0000000..c4c2762 --- /dev/null +++ b/tests/manual/diagnose_context_access.py @@ -0,0 +1,134 @@ +"""Independent, opt-in access recorder for positions 16 and 52 only. + +Uses the existing fallback diagnostic worker as its source; never edits the +production kernels or changes the local-probe guard/replay policy. +""" +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path +import argparse, hashlib, json, re, shutil, time +import diagnose_context_fallback as dx + +ROOT = dx.ROOT +HERE = Path(__file__).parent +BASE = ROOT / 'test/context-fallback-20260917/worker' +OUT = ROOT / 'test/context-access-20260917' +replace = dx.replace + + +def instrument(body): + """Wrap reviewed lvalues first, then actual rvalues; keep short circuits.""" + saved = [] + def hold(text): + saved.append(text) + return f'AXHOLD{len(saved)-1}ZZ' + field = r'(?:s|up|down|cache|m|a|b)->\w+' + def reads(text): + # An address expression is not a value read (e.g. &s->rho). + text = re.sub(r'(?states[', '&AX_R(cache->states)[') + return 'AX_R(*m)' if text.strip() == '*m' else text + body = re.sub(r'([su]\w*->valid)&(NATIVE_PROPERTY_\w+)', + lambda m: hold(f'ax_test({m[1].split("->")[0]},{m[2]},__func__)'), body) + body = body.replace('cache->count++', hold('AX_INC(cache->count)')) + # Struct reset is an explicit write, including equal-valued fields. + body = body.replace('*s=(NativePropertyState){0};', hold('AX_W(*s,((NativePropertyState){0}));')) + def write(m): + lhs, operator, rhs = m.groups() + return hold(('AX_OR' if operator == '|=' else 'AX_W') + f'({lhs},{reads(rhs)});') + body = re.sub(r'(' + field + r')\s*(\|=|=(?!=))\s*([^;]+);', write, body) + body = reads(body) + for i, value in enumerate(saved): + body = body.replace(f'AXHOLD{i}ZZ', value) + # Fail closed if a field write evaded the transformation. + assert not re.search(field + r'\s*(?:\|=|=(?!=)|\+\+)', body), body + return body + + +def change_function(source, name, transform): + a, b, e = dx.ex.function_span(source, name) + return source[:b+1] + transform(source[b+1:e-1]) + source[e-1:] + + +def prepare(): + OUT.mkdir(exist_ok=True) + work = OUT / 'worker' + work.mkdir(exist_ok=True) + sources = {p.name: p.read_text(encoding='utf-8') for p in BASE.glob('*.c')} + assert sources, 'First prepare the existing context-fallback diagnostic worker.' + before = {k: hashlib.sha256(v.encode()).hexdigest() for k, v in sources.items()} + s = sources['properties.c'] + def pt(body): + body = 'ax_query("PT",m,p,T);\n' + body + body = replace(body, 'same_medium(&s->medium,m))return s;', 'same_medium(&s->medium,m)){ax_match("PT",s);return s;}') + body = replace(body, 'return property_new(cache,m,p,T,scratch);', 'ax_match("PT",NULL);return property_new(cache,m,p,T,scratch);') + return body + s = change_function(s, 'property_pt', pt) + def ph(body): + body = 'ax_query("PH",m,p,h);\n' + body + body = replace(body, 'observe_temperature(cache->temperatures,m,s->T,3);return s->T;', 'ax_match("PH",s);observe_temperature(cache->temperatures,m,s->T,3);return s->T;') + return replace(body, 'double T;', 'ax_match("PH",NULL);double T;') + s = change_function(s, 'native_temperature_ph_context', ph) + s = change_function(s, 'property_new', lambda body: replace(body, '*s=(NativePropertyState){0};', 'ax_new(cache,s,valid);*s=(NativePropertyState){0};')) + functions = ['same_medium', 'property_new', 'property_pt', 'property_density', 'property_viscosity', + 'native_temperature_ph_context', 'local_isentropic', 'isentropic', 'state_valve', + 'native_density', 'native_temperature_ph', 'native_viscosity'] + for name in functions: + s = change_function(s, name, instrument) + # Scalar get can write directly into s->rho through an output pointer. + s = change_function(s, 'native_jacobian_scalar_get', lambda body: replace( + replace(body, '*value=entry->value;', 'AX_W(*value,entry->value);ax_scalar("get",cache,kind,medium_kind,inputs,count,1,value);'), + 'return 0;', 'ax_scalar("get",cache,kind,medium_kind,inputs,count,0,NULL);return 0;')) + s = change_function(s, 'native_jacobian_scalar_put', lambda body: + 'ax_scalar("put_attempt",cache,kind,medium_kind,inputs,count,-1,&value);\n' + body) + sources['properties.c'] = s + for name in ['native_pipe_flow_context', 'native_pipe_flow_cached_context']: + sources['pipe.c'] = change_function(sources['pipe.c'], name, instrument) + s = sources['local_probe_support.c'] + s = replace(s, 'dx_region=-1;dx_boundary(0,-2,p,pipes);', 'ax_bind(p,pipes);dx_region=-1;dx_boundary(0,-2,p,pipes);') + s = replace(s, 'void dx_op_begin(int pos,const double *inputs){', + 'void dx_op_begin(int pos,const double *inputs){ax_begin(dx_jac-1,lp_color,pos,dx_time,inputs,dx_in_offset[pos+1]-dx_in_offset[pos]);') + s = replace(s, 'void dx_op_end(int pos,const double *outputs,NativePropertyCache *p,NativePipeCache *pipes){', + 'void dx_op_end(int pos,const double *outputs,NativePropertyCache *p,NativePipeCache *pipes){ax_end(outputs,dx_out_offset[pos+1]-dx_out_offset[pos]);') + s = replace(s, 'void dx_finish(void){', 'void dx_finish(void){ax_finish();') + sources['local_probe_support.c'] = s + sources['model.c'] = replace(sources['model.c'], 'lp_active=-1;dx_eval_end();return result;', 'lp_active=-1;ax_result(result);dx_eval_end();return result;') + sources['context_access_diag.c'] = (HERE/'context_access_diag.c').read_text(encoding='utf-8') + for path in BASE.glob('*.h'): + shutil.copyfile(path, work/path.name) + shutil.copyfile(HERE/'context_access_diag.h', work/'context_access_diag.h') + cc, sun, _ = dx.ex.builder.toolchain() + flags, libs, dlls, exe = dx.ex.builder.platform_build_inputs(sun) + flags += ['-DLP_OBSERVE=0'] + started = time.perf_counter() + def compile_one(item): + name, code = item + path = work/name + if '#define _WIN32_WINNT 0x0600' in code: + code = code.replace('#define _WIN32_WINNT 0x0600', '#define _WIN32_WINNT 0x0600\n#include "context_access_diag.h"', 1) + else: + code = '#include "context_access_diag.h"\n' + code + path.write_text(code, encoding='utf-8', newline='\n') + obj = path.with_suffix('.o'); log = [] + dx.ex.builder._command([cc, *flags, '-I', str(work), '-I', str(dx.ex.builder.NATIVE/'include'), '-I', str(sun/'include'), '-c', str(path), '-o', str(obj)], log=log, timeout=240) + return obj, log + with ThreadPoolExecutor(max_workers=4) as pool: + objects = list(pool.map(compile_one, sources.items())) + log = [] + dx.ex.builder._command([cc, *flags, *[str(o) for o, _ in objects], *dx.ex.builder.link_library_arguments(libs), '-lm', '-o', str(work/exe)], log=log) + for dll in dlls: + shutil.copyfile(dll, work/dll.name) + (work/'build.log').write_text('\n'.join(sum([v for _, v in objects], [])+log), encoding='utf-8') + dx.ex.write(work/'build.json', dict(sourceHashes=before, instrumentedHashes={p.name:hashlib.sha256(p.read_bytes()).hexdigest() for p in work.glob('*.c')}, seconds=time.perf_counter()-started, functions=functions)) + print('BUILT access worker', flush=True) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('action', choices=['prepare', 'run']) + args = parser.parse_args() + if args.action == 'prepare': + prepare() + else: + dx.OUT = OUT + dx.run('audit', mode=1, matrices=True) diff --git a/tests/manual/diagnose_context_fallback.py b/tests/manual/diagnose_context_fallback.py new file mode 100644 index 0000000..6d69bb3 --- /dev/null +++ b/tests/manual/diagnose_context_fallback.py @@ -0,0 +1,126 @@ +"""Build isolated context-fallback diagnostic workers; no implementation edits.""" +from pathlib import Path +from concurrent.futures import ThreadPoolExecutor +import argparse,hashlib,json,os,re,shutil,subprocess,time +import local_probe_experiment as ex + +ROOT=ex.ROOT;SOURCE=ROOT/'test/local-probe-20260917/worker';OUT=ROOT/'test/context-fallback-20260917';HERE=Path(__file__).parent +KERNELS=[('properties','property_pt'),('properties','property_density'),('properties','property_viscosity'), + ('properties','native_temperature_ph_context'),('properties','local_isentropic'),('properties','state_valve'), + ('properties','native_jacobian_scalar_get'),('properties','native_temperature_ph'),('properties','native_density'),('properties','native_viscosity'), + ('pipe','native_pipe_flow_cached_context'),('pipe','native_pipe_flow_context'),('pipe','native_pipe_resistance'), + ('orifice','native_medium_orifice_context')] +replace=ex.replace +def function(s,name):a,b,e=ex.function_span(s,name);return s[a:e] +def vec(refs):return '(double[]){'+(','.join(refs) or '0')+'}' + +def generate_model(source,ops,meta): + original=function(source,'model_eval_local_internal');start=original.index('if(lp_capture){');end=original.index('double node_energy[') + checkpoints=meta['contextCheckpointSlots'];capture=[] + for pos,op in enumerate(ops): + if str(pos) in checkpoints:capture.append(f'lp_snapshot({checkpoints[str(pos)]},properties,pipe_cache);') + capture += [f'dx_op_begin({pos},{vec(sorted(op.inputs))});',*op.code,f'dx_op_end({pos},{vec(op.outputs)},properties,pipe_cache);'] + capture += [f'lp_snapshot({checkpoints[str(len(ops))]},properties,pipe_cache);','lp_save(p,h,q,w,fb);'] + # Use the original capture branch in time-only runs; it is not profiled. + orig_capture=original[start+len('if(lp_capture){'):original.index('}else{',start)] + schedule=['dx_schedule(properties,pipe_cache);','if(lp_capture){','if(dx_mode==1 || dx_mode==5){',*capture,'}else{',orig_capture,'}','}else{','for(int pos=0;pos=0 && dx_reuse(region,properties,pipe_cache,p,h,q,w,fb)){pos=lp_end[region];continue;}', + 'switch(pos){'] + for pos,op in enumerate(ops): + schedule += [f'case {pos}:{{',f'dx_op_begin({pos},(dx_mode==1 || dx_mode==5)?{vec(sorted(op.inputs))}:NULL);',*op.code, + f'dx_op_end({pos},(dx_mode==1 || dx_mode==5)?{vec(op.outputs)}:NULL,properties,pipe_cache);','break;}'] + schedule += ['default:return 0;}','pos++;','if(dx_region>=0 && pos==lp_end[dx_region])dx_region_end(p,h,q,w,fb,properties,pipe_cache);','}}'] + clone=(original[:start]+'\n'.join(schedule)+'\ndx_position=-1;\n'+original[end:]).replace('model_eval_local_internal(', 'dx_model_eval_local_internal(',1) + wrapper=function(source,'lp_eval').replace('int lp_eval(', 'int dx_eval(',1).replace('model_eval_local_internal(', 'dx_model_eval_local_internal(') + a=wrapper.index('{')+1;wrapper=wrapper[:a]+'\nif(!dx_selected)return lp_eval(t,y,dy,w,workspace);\ndx_eval_begin(t,y);\n'+wrapper[a:] + wrapper=replace(wrapper,'lp_active=-1;return result;','lp_active=-1;dx_eval_end();return result;') + return source+'\n'+clone+'\n'+wrapper + +def kernel_wrapper(source,name,index): + a,b,e=ex.function_span(source,name);sig=source[a:b].strip();body=source[a:e] + args=sig[sig.index('(')+1:sig.rindex(')')] + params=[re.search(r'([A-Za-z_]\w*)\s*(?:\[[^]]*\])?$',x.strip())[1] for x in args.split(',')] + prefix=sig[:sig.index(name)].strip();typ=re.sub(r'^(?:static|NATIVE_COMPONENT_INTERNAL)\s+','',prefix).strip() + impl=body.replace(name+'(', 'dx_impl_'+name+'(',1) + call='dx_impl_'+name+'('+','.join(params)+')' + action=(call+';dx_kernel_end('+str(index)+',ticket);') if typ=='void' else (typ+' result='+call+';dx_kernel_end('+str(index)+',ticket);return result;') + wrapper=sig+'{uint64_t ticket=dx_kernel_begin('+str(index)+');'+action+'}' + # Forward declaration preserves recursive calls and cross-calls. + return source[:a]+sig+';\n'+impl+'\n'+wrapper+source[e:] + +def prepare(kernels=False,trace=False): + OUT.mkdir(exist_ok=True);work=OUT/('trace-worker' if trace else 'kernels' if kernels else 'worker');work.mkdir(exist_ok=True) + program,saved=ex.capture(ROOT/'tests/data/test-mql-8-corrected.json') + assert program.source==(SOURCE.parent/'original-model.c').read_text(encoding='utf-8') + schedule=saved['schedule'];ops=[schedule.computations[b.members[0]] for b in schedule.blocks] + meta=json.loads((SOURCE.parent/'plan.json').read_text(encoding='utf-8')) + versions=[0];pure={'if','for','sizeof','fmax','fmin','fabs','sqrt','copysign','pow'} + for op in ops:versions.append(versions[-1]+int(bool(set(re.findall(r'\b([A-Za-z_]\w*)\s*\(', '\n'.join(op.code)))-pure))) + assert all(versions[int(pos)]==slot for pos,slot in meta['contextCheckpointSlots'].items()) + ins=[0];outs=[0] + for op in ops:ins.append(ins[-1]+len(op.inputs));outs.append(outs[-1]+len(op.outputs)) + arrays={'dx_start_pos':[a for a,b in meta['regions']],'dx_contextual':[int(versions[a]!=versions[b]) for a,b in meta['regions']], + 'dx_version':versions,'dx_mutates':[versions[i+1]!=versions[i] for i in range(len(ops))],'dx_in_offset':ins,'dx_out_offset':outs} + tables='\n'.join('const int '+name+'[]={'+','.join(str(int(v)) for v in values)+'};' for name,values in arrays.items()) + tables+=f'\n#define DX_NIN {ins[-1]}\n#define DX_NOUT {outs[-1]}\n' + sources={p.name:p.read_text(encoding='utf-8') for p in SOURCE.glob('*.c')};before={n:hashlib.sha256(s.encode()).hexdigest() for n,s in sources.items()} + sources['model.c']=generate_model(sources['model.c'],ops,meta) + sources['local_probe_support.c']+='\n'+(HERE/'context_fallback_diag.c').read_text(encoding='utf-8').replace('/* DIAG_TABLES */',tables) + sources['common.c']=replace(sources['common.c'],'lp_start();','lp_start();dx_initialize();') + sources['common.c']=replace(sources['common.c'],'lp_finish();','lp_finish();dx_finish();') + sources['cvode_solver.c']=replace(sources['cvode_solver.c'],'lp_eval(t,N_VGetArrayPointer(y),N_VGetArrayPointer(f),outputs,workspace)','dx_eval(t,N_VGetArrayPointer(y),N_VGetArrayPointer(f),outputs,workspace)') + sources['cvode_solver.c']=replace(sources['cvode_solver.c'],'{lp_color=-1;uint64_t start=lp_tick();','{lp_color=-1;dx_jacobian();uint64_t start=lp_tick();') + sources['cvode_solver.c']=replace(sources['cvode_solver.c'],'if(!result)lp_matrix(t,N_VGetArrayPointer(y),SUNDenseMatrix_Data(matrix));', + 'if(!result){lp_matrix(t,N_VGetArrayPointer(y),SUNDenseMatrix_Data(matrix));dx_validate_matrix(t,N_VGetArrayPointer(y),SUNDenseMatrix_Data(matrix));}') + if kernels: + for i,(module,name) in enumerate(KERNELS):sources[module+'.c']=kernel_wrapper(sources[module+'.c'],name,i) + if trace: + a,b,e=ex.function_span(sources['properties.c'],'property_new') + fn=sources['properties.c'][a:e];fn=replace(fn,'return s;','dx_property_created(cache,s);return s;') + sources['properties.c']=sources['properties.c'][:a]+fn+sources['properties.c'][e:] + for name in ('model.h','local_probe.h'):shutil.copyfile(SOURCE/name,work/name) + (work/'context_fallback_diag.h').write_text((HERE/'context_fallback_diag.h').read_text(encoding='utf-8').replace('#include "local_probe.h"','#include "local_probe.h"\n#define DX_NK '+str(len(KERNELS))),encoding='utf-8') + cc,sun,_=ex.builder.toolchain();flags,libs,dlls,exe=ex.builder.platform_build_inputs(sun);flags+=['-DLP_OBSERVE=0'];started=time.perf_counter() + def compile_one(item): + i,(name,s)=item;path=work/name;path.write_text('#include "context_fallback_diag.h"\n'+s,encoding='utf-8',newline='\n');obj=work/f'diag-{i}.o';log=[] + ex.builder._command([cc,*flags,'-I',str(work),'-I',str(ex.builder.NATIVE/'include'),'-I',str(sun/'include'),'-c',str(path),'-o',str(obj)],log=log,timeout=240) + return obj,log + with ThreadPoolExecutor(max_workers=4) as pool:objs=list(pool.map(compile_one,enumerate(sources.items()))) + log=[];ex.builder._command([cc,*flags,*[str(o) for o,_ in objs],*ex.builder.link_library_arguments(libs),'-lm','-o',str(work/exe)],log=log) + for dll in dlls:shutil.copyfile(dll,work/dll.name) + (work/'build.log').write_text('\n'.join(sum([v for _,v in objs],[])+log),encoding='utf-8') + ex.write(work/'build.json',dict(sourceHashes=before,seconds=time.perf_counter()-started,kernels=kernels)) + ex.write(OUT/'plan.json',dict(**meta,versions=versions,kernels=KERNELS,code=[list(o.code) for o in ops],inputs=[sorted(o.inputs) for o in ops])) + print('BUILT',work.name,time.perf_counter()-started,flush=True) + +def run(label,mode=1,stride=16,seed=1,kernels=False,matrices=False,control=False,trace=False): + work=OUT/label;work.mkdir(exist_ok=True);exe=(SOURCE if control else OUT/('trace-worker' if trace else 'kernels' if kernels else 'worker'))/'model.exe' + env=os.environ.copy();env.update(LOCAL_PROBE_MASK='0x7ffffff',CONTEXT_DIAG_MODE=str(mode),CONTEXT_DIAG_STRIDE=str(stride),CONTEXT_DIAG_SEED=str(seed),CONTEXT_DIAG_MATRICES=str(int(matrices))) + args=[str(exe),'--method','BDF','--start','0','--stop','10','--sample-step','.01','--max-step','1e30','--rtol','1e-8','--timeout','300', + '--sample-file',str(work/'states.bin'),'--output-block-file',str(work/'outputs.bin'),'--output',str(work/'result.json')] + start=time.perf_counter() + with (work/'stderr.log').open('wb') as f:p=subprocess.run(args,cwd=work,env=env,stdout=subprocess.PIPE,stderr=f,timeout=330,creationflags=subprocess.CREATE_NO_WINDOW) + elapsed=time.perf_counter()-start + if p.returncode:raise RuntimeError((label,p.returncode,(work/'stderr.log').read_text()[-5000:])) + result=json.loads((work/'result.json').read_text());diag=json.loads((work/'probe.json').read_text());ref=json.loads((SOURCE.parent/'all-run-0/measurement.json').read_text(encoding='utf-8')) + keys=['success','finalState','final','propertyWarnings','acceptedSteps','rejectedSteps','stateTransitions','solverStarts','nfev','njev','nlu'] + assert all(result[k]==ref[k] for k in keys),(label,'result differs') + assert all(diag[k]==ref['diagnostic'][k] for k in ['newtonIterations','newtonConvergenceFailures','modelCalls','groups','contextCopiedBytes','contextComparedBytes']),(label,'counters differ') + hashes={} + for name in ('states','outputs','events','jacobians'): + p=work/(name+'.bin') + if p.exists(): + with p.open('rb') as f:hashes[name]=hashlib.file_digest(f,'sha256').hexdigest() + if name=='jacobians': + with (SOURCE.parent/'all-audit/jacobians.bin').open('rb') as f:assert hashes[name]==hashlib.file_digest(f,'sha256').hexdigest() + else:assert hashes[name]==ref[name+'Sha256'],(label,name) + record=dict(label=label,mode=mode,stride=stride,seed=seed,kernels=kernels,control=control,processSeconds=elapsed, + solveSeconds=result['solveSeconds'],solveCpuSeconds=result['solveCpuSeconds'],jacobianSeconds=diag['jacobianSeconds'],hashes=hashes) + ex.write(work/'measurement.json',record);print('RUN',label,'Jac',diag['jacobianSeconds'],'solve',result['solveSeconds'],'exact OK',flush=True) + return record + +if __name__=='__main__': + p=argparse.ArgumentParser();p.add_argument('action',choices=['prepare','run']);p.add_argument('--kernels',action='store_true');p.add_argument('--matrices',action='store_true');p.add_argument('--control',action='store_true');p.add_argument('--trace',action='store_true');p.add_argument('--label',default='census');p.add_argument('--mode',type=int,default=1);p.add_argument('--stride',type=int,default=16);p.add_argument('--seed',type=int,default=1);a=p.parse_args() + if a.action=='prepare':prepare(a.kernels,a.trace) + else:run(a.label,a.mode,a.stride,a.seed,a.kernels,a.matrices,a.control,a.trace) diff --git a/tests/manual/diagnose_context_shadow.py b/tests/manual/diagnose_context_shadow.py new file mode 100644 index 0000000..549bd36 --- /dev/null +++ b/tests/manual/diagnose_context_shadow.py @@ -0,0 +1,107 @@ +"""Build/run a shadow-only replay experiment, R288 before position 52. + +No Candidate result is used by the live evaluator. Baseline metadata is captured +online; Reference executes on a deep copy of the actual current probe entry. +""" +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path +import argparse,hashlib,json,os,shutil,time +import diagnose_context_access as access + +ROOT=access.ROOT +HERE=Path(__file__).parent +BASE=ROOT/'test/context-access-20260917/worker' +OUT=ROOT/'test/context-shadow-20260917' +replace=access.replace + + +def generated_model(source,plan): + a,b,e=access.dx.ex.function_span(source,'dx_model_eval_local_internal') + fn=source[a:e] + tail=fn[fn.index('double node_energy['):] + generated=[] + for pos in (16,52): + operation=plan['code'][pos][0] + expected=(45,0,3,4,64) if pos==16 else (152,28,44,40,178) + qi,pipe,pi,gi,hi=expected + expr=operation.split('=',1)[1].rstrip(';') + for old,new in [(f'p[{pi}]','x[3]'),(f'g[{gi}].p','x[1]'),(f'g[{gi}].T','x[0]'),(f'h[{hi}]','x[2]')]:expr=expr.replace(old,new) + generated.append(f'static double sr_original_{pos}(NativePropertyCache *properties,NativePipeCache *pipe_cache,const double *x){{return {expr};}}') + header=f'''static int sr_tail_{pos}(double t,const double *y,SRContext *ctx,SRFrame *f){{ +NativePropertyCache *properties=&ctx->context;NativePipeCache *pipe_cache=ctx->pipes; +double *p=f->p,*h=f->h,*q=f->q,*fb=f->fb,*w=f->w,*dy=f->dy;NativeGas *g=f->g; +(void)t;(void)y;(void)p;(void)h;(void)q;(void)fb;(void)w;(void)dy;(void)g;(void)properties;(void)pipe_cache; +''' + generated.append(header+'\n'.join('\n'.join(c) for c in plan['code'][pos+1:])+'\n'+tail) + hook=f'sr_shadow({pos},t,y,properties,pipe_cache,p,h,q,fb,g,w,sr_original_{pos},sr_tail_{pos});\n' + # Both baseline capture and the live probe switch still execute the + # original assignment. The hook returns void and cannot supply q. + assert fn.count(operation)==3 # detailed baseline, timed baseline, probe + fn=fn.replace(operation,hook+operation) + return source[:a]+'\n'.join(generated)+'\n'+fn+source[e:] + + +def prepare(): + OUT.mkdir(exist_ok=True);work=OUT/'worker';work.mkdir(exist_ok=True) + sources={p.name:p.read_text(encoding='utf-8') for p in BASE.glob('*.c')} + assert sources + before={k:hashlib.sha256(v.encode()).hexdigest() for k,v in sources.items()} + certified=json.loads((BASE/'build.json').read_text(encoding='utf-8'))['instrumentedHashes'] + assert before==certified, 'Access-validated source changed; revalidate it before extending the allowlist.' + plan=json.loads((ROOT/'test/context-fallback-20260917/plan.json').read_text(encoding='utf-8')) + sources['model.c']=generated_model(sources['model.c'],plan) + sources['model.c']=replace(sources['model.c'],'ax_result(result);dx_eval_end();','sr_eval_result(result,dy,w);dx_eval_end();') + # Every native entry reachable by either target is guarded during replay. + # Candidate's semantic interpreter has no calls to these physics functions. + native_functions={ + 'properties.c':['property_pt','property_density','property_viscosity','native_temperature_ph_context','local_isentropic','isentropic','state_valve','native_density','native_temperature_ph','native_viscosity'], + 'pipe.c':['native_pipe_flow_cached_context','native_pipe_flow_context','native_pipe_resistance'], + } + for module,names in native_functions.items(): + for name in names:sources[module]=access.change_function(sources[module],name,lambda body:'sr_native_enter(__func__);\n'+body) + del sources['context_access_diag.c'] + sources['context_shadow_replay.c']=(HERE/'context_shadow_replay.c').read_text(encoding='utf-8') + for p in BASE.glob('*.h'):shutil.copyfile(p,work/p.name) + shutil.copyfile(HERE/'context_shadow_replay.h',work/'context_shadow_replay.h') + header=(work/'context_access_diag.h').read_text(encoding='utf-8') + header=header.replace('#include "kernels.h"','#include "kernels.h"\n#include "context_shadow_replay.h"') + header=replace(header,'(x)|=(v); ax_access("write",&(x),sizeof(x),#x,__func__);','(x)|=(v); sr_or(&(x),(v),#x,__func__);') + (work/'context_access_diag.h').write_text(header,encoding='utf-8') + cc,sun,_=access.dx.ex.builder.toolchain();flags,libs,dlls,exe=access.dx.ex.builder.platform_build_inputs(sun) + flags+=['-DLP_OBSERVE=0'];started=time.perf_counter() + def compile_one(item): + name,code=item;path=work/name;path.write_text(code,encoding='utf-8',newline='\n');obj=path.with_suffix('.o');log=[] + access.dx.ex.builder._command([cc,*flags,'-I',str(work),'-I',str(access.dx.ex.builder.NATIVE/'include'),'-I',str(sun/'include'),'-c',str(path),'-o',str(obj)],log=log,timeout=240) + return obj,log + with ThreadPoolExecutor(max_workers=4) as pool:objects=list(pool.map(compile_one,sources.items())) + log=[];access.dx.ex.builder._command([cc,*flags,*[str(o) for o,_ in objects],*access.dx.ex.builder.link_library_arguments(libs),'-lm','-o',str(work/exe)],log=log) + for dll in dlls:shutil.copyfile(dll,work/dll.name) + (work/'build.log').write_text('\n'.join(sum([v for _,v in objects],[])+log),encoding='utf-8') + guard_before=access.dx.function(sources['local_probe_support.c'],'lp_reuse') + guard_certified=access.dx.function((BASE/'local_probe_support.c').read_text(encoding='utf-8'),'lp_reuse') + assert guard_before==guard_certified + access.dx.ex.write(work/'build.json',dict(sourceHashes=before,instrumentedHashes={p.name:hashlib.sha256(p.read_bytes()).hexdigest() for p in work.glob('*.c')},seconds=time.perf_counter()-started,guardedNativeFunctions=native_functions,wholeContextGuardUnchanged=True)) + print('BUILT independent shadow worker',flush=True) + + +def run(stage): + worker_hash=hashlib.sha256((OUT/'worker/model.exe').read_bytes()).hexdigest() + if stage==52: + gate=json.loads((OUT/'r288/shadow-summary.json').read_text(encoding='utf-8')) + assert gate['total']==896 and gate['accepted']==896 and gate['mismatches']==0 and gate['liveContamination']==0, 'R288 must pass first.' + assert gate['validatedWorkerSha256']==worker_hash, 'R288 must validate this exact worker build first.' + os.environ['CONTEXT_SHADOW_POSITION']=str(stage) + access.dx.OUT=OUT + label='r288' if stage==16 else 'position52' + access.dx.run(label,mode=1,matrices=True) + summary=json.loads((OUT/label/'shadow-summary.json').read_text(encoding='utf-8')) + assert summary['total']==896 and summary['mismatches']==0 and summary['liveContamination']==0 + summary['validatedWorkerSha256']=worker_hash + access.dx.ex.write(OUT/label/'shadow-summary.json',summary) + print(json.dumps(summary,indent=2),flush=True) + + +if __name__=='__main__': + p=argparse.ArgumentParser();p.add_argument('action',choices=['prepare','run']);p.add_argument('--position',type=int,choices=[16,52],default=16);a=p.parse_args() + if a.action=='prepare':prepare() + else:run(a.position) diff --git a/tests/manual/diagnose_mql8_force_events.py b/tests/manual/diagnose_mql8_force_events.py new file mode 100644 index 0000000..444abf2 --- /dev/null +++ b/tests/manual/diagnose_mql8_force_events.py @@ -0,0 +1,328 @@ +"""Verify production LSTP integration and probe force at real Amesim timestamps. + +Dense trace sampling operates on an isolated runtime copy, without changing +the step schedule or time-event treatment. It never interpolates across events. +""" +from concurrent.futures import ThreadPoolExecutor +from dataclasses import replace +import argparse +import hashlib +import json +import math +import os +from pathlib import Path +import re +import shutil +import subprocess +import sys +from unittest.mock import patch + +import numpy as np + +ROOT=Path(__file__).resolve().parents[2] +sys.path.insert(0,str(ROOT)) +from app.main import compile_system_xml_network +from app.simulation.backends import simulation_config +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.input import load_input +from app.simulation.native_codegen.runner import execute_native +from tests.manual import evaluate_mql8_correctness as evaluation,mql8_comparison as curves + +PREVIOUS=ROOT/'test/mechanical-events-20260917/baselines' +DEFAULT=ROOT/'test/lstp-mainline-20260917' +save=evaluation.save + + +def model(profile): + project=ROOT/'test/output-semantics-20260917/after'/profile/'platform.json' + xml,doc=load_input(project) + net=compile_system_xml_network(doc) + return project,xml,doc,net,compile_native_program(net) + + +def build(program,out,native=None): + with patch.object(builder,'CACHE',out/'cache'),patch.object(builder,'NATIVE',native or ROOT/'native'), \ + patch.object(builder,'ThreadPoolExecutor',lambda **kw:ThreadPoolExecutor(max_workers=1)): + return builder.build_native(program) + + +def baseline(out): + results={} + for profile in ('full','noncyclic'): + directory=out/'baseline'/profile + if (directory/'summary.json').exists(): + results[profile]=json.loads((directory/'summary.json').read_bytes());continue + directory.mkdir(parents=True,exist_ok=True) + project,xml,doc,net,program=model(profile) + old=json.loads((PREVIOUS/profile/'lstp/event-descriptors.json').read_bytes()) + # The sole generated-source addition is a constant descriptor table. + original=re.sub(r'^const NativeContact model_contacts\[[^\n]+\n','',program.source,flags=re.M) + assert hashlib.sha256(original.encode()).hexdigest()==old['originalModelSourceSha256'] + assert program.jacobian_structure==old['jacobianStructure'] + (directory/'platform.json').write_bytes(project.read_bytes());(directory/'platform.xml').write_bytes(xml) + reference=directory/'amesim';reference.mkdir(exist_ok=True) + for filename in ('test_mql_.var','test_mql_.results'): + if not (reference/filename).exists():os.link(PREVIOUS/profile/'reference'/filename,reference/filename) + audit=json.loads((PREVIOUS/profile/'audit/audit.json').read_bytes()) + print(profile,'production build',flush=True) + compiled=build(program,out) + try: + with patch.object(result_storage,'RESULT_ROOT',out/'results'): + r=execute_native(compiled,replace(simulation_config(doc.simulation),rtol=1e-8),.01, + run_dir=directory/'native',timeout=180) + assert r['success'],r['message'] + summary=evaluation.compare(directory,json.loads(project.read_bytes()),net,audit,evaluation.PROFILES[profile]) + summary['nativeRun']={k:v for k,v in r.items() if k not in ('series','final','finalState')} + with np.load(directory/'curves.npz') as current,np.load(PREVIOUS/profile/'lstp/curves.npz') as expected: + summary['allComparedArraysEqualIsolatedLstp']=all(np.array_equal(current[k],expected[k],equal_nan=True) for k in current.files) + summary['generatedRhsUnchanged']=True + save(directory/'summary.json',summary);results[profile]=summary + print(profile,'done',summary['allComparedArraysEqualIsolatedLstp'],r['contactEvents'],flush=True) + finally:compiled.close() + save(out/'baseline-summary.json',results) + + +def trace(out): + project,xml,doc,net,program=model('full') + audit=json.loads((PREVIOUS/'full/audit/audit.json').read_bytes());curves.configure(audit,net) + ame=curves.read_ame(PREVIOUS/'full/reference') + at=np.array(ame.times) + # Query BEFORE and AFTER every forcing boundary, including the exact saved + # Amesim row. These queries never become solver stop times. + queries=set();points=[] + boundaries=evaluation.event_times(json.loads(project.read_bytes()),50) + for boundary in boundaries: + if boundary<=0:continue + ai=int(np.argmin(abs(at-boundary))) + points.append(dict(boundary=boundary,ameIndex=ai,ameTime=float(at[ai]))) + queries.update((math.nextafter(boundary,-math.inf),boundary,float(at[ai]))) + for delta in (-1e-6,-1e-9,-1e-12,1e-14,1e-13,5e-13,1e-12,2e-12,3e-12,1e-11,1e-10,1e-9,1e-8,1e-7,1e-6,1e-5,1e-4,.001): + queries.add(boundary+delta) + queries=sorted(t for t in queries if 032.39999)&(times<32.4004)) + rows=[dict(time=float(times[i]),**{k:float(v[i]) for k,v in data.items()}) for i in selected] + j=int(np.argmax(abs(data['force']))) + peak=dict(index=j,time=float(times[j]),**{k:float(v[j]) for k,v in data.items()}) + save(directory/'mechanical-events.json',dict(run=run,samples=len(times),around324=rows,maximumAbsoluteForce=peak)) + print('Amesim event output',len(times),'rows',rows,'peak',peak,flush=True) + + +def event_comparison(out): + directory=out/'event-output-comparison';directory.mkdir(exist_ok=True) + (directory/'native').mkdir(exist_ok=True);(directory/'amesim').mkdir(exist_ok=True) + for source,target in [(out/'baseline/full/native/result.json',directory/'native/result.json'), + *[(out/'amesim-event-output'/name,directory/'amesim'/name) for name in ('test_mql_.var','test_mql_.results')]]: + if not target.exists():os.link(source,target) + project,_,_,net,_=model('full') + audit=json.loads((PREVIOUS/'full/audit/audit.json').read_bytes()) + original_reference=curves.read_ame(directory/'amesim') + times=np.array(original_reference.times);order=np.argsort(times,kind='stable') + # Amesim writes its .8 s discontinuity after an already-written grid row + # at .8000000000000009. Preserve every row/value and stable-sort by its + # actual timestamp for this diagnostic matcher; retain the permutation. + sorted_reference=replace(original_reference,times=tuple(times[order]), + series_by_data_path={k:tuple(np.asarray(v)[order]) for k,v in original_reference.series_by_data_path.items()}) + save(directory/'ame-row-order.json',dict(originalTimeInversions=np.flatnonzero(np.diff(times)<0).tolist(),sortedToOriginal=order.tolist())) + with patch.object(curves,'read_ame',return_value=sorted_reference): + summary=evaluation.compare(directory,json.loads(project.read_bytes()),net,audit,evaluation.PROFILES['full']) + # Adding event outputs must preserve every original Amesim saved sample. + old=curves.read_ame(PREVIOUS/'full/reference');new=sorted_reference + oldtimes=np.array(old.times);newtimes=np.array(new.times) + indices=np.searchsorted(newtimes,oldtimes) + assert np.array_equal(newtimes[indices],oldtimes) + unchanged=all(np.array_equal(np.array(new.series(key))[indices],values) for key,values in old.series_by_data_path.items()) + assert unchanged,'Amesim event printout changed original results' + summary['allOriginalAmesimRowsUnchanged']=unchanged + save(directory/'summary.json',summary) + print('event-output comparison',summary['groups']['force']['worstAbsolute'], + 'above5',summary['above5PercentCount'],'unpaired',summary['phaseUnpairedGridCount'],flush=True) + + +def main(): + parser=argparse.ArgumentParser(description=__doc__) + parser.add_argument('--output',type=Path,default=DEFAULT) + parser.add_argument('--stage',choices=('baseline','trace','rebase','analyze','amesim-events','event-comparison','all'),default='all') + args=parser.parse_args();out=args.output.resolve();out.mkdir(parents=True,exist_ok=True) + if args.stage in ('baseline','all'):baseline(out) + if args.stage in ('trace','all'):trace(out) + if args.stage in ('rebase','all'):rebase(out) + if args.stage in ('analyze','all'):analyze(out) + if args.stage in ('amesim-events','all'):amesim_events(out) + if args.stage in ('event-comparison','all'):event_comparison(out) + + +if __name__=='__main__':main() diff --git a/tests/manual/diagnose_valve_tail.py b/tests/manual/diagnose_valve_tail.py new file mode 100644 index 0000000..e3f6e41 --- /dev/null +++ b/tests/manual/diagnose_valve_tail.py @@ -0,0 +1,90 @@ +"""position379 only: diagnose the actual numerical tail before any real skip.""" +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path +import argparse,hashlib,json,os,shutil,subprocess,time +import local_probe_experiment as ex + +ROOT=ex.ROOT +HERE=Path(__file__).parent +BASE=ROOT/'test/local-probe-20260917/worker' +OUT=ROOT/'test/position379-tail-20260917' + +def replace(s,a,b): + assert s.count(a)==1,(a,s.count(a)) + return s.replace(a,b) + +def prepare(trace=True,tail_only=False): + name='tail-only-trace' if tail_only else 'diag-trace' if trace else 'diag-perf' + work=OUT/name;work.mkdir(parents=True,exist_ok=True) + src={p.name:p.read_text(encoding='utf-8') for p in BASE.glob('*.c')} + hashes={k:hashlib.sha256(v.encode()).hexdigest() for k,v in src.items()} + plan=json.loads((ROOT/'test/context-fallback-20260917/plan.json').read_text(encoding='utf-8')) + s=src['model.c'];a,b,e=ex.function_span(s,'model_eval_local_internal');body=s[a:e];op=plan['code'][379][0] + assert body.count(op)==2 + body=body.replace(op,'kd_scope=1;'+op+'kd_scope=0;') + body=replace(body,'return 1;}','kd_eval_exit(t,y,dy,w,1,properties,pipe_cache,jacobian);return 1;}') + src['model.c']=s[:a]+body+s[e:] + s=src['properties.c'];a,b,e=ex.function_span(s,'state_valve');body=s[a:e] + boundary=' double r=fmax(pd/p,0),critical=pow(2*g/(g+1),1/(1-g)),eff;' + body=replace(body,boundary,''' KdRecord kd_record;uint64_t kd_t=0; + if(kd_scope){uint64_t kd_lookup_start=kd_clock();kd_before(&kd_record,p,T,pd,g,rho);kd_record.lookup=kd_clock()-kd_lookup_start; + kd_t=kd_clock(); + } +'''+boundary) + end=body.rfind('}') + body=body[:end]+' if(kd_scope){uint64_t kd_end=kd_clock();kd_after(&kd_record,*cm,*vel,kd_end-kd_t,1);}\n'+body[end:] + if tail_only: + # Cross-check K without key construction, lookup, or environment reads + # immediately before the timed numerical work. Post-tail records in this + # mode MUST NOT be used for memo/FP eligibility or G/H estimates. + before='uint64_t kd_lookup_start=kd_clock();kd_before(&kd_record,p,T,pd,g,rho);kd_record.lookup=kd_clock()-kd_lookup_start;' + body=replace(body,before,'') + body=replace(body,'uint64_t kd_end=kd_clock();kd_after(', 'uint64_t kd_end=kd_clock();kd_before(&kd_record,p,T,pd,g,rho);kd_record.lookup=0;kd_after(') + src['properties.c']=s[:a]+body+s[e:] + src['common.c']=replace(replace(src['common.c'],'lp_start();','lp_start();kd_start();'),'lp_finish();','lp_finish();kd_finish();') + src['cvode_solver.c']=replace(src['cvode_solver.c'],'{lp_color=-1;uint64_t start=lp_tick();','{lp_color=-1;kd_new_jac();uint64_t start=lp_tick();') + src['cvode_solver.c']=replace(src['cvode_solver.c'],'if(!result)lp_matrix(t,N_VGetArrayPointer(y),SUNDenseMatrix_Data(matrix));','if(!result){lp_matrix(t,N_VGetArrayPointer(y),SUNDenseMatrix_Data(matrix));kd_matrix(t,N_VGetArrayPointer(y),SUNDenseMatrix_Data(matrix));}') + src['valve_tail_diag.c']=(HERE/'valve_tail_diag.c').read_text(encoding='utf-8') + for name in ('model.h','local_probe.h'):shutil.copyfile(BASE/name,work/name) + shutil.copyfile(HERE/'valve_tail_diag.h',work/'valve_tail_diag.h') + cc,sun,_=ex.builder.toolchain();flags,libs,dlls,exe=ex.builder.platform_build_inputs(sun) + flags+=['-DLP_OBSERVE=0',f'-DKD_TRACE={int(trace)}'] + def compile_one(item): + name,code=item;path=work/name;path.write_text('#include "valve_tail_diag.h"\n'+code,encoding='utf-8',newline='\n');obj=path.with_suffix('.o');log=[] + ex.builder._command([cc,*flags,'-I',str(work),'-I',str(ex.builder.NATIVE/'include'),'-I',str(sun/'include'),'-c',str(path),'-o',str(obj)],log=log,timeout=240) + return obj,log + with ThreadPoolExecutor(max_workers=4) as pool:objects=list(pool.map(compile_one,src.items())) + log=[];ex.builder._command([cc,*flags,*[str(o) for o,_ in objects],*ex.builder.link_library_arguments(libs),'-lm','-o',str(work/exe)],log=log) + for dll in dlls:shutil.copyfile(dll,work/dll.name) + (work/'build.log').write_text('\n'.join(sum([l for _,l in objects],[])+log),encoding='utf-8') + assert src['local_probe_support.c']==(BASE/'local_probe_support.c').read_text(encoding='utf-8') + ex.write(work/'build.json',dict(originalHashes=hashes,skip=False,trace=trace,tailOnlyTiming=tail_only,wholeContextGuardUnchanged=True,sourceHashes={p.name:hashlib.sha256(p.read_bytes()).hexdigest() for p in work.glob('*.c')})) + print('BUILT',work.name,flush=True) + +def run(label,worker='diag-trace',matrices=False,disabled=False): + folder=OUT/label;folder.mkdir(parents=True,exist_ok=True);env=os.environ.copy();env['LOCAL_PROBE_MASK']='0x7ffffff' + if matrices:env['KD_MATRICES']='1' + if disabled:env['KD_DISABLED']='1' + args=[str(OUT/worker/'model.exe'),'--method','BDF','--start','0','--stop','10','--sample-step','.01','--max-step','1e30','--rtol','1e-8','--timeout','300','--sample-file',str(folder/'states.bin'),'--output-block-file',str(folder/'outputs.bin'),'--output',str(folder/'result.json')] + start=time.perf_counter() + with (folder/'stderr.log').open('wb') as f:r=subprocess.run(args,cwd=folder,env=env,stdout=subprocess.PIPE,stderr=f,timeout=330,creationflags=subprocess.CREATE_NO_WINDOW) + assert r.returncode==0,(label,r.returncode,(folder/'stderr.log').read_text(encoding='utf-8')[-4000:]) + result=json.loads((folder/'result.json').read_text(encoding='utf-8'));d=json.loads((folder/'probe.json').read_text(encoding='utf-8')) + reference=json.loads((BASE.parent/'all-run-0/measurement.json').read_text(encoding='utf-8')) + keys=['success','finalState','final','propertyWarnings','acceptedSteps','rejectedSteps','stateTransitions','solverStarts','nfev','njev','nlu'] + assert all(result[k]==reference[k] for k in keys),[(k,result[k],reference[k]) for k in keys if result[k]!=reference[k]] + for k in ['newtonIterations','newtonConvergenceFailures','modelCalls','groups','contextCopiedBytes','contextComparedBytes']:assert d[k]==reference['diagnostic'][k],k + hashes={} + for name in ['states','outputs','events']+(['jacobians'] if matrices else []): + hashes[name]=hashlib.sha256((folder/(name+'.bin')).read_bytes()).hexdigest() + expected=hashlib.sha256((BASE.parent/'all-audit/jacobians.bin').read_bytes()).hexdigest() if name=='jacobians' else reference[name+'Sha256'] + assert hashes[name]==expected,name + data=json.loads((folder/'tail.json').read_text());assert data['jacobians']==896 + record=dict(label=label,worker=worker,exact=True,hashes=hashes,counters={k:result[k] for k in keys},newtonIterations=d['newtonIterations'],newtonConvergenceFailures=d['newtonConvergenceFailures'],tail=data,wall=result['solveSeconds'],cpu=result['solveCpuSeconds'],jacobian=d.get('jacobianSeconds'),processSeconds=time.perf_counter()-start) + ex.write(folder/'validation.json',record);print(json.dumps(dict(label=label,exact=True,tail=data,wall=record['wall'],cpu=record['cpu']),ensure_ascii=True),flush=True) + return record + +if __name__=='__main__': + p=argparse.ArgumentParser();p.add_argument('action',choices=['prepare','run']);p.add_argument('--label',default='diagnostic-0');p.add_argument('--worker',default='diag-trace');p.add_argument('--no-trace',action='store_true');p.add_argument('--tail-only',action='store_true');p.add_argument('--matrices',action='store_true');p.add_argument('--disabled',action='store_true');a=p.parse_args() + if a.action=='prepare':prepare(not a.no_trace,a.tail_only) + else:run(a.label,a.worker,a.matrices,a.disabled) diff --git a/tests/manual/evaluate_mechanical_events.py b/tests/manual/evaluate_mechanical_events.py new file mode 100644 index 0000000..014cccd --- /dev/null +++ b/tests/manual/evaluate_mechanical_events.py @@ -0,0 +1,140 @@ +"""Run isolated off/MASS/LSTP/all experiments without altering production. + +One fresh Amesim execution per profile supplies the common reference. Accuracy +runs retain full output. Repeated solve-only runs rotate variant order and +exclude compilation/output serialization from integration timing. +""" +from dataclasses import replace +from concurrent.futures import ThreadPoolExecutor +from contextlib import nullcontext +import argparse +import hashlib +import json +import os +from pathlib import Path +import statistics +import sys +import time +from unittest.mock import patch + +ROOT=Path(__file__).resolve().parents[2] +sys.path.insert(0,str(ROOT)) +from app.main import compile_system_xml_network +from app.simulation.backends import simulation_config +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.input import load_input +from app.simulation.native_codegen.runner import execute_native +from tests.manual import evaluate_mql8_correctness as evaluation +from tests.manual.mechanical_event_variant import event_program,prepare_runtime,MODES + + +def tree_hashes(root): + return {str(p.relative_to(root)):hashlib.sha256(p.read_bytes()).hexdigest() + for folder in ('native','app/simulation/native_codegen') for p in (root/folder).rglob('*') + if p.is_file() and '__pycache__' not in p.parts} + + +def main(): + parser=argparse.ArgumentParser(description=__doc__) + parser.add_argument('--output',type=Path,required=True) + parser.add_argument('--profiles',nargs='+',choices=('full','noncyclic'),default=['full','noncyclic']) + parser.add_argument('--repeats',type=int,default=5) + parser.add_argument('--resume',action='store_true',help='Resume completed accuracy/timing runs after a transient toolchain failure.') + parser.add_argument('--serial-build',action='store_true',help='Serialize experiment compilation to recover Windows compiler-launch failures; solve settings are unchanged.') + args=parser.parse_args();out=args.output.resolve();out.mkdir(parents=True,exist_ok=args.resume) + if args.repeats<3:raise ValueError('Use at least three repeated integration timings') + control=ROOT/'test/mechanical-events-20260917/source-control/native' + runtime=out/'experimental-native';prepare_runtime(control,runtime) + production=tree_hashes(ROOT) + evaluation.save(out/'production-before.json',production) + results=json.loads((out/'summary.json').read_bytes()) if args.resume and (out/'summary.json').exists() else {} + for profile in args.profiles: + if profile in results and all('timing' in r for r in results[profile].values()):continue + base=out/profile;base.mkdir(exist_ok=args.resume) + ame=ROOT/('tests/data/test_mql.ame' if profile=='full' else 'test/node-fixes-amesim-20260914/test_mql.ame') + project=ROOT/'test/output-semantics-20260917/after'/profile/'platform.json' + audit=(json.loads((base/'audit/audit.json').read_bytes()) if (base/'audit/audit.json').exists() + else evaluation.audit_input(ame,project,base/'audit')) + settings=evaluation.PROFILES[profile];stop,step,rtol=settings + if not (base/'reference').exists():evaluation.prepare_ame(ame,base/'reference',stop,step,rtol) + if args.resume and (base/'reference/run-summary.json').exists(): + ame_run=json.loads((base/'reference/run-summary.json').read_bytes()) + assert ame_run['normalTermination'] and ame_run['returncode']==0 + else: + print(profile,'fresh Amesim reference',flush=True) + ame_run=evaluation.run_ame(base/'reference',Path('F:/AMESim2404/Amesim')) + xml,document=load_input(project);network=compile_system_xml_network(document) + raw_project=json.loads(project.read_bytes()) + original=compile_native_program(network) + builds={};records=json.loads((base/'summary.json').read_bytes()) if args.resume and (base/'summary.json').exists() else {} + config=replace(simulation_config(document.simulation),rtol=rtol) + try: + for mode in MODES: + directory=base/mode;directory.mkdir(exist_ok=args.resume);(directory/'platform.json').write_bytes(project.read_bytes()) + (directory/'platform.xml').write_bytes(xml) + reference=directory/'amesim';reference.mkdir(exist_ok=args.resume) + for name in ('test_mql_.results','test_mql_.var'): + if not (reference/name).exists():os.link(base/'reference'/name,reference/name) + program,metadata=event_program(original,network,mode) + evaluation.save(directory/'event-descriptors.json',metadata) + assert program.state_keys==original.state_keys + assert program.jacobian_structure==original.jacobian_structure + assert program.evaluation_schedule==original.evaluation_schedule + assert program.source.startswith(original.source) + for attempt in range(3): + cache=out/('build-cache' if not attempt else f'build-cache-retry-{profile}-{mode}-{time.time_ns()}') + try: + build_workers=(patch.object(builder,'ThreadPoolExecutor',lambda **kwargs: ThreadPoolExecutor(max_workers=1)) + if args.serial_build else nullcontext()) + with patch.object(builder,'NATIVE',control if mode=='off' else runtime),patch.object(builder,'CACHE',cache),build_workers: + print(profile,mode,'build',attempt+1,flush=True) + builds[mode]=builder.build_native(program) + break + except PermissionError as exc: + if getattr(exc,'winerror',None)!=5 or attempt==2:raise + print('Windows cache rename denied; retaining artifacts and using a fresh isolated cache.',flush=True) + if mode in records and not records[mode].get('failed'): + print(profile,mode,'retaining completed accuracy result',flush=True) + continue + print(profile,mode,'accuracy run',flush=True) + with patch.object(result_storage,'RESULT_ROOT',out/'result-storage'): + r=execute_native(builds[mode],config,step,run_dir=directory/'native',timeout=180) + native={k:v for k,v in r.items() if k not in ('series','final','finalState')} + evaluation.save(directory/'native-summary.json',native) + if not r['success']: + records[mode]=dict(nativeRun=native,failed=True) + evaluation.save(base/'summary.json',records) + raise RuntimeError(f'{profile}/{mode} failed: '+r['message']) + summary=evaluation.compare(directory,raw_project,network,audit,settings) + summary.update(nativeRun=native,amesimRun=ame_run,settings=dict(stop=stop,sampleStep=step,rtol=rtol)) + records[mode]=summary;evaluation.save(base/'summary.json',records) + print(profile,mode,'accuracy done; force=',summary['groups']['force']['worstAbsolute']['maxAbsoluteError'], + 'events=',native.get('experimentalEvents',{}),flush=True) + timings=json.loads((base/'timings.json').read_bytes()) if args.resume and (base/'timings.json').exists() else {mode:[] for mode in MODES} + for repeat in range(args.repeats): + order=MODES[repeat%4:]+MODES[:repeat%4] + for mode in order: + if len(timings[mode])>repeat:continue + print(profile,'timing',repeat+1,mode,flush=True) + r=execute_native(builds[mode],config,step,record_samples=False, + run_dir=base/mode/f'timing-{repeat+1}',timeout=180) + if not r['success']:raise RuntimeError(r['message']) + selected={k:v for k,v in r.items() if k in ('solveSeconds','solveCpuSeconds','processWallSeconds', + 'nfev','acceptedSteps','rejectedSteps','solverStarts','stateTransitions','njev','nlu','experimentalEvents')} + timings[mode].append(selected) + evaluation.save(base/'timings.json',timings) + for mode in MODES: + values=[r['solveSeconds'] for r in timings[mode]] + records[mode]['timing']=dict(repeats=len(values),medianSeconds=statistics.median(values), + minimumSeconds=min(values),maximumSeconds=max(values),runs=timings[mode]) + results[profile]=records;evaluation.save(out/'summary.json',results) + finally: + for build in builds.values():build.close() + after=tree_hashes(ROOT) + evaluation.save(out/'production-verification.json',dict(unchanged=production==after,before=production,after=after)) + assert production==after,'Production code changed during the isolated experiment' + print('Completed; production source and existing numerical optimizations unchanged.',flush=True) + + +if __name__=='__main__':main() diff --git a/tests/manual/evaluate_mql8_correctness.py b/tests/manual/evaluate_mql8_correctness.py new file mode 100644 index 0000000..bfe20ea --- /dev/null +++ b/tests/manual/evaluate_mql8_correctness.py @@ -0,0 +1,412 @@ +"""Re-run audited eight-branch inputs against the AME archive's executable. + +Use project Python 3.12; --output must be a new directory. Only copied time +settings change unless --align-cyclic-from-ame is explicitly supplied, which +permits only the audited UD00 cyclic flags to change in a new JSON copy. +Numeric differences are evidence, not a claim of physical validation. +""" +from __future__ import annotations + +import argparse +from contextlib import redirect_stdout +from dataclasses import replace +import hashlib +import json +import os +from pathlib import Path +import re +import subprocess +import sys +import tarfile +import time +from unittest.mock import patch + +import numpy as np + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +from app.main import compile_system_xml_network +from app.simulation.backends import simulation_config +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.input import load_input +from app.simulation.native_codegen.runner import execute_native +from tests.manual import mql8_comparison as curves +from tests.manual.event_phase_comparison import pair_saved_phases +from tools import audit_test_mql8_model as auditor + +# Report boundaries, not accepted engineering tolerances. Preserve absolute +# errors when the reference is small, and report the sensitivity to this choice. +EPS = dict(pressure=1., temperature=1e-6, enthalpy_flow=1., mass_flow=1e-6, + displacement=1e-9, velocity=1e-6, gap=1e-9, mass=1e-12, + volume=1e-12, piston_volume=1e-12, volume_rate=1e-12, + chamber_volume_rate=1e-12, volume_work=1., force=1e-6, signal=1e-12) +PROFILES = {'default': (10., .01, 1e-8), 'full': (50., .01, 1e-8), + 'startup': (.15, .0001, 1e-8), 'startup-refined': (.15, .0001, 1e-10), + 'volume-startup': (2e-6, 2e-9, 1e-10), 'noncyclic': (50., .01, 1e-8)} + + +def digest(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def save(path, data): + path.write_text(json.dumps(data, ensure_ascii=False, indent=2, allow_nan=False) + '\n', encoding='utf-8') + + +def audit_input(ame, project, out, allow_cyclic_mismatch=False): + ame, project = ame.resolve(), project.resolve() + out.mkdir(parents=True) + with patch.object(auditor, 'HERE', out), patch.object(auditor, 'AME', ame), \ + patch.object(auditor, 'INPUT', project), patch.object(sys, 'argv', ['audit', '--check']), \ + (out / 'audit.log').open('w', encoding='utf-8') as log, redirect_stdout(log): + try: + auditor.main() + except AssertionError: + if not allow_cyclic_mismatch or not (out / 'audit.json').exists(): + raise + evidence = json.loads((out / 'audit.json').read_bytes()) + changes = evidence['parameterChanges'] + if evidence['connectionChanges'] or not changes or not all( + row['parameter'] == 'iscyclic' and row['component'].startswith('amesim_ud00_') + for row in changes): + raise + return json.loads((out / 'audit.json').read_bytes()) + + +def prepare_ame(archive_path, target, stop, step, rtol): + target.mkdir() + hashes = {} + with tarfile.open(archive_path) as archive: + for member in archive: + if not member.isfile() or member.name.endswith(('.results', '.ameperf')): + continue + path = (target / member.name).resolve() + if not path.is_relative_to(target.resolve()): + raise ValueError('Archive path outside target: ' + member.name) + path.parent.mkdir(parents=True, exist_ok=True) + data = archive.extractfile(member).read() + path.write_bytes(data) + hashes[member.name] = hashlib.sha256(data).hexdigest() + sim = target / 'test_mql_.sim' + lines = sim.read_text(encoding='ascii').splitlines() + original = lines[0].split() + fields = original[:] + fields[:5] = ['0', str(stop), str(step), '1e30', str(rtol)] + lines[0] = ' '.join(fields) + sim.write_text('\n'.join(lines) + '\n', encoding='ascii') + assert all(digest(target / name) == value for name, value in hashes.items() if name != sim.name) + save(target / 'source-verification.json', dict(archiveSha256=digest(archive_path), + originalSim=original, actualSim=fields, unchangedArchiveFiles=hashes)) + + +def run_ame(target, ame_home): + env = dict(os.environ, AME=str(ame_home)) + env['PATH'] = str(ame_home / 'win64') + os.pathsep + str(ame_home) + os.pathsep + env['PATH'] + start = time.perf_counter() + with (target / 'run.log').open('wb') as log: + proc = subprocess.run([str(target / 'test_mql_.exe')], cwd=target, env=env, + stdout=log, stderr=subprocess.STDOUT, timeout=240, + creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0) + elapsed = time.perf_counter() - start + log = (target / 'run.log').read_text(encoding='utf-8', errors='replace') + cpu = re.search(r'Total CPU time:\s*([\d.eE+-]+)', log) + result = dict(returncode=proc.returncode, processWallSeconds=elapsed, + cpuSeconds=float(cpu.group(1)) if cpu else None, + normalTermination='terminated normally' in log) + save(target / 'run-summary.json', result) + if proc.returncode or not result['normalTermination']: + raise RuntimeError(log[-3000:]) + result['resultsSha256'] = digest(target / 'test_mql_.results') + return result + + +def event_times(project, stop): + result = set() + for node in project['nodes']: + kind, p = node['data']['modelType'], node['data']['parameters'] + if kind == 'amesim_step0': + result.add(float(p['time'])) + elif kind == 'amesim_ud00': + durations = [float(p['t' + str(i)]) for i in range(1, int(p['nstages']) + 1)] + period = sum(durations) + cycles = range(int(stop / period) + 1) if int(p['iscyclic']) else range(1) + for cycle in cycles: + at = float(p['tstart']) + cycle * period + result.add(at) + for duration in durations: + at += duration + result.add(at) + return sorted(t for t in result if 0 < t <= stop) + + +def metric(actual, expected, grid, quantity, exact_events, quiet): + error = actual - expected + absolute = np.abs(error) + nonzero = np.abs(expected) > EPS[quantity] + relative = np.zeros_like(error) + relative[nonzero] = 100 * error[nonzero] / expected[nonzero] + bad = nonzero & (np.abs(relative) > 5) + near = ~nonzero + index = int(np.argmax(absolute)) + def maximum(values, mask): + return float(np.max(values[mask])) if np.any(mask) else None + examples = [] + for j in np.flatnonzero(bad)[np.argsort(absolute[bad])[-5:][::-1]]: + examples.append(dict(time=float(grid[j]), platform=float(actual[j]), amesim=float(expected[j]), + absoluteError=float(absolute[j]), relativePercent=float(relative[j]), + atSignalEvent=bool(exact_events[j]))) + return dict(maxAbsoluteError=float(absolute[index]), worstTime=float(grid[index]), + platformAtWorst=float(actual[index]), amesimAtWorst=float(expected[index]), + rmse=float(np.sqrt(np.mean(error**2))), finalError=float(error[-1]), + referencePeak=float(np.max(np.abs(expected))), + maxRelativePercent=maximum(np.abs(relative), nonzero), + maxRelativeOutsideEvents=maximum(np.abs(relative), nonzero & ~exact_events), + maxAbsoluteOutsideEvents=maximum(absolute, ~exact_events), + quietMaxAbsolute=maximum(absolute, quiet), + nonzeroCount=int(nonzero.sum()), above5PercentCount=int(bad.sum()), + above5PercentOutsideEvents=int((bad & ~exact_events).sum()), + nearZeroCount=int(near.sum()), nearZeroMaxAbsolute=maximum(absolute, near), + nearZeroBeyondEpsilon=int((near & (absolute > EPS[quantity])).sum()), + epsilon=EPS[quantity], examples=examples, + relativeScreenSensitivity={str(factor): int(((np.abs(expected) > EPS[quantity] * factor) + & (absolute > .05 * np.abs(expected))).sum()) for factor in (.1, 1., 10.)}) + + +def sample_grid(times, values, grid, step): + """Use the actual saved value at the same nominal output-grid position. + + Repeated floating-point additions move an Amesim output timestamp slightly + off its nominal grid. Interpolating 1e17 -> 49000 immediately before an + almost-equal endpoint invents a plateau error through cancellation. Match + only within 1e-7 of one output interval; retain that record's original side + of an event. Do not move event times, average duplicates, or smooth spikes. + """ + times = np.asarray(times) + right = np.clip(np.searchsorted(times, grid), 0, len(times) - 1) + left = np.maximum(right - 1, 0) + closest = np.where(np.abs(times[left] - grid) < np.abs(times[right] - grid), left, right) + same_output = np.abs(times[closest] - grid) <= step * 1e-7 + result = np.interp(grid, times, values) + result[same_output] = np.asarray(values)[closest[same_output]] + return result + + +def compare(directory, project, network, audit, settings): + stop, step, rtol = settings + for node in project['nodes']: + if node['data']['modelType'] == 'amesim_ud00': + p = node['data']['parameters'] + if any(float(p['start'+str(i)]) != float(p['end'+str(i)]) for i in range(1, int(p['nstages'])+1)): + raise ValueError('This baseline phase matcher requires piecewise-constant UD00 stages; ramps need explicit stage metadata.') + curves.configure(audit, network) + mapping = curves.curve_mapping() + save(directory / 'curve-mapping.json', mapping) + raw = json.loads((directory / 'native/result.json').read_bytes()) + native = curves.native_curves(raw['series']) + ame = curves.read_ame(directory / 'amesim') + nt, at = native['time'], np.array(ame.times) + assert raw['success'] and raw['simulatedUntil'] == stop + assert nt[0] == at[0] == 0 and abs(at[-1] - stop) < 1e-9 and nt[-1] == stop + assert np.all(np.diff(nt) > 0) and np.all(np.diff(at) >= 0) + assert all(np.isfinite(v).all() for v in native.values()) + assert all(np.isfinite(v).all() for v in ame.series_by_data_path.values()) + grid = np.arange(round(stop / step) + 1) * step + events = event_times(project, stop) + exact_events = np.zeros(grid.shape, dtype=bool) + quiet = grid >= .1 + for event in events: + exact_events |= np.abs(grid - event) <= 1e-9 + quiet &= np.abs(grid - event) > .0200001 + signal_mapping = [m for m in mapping if m['quantity'] == 'signal'] + ni, ai, pairing = pair_saved_phases(nt, at, + np.column_stack([native[m['key']] for m in signal_mapping]), + np.column_stack([curves.ame_curve(ame, m) for m in signal_mapping]), grid, step) + valid = (ni >= 0) & (ai >= 0) + save(directory / 'phase-pairing.json', dict( + policy='Reference saved forcing phase; one shared row pair for all curves; no cross-event interpolation', + scope='Observed STEP/UD00 forcing phases; unregistered contact mode is not certified', + signalKeys=[m['key'] for m in signal_mapping], toleranceSeconds=step*1e-7, + gridCount=len(grid), pairedCount=int(valid.sum()), unpairedCount=int((~valid).sum()), + adjustedCount=sum(p['status'] == 'matched-other-event-side' for p in pairing), records=pairing)) + if not np.any(valid): + raise ValueError('No matching saved physical phases; see phase-pairing.json') + rows, raw_rows, arrays = [], [], {'time': grid, 'phaseMatched': valid} + for m in mapping: + raw_actual = sample_grid(nt, native[m['key']], grid, step) + reference = curves.ame_curve(ame, m) + raw_expected = sample_grid(at, reference, grid, step) + raw_rows.append(m | metric(raw_actual, raw_expected, grid, m['quantity'], exact_events, quiet)) + actual, expected = np.full(len(grid), np.nan), np.full(len(grid), np.nan) + actual[valid], expected[valid] = native[m['key']][ni[valid]], reference[ai[valid]] + rows.append(m | metric(actual[valid], expected[valid], grid[valid], m['quantity'], exact_events[valid], quiet[valid])) + arrays['platform|' + m['key']] = actual + arrays['amesim|' + m['key']] = expected + np.savez_compressed(directory / 'curves.npz', **arrays) + save(directory / 'raw-time-comparison.json', dict(curves=raw_rows, + above5PercentCount=sum(r['above5PercentCount'] for r in raw_rows))) + groups = {} + for quantity in sorted({r['quantity'] for r in rows}): + selected = [r for r in rows if r['quantity'] == quantity] + groups[quantity] = dict(curveCount=len(selected), + worstAbsolute=max(selected, key=lambda r: r['maxAbsoluteError']), + worstRelative=max(selected, key=lambda r: r['maxRelativePercent'] or 0), + nonzeroCount=sum(r['nonzeroCount'] for r in selected), + above5PercentCount=sum(r['above5PercentCount'] for r in selected), + above5PercentOutsideEvents=sum(r['above5PercentOutsideEvents'] for r in selected), + nearZeroBeyondEpsilon=sum(r['nearZeroBeyondEpsilon'] for r in selected)) + mass_keys = [k for k in raw['series'] if k.rsplit('.', 1)[-1] in ('m', 'm1', 'm2')] + mass = sum(native[k] for k in mass_keys) + pressures = [v for k, v in native.items() if k.rsplit('.', 1)[-1] in ('p', 'p1', 'p2')] + temperatures = [v for k, v in native.items() if k.rsplit('.', 1)[-1] in ('T', 'T1', 'T2')] + physical = dict(massKeys=mass_keys, initialMassKg=float(mass[0]), + maxTotalMassDriftKg=float(np.max(np.abs(mass - mass[0]))), + relativeMassDrift=float(np.max(np.abs(mass - mass[0])) / mass[0]), + minimumGasMassKg=float(min(np.min(native[k]) for k in mass_keys)), + minimumAbsolutePressurePa=float(min(np.min(v) for v in pressures)), + minimumTemperatureK=float(min(np.min(v) for v in temperatures)), + maximumTemperatureK=float(max(np.max(v) for v in temperatures))) + event_samples = [] + for event in events: + for m in mapping: + if m['quantity'] not in ('signal', 'force'): + continue + entry = dict(eventTime=event, key=m['key']) + for label, times, values in [('platform', nt, native[m['key']]), + ('amesim', at, curves.ame_curve(ame, m))]: + idx = int(np.searchsorted(times, event)) + entry[label] = [dict(time=float(times[i]), timeHex=float(times[i]).hex(), value=float(values[i])) + for i in range(max(0, idx-2), min(len(times), idx+3))] + event_samples.append(entry) + save(directory / 'event-samples.json', event_samples) + summary = dict(curveCount=len(rows), gridCount=len(grid), nativeSampleCount=len(nt), + phaseMatchedGridCount=int(valid.sum()), phaseUnpairedGridCount=int((~valid).sum()), + phaseAdjustedGridCount=sum(p['status'] == 'matched-other-event-side' for p in pairing), + rawTimeAbove5PercentCount=sum(r['above5PercentCount'] for r in raw_rows), + amesimSampleCount=len(at), platformOutputCount=len(raw['series']) - 1, allFinite=True, + signalEvents=events, physical=physical, groups=groups, curves=rows, + extraEventPointMaxContactForce=max(float(np.max(np.abs(native[m['key']]))) + for m in mapping if m['quantity'] == 'force'), + above5PercentCurveCount=sum(r['above5PercentCount'] > 0 for r in rows), + above5PercentCount=sum(r['above5PercentCount'] for r in rows), + above5PercentOutsideEvents=sum(r['above5PercentOutsideEvents'] for r in rows)) + save(directory / 'comparison.json', summary) + return {k: v for k, v in summary.items() if k != 'curves'} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--output', type=Path, required=True) + parser.add_argument('--ame', type=Path, default=ROOT / 'tests/data/test_mql.ame') + parser.add_argument('--project', type=Path, default=ROOT / 'tests/data/test-mql-8-corrected.json') + parser.add_argument('--ame-home', type=Path, required=True) + parser.add_argument('--noncyclic-ame', type=Path) + parser.add_argument('--noncyclic-project', type=Path) + parser.add_argument('--align-cyclic-from-ame', action='store_true') + parser.add_argument('--analyze-only', action='store_true', help='Reanalyze existing raw results without solving.') + parser.add_argument('--profiles', nargs='+', choices=tuple(PROFILES), default=list(PROFILES)) + parser.add_argument('--native-source', type=Path, help='Frozen native source tree for a controlled before/after run.') + args = parser.parse_args() + if args.native_source: + builder.NATIVE = args.native_source.resolve() + out = args.output.resolve() + if args.analyze_only: + summaries = json.loads((out / 'summary.json').read_bytes()) + for name, old in list(summaries.items()): + directory = out / name + project = json.loads((directory / 'platform.json').read_bytes()) + _, document = load_input(directory / 'platform.json') + audit_name = 'audit-noncyclic' if name == 'noncyclic' else 'audit-aligned' + audit = json.loads((out / audit_name / 'audit.json').read_bytes()) + summary = compare(directory, project, compile_system_xml_network(document), audit, PROFILES[name]) + summary.update({key: old[key] for key in ('settings', 'amesimRun', 'nativeRun')}) + summaries[name] = summary + print(name, 'reanalyzed', summary['above5PercentCount'], flush=True) + save(out / 'summary.json', summaries) + manifest = json.loads((out / 'manifest.json').read_bytes()) + manifest['interpolation'] = 'Same saved forcing phase within 1e-7 output intervals. Unpaired points explicitly reported; raw time-only metrics retained.' + save(out / 'manifest.json', manifest) + return + out.mkdir(parents=True, exist_ok=False) + if bool(args.noncyclic_ame) != bool(args.noncyclic_project): + parser.error('Both noncyclic input paths are required together.') + inputs = [args.ame, args.project] + ([args.noncyclic_ame, args.noncyclic_project] if args.noncyclic_ame else []) + tracked = subprocess.check_output(['git', 'ls-files', 'app', 'native', 'frontend/src'], cwd=ROOT, text=True).splitlines() + sources = {str(p.resolve()): digest(p) for p in inputs} + code = {p: digest(ROOT / p) for p in tracked if (ROOT / p).is_file()} + manifest = dict(gitCommit=subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=ROOT, text=True).strip(), + python=sys.version, sources=sources, productionSources=code, epsilons=EPS, + criterion='5% is a diagnostic screen, not an approved engineering tolerance', + freshAmesimExecution=True, amesimExecutable='Extracted from source AME; not recompiled', + nativeSource=str(builder.NATIVE), + interpolation='Same saved forcing phase within 1e-7 output intervals. Unpaired points explicitly reported; raw time-only metrics retained.') + save(out / 'manifest.json', manifest) + original_audit = audit_input(args.ame, args.project, out / 'audit-original', args.align_cyclic_from_ame) + if original_audit['parameterChanges']: + aligned = json.loads(args.project.read_bytes()) + nodes = {node['id']: node for node in aligned['nodes']} + for row in original_audit['parameterChanges']: + nodes[row['component']]['data']['parameters'][row['parameter']] = row['expected'] + aligned_path = out / 'aligned-source.json' + save(aligned_path, aligned) + save(out / 'alignment-changes.json', original_audit['parameterChanges']) + args.project = aligned_path + audits = {'standard': audit_input(args.ame, args.project, out / 'audit-aligned')} + if args.noncyclic_ame: + audits['noncyclic'] = audit_input(args.noncyclic_ame, args.noncyclic_project, out / 'audit-noncyclic') + summaries = {} + for name, settings in PROFILES.items(): + if name not in args.profiles: + continue + if name == 'noncyclic' and not args.noncyclic_ame: + continue + stop, step, rtol = settings + ame_path = args.noncyclic_ame if name == 'noncyclic' else args.ame + project_path = args.noncyclic_project if name == 'noncyclic' else args.project + audit = audits['noncyclic' if name == 'noncyclic' else 'standard'] + directory = out / name + directory.mkdir() + project = json.loads(project_path.read_bytes()) + project['simulation'].update(t_start=0., t_stop=stop, step=step, max_step=1e30, method='BDF') + save(directory / 'platform.json', project) + prepare_ame(ame_path, directory / 'amesim', stop, step, rtol) + print(name, 'Amesim running', flush=True) + ame_summary = run_ame(directory / 'amesim', args.ame_home) + xml, document = load_input(directory / 'platform.json') + (directory / 'platform.xml').write_bytes(xml) + network = compile_system_xml_network(document) + program = compile_native_program(network) + print(name, 'current platform running', flush=True) + started = time.perf_counter() + # Isolate cache and result quotas from the user's live application. + with patch.object(builder, 'CACHE', out / 'build-cache'), \ + patch.object(result_storage, 'RESULT_ROOT', out / 'result-storage'): + build = builder.build_native(program) + try: + result = execute_native(build, replace(simulation_config(document.simulation), rtol=rtol), + step, run_dir=directory / 'native', timeout=240) + native_summary = {k: v for k, v in result.items() if k not in ('series', 'final', 'finalState')} + native_summary.update(buildSeconds=build.seconds, buildCacheHit=build.cache_hit, + buildKey=build.manifest['buildKey'], pipelineWallSeconds=time.perf_counter()-started, + stateKeys=program.state_keys) + save(directory / 'native-summary.json', native_summary) + finally: + build.close() + summary = compare(directory, project, network, audit, settings) + summary.update(settings=dict(stop=stop, sampleStep=step, rtol=rtol), + amesimRun=ame_summary, nativeRun=native_summary) + summaries[name] = summary + save(out / 'summary.json', summaries) + print(name, 'completed', 'pressure=', summary['groups']['pressure']['worstAbsolute']['maxAbsoluteError'], + 'temperature=', summary['groups']['temperature']['worstAbsolute']['maxAbsoluteError'], + 'above5%=', summary['above5PercentCount'], flush=True) + unchanged = all(digest(Path(p)) == h for p, h in sources.items()) + unchanged_code = all(digest(ROOT / p) == h for p, h in code.items()) + save(out / 'source-verification.json', dict(inputsUnchanged=unchanged, productionUnchanged=unchanged_code, + checkedProductionFiles=len(code))) + assert unchanged and unchanged_code + print('All profiles completed; original inputs and production sources unchanged.', flush=True) + + +if __name__ == '__main__': + main() diff --git a/tests/manual/event_phase_comparison.py b/tests/manual/event_phase_comparison.py new file mode 100644 index 0000000..0a1b711 --- /dev/null +++ b/tests/manual/event_phase_comparison.py @@ -0,0 +1,49 @@ +"""Pair saved samples by forcing phase, never by the size of output errors. + +All curves share ONE pair of row indices. No interpolation across jumps, no +time shifting, and no use of force/pressure agreement to select a sample. +The reference's nearest saved grid row is authoritative; a different native +row is allowed only in the tiny output-timestamp roundoff window. +""" +from __future__ import annotations + +import numpy as np + + +def pair_saved_phases(native_times, reference_times, native_signals, reference_signals, grid, step): + nt, rt, grid = map(np.asarray, (native_times, reference_times, grid)) + ns, rs = map(np.asarray, (native_signals, reference_signals)) + if ns.shape != (len(nt), rs.shape[1]) or len(rs) != len(rt): + raise ValueError('Phase signatures must be rows by the same signal columns.') + tolerance = step * 1e-7 + ni = np.full(len(grid), -1, dtype=int) + ri = np.full(len(grid), -1, dtype=int) + records = [] + for j, t in enumerate(grid): + candidates = np.arange(np.searchsorted(nt, t-tolerance, side='left'), + np.searchsorted(nt, t+tolerance, side='right')) + references = np.arange(np.searchsorted(rt, t-tolerance, side='left'), + np.searchsorted(rt, t+tolerance, side='right')) + if not len(candidates) or not len(references): + records.append(dict(gridIndex=j, time=float(t), status='missing-saved-sample')) + continue + # Prefer the later row on an exact tie / duplicate timestamp, matching + # the comparison's existing right-side duplicate policy. + ref = min(references, key=lambda i: (abs(rt[i]-t), -int(i))) + closest = min(candidates, key=lambda i: (abs(nt[i]-t), -int(i))) + matches = candidates[np.all(np.isclose(ns[candidates], rs[ref], rtol=1e-12, atol=1e-12), axis=1)] + ri[j] = ref + if not len(matches): + records.append(dict(gridIndex=j, time=float(t), status='unmatched-forcing-phase', + referenceTime=float(rt[ref]), referenceSignals=rs[ref].tolist(), + nativeCandidateTimes=nt[candidates].tolist(), + nativeCandidateSignals=ns[candidates].tolist())) + continue + chosen = min(matches, key=lambda i: (abs(nt[i]-t), -int(i))) + ni[j] = chosen + if chosen != closest: + records.append(dict(gridIndex=j, time=float(t), status='matched-other-event-side', + platformTime=float(nt[chosen]), referenceTime=float(rt[ref]), + originalPlatformTime=float(nt[closest]), + signals=ns[chosen].tolist(), referenceSignals=rs[ref].tolist())) + return ni, ri, records diff --git a/tests/manual/fallback_profitability_intervals.md b/tests/manual/fallback_profitability_intervals.md new file mode 100644 index 0000000..1497b1d --- /dev/null +++ b/tests/manual/fallback_profitability_intervals.md @@ -0,0 +1,618 @@ +# 全部 fallback 区间及预算 + +由 `analyze_fallback_profitability.py` 从历史数据生成。ID、group、position 为0基,范围为[start,end)。ms为完整896个Jacobian折算累计;µs为每次fallback。按累计原计算时间排序。 + +P=probe总开销,H=每个Jacobian新增baseline捕获,m=结构上最多共享的失败组数。盈利要求 P+H/m63:raise ValueError('Experimental group mask supports at most 63 groups') + order=[b.members[0] for b in schedule.blocks];ops=[schedule.computations[i] for i in order] + masks,unknown=dependency_masks(d);full=(1<=0])) + boundaries=sorted({x for pair in regions for x in pair}) + # Pure assignments cannot mutate either context. Share their checkpoint + # slot, retaining a new checkpoint after every unreviewed/native call. + pure_calls={'if','for','sizeof','fmax','fmin','fabs','sqrt','copysign','pow'} + versions=[0] + for op in ops: + calls=set(re.findall(r'\b([A-Za-z_]\w*)\s*\(', '\n'.join(op.code))) + versions.append(versions[-1]+bool(calls-pure_calls)) + checkpoints=sorted({versions[x] for x in boundaries}) + boundary={x:checkpoints.index(versions[x]) for x in boundaries} + source=program.source + dims={name:int(re.search(pattern,source)[1]) for name,pattern in { + 'NP':r'double p\[(\d+)\]', 'NQ':r'h\[(\d+)\]', 'NFB':r'double fb\[(\d+)\]', + 'NPC':r'NativePipeCache pipe_cache\[(\d+)\]', 'NPS':r'NativePropertyState property_states\[(\d+)\]'}.items()} + outputs=[];offsets=[0] + arraynames=['p','h','q','w','fb'] + for op in ops: + for target in op.outputs: + a,n=re.fullmatch(r'(p|h|q|w|fb)\[(\d+)\]',target).groups();outputs.append((arraynames.index(a),int(n))) + offsets.append(len(outputs)) + macros={'NC':structure.color_count,'NO':len(ops),'NR':len(regions),'NB':len(checkpoints),'NK':len(KERNELS),**dims} + header='''#ifndef LOCAL_PROBE_EXPERIMENT_H +#define LOCAL_PROBE_EXPERIMENT_H +#include "model.h" +#include +'''+''.join(f'#define LP_{k} {v}\n' for k,v in macros.items())+''' +extern int lp_color,lp_active,lp_capture,lp_observe; +extern uint64_t lp_mask; +extern const int lp_plan[LP_NC][LP_NO],lp_end[LP_NR]; +void lp_start(void);void lp_finish(void);void lp_begin(double,const double*);void lp_ready(int); +void lp_snapshot(int,NativePropertyCache*,NativePipeCache*); +void lp_save(double*,double*,double*,double*,double*); +int lp_reuse(int,NativePropertyCache*,NativePipeCache*,double*,double*,double*,double*,double*); +#if LP_OBSERVE +void lp_operation(int); +#else +#define lp_operation(position) ((void)(position)) +#endif +void lp_kernel(int);int lp_valid(double,const double*); +void lp_note_eval(int); +void lp_compare(int,const double*,const double*,int,const double*,const double*); +void lp_matrix(double,const double*,const double*);void lp_event(double,const double*); +uint64_t lp_tick(void);void lp_jac_time(uint64_t);void lp_newton(long,long); +int lp_eval(double,const double*,double*,double*,ModelJacobianWorkspace*); +#endif +''' + declarations='const int lp_end[LP_NR]={'+','.join(str(b) for a,b in regions)+'};\n' + declarations+='const int lp_plan[LP_NC][LP_NO]={'+','.join('{'+','.join(map(str,p))+'}' for p in plans)+'};\n' + declarations+='static const int lp_before[LP_NR]={'+','.join(str(boundary[a]) for a,b in regions)+'};\n' + declarations+='static const int lp_after[LP_NR]={'+','.join(str(boundary[b]) for a,b in regions)+'};\n' + declarations+='static const int lp_first_output[LP_NR]={'+','.join(str(offsets[a]) for a,b in regions)+'};\n' + declarations+='static const int lp_last_output[LP_NR]={'+','.join(str(offsets[b]) for a,b in regions)+'};\n' + declarations+='static const int lp_start_op[LP_NR]={'+','.join(str(a) for a,b in regions)+'};\n' + declarations+='static const int lp_output_map[][2]={'+','.join('{'+str(a)+','+str(i)+'}' for a,i in outputs)+'};\n' + declarations+='static const int lp_operation_id[LP_NO]={'+','.join(map(str,order))+'};\n' + declarations+='static const int lp_colors[NSTATES]={'+','.join(map(str,structure.colors))+'};\n' + declarations+='static const char *lp_kernel_names[LP_NK]={'+','.join(json.dumps(f) for _,f in KERNELS)+'};\n' + # Keep the original entire model evaluator byte-for-byte. Add an opt-in clone + # whose only numerical statement change is replacement of the schedule. + a,b,e=function_span(source,'model_eval_internal');original=source[a:e] + schedule_text='\n'.join(saved['schedule_lines']);assert original.count(schedule_text)==1 + capture_lines=[] + for pos,op in enumerate(ops): + if pos in boundary:capture_lines.append(f'lp_snapshot({boundary[pos]},properties,pipe_cache);') + capture_lines.extend(op.code) + if len(ops) in boundary:capture_lines.append(f'lp_snapshot({boundary[len(ops)]},properties,pipe_cache);') + capture_lines.append('lp_save(p,h,q,w,fb);') + local=['if(lp_capture){',*capture_lines,'}else{','for(int pos=0;pos=0 && lp_reuse(region,properties,pipe_cache,p,h,q,w,fb)){pos=lp_end[region];continue;}', + 'lp_operation(pos);','switch(pos){'] + for pos,op in enumerate(ops):local += [f'case {pos}:{{',*op.code,'break;}'] + local += ['default:return 0;}','pos++;','}}'] + clone=original.replace('model_eval_internal(', 'model_eval_local_internal(',1).replace(schedule_text,'\n'.join(local)) + wrapper=''' +int lp_eval(double t,const double *y,double *dy,double *w,ModelJacobianWorkspace *workspace){ + static ModelJacobianWorkspace shadow; + int active=lp_color+1;lp_active=active; + int eligible=lp_mask && workspace && (lp_color<0 || (lp_mask&(UINT64_C(1)<scalars.recording=0; + if(lp_color<0)lp_ready(eligible && result); + if(lp_observe){ + double expected_dy[NSTATES],expected_w[NOUTPUTS];lp_active=-1; + int expected=model_eval_jacobian_reuse(t,y,expected_dy,expected_w,&shadow); + lp_compare(result,dy,w,expected,expected_dy,expected_w); + } + lp_active=-1;return result; +} +''' + local_source=source+'\n'+clone+'\n'+wrapper + # The reference original is never replaced in the timing worker. Observation + # counts reference operations analytically in support code, not by modifying it. + assert original in local_source + metadata=dict(stateKeys=program.state_keys,groups=groups,order=order,regions=regions,boundaries=boundaries,contextCheckpointSlots=boundary, + operations=[dict(id=order[i],key=op.key,inputs=sorted(op.inputs),outputs=list(op.outputs),stateIndices=[j for j in range(d.state_count) if opmask[i]>>j&1]) for i,op in enumerate(ops)], + unknownDependencyLeaves=sorted(unknown),macros=macros,originalEvaluatorSha256=hashlib.sha256(original.encode()).hexdigest()) + return '#include "local_probe.h"\n'+local_source,header,declarations,metadata + +def kernel_counter(s,fn,index): + a,b,e=function_span(s,fn) + return s[:b+1]+f'if(lp_active>=0)lp_kernel({index});'+s[b+1:] + +def prepare(model,observe=False): + OUT.mkdir(parents=True,exist_ok=True);started=time.perf_counter();program,saved=capture(model) + local,header,tables,meta=generate(program,saved) + write(OUT/'plan.json',meta);(OUT/'original-model.c').write_text(program.source,encoding='utf-8') + work=OUT/('audit' if observe else 'worker');work.mkdir(exist_ok=True) + (work/'model.h').write_text(program.header,encoding='utf-8');(work/'local_probe.h').write_text(header,encoding='utf-8') + sources={p.relative_to(builder.NATIVE).as_posix():p.read_text(encoding='utf-8') for p in builder._runtime_sources(program)} + source_hashes={key:hashlib.sha256(value.encode()).hexdigest() for key,value in sources.items()} + sources['model.c']=local + common=sources['runtime/common.c'] + common=replace(common,'int ok=r->options.bdf ? native_bdf(r) : native_rk45(r);','lp_start();int ok=r->options.bdf ? native_bdf(r) : native_rk45(r);') + common=replace(common,'r->solve_cpu_seconds=native_cpu_time()-r->cpu_start;','r->solve_cpu_seconds=native_cpu_time()-r->cpu_start;lp_finish();') + common=replace(common,'r->events++;','lp_event(stop,accepted_state);r->events++;') + sources['runtime/common.c']=common + cv=sources['runtime/cvode_solver.c'] + cv=replace(cv,'model_eval_jacobian_reuse(t,N_VGetArrayPointer(y),N_VGetArrayPointer(f),outputs,workspace)', + 'lp_eval(t,N_VGetArrayPointer(y),N_VGetArrayPointer(f),outputs,workspace)') + cv=replace(cv,'for (int color=0;color +#define CHECK(x) do{if(!(x)){fprintf(stderr,"guard self-test failed line %d\n",__LINE__);return 1;}}while(0) +int main(void){ + lp_start();CHECK(saved && lp_mask); + int group=-1,region=-1; + for(int g=0;g=0){group=g;region=lp_plan[g][p];break;} + CHECK(group>=0);lp_color=group;lp_active=group+1; + double y[NSTATES]={0},p[LP_NP]={0},h[LP_NQ]={0},q[LP_NQ]={0},w[NOUTPUTS]={0},fb[LP_NFB]={0}; + NativePropertyState states[LP_NPS]={0}; + NativePipeCache pipes[LP_NPC]={0};NativeJacobianScalars jacobian={0}; + NativePropertyCache context={.states=states,.count=1,.capacity=LP_NPS,.jacobian=&jacobian}; + states[0].p=123;states[0].T=456;states[0].valid=NATIVE_PROPERTY_PT;states[0].jacobian=&jacobian; + lp_begin(.5,y);lp_snapshot(lp_before[region],&context,pipes); + states[0].rho=42;states[0].valid|=NATIVE_PROPERTY_RHO; + lp_snapshot(lp_after[region],&context,pipes); + lp_save(p,h,q,w,fb);lp_ready(1); + CHECK(lp_valid(.5,y));CHECK(!lp_valid(nextafter(.5,1),y)); + int own=-1,other=-1; + for(int i=0;i=0 && other>=0);y[own]=1;CHECK(lp_valid(.5,y));y[other]=1;CHECK(!lp_valid(.5,y));y[other]=0; + Snapshot *before=&saved->snapshots[lp_before[region]],*after=&saved->snapshots[lp_after[region]]; + context.count=before->count;memcpy(states,before->states,before->count*sizeof(*states));memcpy(pipes,before->pipes,sizeof(pipes)); + CHECK(lp_reuse(region,&context,pipes,p,h,q,w,fb)); + CHECK(context.count==after->count && !memcmp(states,after->states,after->count*sizeof(*states))); + memcpy(states,before->states,before->count*sizeof(*states));context.count=before->count; + states[0].rho=nextafter(states[0].rho,1);CHECK(!lp_reuse(region,&context,pipes,p,h,q,w,fb)); + memcpy(states,before->states,before->count*sizeof(*states));pipes[0].valid=1;CHECK(!lp_reuse(region,&context,pipes,p,h,q,w,fb)); + memcpy(pipes,before->pipes,sizeof(pipes));context.count++;CHECK(!lp_reuse(region,&context,pipes,p,h,q,w,fb)); + context.count=before->count;context.jacobian=NULL;CHECK(!lp_reuse(region,&context,pipes,p,h,q,w,fb)); + int pure=-1;for(int r=0;r=0);NativePropertyState held=states[0];NativePipeCache held_pipe=pipes[0]; + size_t held_count=context.count; + CHECK(lp_reuse(pure,&context,pipes,p,h,q,w,fb)); + CHECK(context.count==held_count && context.jacobian==NULL); + CHECK(!memcmp(&held,states,sizeof(held)) && !memcmp(&held_pipe,pipes,sizeof(held_pipe))); + lp_begin(.5,y);CHECK(!lp_valid(.5,y)); + lp_finish();puts("exact context, pipe-cache, time, state and lifetime guards passed");return 0; +} diff --git a/tests/manual/local_probe_profile.c b/tests/manual/local_probe_profile.c new file mode 100644 index 0000000..b9b707a --- /dev/null +++ b/tests/manual/local_probe_profile.c @@ -0,0 +1,86 @@ +/* No numerical work lives here. Runtime selection is outside the hot model. */ +#include "local_probe_profile.h" +#include +#include +#include +/* PROFILE_TABLES */ +PfBucket pf_buckets[LP_NC+2][PF_CATEGORIES],*pf_current; +uint64_t pf_previous_tick; +unsigned pf_row; +int pf_coarse; +static uint64_t evaluations[LP_NC+2]; +uint64_t pf_operations[LP_NC][2]; +static uint64_t calls,sampled,sampled_ticks,frequency,total_events; +static uint64_t sampled_qpc_ticks,tsc_start,tsc_end,clock_t0; +static double clock_q0; +static unsigned stride=16,offset,seed=1; +static FILE *matrix; +static const char *names[PF_CATEGORIES]={"other","perturbation_amount","state_copy_perturb", + "matrix_zero","difference_matrix_write","baseline_compute","initialization","gas_state_preparation", + "schedule_retained","schedule_context_fallback","context_compare","snapshot_capture_save", + "context_output_restore","node_energy","port_outputs","mechanical_equations", + "gas_mass_energy","remaining_outputs","pipe_diagnostics","finite_check","schedule_dispatch","whole_probe_fallback"}; +void pf_initialize(void){ + LARGE_INTEGER f;QueryPerformanceFrequency(&f);frequency=(uint64_t)f.QuadPart; + unsigned a,b,c,d; + if(!__get_cpuid(0x80000007,&a,&b,&c,&d) || !(d&(1u<<8))){fprintf(stderr,"invariant TSC required for diagnostic clock\n");abort();} + uint64_t q0=lp_tick();clock_t0=pf_clock();uint64_t q1=lp_tick();clock_q0=((double)q0+(double)q1)/2; + const char *s=getenv("PROBE_PROFILE_STRIDE"),*o=getenv("PROBE_PROFILE_OFFSET"),*v=getenv("PROBE_PROFILE_MATRICES"); + const char *coarse=getenv("PROBE_PROFILE_COARSE");pf_coarse=coarse?atoi(coarse):0; + if(s)stride=(unsigned)strtoul(s,NULL,0); + if(o)offset=(unsigned)strtoul(o,NULL,0); + seed=offset+1; + if(v && atoi(v)){matrix=fopen("jacobians.bin","wb");if(!matrix)abort();setvbuf(matrix,NULL,_IOFBF,1024*1024);} +} +int pf_select(void){ + /* One randomly positioned callback per consecutive stratum. The offset is + reproducible, and avoids locking onto a recurring Newton/event pattern. */ + unsigned pos=(unsigned)(calls++% (stride?stride:1)); + if(!stride)return 0; + if(!pos){seed^=seed<<13;seed^=seed>>17;seed^=seed<<5;offset=seed%stride;} + if(pos!=offset)return 0; + pf_row=0;return 1; +} +void pf_eval_count(void){evaluations[pf_row]++;} +void pf_open(void){pf_current=&pf_buckets[0][PF_OTHER];tsc_start=pf_clock();pf_previous_tick=tsc_start;} +void pf_close(void){tsc_end=pf_clock();pf_current->ticks+=tsc_end-pf_previous_tick;} +void pf_flush(uint64_t start,uint64_t end){ + sampled++;sampled_qpc_ticks+=end-start;sampled_ticks+=tsc_end-tsc_start; +} +void pf_validate_matrix(double t,const double *y,const double *m){ + if(matrix){fwrite(&t,8,1,matrix);fwrite(y,8,NSTATES,matrix);fwrite(m,8,NSTATES*NSTATES,matrix);} +} +static void array(FILE *f,const uint64_t *a,int n){fputc('[',f);for(int i=0;i=2){fprintf(f,",\"operations\":");array(f,pf_operations[r-2],2);}fputc('}',f); + for(int c=0;c +#include +#include +#include "local_probe.h" +enum { PF_OTHER, PF_INCREMENT, PF_PERTURB, PF_ZERO, PF_ASSEMBLY, + PF_BASELINE, PF_INIT, PF_GAS_PREP, PF_RETAINED, PF_FALLBACK, + PF_CONTEXT, PF_SNAPSHOT, PF_RESTORE, PF_NODE, PF_PORT, + PF_MECHANICAL, PF_GAS_EQUATIONS, PF_OUTPUTS, PF_PIPE, + PF_FINITE, PF_DISPATCH, PF_FULL_FALLBACK, PF_CATEGORIES }; +/* row 0 = outer callback, row 1 = baseline, row 2+g = probe group g. */ +typedef struct { uint64_t ticks,intervals; } PfBucket; +extern PfBucket pf_buckets[LP_NC+2][PF_CATEGORIES],*pf_current; +extern uint64_t pf_previous_tick; +extern unsigned pf_row; +extern int pf_coarse; +extern const unsigned char pf_affected[LP_NC][LP_NO]; +extern uint64_t pf_operations[LP_NC][2]; +static inline uint64_t pf_clock(void){ + unsigned lo,hi; + __asm__ __volatile__("lfence\n\trdtsc\n\tlfence" : "=a"(lo),"=d"(hi) :: "memory"); + return ((uint64_t)hi<<32)|lo; +} +static inline void pf_mark(unsigned category) { + uint64_t t=pf_clock(); + pf_current->ticks+=t-pf_previous_tick; + pf_current->intervals++; + pf_previous_tick=t;pf_current=&pf_buckets[pf_row][category]; +} +#define PF_MARK(category) pf_mark(category) +#define PF_SCOPE(row,category) do {pf_row=(row);pf_mark(category);} while(0) +#define PF_PROBE(category) do {if(lp_color>=0)pf_mark(category);} while(0) +void pf_initialize(void);void pf_finish(void); +int pf_select(void);void pf_flush(uint64_t,uint64_t); +void pf_eval_count(void); +static inline void pf_operation(int position){ + int retained=pf_affected[lp_color][position]; + pf_operations[lp_color][retained?0:1]++; + PF_MARK(retained?PF_RETAINED:PF_FALLBACK); +} +void pf_open(void);void pf_close(void); +void pf_snapshot(int,NativePropertyCache*,NativePipeCache*); +void pf_save(double*,double*,double*,double*,double*); +int pf_reuse(int,NativePropertyCache*,NativePipeCache*,double*,double*,double*,double*,double*); +int pf_lp_eval(double,const double*,double*,double*,ModelJacobianWorkspace*); +void lp_jac_time_end(uint64_t,uint64_t); +void pf_validate_matrix(double,const double*,const double*); +#endif diff --git a/tests/manual/local_probe_profile.md b/tests/manual/local_probe_profile.md new file mode 100644 index 0000000..a139179 --- /dev/null +++ b/tests/manual/local_probe_profile.md @@ -0,0 +1,34 @@ +**局部 probe 的独立诊断计时工具** + +本工具只读取上一轮 `test/local-probe-20260917/worker`,在 +`test/local-probe-profile-direct-20260917` 构建带计时副本的独立 worker。 +不编辑旧实验实现或生产文件,不改变求解容差、物性算法或执行范围。 +仅适用于本次已核验的132状态、27组、484个schedule操作的八路模型。 + +```powershell +& .venv-win/Scripts/python.exe -B tests/manual/profile_local_probe.py prepare +& .venv-win/Scripts/python.exe -B tests/manual/profile_local_probe.py run --label validate-all --stride 1 --matrices +& .venv-win/Scripts/python.exe -B tests/manual/profile_local_probe.py batch +& .venv-win/Scripts/python.exe -B tests/manual/profile_local_probe.py prepare --coarse +& .venv-win/Scripts/python.exe -B tests/manual/profile_local_probe.py run --label coarse-0 --stride 1 --coarse +& .venv-win/Scripts/python.exe -B tests/manual/profile_local_probe.py run --label coarse-1 --stride 1 --coarse +& .venv-win/Scripts/python.exe -B tests/manual/profile_local_probe.py run --label coarse-2 --stride 1 --coarse +& .venv-win/Scripts/python.exe -B tests/manual/analyze_local_probe_profile.py +``` + +相同label会覆盖同名诊断运行。请串行测量,期间不要并行编译或运行其他仿真。 +完整数值核验保留所有矩阵;主性能测量不保存矩阵、不做shadow求值。 +每个run都会比较states/outputs/events、warning、求解器计数以及context保护计数。 + +正常callback走未改写的原函数;分层随机抽中的callback才进入细分计时副本。 +粗粒度worker进入原模型函数,只测完整baseline/probe及Jacobian外层。 +分类定义、全部27组均值、原始/扣空标记值和测量误差见生成的report.md; +每组全部分类数据见comparison.json的main.rows[group+2]。 + +外层时钟为QPC,内部使用带lfence的invariant TSC,以整轮QPC/TSC读数校准频率。 +内部计数桶逐tick闭合;内外计时边界差单独归入outer other。 +扣空标记只是估计,不能去除计时屏障、编译布局与缓存造成的间接扰动。 +本次估计总量仍高于未插桩约7%,小group的偏差更大;不得把细分百分比 +直接用作优化收益承诺。原QPC记录版及TSC记录版分别保存在 +`test/local-probe-profile-20260917`、`test/local-probe-profile-tsc-20260917`, +用于保留计时方法改进的证据,不与最终主样本混用。 diff --git a/tests/manual/local_probe_support.c b/tests/manual/local_probe_support.c new file mode 100644 index 0000000..509057a --- /dev/null +++ b/tests/manual/local_probe_support.c @@ -0,0 +1,133 @@ +/* Standalone experiment support; never linked by the production builder. */ +#define _WIN32_WINNT 0x0600 +#include "local_probe.h" +#include +#include +#include +#include +#include + +/* GENERATED_TABLES */ + +int lp_color=-1,lp_active=-1,lp_capture=0,lp_observe=LP_OBSERVE; +uint64_t lp_mask; +typedef struct { + size_t count,capacity; + NativePropertyTemperatures *temperatures; + NativeJacobianScalars *jacobian; + NativePropertyState states[LP_NPS]; + NativePipeCache pipes[LP_NPC]; +} Snapshot; +typedef struct { + Snapshot snapshots[LP_NB]; + double p[LP_NP],h[LP_NQ],q[LP_NQ],w[NOUTPUTS],fb[LP_NFB]; + double t,y[NSTATES];int ready; +} Workspace; +static Workspace *saved; +static unsigned char needed[LP_NB],captured[LP_NB]; +static FILE *matrix_file,*event_file; +static uint64_t frequency,jac_ticks,jac_calls,newton_iterations,newton_failures,audit_count,audit_diff; +static uint64_t executed[LP_NC][LP_NO],skipped[LP_NC][LP_NO],kernel_calls[LP_NC+1][LP_NK]; +static uint64_t attempts[LP_NC],hits[LP_NC],context_misses[LP_NC],invalid_trials[LP_NC]; +static uint64_t compared_bytes,copied_bytes,model_calls[LP_NC+1]; + +uint64_t lp_tick(void){LARGE_INTEGER t;QueryPerformanceCounter(&t);return (uint64_t)t.QuadPart;} +void lp_jac_time(uint64_t start){jac_ticks+=lp_tick()-start;jac_calls++;} +void lp_newton(long iterations,long failures){newton_iterations+=(uint64_t)iterations;newton_failures+=(uint64_t)failures;} +void lp_start(void){ + LARGE_INTEGER f;QueryPerformanceFrequency(&f);frequency=(uint64_t)f.QuadPart; + const char *mask=getenv("LOCAL_PROBE_MASK");lp_mask=mask?strtoull(mask,NULL,0):0; + if(lp_mask){saved=calloc(1,sizeof(*saved));if(!saved){fprintf(stderr,"local probe workspace allocation failed\n");abort();}} + for(int g=0;g=0){needed[lp_before[r]]=1;needed[lp_after[r]]=1;} + } + event_file=fopen("events.bin","wb");if(!event_file)abort(); + if(lp_observe){matrix_file=fopen("jacobians.bin","wb");if(!matrix_file)abort();setvbuf(matrix_file,NULL,_IOFBF,1024*1024);} +} +void lp_begin(double t,const double *y){ + if(saved){saved->ready=0;saved->t=t;memcpy(saved->y,y,sizeof(saved->y));memset(captured,0,sizeof(captured));} +} +void lp_ready(int ok){if(saved)saved->ready=ok;} +int lp_valid(double t,const double *y){ + if(!saved || !saved->ready || memcmp(&t,&saved->t,sizeof(double)))return 0; + for(int i=0;iy+i,sizeof(double))){invalid_trials[lp_color]++;return 0;} + return 1; +} +void lp_snapshot(int index,NativePropertyCache *properties,NativePipeCache *pipes){ + if(!needed[index] || captured[index])return; + captured[index]=1; + Snapshot *s=&saved->snapshots[index]; + if(properties->count>LP_NPS || properties->capacity!=LP_NPS || properties->temperatures){fprintf(stderr,"unsupported local property context\n");abort();} + s->count=properties->count;s->capacity=properties->capacity;s->temperatures=properties->temperatures;s->jacobian=properties->jacobian; + memcpy(s->states,properties->states,s->count*sizeof(*s->states));memcpy(s->pipes,pipes,sizeof(s->pipes)); + copied_bytes+=s->count*sizeof(*s->states)+sizeof(s->pipes); +} +void lp_save(double *p,double *h,double *q,double *w,double *fb){ + memcpy(saved->p,p,sizeof(saved->p));memcpy(saved->h,h,sizeof(saved->h));memcpy(saved->q,q,sizeof(saved->q)); + memcpy(saved->w,w,sizeof(saved->w));memcpy(saved->fb,fb,sizeof(saved->fb)); +} +int lp_reuse(int region,NativePropertyCache *properties,NativePipeCache *pipes,double *p,double *h,double *q,double *w,double *fb){ + Snapshot *before=&saved->snapshots[lp_before[region]],*after=&saved->snapshots[lp_after[region]]; + attempts[lp_color]++; + int contextual=lp_before[region]!=lp_after[region]; + if(contextual && (properties->count!=before->count || properties->capacity!=before->capacity || + properties->temperatures!=before->temperatures || properties->jacobian!=before->jacobian)){context_misses[lp_color]++;return 0;} + size_t n=before->count*sizeof(*before->states); + if(contextual){compared_bytes+=n+sizeof(before->pipes); + if(memcmp(properties->states,before->states,n) || memcmp(pipes,before->pipes,sizeof(before->pipes))){context_misses[lp_color]++;return 0;} + } + /* Preserve the exact baseline cache mutations, including seeded rho/h and + computed isentropic fields; never synthesize a context from (p,T) alone. + Jacobian scalar entries are immutable after baseline; only their telemetry + counters differ when actual kernel requests are avoided. */ + if(contextual){properties->count=after->count; + memcpy(properties->states,after->states,after->count*sizeof(*after->states));memcpy(pipes,after->pipes,sizeof(after->pipes)); + copied_bytes+=after->count*sizeof(*after->states)+sizeof(after->pipes); + } + double *dst[]={p,h,q,w,fb};double *src[]={saved->p,saved->h,saved->q,saved->w,saved->fb}; + for(int i=lp_first_output[region];i0)executed[lp_active-1][lp_operation_id[position]]++;} +#endif +void lp_kernel(int kind){if(lp_active>=0 && lp_active<=LP_NC)kernel_calls[lp_active][kind]++;} +void lp_compare(int ok,const double *dy,const double *w,int expected_ok,const double *expected_dy,const double *expected_w){ + audit_count++; + if(ok!=expected_ok || (ok && (memcmp(dy,expected_dy,NSTATES*sizeof(double)) || memcmp(w,expected_w,NOUTPUTS*sizeof(double))))){ + audit_diff++;fprintf(stderr,"probe exact mismatch color=%d comparison=%llu\n",lp_color,(unsigned long long)audit_count); + for(int i=0;i=0 && !local)for(int i=0;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) diff --git a/tests/manual/mechanical_events_experiment.md b/tests/manual/mechanical_events_experiment.md new file mode 100644 index 0000000..771e0da --- /dev/null +++ b/tests/manual/mechanical_events_experiment.md @@ -0,0 +1,37 @@ +# MASS / LSTP 独立事件实验 + +此实验只修改复制的原生运行库和实验生成程序,不改生产 `native/`、组件公式、生成器、工况、容差、雅可比分色或物性复用。平台默认运行不启用这里的事件。 + +`mechanical_event_variant.py` 在原生成源码后追加只读事件描述与限位释放驱动力求值入口;原 RHS 和雅可比源码保持为完全相同的前缀,状态/输出布局、求值调度、稀疏结构元数据一致。`prepare_runtime` 仅在指定的独立目标目录增加事件定位、提交和诊断;严格检查替换位置,生产代码变化后不静默套用未知补丁。 + +四种构建: + +- `off`:使用未修改的原生源码和生成程序。 +- `mass`:弹性上下限接触/脱离;非负力模式的原始力过零;理想/恢复限位保持后的释放。已有碰撞、恢复系数和摩擦事件保持原处理。 +- `lstp`:LSTP 间隙过零;非负力模式的原始力过零。允许负力时不注册力截断事件。 +- `all`:两类同时启用。 + +弹性事件不重置位置或速度;限位释放从约束位置、零速度继续。已接受事件才更新防重复触发记录;试算、雅可比和结果重放不写物理模式。接触力仍由原来的时间、状态、参数纯函数求值,事件记录不改变力公式。相同浮点时刻的事件一次提交和重启,保留已有输出语义。 + +定位使用已接受步的密集插值及二分,浮点区间不可再分时停止。相对速度换向时分两段查找间隙根,覆盖端点同号但中间接触/脱离的情况;孤立切触不制造来回事件。本实验与原有事件机制一样依赖积分步已经解析运动,未证明可捕获一个接受步内任意多次高频振荡。根定位误差不等于积分状态误差,必须做步长与容差核验。 + +MASS 释放检查只在已贴近限位且速度接近零时执行。为隔离已有优化,本实验采用独立只读整模型求值取得未施加限位约束的加速度,不修改正常 RHS 的数据通路;这会产生可单独计数的 `releaseRhsCalls`。因此其耗时是本实现的代价,不能当成所有限位释放算法必然的开销。 + +运行独立解析解、非负力、指数阻尼、无接触/初始贴边、密集插值换向及已有 Amesim 参考测试: + +```powershell +.\.venv-win\Scripts\python.exe -X utf8 -m unittest tests.test_mechanical_event_experiment -v +``` + +八路对照依赖本工作站已有 Amesim 2404、当前循环 AME 和之前保存的非循环 AME,以及上轮审计对齐的工程副本: + +```powershell +.\.venv-win\Scripts\python.exe -X utf8 tests/manual/evaluate_mechanical_events.py --output test/mechanical-events-new-run --repeats 5 +.\.venv-win\Scripts\python.exe -X utf8 tests/manual/summarize_mechanical_events.py test/mechanical-events-new-run +``` + +主目录必须新建;`--resume` 仅用于未变更实验源码情况下,保留已经完成的准确性/计时结果并续跑。因 Windows 偶发缓存目录重命名拒绝,构建可在独立新缓存重试。若编译子进程启动不稳定,可追加 `--serial-build`,只串行编译实验程序,不修改求解配置或生产构建逻辑。 + +每个工况重新运行一次 Amesim 作为四种配置共同的参照。每种配置保留一次完整输出用于同阶段对照;再轮换配置顺序做五次纯求解计时。编译和输出序列化不计入纯求解耗时,单独的完整运行数据仍保存。每个比较点同时核验十个信号阶段,所有曲线共用同一对真实保存行;不能选择误差最小的力值来配对。 + +完整指标与限制见本次 `docs/other/` 的独立实验报告。 diff --git a/tests/manual/mechanical_events_runtime.c b/tests/manual/mechanical_events_runtime.c new file mode 100644 index 0000000..7261b6a --- /dev/null +++ b/tests/manual/mechanical_events_runtime.c @@ -0,0 +1,178 @@ +/* Included only by an isolated runtime copy. No production RHS mode, force + * formula, state dimension, Jacobian coloring or property cache is changed. */ +#include + +#if NEXPERIMENT_CONTACTS +static double experiment_penetration(int j,const double *y) { + ExperimentContact c=experiment_contacts[j]; + if(c.kind==1) return c.boundary-y[c.v1+1]; + if(c.kind==2) return y[c.v1+1]-c.boundary; + /* Preserve exactly the production gap expression and operation order. */ + return -(c.boundary+y[c.v2+1]-y[c.v1+1]); +} +static double experiment_velocity(int j,const double *y) { + ExperimentContact c=experiment_contacts[j]; + if(c.kind==1) return -y[c.v1]; + if(c.kind==2) return y[c.v1]; + return y[c.v1]-y[c.v2]; +} +static double experiment_value(int j,int force,const double *y) { + double p=experiment_penetration(j,y); + if(force==2) return experiment_velocity(j,y); + if(!force) return p; + ExperimentContact c=experiment_contacts[j]; + double fraction=c.pdis>0 ? -expm1(-fmax(p,0)/c.pdis) : 1; + return c.stiffness*p+fraction*c.damping*experiment_velocity(j,y); +} +static double experiment_locate(NativeRun *r,int j,int force,double left,double right, + double sign,NativeDense dense,void *context) { + double y[NSTATES]; + for(int k=0;k<60;k++) { + double mid=left+.5*(right-left); + if(mid<=left || mid>=right) break; + r->experiment_dense++;r->experiment_roots++; + if(!dense(context,mid,y)) return NAN; + double g=experiment_value(j,force,y); + if(!isfinite(g)) return NAN; + if(sign<0 ? g>=0 : g<=0) right=mid; else left=mid; + } + return right; +} +static double experiment_bracket(NativeRun *r,int j,int force,double left,double right, + double a,double b,NativeDense dense,void *context,int *direction) { + int slot=2*j+force; + if(r->experiment_direction[slot] && r->experiment_last[slot]==left) + a=r->experiment_direction[slot]*DBL_MIN; + if(a==0 || (a>0 ? b>0 : b<0)) return INFINITY; + *direction=a<0 ? 1 : -1; + return experiment_locate(r,j,force,left,right,a,dense,context); +} +static double experiment_contact_candidate(NativeRun *r,int j,int force,double t,double next, + const double *old,const double *trial, + NativeDense dense,void *context,int *direction) { + double a=experiment_value(j,force,old), b=experiment_value(j,force,trial); + if(!isfinite(a) || !isfinite(b)) return NAN; + /* Detect a gap excursion and return through the same boundary even when + * the endpoint gaps have the same sign: split at the velocity reversal. + * Like the existing event locator, this relies on resolved accepted steps; + * arbitrarily many unresolved oscillations in one step are not certified. */ + if(!force) { + double va=experiment_velocity(j,old),vb=experiment_velocity(j,trial); + if((va<0 && vb>0) || (va>0 && vb<0)) { + double turn=experiment_locate(r,j,2,t,next,va,dense,context),y[NSTATES]; + if(!isfinite(turn)) return NAN; + r->experiment_dense++; + if(!dense(context,turn,y)) return NAN; + double g=experiment_value(j,force,y); + if(g==0 && ((a<0 && b<0) || (a>0 && b>0))) return INFINITY; + double found=experiment_bracket(r,j,force,t,turn,a,g,dense,context,direction); + if(isfinite(found) || isnan(found)) return found; + return experiment_bracket(r,j,force,turn,next,g,b,dense,context,direction); + } + } + return experiment_bracket(r,j,force,t,next,a,b,dense,context,direction); +} +#endif + +#if NEXPERIMENT_RELEASES +static int experiment_drives(NativeRun *r,double t,const double *y,double *drives) { + r->nfev++;r->experiment_rhs++; + return model_experiment_release_drives(t,y,drives); +} +static double experiment_release_root(NativeRun *r,int j,int lower,double left,double right, + NativeDense dense,void *context) { + double y[NSTATES],drives[NEXPERIMENT_RELEASES]; + for(int k=0;k<60;k++) { + double mid=left+.5*(right-left); + if(mid<=left || mid>=right) break; + r->experiment_dense++;r->experiment_roots++; + if(!dense(context,mid,y) || !experiment_drives(r,mid,y,drives)) return NAN; + if(lower ? drives[j]>0 : drives[j]<0) right=mid; else left=mid; + } + return right; +} +#endif + +static int experiment_candidates(NativeRun *r,double t,double next,const double *old,const double *trial, + NativeDense dense,void *context,double *when,int *indices,int *kinds,int *count) { + (void)r;(void)t;(void)next;(void)old;(void)trial;(void)dense;(void)context; + (void)when;(void)indices;(void)kinds;(void)count; +#if NEXPERIMENT_CONTACTS + for(int j=0;jexperiment_checks++; + for(int force=0;force<2;force++) { + if(force && (experiment_contacts[j].signed_force==1 || + experiment_penetration(j,old)<=0)) continue; + int direction=0; + double at=experiment_contact_candidate(r,j,force,t,next,old,trial,dense,context,&direction); + if(isnan(at)) return 0; + if(isfinite(at)) { + if(force) { + double y[NSTATES];r->experiment_dense++; + if(!dense(context,at,y)) return 0; + if(experiment_penetration(j,y)<=0) continue; + } + int n=(*count)++; + when[n]=at;indices[n]=j;kinds[n]=force?-3:-2; + r->experiment_pending[2*j+force]=direction; + } + } + } +#endif +#if NEXPERIMENT_RELEASES + double before[NEXPERIMENT_RELEASES],after[NEXPERIMENT_RELEASES]; + int evaluated=0; + for(int j=0;j1e-12*fmax(fabs(old[v]),1)) continue; + if(r->experiment_direction[2*NEXPERIMENT_CONTACTS+j] && + r->experiment_last[2*NEXPERIMENT_CONTACTS+j]==t) continue; + for(int lower=0;lower<2;lower++) { + double bound=lower?s.lower:s.upper,tol=1e-12*fmax(fabs(bound),1); + if(lower ? old[x]>bound+tol : old[x]experiment_checks++; + if(lower ? !(before[j]<=0 && after[j]>0) : !(before[j]>=0 && after[j]<0)) continue; + double at=experiment_release_root(r,j,lower,t,next,dense,context); + if(!isfinite(at)) return 0; + int n=(*count)++;when[n]=at;indices[n]=j;kinds[n]=lower?-4:-5; + } + } +#endif + return 1; +} + +static void experiment_commit(NativeRun *r,double t,double *y,const double *when,const int *indices,const int *kinds,int count) { + (void)y; + for(int i=0;i-2 || when[i]!=t) continue; + int j=indices[i]; + (void)j; +#if NEXPERIMENT_CONTACTS + if(kinds[i]==-2 || kinds[i]==-3) { + int force=kinds[i]==-3,slot=2*j+force; + r->experiment_last[slot]=t; + r->experiment_direction[slot]=r->experiment_pending[slot]; + if(force) r->experiment_clipping++; + else if(experiment_contacts[j].kind==3) r->experiment_lstp++; + else r->experiment_mass++; + fprintf(stderr,"{\"phase\":\"mechanical-event\",\"kind\":\"%s\",\"index\":%d,\"time\":%.17g,\"direction\":%d,\"penetration\":%.17g,\"relativeVelocity\":%.17g}\n", + force?"force-clip":experiment_contacts[j].kind==3?"lstp-contact":"mass-contact",j,t, + r->experiment_direction[slot],experiment_penetration(j,y),experiment_velocity(j,y)); + } +#endif +#if NEXPERIMENT_RELEASES + if(kinds[i]==-4 || kinds[i]==-5) { + NativeStop s=model_stops[j];int slot=2*NEXPERIMENT_CONTACTS+j; + y[s.velocity_index]=0;y[s.velocity_index+1]=kinds[i]==-4?s.lower:s.upper; + r->experiment_last[slot]=t;r->experiment_direction[slot]=1;r->experiment_release++; + fprintf(stderr,"{\"phase\":\"mechanical-event\",\"kind\":\"mass-release\",\"index\":%d,\"time\":%.17g,\"side\":\"%s\"}\n", + j,t,kinds[i]==-4?"lower":"upper"); + } +#endif + } + (void)r; +} diff --git a/tests/manual/mql8_comparison.py b/tests/manual/mql8_comparison.py new file mode 100644 index 0000000..a8a0b2a --- /dev/null +++ b/tests/manual/mql8_comparison.py @@ -0,0 +1,118 @@ +"""Eight-branch SI mapping, carried forward from the audited 2026-09-14 comparison. + +The caller supplies a fresh topology audit and network; no historical curves are loaded. +""" +import numpy as np +from app.simulation.reporting.amesim_results import _parse_variable_line, parse_amesim_results_bytes + + +def configure(current_audit, current_network): + global audit, network, aliases, nodes, adj + audit, network = current_audit, current_network + aliases = {row['component']: row['ameAlias'] for row in audit['mapping']} + nodes = {c.name: c for c in network.components.values() + if c.model_type in ('amesim_pn3node2', 'amesim_p4node2')} + adj = {} + for edge in network.connections: + if edge.kind == 'physical': + a, b = (e.key for e in edge.endpoints) + adj[a], adj[b] = b, a + + +def native_curves(series): + values = {k: np.asarray(v) for k, v in series.items()} + def energy(name): + key = name + '.reference_enthalpy_flow' + if key not in values: + result = np.zeros_like(values['time']) + for port in nodes[name].active_port_definitions: + if port.name == 'port_2': + continue + other = adj[name, port.name] + if other[0] in nodes and other[1] == 'port_2': + result += energy(other[0]) + else: + q = values[name + '.' + port.name + '.m_flow'] + result += q * np.where(q > 0, values['.'.join(other) + '.h_outflow'], + values[name + '.' + port.name + '.h_outflow']) + values[key] = result + return values[key] + for name in nodes: + energy(name) + values[name + '.reference_mass_flow'] = -values[name + '.port_2.m_flow'] + for component in network.components.values(): + if component.model_type == 'amesim_pnch012': + name = component.name + values[name + '.volume_work'] = -values[name + '.p'] * values[name + '.dvol'] + return values + + +def read_ame(directory): + variables = tuple(_parse_variable_line(i, line) for i,line in enumerate( + (directory/'test_mql_.var').read_text(encoding='latin1').splitlines())) + return parse_amesim_results_bytes((directory/'test_mql_.results').read_bytes(), variables) + +def curve_mapping(): + result = [] + for row in audit['mapping']: + kind, name, alias = row['type'],row['component'],row['ameAlias'] + fields = [] + if kind == 'amesim_mecmas21': + fields = [('x','x1','displacement','m',1,0),('v','v1','velocity','m/s',1,0)] + elif kind == 'amesim_lstp00a': + fields = [('force','f1','force','N',1,0),('gap','gap','gap','m',.001,0)] + elif kind in ('amesim_pnch023','amesim_pnch012'): + fields = [('p','press','pressure','Pa',1,101300),('T','temp','temperature','K',1,0)] + if kind == 'amesim_pnch012': + fields += [('vol','vol','volume','m3',1e-6,0),('m','mgas1','mass','kg',1e-3,0)] + elif kind in ('amesim_pnl0001','amesim_pnl0002'): + s = '2' if kind.endswith('1') else 'ctr' + fields = [('p','p'+s,'pressure','Pa',1,101300),('T','t'+s,'temperature','K',1,0)] + elif kind == 'amesim_pnl0003': + fields = [(k+str(i), k.lower()+str(i), 'pressure' if k=='p' else 'temperature', + 'Pa' if k=='p' else 'K',1,101300 if k=='p' else 0) for k in ('p','T') for i in (1,2)] + elif kind in ('amesim_ud00','amesim_step0'): + fields = [('out.signal','output' if kind=='amesim_ud00' else 'out','signal','1',1,0)] + elif kind in ('amesim_pn3node2','amesim_p4node2'): + fields = [('reference_enthalpy_flow','dh2','enthalpy_flow','W',1,0), + ('reference_mass_flow','dm2','mass_flow','kg/s',1e-3,0)] + elif kind == 'amesim_pnrp17': + fields = [('volume','vol1','piston_volume','m3',1e-6,0), + ('volume_flow','vvol1','volume_rate','m3/s',1e-3/60,0)] + for prop, field, quantity, unit, scale, offset in fields: + result.append(dict(key=name+'.'+prop, amePath=field+'@'+alias, + quantity=quantity, unit=unit, scale=scale, offset=offset)) + if kind == 'amesim_pnch012': + sources = [] + volumes = [] + for p in network.components[name].active_port_definitions: + other = adj[name,p.name] + component = network.components[other[0]] + if component.model_type == 'amesim_pnrp17': + sources.append('vvol1@'+aliases[other[0]]) + volumes.append('vol1@'+aliases[other[0]]) + elif other[0] in nodes and other[1] == 'port_2': + sources.append('dvol2@'+aliases[other[0]]) + volumes.append('vol2@'+aliases[other[0]]) + chamber = network.components[name] + effective_volume = dict(volumePaths=volumes, deadVolume=chamber.cvol0, + prescribedVolume=sum(chamber.external_volumes.values())) + result.append(dict(key=name+'.dvol',amePaths=sources, quantity='chamber_volume_rate',unit='m3/s', **effective_volume)) + result.append(dict(key=name+'.volume_work',amePaths=sources,pressurePath='press@'+alias, + quantity='volume_work',unit='W', **effective_volume)) + return result + +def ame_curve(ame, m): + if 'amePath' in m: + return np.array(ame.series(m['amePath']))*m['scale']+m['offset'] + rate = sum((np.array(ame.series(p)) for p in m['amePaths']), np.zeros(len(ame.times)))*(1e-3/60) + volume = sum((np.array(ame.series(p)) for p in m['volumePaths']), np.zeros(len(ame.times)))*1e-6 + volume += m['deadVolume']+m['prescribedVolume'] + # PNCH012.c sets its internal dvol to zero while the volume is limited. + # Summing raw piston rates without this condition invents enormous work + # that the original Amesim chamber does not actually apply. + limit = m['deadVolume']/100 + rate = np.where((volume= .1) + for event in summary['full']['signalEvents']: + quiet &= np.abs(t - event) > .0200001 + visible = np.where(quiet, error, np.nan) + axes[0, 1].plot(t, visible, linewidth=.8, label=str(i)) + axes[0, 0].set(title='8 路接触力差:全部共同采样点', ylabel='差值 / kN') + axes[0, 1].set(title='诊断视图:t≥0.1 s,避开切换前后 0.02 s', ylabel='差值 / N') + axes[0, 0].legend(title='支路', ncol=4, fontsize=7) + for row, (quantity, label) in enumerate([('enthalpy_flow', '节点焓流 / W'), ('mass_flow', '节点质量流 / kg/s')], 1): + key = summary['default']['groups'][quantity]['worstRelative']['key'] + for column in range(2): + ax = axes[row, column] + t = full['time'] + mask = (t >= .1) & (t <= .5) + n, a = full['platform|' + key], full['amesim|' + key] + if column == 0: + ax.plot(t[mask], n[mask], color='#1368a8', label='当前平台') + ax.plot(t[mask], a[mask], '--', color='#e07832', label='本次 Amesim') + ax.legend(fontsize=8) + ax.set_ylabel(label) + ax.set_title(key, fontsize=9) + else: + ax.plot(t[mask], (n-a)[mask], color='#925034') + ax.set_ylabel('绝对差,沿用左侧单位') + ax.set_title('小量差异单列,避免峰值归一化掩盖') + for ax in axes.flat: + ax.grid(alpha=.18) + ax.set_xlabel('时间 / s') + fig.suptitle('问题定位 · 切换点误差与小流量差异分开评价\n右上筛选只用于诊断;全部采样点仍进入主报告统计', fontsize=13) + fig.savefig(out / 'diagnostics.png', dpi=160) + fig.savefig(out / 'diagnostics.svg') + plt.close(fig) + + data = np.load(out / 'volume-startup/curves.npz') + fig, axes = plt.subplots(1, 2, figsize=(12, 4), constrained_layout=True) + for ax, key, label, scale in [(axes[0], 'amesim_pnch012_15.vol', '有效气室容积 / L', 1000), + (axes[1], 'amesim_pnch012_15.volume_work', '气室体积功率 / TW', 1e-12)]: + mask = data['time'] <= 8e-8 + for prefix, name, color, style in [('platform', '当前平台', '#1368a8', '-'), ('amesim', '本次 Amesim', '#e07832', '--')]: + ax.plot(data['time'][mask]*1e9, data[prefix+'|'+key][mask]*scale, style, color=color, label=name) + ax.set(xlabel='时间 / ns', ylabel=label) + ax.grid(alpha=.18) + ax.legend() + fig.suptitle('最初 80 ns:容积下限与体积功率 · 采样间隔 2 ns', fontsize=13) + fig.savefig(out / 'volume-startup.png', dpi=160) + fig.savefig(out / 'volume-startup.svg') + plt.close(fig) + + +if __name__ == '__main__': + main() diff --git a/tests/manual/probe_mql8_signal_boundaries.py b/tests/manual/probe_mql8_signal_boundaries.py new file mode 100644 index 0000000..312177e --- /dev/null +++ b/tests/manual/probe_mql8_signal_boundaries.py @@ -0,0 +1,51 @@ +"""Read-only numerical probe of current UD00 event/evaluation consistency.""" +import argparse +from pathlib import Path +import subprocess +import sys + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +from app.simulation.native_codegen.build import _command, toolchain + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--output', type=Path, required=True) + args = parser.parse_args() + out = args.output.resolve() + out.mkdir(parents=True, exist_ok=True) + source = out / 'signal-boundary-probe.c' + code = r''' +#include "SIGNAL_SOURCE" +#include +int main(void) { + double d[24]={0}; + d[0]=1e17; d[8]=1e17; d[1]=49000; d[9]=49000; d[16]=.8; d[17]=10; + double t=0; + puts("boundary,left,at,right,expected_after,at_matches_after"); + for(int i=0;i<10;i++) { + t=native_signal_break(t,60,0,2,1,d); + double expected=i%2==0?49000:1e17; + printf("%.17g,%.17g,%.17g,%.17g,%.17g,%d\n",t, + native_signal(nextafter(t,-INFINITY),0,2,1,d),native_signal(t,0,2,1,d), + native_signal(nextafter(t,INFINITY),0,2,1,d),expected, + native_signal(t,0,2,1,d)==expected); + } + return 0; +} +''' + source.write_text(code.replace('SIGNAL_SOURCE', (ROOT / 'native/components/modules/signal.c').as_posix()), encoding='ascii') + compiler, _, version = toolchain() + exe = out / ('signal-boundary-probe.exe' if sys.platform == 'win32' else 'signal-boundary-probe') + command = [compiler, '-std=c11', '-O2', '-ffp-contract=off', '-fno-fast-math', + '-I' + str(ROOT / 'native/include'), str(source), '-lm', '-o', str(exe)] + _command(command, log=[], timeout=60) + data = subprocess.check_output([str(exe)], timeout=10) + (out / 'signal-boundaries.csv').write_bytes(data) + print(version) + print(data.decode('ascii')) + + +if __name__ == '__main__': + main() diff --git a/tests/manual/profile_local_probe.py b/tests/manual/profile_local_probe.py new file mode 100644 index 0000000..2325779 --- /dev/null +++ b/tests/manual/profile_local_probe.py @@ -0,0 +1,218 @@ +"""Sampled, exclusive timing of a COPY of the local-probe performance worker. + +Original numerical functions remain in the executable verbatim. Only sampled +Jacobian callbacks use timing clones; ordinary residuals never reach a clone. +No production source or existing experiment implementation is edited. +""" +from pathlib import Path +from concurrent.futures import ThreadPoolExecutor +import argparse, hashlib, json, os, re, shutil, statistics, subprocess, time +import local_probe_experiment as experiment + +ROOT=experiment.ROOT +SOURCE=ROOT/'test/local-probe-20260917/worker' +OUT=ROOT/'test/local-probe-profile-direct-20260917' +HERE=Path(__file__).parent +replace=experiment.replace +span=experiment.function_span +write=experiment.write + +def function(source,name): + a,b,e=span(source,name) + return source[a:e] + +def annotate_tail(tail): + """Fail closed for the reviewed generated model, not a general C parser. + Stage ownership follows extended.py; declarations remain in original scope. + Pipe argument PH inversion and accumulation belong to diagnostics; writes + of diagnostic outputs belong to remaining_outputs. + """ + phase='PF_NODE';mode='node';pipe=False;result=['PF_PROBE(PF_NODE);'] + counts={};transitions=[] + for i,line in enumerate(tail.splitlines()): + category=phase + if mode=='node' and re.match(r'w\[\d+\] = [phq]\[\d+\];$',line):mode='port' + if mode=='port' and not re.match(r'w\[\d+\] = [phq]\[\d+\];$',line):mode='mechanical' + if mode=='mechanical' and line.startswith('dy[0]='):mode='gas' + if line.startswith('for(int i=0;i=0 && !(eligible && valid))PF_MARK(PF_FULL_FALLBACK);\n int result=eligible && valid ?') + return source+'\n'+clone+'\n'+wrapper+'\n',meta + +def instrument_support(source): + reuse=function(source,'lp_reuse').replace('int lp_reuse(','int pf_reuse(',1) + reuse=replace(reuse,'int contextual=lp_before[region]!=lp_after[region];', + 'int contextual=lp_before[region]!=lp_after[region];\n if(contextual)PF_MARK(PF_CONTEXT);') + assert reuse.count('{context_misses[lp_color]++;return 0;}')==2 + reuse=reuse.replace('{context_misses[lp_color]++;return 0;}', + '{context_misses[lp_color]++;PF_MARK(PF_DISPATCH);return 0;}') + reuse=replace(reuse,'if(contextual){properties->count=after->count;', + 'PF_MARK(PF_RESTORE);\n if(contextual){properties->count=after->count;') + reuse=replace(reuse,'return 1;','PF_MARK(PF_DISPATCH);return 1;') + wrappers=''' +void pf_snapshot(int index,NativePropertyCache *p,NativePipeCache *c){ + if(!needed[index] || captured[index]){lp_snapshot(index,p,c);return;} + PF_MARK(PF_SNAPSHOT);lp_snapshot(index,p,c);PF_MARK(PF_BASELINE); +} +void pf_save(double *p,double *h,double *q,double *w,double *fb){ + PF_MARK(PF_SNAPSHOT);lp_save(p,h,q,w,fb);PF_MARK(PF_BASELINE); +} +void lp_jac_time_end(uint64_t start,uint64_t end){jac_ticks+=end-start;jac_calls++;} +''' + return source+'\n'+reuse+'\n'+wrappers + +def instrument_cv(source): + rhs=function(source,'jac_rhs_reuse').replace('jac_rhs_reuse(', 'pf_jac_rhs_reuse(',1).replace('lp_eval(', 'pf_lp_eval(') + colored=function(source,'colored_difference').replace('colored_difference(', 'pf_colored_difference(',1).replace('jac_rhs_reuse(', 'pf_jac_rhs_reuse(') + colored=replace(colored,'if (SUNMatZero(matrix)) return -1;', + 'PF_SCOPE(0,PF_ZERO);if (SUNMatZero(matrix)) return -1;PF_MARK(PF_OTHER);') + colored=replace(colored,'lp_color=color;memcpy(test,state,NSTATES*sizeof(double));', + 'PF_SCOPE(0,PF_PERTURB);lp_color=color;memcpy(test,state,NSTATES*sizeof(double));') + colored=replace(colored,'#if MODEL_JACOBIAN_GAS_REUSE', 'PF_MARK(PF_OTHER);\n#if MODEL_JACOBIAN_GAS_REUSE') + colored=replace(colored,'if (flag) return flag;','PF_SCOPE(0,PF_ASSEMBLY);if (flag) return flag;') + colored=replace(colored,' }\n context->run->jacobian_colored_evals++;', + ' PF_MARK(PF_OTHER);\n }\n context->run->jacobian_colored_evals++;') + jac=function(source,'cv_jacobian_original').replace('cv_jacobian_original(', 'pf_cv_jacobian_original(',1).replace('jac_rhs_reuse(', 'pf_jac_rhs_reuse(').replace('colored_difference(', 'pf_colored_difference(') + jac=replace(jac,'int flag=jac_increments(context,y,fy,increments);', + 'PF_SCOPE(0,PF_INCREMENT);int flag=jac_increments(context,y,fy,increments);PF_MARK(PF_OTHER);') + jac=replace(jac,'fy=tmp3;', 'PF_SCOPE(0,PF_OTHER);fy=tmp3;') + a,b,e=span(source,'cv_jacobian');sig=source[a:b] + wrapper=sig+'''{ + lp_color=-1;uint64_t start=lp_tick();int selected=pf_select(); + if(selected)pf_open(); + int result=selected?pf_cv_jacobian_original(t,y,fy,matrix,user,tmp1,tmp2,tmp3):cv_jacobian_original(t,y,fy,matrix,user,tmp1,tmp2,tmp3); + if(selected)pf_close(); + uint64_t finish=lp_tick();lp_jac_time_end(start,finish); + if(selected)pf_flush(start,finish); + if(!result){lp_matrix(t,N_VGetArrayPointer(y),SUNDenseMatrix_Data(matrix));pf_validate_matrix(t,N_VGetArrayPointer(y),SUNDenseMatrix_Data(matrix));} + return result; +}''' + return source[:a]+rhs+'\n'+colored+'\n'+jac+'\n'+wrapper+source[e:] + +def prepare(coarse=False): + OUT.mkdir(exist_ok=True);work=OUT/('coarse-worker' if coarse else 'worker');work.mkdir(exist_ok=True) + sources={p.name:p.read_text(encoding='utf-8') for p in SOURCE.glob('*.c')} + before={k:hashlib.sha256(v.encode()).hexdigest() for k,v in sources.items()} + sources['model.c'],boundaries=instrument_model(sources['model.c'],coarse) + sources['local_probe_support.c']=instrument_support(sources['local_probe_support.c']) + sources['cvode_solver.c']=instrument_cv(sources['cvode_solver.c']) + sources['common.c']=replace(sources['common.c'],'lp_start();','lp_start();pf_initialize();') + sources['common.c']=replace(sources['common.c'],'lp_finish();','lp_finish();pf_finish();') + plan=json.loads((SOURCE.parent/'plan.json').read_text(encoding='utf-8')) + affected=[[int(i in set(g['affectedOperations'])) for i in plan['order']] for g in plan['groups']] + tables='const unsigned char pf_affected[LP_NC][LP_NO]={'+','.join('{'+','.join(map(str,a))+'}' for a in affected)+'};' + sources['local_probe_profile.c']=(HERE/'local_probe_profile.c').read_text(encoding='utf-8').replace('/* PROFILE_TABLES */',tables) + for name in ('model.h','local_probe.h'):shutil.copyfile(SOURCE/name,work/name) + shutil.copyfile(HERE/'local_probe_profile.h',work/'local_probe_profile.h') + cc,sun,_=experiment.builder.toolchain();flags,libs,dlls,exe=experiment.builder.platform_build_inputs(sun);flags+=['-DLP_OBSERVE=0'] + started=time.perf_counter() + def compile_one(item): + i,(name,src)=item;path=work/name;path.write_text('#include "local_probe_profile.h"\n'+src,encoding='utf-8',newline='\n');obj=work/f'profile-{i}.o';log=[] + experiment.builder._command([cc,*flags,'-I',str(work),'-I',str(experiment.builder.NATIVE/'include'),'-I',str(sun/'include'),'-c',str(path),'-o',str(obj)],log=log,timeout=180) + return obj,log + with ThreadPoolExecutor(max_workers=4) as pool:objects=list(pool.map(compile_one,enumerate(sources.items()))) + log=[];experiment.builder._command([cc,*flags,*[str(o) for o,_ in objects],*experiment.builder.link_library_arguments(libs),'-lm','-o',str(work/exe)],log=log) + for dll in dlls:shutil.copyfile(dll,work/dll.name) + (work/'build.log').write_text('\n'.join(sum([x for _,x in objects],[])+log),encoding='utf-8') + write(OUT/('build-coarse.json' if coarse else 'build.json'),dict(sourceHashes=before,sourceDirectory=str(SOURCE),boundaries=boundaries,seconds=time.perf_counter()-started)) + print('BUILT',time.perf_counter()-started,flush=True) + +def run(label,stride=16,offset=0,control=False,matrices=False,coarse=False): + work=OUT/label;work.mkdir(exist_ok=True) + executable=(SOURCE if control else OUT/('coarse-worker' if coarse else 'worker'))/'model.exe' + env=os.environ.copy();env.update(LOCAL_PROBE_MASK='0x7ffffff',PROBE_PROFILE_STRIDE=str(stride),PROBE_PROFILE_OFFSET=str(offset),PROBE_PROFILE_MATRICES=str(int(matrices)),PROBE_PROFILE_COARSE=str(int(coarse))) + args=[str(executable),'--method','BDF','--start','0','--stop','10','--sample-step','.01','--max-step','1e30','--rtol','1e-8','--timeout','300', + '--sample-file',str(work/'states.bin'),'--output-block-file',str(work/'outputs.bin'),'--output',str(work/'result.json')] + start=time.perf_counter() + with (work/'stderr.log').open('wb') as f: + p=subprocess.run(args,cwd=work,env=env,stdout=subprocess.PIPE,stderr=f,timeout=330,creationflags=subprocess.CREATE_NO_WINDOW) + elapsed=time.perf_counter()-start + if p.returncode:raise RuntimeError((label,p.returncode,(work/'stderr.log').read_text()[-4000:])) + r=json.loads((work/'result.json').read_text());diag=json.loads((work/'probe.json').read_text()) + fields=('success','finalState','final','propertyWarnings','acceptedSteps','rejectedSteps','stateTransitions','solverStarts','nfev','njev','nlu','solveSeconds','solveCpuSeconds') + record={k:r[k] for k in fields} + for name in ('states','outputs','events','jacobians'): + path=work/f'{name}.bin' + if path.exists(): + with path.open('rb') as f:record[name+'Sha256']=hashlib.file_digest(f,'sha256').hexdigest() + record.update(label=label,stride=stride,offset=offset,control=control,matrices=matrices,coarse=coarse,processSeconds=elapsed,diagnostic=diag) + write(work/'measurement.json',record) + # Numerical gate on every run, including the sampling-density controls. + reference=json.loads((SOURCE.parent/'all-run-0/measurement.json').read_text(encoding='utf-8')) + exact=['statesSha256','outputsSha256','eventsSha256','finalState','final','propertyWarnings','acceptedSteps','rejectedSteps','stateTransitions','solverStarts','nfev','njev','nlu'] + differences=[k for k in exact if record[k]!=reference[k]] + differences += [k for k in ('newtonIterations','newtonConvergenceFailures','contextComparedBytes','contextCopiedBytes','modelCalls','groups') if diag[k]!=reference['diagnostic'][k]] + assert not differences,(label,differences) + if matrices: + ref=SOURCE.parent/'all-audit/jacobians.bin' + with ref.open('rb') as f:assert record['jacobiansSha256']==hashlib.file_digest(f,'sha256').hexdigest() + print('RUN',label,'Jacobian',diag['jacobianSeconds'],'solve',r['solveSeconds'],'CPU',r['solveCpuSeconds'],'process',elapsed,'exact OK',flush=True) + return record + +def batch(): + run('control-warmup',control=True) + run('profile-warmup',16,101) + for i in range(5): + jobs=[(f'control-{i}',0,0,True),(f'profile-{i}',16,i+17,False)] + if i%2:jobs.reverse() + for label,stride,offset,control in jobs:run(label,stride,offset,control) + for i in range(2): + run(f'disabled-{i}',0,i) + run(f'density8-{i}',8,i+37) + run(f'density32-{i}',32,i+71) + +if __name__=='__main__': + p=argparse.ArgumentParser();p.add_argument('action',choices=['prepare','run','batch']);p.add_argument('--label',default='sample');p.add_argument('--stride',type=int,default=16);p.add_argument('--offset',type=int,default=0);p.add_argument('--control',action='store_true');p.add_argument('--matrices',action='store_true');p.add_argument('--coarse',action='store_true');a=p.parse_args() + if a.action=='prepare':prepare(a.coarse) + elif a.action=='batch':batch() + else:run(a.label,a.stride,a.offset,a.control,a.matrices,a.coarse) diff --git a/tests/manual/r288_real_skip_report.md b/tests/manual/r288_real_skip_report.md new file mode 100644 index 0000000..5d8c0e9 --- /dev/null +++ b/tests/manual/r288_real_skip_report.md @@ -0,0 +1,137 @@ +# R288 / position16 最小 real skip 实验 + +## 结论 + +1. **完整求解结果逐位一致。** 0–10 s 全轨迹,896 个 132×132 Jacobian(15,611,904 个元素)及每个 t/y、states、outputs、事件、最终状态与既有基线一致;没有使用数值容差。 +2. **896/896 次真实 skip;自然 reject/fallback 为 0。** commit 896 次,原目标 native operation 实际执行 0 次,Reference 双路执行 0 次。 +3. **当前 replay 更贵。** 7 轮中位数:原 operation 1.143 µs/次,validation + overlay/patch + commit 22.521 µs/次(19.70 倍)。目标完整路径 control 1.186 µs/次,real skip 22.913 µs/次。 +4. **暂不把这一实现直接扩展到 position52。** 正确性门槛已满足,净收益门槛未满足。应先降低 metadata 采集、事务复制和解释执行成本;本轮不能据此推断 position52 或其他 interval 的收益。 + +## 范围与实现 + +- 全部改动仅在独立生成的实验 worker 及 `tests/manual`;生产路径、默认开关、property cache 语义、原 whole-context guard 未改动。 +- 入口仅为 `lp_color == 6 && region == 288` 的 `case 16`,位于原 `lp_reuse` 失败之后。其他位置继续原执行。 +- baseline position16 用单独命名空间的 kernels 采集必需的有序事件 metadata。其他 operation 使用原 kernels,没有全局访问插桩。 +- guard 来自已验证的 shadow 源码,构建时校验其 SHA-256,并断言 guard 函数保持一致;唯一变量替换是将原事务入口 count 改为当前真实 probe 入口 count。 +- 复制当前 probe 到私有 overlay;按当前 entries 验证 first-match/miss,重定位逻辑 slot,从当前 count 追加。通过后构造地址已转换的有序 write set,再 commit。已有 entries、未写字段和其他 pipe slots 保持原值。 +- Observer、capacity/scratch、memo lifetime/value、消费字段、valid、pipe branch、未知副作用/非有限值等原保护条件保留;失败只回退原 operation。 +- 性能版不执行 Reference、不导出访问日志或 Jacobian、不逐次比较完整 context。保留运行所需 metadata、严格 guard、overlay/patch、计时和累计计数。 + +## 正确性证据 + +| 检查 | 结果 | +|---|---:| +| Jacobian、t/y 与既有 all-audit 文件逐字节比较 | 896/896 一致 | +| 每个 Jacobian 的 baseline + 27 group evaluator 出口 | 25,088/25,088 一致 | +| 目标 operation 入口 / 出口 | 896 / 896 一致 | +| 返回状态、dy/w、active property entries 有序字段/valid、全部 pipe slots | 逐位一致 | +| memo 每次 Jacobian 的表内容、owner 绑定、recording 生命周期 | 一致 | +| 所有 probe 的 memo entries 只读检查 | 通过 | +| gas memo 内容、kernel 绑定及计数 | 一致 | +| errno、x87/SSE flags/rounding、warning/observer | 一致 | +| count 差异 / slot relocation 的目标 probe | 638 / 571 | +| 顺序 append | 1,658 | +| PT miss → PT miss / 近零流量无查询 | 829 / 67 | + +Audit 对照来自**独立 control 进程真实执行当前 probe**的出口,约 1.88 GB 二进制数据。跨进程地址比较采用 owner/function 绑定身份,数值字段保持原始位模式;不把 C padding 当成数值。memo 表逐 Jacobian 比较,后续每个 probe 同时检查整表未变。目标入口、出口与所有 group 的完整 evaluator 出口均参与比较。audit 的 I/O 保留并恢复 errno、x87 和 SSE 环境。 + +| 求解器计数 | control / real skip | +|---|---:| +| accepted / rejected | 10840 / 918 | +| Newton iterations / convergence failures | 19371 / 798 | +| nfev / njev / nlu | 44467 / 896 / 3106 | +| solverStarts / stateTransitions | 4 / 1 | + +额外负例:每 128 个目标 probe 注入一次晚期 nonfinite-output reject,发生在 overlay 已执行有序更新之后。7 次 reject 均完成无污染检查,fallback/native 原执行各 7 次,commit/skip 各 889 次;全轨迹仍与同一 control 和既有基线逐位一致。没有放宽 guard,也未改变容差。 + +## 性能方法与原始数据 + +Windows / MinGW GCC,原构建优化选项(`-O3 -ffp-contract=off -fno-fast-math`)。完成正确性和回退测试后单独编译性能 worker;control、skip 各预热一次,再交替串行运行 7 组。表中顺序就是实际顺序。每轮 states/outputs/events 指纹和全部指定求解器计数保持一致,skip 每轮均为 896、reject 0、原执行 0。 + +QPC 粗粒度计时;累计整数 tick,积分过程中不做浮点时间换算。validation 包括语义校验所必需的临时有序更新;overlay/patch 包括当前 context 复制及提交 write set 构造;commit 是真实字段写回。完整 target 路径另计,包含调度、计数和额外计时开销。未减去计时器自身成本,未使用 shadow/audit 时间推断性能。 + +| 运行顺序 | 原执行 µs/次 | validation µs/次 | overlay/patch µs/次 | commit µs/次 | fallback ms | target 总 ms | baseline pos16 总 ms | Jacobian s | 积分 CPU s | 积分 wall s | skip/reject | +|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:| +| pair-1-control | 1.562 | 0.000 | 0.000 | 0.000 | 0.000 | 1.439 | 1.760 | 2.190858 | 6.375000 | 6.564619 | 0/0 | +| pair-1-skip | 0.000 | 6.172 | 15.011 | 0.434 | 0.000 | 19.785 | 19.239 | 2.007014 | 5.953125 | 6.076049 | 896/0 | +| pair-2-skip | 0.000 | 6.451 | 14.713 | 0.435 | 0.000 | 19.716 | 18.944 | 1.971936 | 5.921875 | 5.910387 | 896/0 | +| pair-2-control | 1.170 | 0.000 | 0.000 | 0.000 | 0.000 | 1.088 | 1.533 | 2.000030 | 6.046875 | 6.140446 | 0/0 | +| pair-3-control | 1.143 | 0.000 | 0.000 | 0.000 | 0.000 | 1.063 | 1.540 | 2.018777 | 6.015625 | 6.213678 | 0/0 | +| pair-3-skip | 0.000 | 6.831 | 15.331 | 0.436 | 0.000 | 20.563 | 19.928 | 2.014426 | 6.031250 | 6.120732 | 896/0 | +| pair-4-skip | 0.000 | 7.101 | 15.141 | 0.434 | 0.000 | 20.651 | 19.911 | 2.035451 | 6.015625 | 6.135601 | 896/0 | +| pair-4-control | 1.093 | 0.000 | 0.000 | 0.000 | 0.000 | 1.017 | 1.508 | 1.933127 | 5.921875 | 5.940958 | 0/0 | +| pair-5-control | 1.080 | 0.000 | 0.000 | 0.000 | 0.000 | 1.009 | 1.676 | 1.935641 | 5.906250 | 5.900561 | 0/0 | +| pair-5-skip | 0.000 | 6.334 | 15.975 | 0.451 | 0.000 | 20.720 | 19.466 | 2.038743 | 5.984375 | 6.166865 | 896/0 | +| pair-6-skip | 0.000 | 6.508 | 14.442 | 0.444 | 0.000 | 19.496 | 19.729 | 2.016491 | 5.906250 | 6.033967 | 896/0 | +| pair-6-control | 1.177 | 0.000 | 0.000 | 0.000 | 0.000 | 1.093 | 1.512 | 2.030009 | 6.250000 | 6.418474 | 0/0 | +| pair-7-control | 1.119 | 0.000 | 0.000 | 0.000 | 0.000 | 1.039 | 1.499 | 1.970795 | 5.953125 | 6.030361 | 0/0 | +| pair-7-skip | 0.000 | 6.166 | 15.893 | 0.462 | 0.000 | 20.530 | 19.112 | 2.008419 | 6.078125 | 6.126521 | 896/0 | + +baseline pos16:control 为原 baseline operation;skip 包含原 baseline operation + 本次 replay 必需 metadata 采集,不能漏算这部分成本。性能各轮没有自然 reject,因此 fallback 总时间为 0;这不代表一次 fallback 的成本为零,本轮未估计该分支的单次性能。 + +### 中位数及 min/max + +| 项目 | control 中位数 [min, max] | real skip 中位数 [min, max] | +|---|---:|---:| +| originalSeconds(s,896 次累计) | 0.001024400 [0.000967700, 0.001399500] | 0.000000000 [0.000000000, 0.000000000] | +| validationSeconds(s,896 次累计) | 0.000000000 [0.000000000, 0.000000000] | 0.005780000 [0.005525000, 0.006362100] | +| overlayPatchSeconds(s,896 次累计) | 0.000000000 [0.000000000, 0.000000000] | 0.013566200 [0.012939600, 0.014314000] | +| commitSeconds(s,896 次累计) | 0.000000000 [0.000000000, 0.000000000] | 0.000390700 [0.000388500, 0.000414100] | +| fallbackSeconds(s,896 次累计) | 0.000000000 [0.000000000, 0.000000000] | 0.000000000 [0.000000000, 0.000000000] | +| pathSeconds(s,896 次累计) | 0.001062600 [0.001009100, 0.001439000] | 0.020530400 [0.019496400, 0.020720200] | +| baselineRecordSeconds(s,896 次累计) | 0.001533400 [0.001499200, 0.001760000] | 0.019465900 [0.018944100, 0.019927500] | +| jacobianSeconds(s,896 次累计) | 2.000029500 [1.933127300, 2.190857900] | 2.014426400 [1.971935900, 2.038742900] | +| solveCpuSeconds(s,896 次累计) | 6.015625000 [5.906250000, 6.375000000] | 5.984375000 [5.906250000, 6.078125000] | +| solveSeconds(s,896 次累计) | 6.140445900 [5.900561100, 6.564618800] | 6.120731700 [5.910387000, 6.166865500] | + +### 配对差值 + +| 组 | 原计算 − replay 三阶段 ms | target 完整路径净节省 ms | 新增 metadata 采集 ms | Jacobian Δ s | 积分 CPU Δ s | 积分 wall Δ s | +|---|---:|---:|---:|---:|---:|---:| +| 1 | -17.969 | -18.346 | 17.479 | -0.183844 | -0.421875 | -0.488570 | +| 2 | -18.304 | -18.628 | 17.411 | -0.028094 | -0.125000 | -0.230059 | +| 3 | -19.224 | -19.501 | 18.387 | -0.004351 | 0.015625 | -0.092946 | +| 4 | -19.338 | -19.633 | 18.403 | 0.102324 | 0.093750 | 0.194642 | +| 5 | -19.425 | -19.711 | 17.790 | 0.103102 | 0.078125 | 0.266304 | +| 6 | -18.114 | -18.404 | 18.217 | -0.013517 | -0.343750 | -0.384508 | +| 7 | -19.176 | -19.492 | 17.613 | 0.037623 | 0.125000 | 0.096160 | + +净节省为正表示节省,Δ = skip − control。 + +- 原计算成本 − replay 三阶段成本:配对中位数 **-19.176 ms / 896 次**。 +- target 完整路径净节省:配对中位数 **-19.492 ms / 896 次**;范围 [-19.711, -18.346] ms。 +- 此外 baseline metadata 采集增加:配对中位数 **17.790 ms**。 +- Jacobian callback 配对 Δ:中位数 -0.004351 s,范围 [-0.183844, 0.103102] s。 +- 积分 wall 配对 Δ:中位数 -0.092946 s,范围 [-0.488570, 0.266304] s。 + +全局时间受调频、调度和系统负载影响,不能把某轮 Jacobian/积分变快归因于这个 operation。目标路径在全部配对中均更慢,已经足以否定当前实现的局部净收益;本轮不预测整个 local probe 的最终加速比例。 + +## 成本解释及下一阶段条件 + +原 operation 在现有 Jacobian memo 的只读复用环境下已经很便宜;当前严格 replay 仍要初始化/复制 156,816 B 的 overlay(含完整 memo),解释事件并构造 write set。运行所需有效 metadata 为 2,224–23,344 B,Plan 固定预留 90,224 B,patch 预留 16,384 B。这些是当前隔离实现的实际成本,不是机制理论上的下限。 + +因此,本轮证明了目标路径可以安全真实跳过,但没有证明性能优化成立。下一步应先针对事务存储和 metadata 表达做最小化,再重复同样的正确性与交替性能验收;不因 position16 正确就直接扩大到 position52/整个 R475/全部 reuse interval。 + +## 复现与证据位置 + +运行目录:`test/r288-real-skip-20260917/`。依赖上一轮生成的 local-probe worker、context-access worker 和 shadow-certified 源码;工具链复用原实验配置,不安装依赖。 + +```powershell +.venv-win/Scripts/python.exe tests/manual/real_skip_experiment.py prepare --audit +.venv-win/Scripts/python.exe tests/manual/real_skip_experiment.py prepare --audit --skip +.venv-win/Scripts/python.exe tests/manual/real_skip_experiment.py run --audit +.venv-win/Scripts/python.exe tests/manual/real_skip_experiment.py run --audit --skip +.venv-win/Scripts/python.exe tests/manual/real_skip_experiment.py run --audit --skip --label audit-forced-reject --force 128 +.venv-win/Scripts/python.exe tests/manual/real_skip_experiment.py prepare +.venv-win/Scripts/python.exe tests/manual/real_skip_experiment.py prepare --skip +.venv-win/Scripts/python.exe tests/manual/real_skip_experiment.py benchmark --pairs 7 +.venv-win/Scripts/python.exe tests/manual/analyze_real_skip.py +``` + +- `audit-{control,skip}-run/validation.json`:正确性、独立执行计数、完整求解器计数及文件指纹。 +- `audit-forced-reject/validation.json`:真实回退及 rollback 检查。 +- `byte-comparison.json`:全部 Jacobian/t/y、states、outputs、events 与原始基线的逐字节比较。 +- `audit-control-run/audit.bin`:所有 group 和目标 operation 的实际执行对照出口。 +- `performance.json`:14 次按实际顺序记录的原始数据;各轮目录保留独立日志和结果。 +- `performance-summary.json`:中位数、min/max 和逐对差值。 +- `{audit,perf}-{control,skip}/build.json`:源文件哈希、guard 一致性和构建记录。 diff --git a/tests/manual/r288_typed_replay_report.md b/tests/manual/r288_typed_replay_report.md new file mode 100644 index 0000000..54c5c37 --- /dev/null +++ b/tests/manual/r288_typed_replay_report.md @@ -0,0 +1,212 @@ +# R288 / position16:typed semantic replay 成本下限实验 + +## 三个问题的答案 + +1. 去掉完整 overlay 与通用事件解释器后,三阶段 replay 的每轮均值中位数为 **7.109 µs/次**,各轮范围 **4.082–13.011 µs/次**。包含调度、计时及计数的完整目标路径中位数为 **7.575 µs/次**。这是本实现、工具链和机器的实测结果,不是理论最低成本。 +2. 同批 A 的原 operation 为 **1.775 µs/次**;专用 replay **仍高于原计算**。与历史 1.143 µs 不直接跨批比较。 +3. 计入 baseline 捕获新增成本,配对完整机制净节省中位数为 **-20.415 ms / 896 次**;含完整路径计时/统计开销的口径为 **-21.082 ms**。**当前实现对 R288 没有净收益;这种低成本 operation 应直接原计算。** + +## 范围与事务语义 + +所有实验仅针对 group6 / R288 / position16,在独立 worker 中、原 whole-context guard 失败之后启用。没有扩展 position52、R475 或其他 interval,没有接入生产默认路径。已有工作区改动不属于本实验的修改范围。 + +- `fp_validate` 只读当前真实 probe;保留输入位比较、有限值、FP/errno、observer、count/capacity、memo owner/recording、全部 active entry 绑定、pipe branch 等保护。 +- 两次 PT 查询依次按当前有序 entries 验证 miss;第二次查询还显式检查第一个 pending entry 的虚拟匹配。append 位置由当前 count 决定,不使用 baseline slot。 +- memo key 的 hash 在 baseline 捕获时计算;probe 仍按相同容量、同一 bounded linear-probe 顺序查找,并逐位比较完整 key 和 value。没有复制 memo,也没有绕过 memo 验证。 +- 只支持已验证的两个固定 schema:67 次近零流量无查询、829 次 PT miss → PT miss。不支持的路径直接 reject。 +- 这两个 schema 中,物性计算消费的条目均是本 operation 新建的条目。初始化、字段写入、valid 测试和读取之间的条件由固定 schema 保证,并在正确性版对每个 baseline 的所有原始访问逐条证明;不是把这些 guard 删除。 +- `fp_prepare` 最多构造两个 pending property entries、一个 pipe[0]、新 count、q[45] 和 density/pipe memo reuse 增量。所有真实写入仅在 `fp_commit` 发生。未被写入的 probe 数据保持原值。 + +专用版是固定 R288 schema 的 typed 原型,不是通用生产 codegen。静态 schema 的核对表从全轨迹真实 generic 记录自动生成;原 native 源文件受既有 SHA-256 约束,未知源代码变化会停止构建。 + +## Metadata 与临时存储 + +| 项目 | generic | specialized | +|---|---:|---:| +| 每次 baseline 有效 metadata | 2,224–23,344 B | 240 B | +| 持久 metadata 固定预留 | 90,224 B | 240 B | +| 捕获临时区 | 通用事件记录器 | 96 B | +| context/memo/pipe 完整 overlay | 156,816 B | 0 B | +| pending patch | 16,384 B write-set,另有 overlay | 456 B,总计且不重复计算 | + +- **静态可确定**:medium 常量、字段布局、读取/写入顺序、valid 位演变、最多两个追加条目、memo 类型、pipe[0]、q[45]。 +- **每个 baseline 动态捕获**:输入、输出、U/D 物性字段、pipe memo key/value、三个 memo hash、入口 count、owner/Jacobian 生命周期及 FP/errno 条件,共 240 B。 +- **probe 才解析**:当前有序 entries、query miss、当前 count/capacity、memo 实际位置和值、owner/observer/pipe 条件;验证成功后构造 456 B pending。 + +## 正确性验收 + +C 验收版:同一次 baseline 真实执行同时生成 generic 事件与小型 metadata;核对 896 份完整访问 schema、896 次 generic/typed accept/reject 和最终 patch,包括逻辑 slot 映射。 +D 验收版:使用性能版相同的最小 native 捕获 hooks,逐位核对先前真实 generic baseline 记录(跨进程 memo 地址转换为 owner 绑定身份),然后重复全部求解验收。该离线记录只用于 audit;性能 worker 不读取这些记录。 + +| 检查 | 结果 | +|---|---:| +| Jacobian 全元素与 t/y | 896/896 逐字节一致,15,611,904 个矩阵元素 | +| evaluator 返回状态、dy/w 和完整 context 出口 | 25,088/25,088 一致 | +| 目标 operation 入口/出口、property count/有序字段/valid、pipe cache | 全部一致 | +| memo entries、只读生命周期、owner/kernel 绑定及计数 | 全部一致 | +| states/outputs/events、最终状态、warning、FP/errno | 全部一致 | +| 正常目标 attempts / skip / reject / 原执行 | 896 / 896 / 0 / 0 | +| 负例 guard 判定及无写入检查 | C、D 各 20 项通过 | +| 晚期 forced reject | C、D 各 7 次,无污染;889 skip、7 原执行 | +| count 差异 / relocation / append | 638 / 571 / 1,658 | + +负例包含 observer、无容量/第二次追加不足、pipe hit、memo recording/miss/value、输入改变、晚期非有限输出、过期生命周期、FP flags/rounding、entry owner、U/D 查询转 hit,以及已拒绝 metadata 的 consumed/valid/已有条目更新/未知副作用标记。后四项是错误记录传播检查;逐字段读写与 valid 演变的实质检查来自 896 份完整访问 contract 对照。 + +| accepted | rejected | Newton iterations | convergence failures | nfev | njev | nlu | solverStarts | stateTransitions | +|---:|---:|---:|---:|---:|---:|---:|---:|---:| +|10840|918|19371|798|44467|896|3106|4|1| + +全部使用位比较和逐字节比较,没有更改容差。性能版二进制符号检查确认无 generic interpreter、完整 overlay 或 audit oracle。 + +## Generic 成本归因 + +单独归因 worker 的总 replay 阶段为 42.544 µs/次;该数值含细粒度插桩,不与历史 22.521 µs 强行逐项相加。空 QPC bracket 均值 0.01622 µs,细粒度嵌套计时会额外放大短事件成本。 +以下分类均为每个目标 probe 的平均原始计时;父项含子项,明确标记重叠,不能相加当作净成本。 + +| 分类 | µs/次 | 口径 | +|---|---:|---| +| baseline 原执行 + metadata 捕获 | 31.7568 | 新增成本由后面的同批 A/B 配对得出 | +| PT first-match / miss | 0.2964 | 真实顺序扫描;包含一组自身计时 | +| consumed-field 读取事件 | 5.3567 | 含 metadata 分派和嵌套字段比较 | +| 其中:字段值/绑定比较 | 1.6404 | 是上一行的子项 | +| valid 测试事件 | 0.1645 | 含事件分派 | +| memo 验证 | 0.3146 | 完整 key/value 与 bounded lookup;SCALAR 事件的子项 | +| 完整 overlay 初始化/复制/绑定 | 20.8996 | 包含 context、memo、全部 pipe | +| logical slot relocation | 0.4641 | 包含映射冲突检查;属于 ALLOCATE/QUERY 子项 | +| pending entry 字段写入 | 0.9435 | 原始 generic 实测,含自身计时 | +| pending valid 更新 | 0.2103 | 原始 generic 实测,含自身计时 | +| 追加与 count 更新 | 5.3371 | 原始 generic 实测,含自身计时 | +| pipe patch 写入 | 0.3973 | 原始 generic 实测,含自身计时 | +| write-set 构造 | 0.8175 | 原始 generic 实测,含自身计时 | +| commit | 0.5316 | 原始 generic 实测,含自身计时 | +| 公共 guard(映射初始化之前) | 3.9068 | 细分项;已经包含在对应父项内 | +| overlay 清零/poison 初始化 | 7.5767 | 细分项;已经包含在对应父项内 | +| property context/entries 复制 | 0.1152 | 细分项;已经包含在对应父项内 | +| 全部 pipe 复制 | 0.2791 | 细分项;已经包含在对应父项内 | +| memo header/entries 复制 | 12.2629 | 细分项;已经包含在对应父项内 | +| overlay 指针绑定 | 0.2096 | 细分项;已经包含在对应父项内 | +| 仅 event metadata 遍历/类型分派微基准 | 0.1790 | 每份真实 plan 重复 100 次;不做 guard/write,不能当作真实解释器的独立可加项 | +| 空计时 bracket | 0.01622 | 每组含首尾 QPC;累计统计另有开销,最终以低扰动 A/B/C 为准 | + +通用解释器的实际成本分布在 metadata 访问、字段比较、slot 映射、写入分派和循环控制中,没有一个能独立相减的精确“dispatch 时间”。归因版保留原始嵌套数据,最终性能版移除这些细粒度计时。 + +## 低扰动 A/B/C 性能 + +A=原 operation,B=原 generic replay,C=typed/minimal replay。各预热一次,9 组按 ABC → BCA → CAB 循环,全部串行。无 Reference、完整 context 比较或详细访问日志;保留机制所需 metadata、guard、patch 和少量累计计时。每轮 B/C 均 skip 896、reject 0、原执行 0,并核对采样结果和全部指定计数。 + +### 中位数 [min, max] + +| 项目 | A | B | C | +|---|---:|---:|---:| +| original (µs/次) | 1.775 [1.574, 2.779] | 0.000 [0.000, 0.000] | 0.000 [0.000, 0.000] | +| validation (µs/次) | 0.000 [0.000, 0.000] | 15.540 [11.031, 50.369] | 5.615 [3.718, 10.492] | +| patch (µs/次) | 0.000 [0.000, 0.000] | 36.240 [24.357, 60.464] | 0.308 [0.237, 1.084] | +| commit (µs/次) | 0.000 [0.000, 0.000] | 0.672 [0.548, 1.210] | 0.144 [0.128, 7.584] | +| stages (µs/次) | 0.000 [0.000, 0.000] | 50.367 [35.999, 92.380] | 7.109 [4.082, 13.011] | +| path (µs/次) | 1.829 [1.650, 2.836] | 51.429 [36.540, 93.061] | 7.575 [4.366, 13.597] | +| outsideStages (µs/次) | 0.053 [0.048, 0.151] | 0.681 [0.541, 1.062] | 0.377 [0.284, 1.001] | +| baseline (µs/次) | 3.812 [2.282, 8.872] | 46.228 [31.371, 67.965] | 22.120 [14.653, 44.772] | +| jacobian (s) | 4.657 [3.108, 5.589] | 4.564 [3.524, 5.353] | 4.275 [2.912, 4.820] | +| cpu (s) | 8.531 [7.469, 8.797] | 8.641 [7.969, 9.062] | 8.672 [7.562, 8.906] | +| wall (s) | 13.458 [9.312, 16.059] | 13.025 [10.581, 15.476] | 13.093 [8.982, 14.628] | + +replay 三阶段先在每一轮内求和再取中位数,因此不必等于三个分项中位数之和。baseline 项包括该次 baseline operation 本身;捕获新增成本应减去 A 的 baseline 项。B 的 patch 项含全 overlay copy + write-set;C 的 patch 项仅小型 pending 构造。outsideStages 是完整路径减去被包围的阶段时间,包含外层计时、统计、调度与元数据 bookkeeping;它不是纯计时器成本。无自然 reject,fallback 总时间为 0,不代表单次 fallback 免费。 + +### 每轮原始数据(实际执行顺序) + +| 轮 / 模式 | 原 op µs | validation µs | patch µs | commit µs | replay 三阶段 µs | 完整路径 µs | baseline µs | Jacobian s | CPU s | wall s | +|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:| +| round-1 / A | 2.779 | 0.000 | 0.000 | 0.000 | 0.000 | 2.836 | 3.812 | 5.589474 | 8.765625 | 16.059343 | +| round-1 / B | 0.000 | 11.932 | 30.241 | 0.619 | 42.792 | 43.494 | 49.403 | 4.178009 | 8.640625 | 12.130358 | +| round-1 / C | 0.000 | 4.836 | 0.301 | 0.569 | 5.706 | 6.083 | 17.992 | 4.274571 | 8.687500 | 12.886425 | +| round-2 / B | 0.000 | 15.540 | 60.464 | 0.639 | 76.643 | 77.297 | 67.965 | 4.720768 | 8.812500 | 13.688767 | +| round-2 / C | 0.000 | 6.669 | 0.319 | 0.218 | 7.206 | 7.575 | 25.742 | 4.820219 | 8.625000 | 13.576249 | +| round-2 / A | 1.745 | 0.000 | 0.000 | 0.000 | 0.000 | 1.794 | 2.880 | 5.162364 | 8.468750 | 14.128570 | +| round-3 / C | 0.000 | 7.646 | 0.291 | 0.141 | 8.079 | 9.080 | 21.240 | 4.792801 | 8.734375 | 14.628191 | +| round-3 / A | 1.682 | 0.000 | 0.000 | 0.000 | 0.000 | 1.730 | 2.282 | 3.242320 | 7.531250 | 9.505273 | +| round-3 / B | 0.000 | 13.321 | 36.411 | 0.635 | 50.367 | 51.429 | 48.727 | 4.563694 | 8.609375 | 13.024813 | +| round-4 / A | 1.574 | 0.000 | 0.000 | 0.000 | 0.000 | 1.650 | 4.330 | 5.109714 | 8.671875 | 14.935839 | +| round-4 / B | 0.000 | 20.122 | 36.240 | 0.673 | 57.035 | 57.856 | 44.720 | 5.159674 | 9.046875 | 15.093424 | +| round-4 / C | 0.000 | 5.615 | 1.084 | 0.144 | 6.843 | 7.334 | 18.844 | 4.173146 | 8.437500 | 12.573423 | +| round-5 / B | 0.000 | 11.094 | 24.357 | 0.548 | 35.999 | 36.540 | 31.371 | 3.523633 | 7.968750 | 10.581341 | +| round-5 / C | 0.000 | 10.492 | 0.325 | 0.178 | 10.995 | 11.341 | 23.241 | 4.240836 | 8.671875 | 13.092565 | +| round-5 / A | 2.121 | 0.000 | 0.000 | 0.000 | 0.000 | 2.174 | 4.388 | 4.796397 | 8.531250 | 13.405962 | +| round-6 / C | 0.000 | 5.266 | 0.294 | 0.133 | 5.694 | 6.061 | 22.120 | 3.977201 | 8.203125 | 11.476165 | +| round-6 / A | 2.652 | 0.000 | 0.000 | 0.000 | 0.000 | 2.702 | 3.862 | 4.054356 | 8.125000 | 12.824942 | +| round-6 / B | 0.000 | 19.544 | 28.453 | 1.173 | 49.170 | 49.836 | 40.725 | 3.719785 | 8.500000 | 11.533035 | +| round-7 / A | 1.775 | 0.000 | 0.000 | 0.000 | 0.000 | 1.829 | 2.834 | 4.657222 | 8.578125 | 14.247693 | +| round-7 / B | 0.000 | 16.876 | 30.680 | 1.210 | 48.766 | 49.492 | 46.228 | 5.353380 | 8.687500 | 15.475863 | +| round-7 / C | 0.000 | 5.111 | 0.317 | 7.584 | 13.011 | 13.597 | 44.772 | 4.426264 | 8.906250 | 13.147138 | +| round-8 / B | 0.000 | 50.369 | 41.295 | 0.716 | 92.380 | 93.061 | 47.970 | 5.143477 | 9.062500 | 14.801028 | +| round-8 / C | 0.000 | 3.718 | 0.237 | 0.128 | 4.082 | 4.366 | 14.653 | 2.911920 | 7.562500 | 8.981533 | +| round-8 / A | 1.624 | 0.000 | 0.000 | 0.000 | 0.000 | 1.674 | 2.293 | 3.108083 | 7.468750 | 9.311726 | +| round-9 / C | 0.000 | 6.660 | 0.308 | 0.140 | 7.109 | 8.006 | 26.959 | 4.731763 | 8.687500 | 13.629807 | +| round-9 / A | 2.412 | 0.000 | 0.000 | 0.000 | 0.000 | 2.563 | 8.872 | 4.641674 | 8.796875 | 13.458097 | +| round-9 / B | 0.000 | 11.031 | 44.264 | 0.672 | 55.967 | 56.590 | 35.306 | 3.917557 | 8.140625 | 11.486951 | + +### 两种收益口径 + +- probe 局部净节省 = A 原 operation − validation − patch − commit。 +- 完整机制净节省 = 896 × probe 局部净节省 − (baseline 捕获总耗时 − A baseline 原执行耗时)。 +- 再给出包含外层调度、计时与统计的保守口径:A 完整目标路径 − replay 完整目标路径 − baseline 新增成本。 +- 正数为节省,负数为额外成本。时间均使用同组配对,不拿历史 1.143 µs 作分母。 + +| 轮 / 模式 | probe 净节省 µs/次 | baseline 新增 µs/次 | 完整机制净节省 ms/896 次 | 含外层开销净节省 ms | Jacobian Δ s | CPU Δ s | wall Δ s | +|---|---:|---:|---:|---:|---:|---:|---:| +| 1 / B | -40.013 | 45.591 | -76.701 | -77.279 | -1.411465 | -0.125000 | -3.928985 | +| 1 / C | -2.927 | 14.180 | -15.328 | -15.614 | -1.314904 | -0.078125 | -3.172919 | +| 2 / B | -74.899 | 65.085 | -125.425 | -125.966 | -0.441596 | 0.343750 | -0.439802 | +| 2 / C | -5.461 | 22.862 | -25.378 | -25.664 | -0.342145 | 0.156250 | -0.552320 | +| 3 / B | -48.684 | 46.446 | -85.237 | -86.146 | 1.321374 | 1.078125 | 3.519541 | +| 3 / C | -6.396 | 18.959 | -22.718 | -23.572 | 1.550480 | 1.203125 | 5.122919 | +| 4 / B | -55.461 | 40.390 | -85.882 | -86.551 | 0.049960 | 0.375000 | 0.157585 | +| 4 / C | -5.269 | 14.514 | -17.726 | -18.098 | -0.936568 | -0.234375 | -2.362415 | +| 5 / B | -33.879 | 26.983 | -54.532 | -54.969 | -1.272764 | -0.562500 | -2.824622 | +| 5 / C | -8.874 | 18.853 | -24.844 | -25.106 | -0.555561 | 0.140625 | -0.313398 | +| 6 / B | -46.518 | 36.863 | -74.709 | -75.261 | -0.334571 | 0.375000 | -1.291907 | +| 6 / C | -3.041 | 18.258 | -19.084 | -19.368 | -0.077155 | 0.078125 | -1.348777 | +| 7 / B | -46.991 | 43.394 | -80.985 | -81.587 | 0.696158 | 0.109375 | 1.228170 | +| 7 / C | -11.236 | 41.938 | -47.644 | -48.121 | -0.230959 | 0.328125 | -1.100554 | +| 8 / B | -90.755 | 45.677 | -122.243 | -122.809 | 2.035395 | 1.593750 | 5.489302 | +| 8 / C | -2.458 | 12.360 | -13.277 | -13.487 | -0.196162 | 0.093750 | -0.330193 | +| 9 / B | -53.555 | 26.434 | -71.670 | -72.093 | -0.724117 | -0.656250 | -1.971146 | +| 9 / C | -4.697 | 18.087 | -20.415 | -21.082 | 0.090088 | -0.109375 | 0.171710 | + +| 配对统计:中位数 [min, max] | B | C | +|---|---:|---:| +| probeSavingUs | -48.684375 [-90.755246, -33.878683] | -5.268973 [-11.236272, -2.457924] | +| metadataIncrementUs | 43.393862 [26.434040, 65.084598] | 18.257701 [12.360156, 41.938281] | +| mechanismSavingMs | -80.985100 [-125.425100, -54.532400] | -20.414600 [-47.644400, -13.277000] | +| completePathSavingMs | -81.587000 [-125.966400, -54.969100] | -21.082500 [-48.120900, -13.486700] | +| jacobianDelta | -0.334571 [-1.411465, 2.035395] | -0.230959 [-1.314904, 1.550480] | +| cpuDelta | 0.343750 [-0.656250, 1.593750] | 0.093750 [-0.234375, 1.203125] | +| wallDelta | -0.439802 [-3.928985, 5.489302] | -0.552320 [-3.172919, 5.122919] | + +所有 operation 阶段使用 QPC 墙钟计时,包含调度长尾;积分 CPU 时间单独来自求解器统计。系统负载和频率使本批数据存在较大波动(包括 commit 分项的极端值),因此报告同时保留中位数、min/max 和每组配对结果,不将归因插桩或调度延迟声称为精确的算法 CPU 成本。9 组局部与完整机制净收益均为负;不从总 Jacobian/积分墙钟的正负波动推断整个 local probe 的收益,也不声称已证明理论成本下限。计时未做不可靠的逐事件扣减。 + +## 停止条件与交付 + +当前实现对 R288 没有净收益;这种低成本 operation 应直接原计算。 +本轮到此停止,不继续为 R288 寻找更多微优化,也不扩展到 position52。保留生产原计算路径和原 whole-context guard。 + +代码入口:`tests/manual/specialized_replay_experiment.py`。核心为 `typed_replay_core.inc`、`typed_replay_runtime.inc`;`typed_contract_check.inc` 与 `typed_oracle.inc` 只用于 audit。报告生成器为 `analyze_typed_replay.py`。 + +复现顺序(项目根目录,使用既有工具链,不安装依赖): + +```powershell +.venv-win/Scripts/python.exe tests/manual/specialized_replay_experiment.py prepare --mode P +.venv-win/Scripts/python.exe tests/manual/specialized_replay_experiment.py run --mode P +.venv-win/Scripts/python.exe tests/manual/specialized_replay_experiment.py prepare --mode C --audit +.venv-win/Scripts/python.exe tests/manual/specialized_replay_experiment.py run --mode C --audit +.venv-win/Scripts/python.exe tests/manual/specialized_replay_experiment.py run --mode C --audit --label forced-reject --force 128 +.venv-win/Scripts/python.exe tests/manual/specialized_replay_experiment.py prepare --mode D --audit +.venv-win/Scripts/python.exe tests/manual/specialized_replay_experiment.py run --mode D --audit +.venv-win/Scripts/python.exe tests/manual/specialized_replay_experiment.py run --mode D --audit --label forced-reject --force 128 +.venv-win/Scripts/python.exe tests/manual/specialized_replay_experiment.py prepare --mode A +.venv-win/Scripts/python.exe tests/manual/specialized_replay_experiment.py prepare --mode B +.venv-win/Scripts/python.exe tests/manual/specialized_replay_experiment.py prepare --mode C +.venv-win/Scripts/python.exe tests/manual/specialized_replay_experiment.py benchmark --rounds 9 +.venv-win/Scripts/python.exe tests/manual/analyze_typed_replay.py +``` + +证据目录:`test/r288-typed-replay-20260917/`。`performance.json` 保存 27 次原始记录;`performance-summary.json` 保存统计和逐组净收益;`byte-comparison.json` 保存逐字节结果;C/D 的 `validation.json` 和 `typed-summary.json` 保存全部正确性计数。P 的 `attribution.json` 保存原始 tick、调用次数和计时器测量。 diff --git a/tests/manual/real_skip.h b/tests/manual/real_skip.h new file mode 100644 index 0000000..b4275e7 --- /dev/null +++ b/tests/manual/real_skip.h @@ -0,0 +1,28 @@ +#ifndef R288_REAL_SKIP_H +#define R288_REAL_SKIP_H +#include "local_probe.h" +#include "context_shadow_replay.h" +#ifndef RR_SKIP +#define RR_SKIP 0 +#endif +#ifndef RR_AUDIT +#define RR_AUDIT 0 +#endif +extern int rr_original_scope,rr_replaying; +extern unsigned long long rr_native_calls; +void rr_start(void); +void rr_finish(void); +void rr_jacobian(void); +void rr_record_begin(NativePropertyCache*,NativePipeCache*,const double*); +void rr_record_end(double); +double rr_execute(NativePropertyCache*,NativePipeCache*,const double*,SROperation); +#if RR_AUDIT +void rr_eval_exit(double,const double*,const double*,const double*,int,NativePropertyCache*,NativePipeCache*,ModelJacobianWorkspace*); +void rr_matrix(double,const double*,const double*); +#else +#define rr_eval_exit(...) ((void)0) +#define rr_matrix(...) ((void)0) +#endif +double rrrec_native_pipe_flow_cached_context(NativePropertyCache*,NativePipeCache*,const NativeMedium*,double,double,double,double,double,double,int); +double rrrec_native_temperature_ph_context(NativePropertyCache*,const NativeMedium*,double,double); +#endif diff --git a/tests/manual/real_skip_experiment.py b/tests/manual/real_skip_experiment.py new file mode 100644 index 0000000..4754434 --- /dev/null +++ b/tests/manual/real_skip_experiment.py @@ -0,0 +1,180 @@ +"""Only group 6 / region 288 / position 16, independent real-skip workers.""" +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path +import argparse,hashlib,json,os,re,shutil,subprocess,time +import diagnose_context_shadow as shadow + +ROOT=shadow.ROOT;HERE=Path(__file__).parent +BASE=ROOT/'test/local-probe-20260917/worker' +ACCESS=ROOT/'test/context-access-20260917/worker' +OUT=ROOT/'test/r288-real-skip-20260917' +ex=shadow.access.dx.ex +replace=shadow.replace + + +def write(path,obj):ex.write(path,obj) + + +def fn(source,name): + if name=='ax_test': + start=source.index('unsigned ax_test(');end=source.index('\nvoid ax_scalar',start) + return source[start:end] + if name=='event': + start=source.index('static Event *event(');end=source.index('\nstatic int state_pointer',start) + return source[start:end] + return shadow.access.dx.function(source,name) + + +def core(audit): + source=(HERE/'context_shadow_replay.c').read_text(encoding='utf-8') + certified=json.loads((shadow.OUT/'worker/build.json').read_text(encoding='utf-8'))['instrumentedHashes']['context_shadow_replay.c'] + assert hashlib.sha256(source.encode()).hexdigest()==certified, 'Use the shadow-certified source unchanged.' + prefix=source[:source.index('typedef struct {\n unsigned long long total')] + prefix=prefix.replace('static void save_environment','static __attribute__((unused)) void save_environment').replace('static void restore_environment','static __attribute__((unused)) void restore_environment') + globals=''' +static Plan plan; +static int phase,position=16,group,configured=16; +static unsigned long long jac; +static double sim_time,op_inputs[4]; +static NativePropertyCache *bound; +static NativePipeCache *bound_pipes; +static SRContext overlay; +static size_t probe_entry_count; +static void fatal(const char *s){fprintf(stderr,"real-skip fatal: %s\\n",s);abort();} +static void initialize(void){} +void sr_native_enter(const char *name){(void)name;if(phase==CANDIDATE)fatal("Candidate called physics");} +''' + names=['locate','event','state_pointer','floating_field','ax_access','sr_or','ax_bind','ax_begin','ax_query','ax_match','ax_new','ax_test','ax_scalar','path_of_plan','ax_end','ax_result','from_live','same_medium_key','first_match','map_slot','scalar_lookup','state_read_equal','replay_overlay'] + parts=[prefix,globals]+[fn(source,n) for n in names] + result='\n'.join(parts).replace('(size_t)slot +#include "real_skip_core.inc" + +int rr_original_scope,rr_replaying; +unsigned long long rr_native_calls; +static unsigned long long visits,attempts,successes,rejects,commits,fallbacks,reject_reason[NREASONS]; +static unsigned long long eval_audits,entry_audits,exit_audits,rollback_checks; +static unsigned long long appends_total,relocations,count_different,paths[4]; +static uint64_t frequency,path_ticks,original_ticks,validation_ticks,overlay_ticks,commit_ticks,fallback_ticks,baseline_ticks,baseline_start; +static size_t metadata_min=(size_t)-1,metadata_max; +static int force_every; + +#if RR_AUDIT +/* Pointer identities are compared as owner bindings, never as cross-process addresses. + * Padding is excluded; every declared numerical field is copied byte-for-byte. */ +typedef struct { + unsigned long long jacobian; + int color,stage,status,saved_errno,flags,rounding; + unsigned sse; + double t,output,y[NSTATES],dy[NSTATES],w[NOUTPUTS]; + size_t count,capacity,memo_capacity; + int recording; + unsigned long evaluations[NATIVE_JACOBIAN_SCALAR_KINDS],reuses[NATIVE_JACOBIAN_SCALAR_KINDS]; + NativePropertyState states[SR_STATES]; + NativePipeCache pipes[SR_PIPES]; + NativeJacobianGasMemo gases[MODEL_JACOBIAN_GAS_COUNT]; + NativeJacobianGasStats gas_stats; +} Audit; +static FILE *audit_file,*matrix_file; +static Audit actual,expected; +static NativeJacobianScalarEntry baseline_entries[MODEL_JACOBIAN_SCALAR_COUNT],expected_entries[MODEL_JACOBIAN_SCALAR_COUNT]; +static NativeJacobianScalars *memo_owner; +static SRContext before_reject,after_reject; + +static void audit_fail(const char *where,const void *a,const void *b,size_t n){ + size_t offset=0;while(offset=offsetof(Audit,n)&&off=offsetof(Audit,states)&&off=state_fields[i].offset && local=offsetof(Audit,pipes)&&off=pipe_fields[i].offset && localcount>SR_STATES || p->capacity!=SR_STATES || p->temperatures || !p->jacobian || p->jacobian->capacity!=MODEL_JACOBIAN_SCALAR_COUNT)fatal("audit context schema"); + NativeJacobianScalars *m=p->jacobian; + if(stage==3 && lp_color<0)memo_owner=m; + else if(m!=memo_owner)fatal("memo owner lifetime"); + if(m->recording!=(lp_color<0))fatal("memo recording lifetime"); + if(workspace && (m!=&workspace->scalars || m->entries!=workspace->scalar_entries))fatal("workspace memo binding"); + actual.count=p->count;actual.capacity=p->capacity;actual.memo_capacity=m->capacity;actual.recording=m->recording; + memcpy(actual.evaluations,m->evaluations,sizeof(actual.evaluations));memcpy(actual.reuses,m->reuses,sizeof(actual.reuses)); + for(size_t s=0;scount;s++){ + if(p->states[s].jacobian!=m || p->states[s].temperatures)fatal("entry memo/observer binding"); + for(size_t k=0;kstates[s]+state_fields[k].offset,state_fields[k].size); + } + for(int s=0;sstats; + for(int i=0;igases[i],*dst=&actual.gases[i]; + uintptr_t id=src->kernel==NULL?0:src->kernel==native_medium_gas_context?1:src->kernel==native_polytropic_gas_context?2:999; + if(id==999)fatal("unknown gas memo kernel"); + memcpy(&dst->kernel,&id,sizeof(id));memcpy(dst->inputs,src->inputs,sizeof(dst->inputs));dst->value=src->value;dst->medium_kind=src->medium_kind;dst->valid=src->valid; + } + } + if(stage==3 && lp_color<0){ + memcpy(baseline_entries,m->entries,sizeof(baseline_entries)); + if(RR_SKIP){if(fread(expected_entries,1,sizeof(expected_entries),audit_file)!=sizeof(expected_entries))fatal("memo audit EOF");if(memcmp(expected_entries,baseline_entries,sizeof(baseline_entries)))audit_fail("memo_entries",expected_entries,baseline_entries,sizeof(baseline_entries));} + else if(fwrite(baseline_entries,1,sizeof(baseline_entries),audit_file)!=sizeof(baseline_entries))fatal("memo audit write"); + }else if(memcmp(baseline_entries,m->entries,sizeof(baseline_entries)))audit_fail("memo_readonly",baseline_entries,m->entries,sizeof(baseline_entries)); + if(RR_SKIP){ + if(fread(&expected,1,sizeof(expected),audit_file)!=sizeof(expected))fatal("audit EOF"); + if(memcmp(&expected,&actual,sizeof(actual))){size_t i=0;while(((unsigned char*)&expected)[i]==((unsigned char*)&actual)[i])i++;audit_fail(audit_field(i),&expected,&actual,sizeof(actual));} + }else if(fwrite(&actual,1,sizeof(actual),audit_file)!=sizeof(actual))fatal("audit write"); + restore_environment(&env);errno=saved_errno; +} +void rr_eval_exit(double t,const double *y,const double *dy,const double *w,int result,NativePropertyCache *p,NativePipeCache *pipes,ModelJacobianWorkspace *workspace){ + audit_context(3,t,y,dy,w,result,0,p,pipes,workspace);eval_audits++; +} +void rr_matrix(double t,const double *y,const double *matrix){ + int e=errno;SREnvironment env;save_environment(&env); + if(fwrite(&t,8,1,matrix_file)!=1 || fwrite(y,8,NSTATES,matrix_file)!=NSTATES || fwrite(matrix,8,NSTATES*NSTATES,matrix_file)!=NSTATES*NSTATES)fatal("matrix write"); + restore_environment(&env);errno=e; +} +#endif + +void rr_start(void){ + LARGE_INTEGER f;QueryPerformanceFrequency(&f);frequency=(uint64_t)f.QuadPart; +#if RR_AUDIT + audit_file=fopen(RR_SKIP?getenv("RR_EXPECTED"):"audit.bin",RR_SKIP?"rb":"wb");matrix_file=fopen("jacobians.bin","wb"); + if(!audit_file || !matrix_file)fatal("open audit files"); + setvbuf(audit_file,NULL,_IOFBF,1024*1024);setvbuf(matrix_file,NULL,_IOFBF,1024*1024); + const char *s=getenv("RR_FORCE_REJECT_EVERY");force_every=s?atoi(s):0; +#endif +} +void rr_jacobian(void){jac++;plan.ready=0;} +void rr_record_begin(NativePropertyCache *p,NativePipeCache *pipes,const double *x){ + baseline_start=lp_tick(); + if(RR_SKIP){ax_bind(p,pipes);ax_begin(jac,-1,16,0,x,4);} +} +void rr_record_end(double output){if(RR_SKIP)ax_end(&output,1);baseline_ticks+=lp_tick()-baseline_start;} + +/* The translated write set is prepared before touching live storage. */ +typedef struct {void *destination;const void *source;size_t size;int kind;unsigned mask;} Patch; +static Patch patches[SR_EVENTS]; +static NativeJacobianScalars *patch_owner; +static NativePropertyTemperatures *patch_observer; +static int build_patch(NativePropertyCache *p,NativePipeCache *pipes,const int *map){ + int n=0;patch_owner=p->jacobian;patch_observer=NULL; + for(int i=0;itype==ALLOCATE){item.destination=&p->count;item.kind=1;} + else if(e->type==SCALAR){item.destination=&p->jacobian->reuses[e->slot];item.kind=2;} + else if(e->type==WRITE_PIPE){item.destination=(char*)&pipes[e->slot]+e->offset;item.source=e->data;item.size=e->size;} + else if(e->type==WRITE_STATE || e->type==WRITE_OR){ + item.destination=(char*)&p->states[map[e->slot]]+e->offset;item.source=e->data;item.size=e->size; + if(e->type==WRITE_OR){item.kind=3;item.mask=e->mask;} + else if(e->offset==(int)offsetof(NativePropertyState,jacobian))item.source=&patch_owner; + else if(e->offset==(int)offsetof(NativePropertyState,temperatures))item.source=&patch_observer; + }else continue; + patches[n++]=item; + } + return n; +} +static void commit(int n){ + for(int i=0;ikind==1)(*(size_t*)p->destination)++; + else if(p->kind==2)(*(unsigned long*)p->destination)++; + else if(p->kind==3)(*(unsigned*)p->destination)|=p->mask; + else memcpy(p->destination,p->source,p->size); + } +} +double rr_execute(NativePropertyCache *p,NativePipeCache *pipes,const double *x,SROperation original){ + visits++; +#if RR_AUDIT + audit_context(1,0,NULL,NULL,NULL,1,0,p,pipes,NULL);entry_audits++; +#endif + uint64_t start=lp_tick(),tick;double output; + if(RR_SKIP){ + attempts++;bound=p;bound_pipes=pipes;position=16;probe_entry_count=p->count;rr_replaying=1; + int reason=OK,mapping[SR_STATES],appends=0; + count_different+=(p->count!=plan.entry_count); + tick=lp_tick(); + if(p->count>SR_STATES || p->capacity>SR_STATES || p->count>p->capacity)reason=CAPACITY; + else if(!p->jacobian || !p->jacobian->entries || p->jacobian->capacity>MODEL_JACOBIAN_SCALAR_COUNT)reason=MEMO_BINDING; + else from_live(&overlay,p,pipes); + overlay_ticks+=lp_tick()-tick; +#if RR_AUDIT + from_live(&before_reject,p,pipes); +#endif + tick=lp_tick();phase=CANDIDATE; + double saved_output=plan.output; + if(force_every && (attempts-1)%(unsigned)force_every==0)plan.output=NAN; + if(!reason)reason=replay_overlay(&overlay,x,mapping,&appends); + plan.output=saved_output;phase=OFF;validation_ticks+=lp_tick()-tick; + if(!reason){ + tick=lp_tick();int n=build_patch(p,pipes,mapping);overlay_ticks+=lp_tick()-tick; + tick=lp_tick();commit(n);output=plan.output;commit_ticks+=lp_tick()-tick; + commits++;successes++;appends_total+=appends;paths[plan.path]++; + int relocated=0;for(int i=0;i=0 && mapping[i]!=i)relocated=1; + relocations+=relocated; + size_t bytes=sizeof(plan)-sizeof(plan.events)+(size_t)plan.n*sizeof(Event); + if(bytesmetadata_max)metadata_max=bytes; + }else{ + rejects++;reject_reason[reason]++;rr_replaying=0; +#if RR_AUDIT + from_live(&after_reject,p,pipes); + if(compare_contexts(&before_reject,&after_reject))fatal("reject altered live context"); + rollback_checks++; +#endif + tick=lp_tick();rr_original_scope=1;output=original(p,pipes,x);rr_original_scope=0;fallback_ticks+=lp_tick()-tick;fallbacks++; + } + rr_replaying=0; + }else{ + tick=lp_tick();rr_original_scope=1;output=original(p,pipes,x);rr_original_scope=0;original_ticks+=lp_tick()-tick; + } + path_ticks+=lp_tick()-start; +#if RR_AUDIT + audit_context(2,0,NULL,NULL,NULL,1,output,p,pipes,NULL);exit_audits++; +#endif + return output; +} +void rr_finish(void){ +#if RR_AUDIT + if(RR_SKIP && fgetc(audit_file)!=EOF)fatal("unconsumed audit records"); + fclose(audit_file);fclose(matrix_file); +#endif + FILE *f=fopen("real-skip.json","wb");if(!f)fatal("summary"); + fprintf(f,"{\"targetVisits\":%llu,\"candidateAttempts\":%llu,\"semanticSuccess\":%llu,\"realSkips\":%llu,\"rejects\":%llu,\"fallbackExecutions\":%llu,\"nativeOriginalExecutions\":%llu,\"commits\":%llu,\"referenceExecutions\":0,\"mismatches\":0,\"evaluationAudits\":%llu,\"targetEntryAudits\":%llu,\"targetExitAudits\":%llu,\"rollbackChecks\":%llu,",visits,attempts,successes,successes,rejects,fallbacks,rr_native_calls,commits,eval_audits,entry_audits,exit_audits,rollback_checks); + fprintf(f,"\"pathSeconds\":%.12g,\"originalSeconds\":%.12g,\"validationSeconds\":%.12g,\"overlayPatchSeconds\":%.12g,\"commitSeconds\":%.12g,\"fallbackSeconds\":%.12g,\"baselineRecordSeconds\":%.12g,",(double)path_ticks/frequency,(double)original_ticks/frequency,(double)validation_ticks/frequency,(double)overlay_ticks/frequency,(double)commit_ticks/frequency,(double)fallback_ticks/frequency,(double)baseline_ticks/frequency); + fprintf(f,"\"countDifferent\":%llu,\"slotRelocationTrials\":%llu,\"appends\":%llu,\"paths\":[%llu,%llu,%llu,%llu],\"metadataMin\":%llu,\"metadataMax\":%llu,\"metadataReserved\":%llu,\"overlayBytes\":%llu,\"patchReserved\":%llu,\"rejectReasons\":{",count_different,relocations,appends_total,paths[0],paths[1],paths[2],paths[3],(unsigned long long)(metadata_min==(size_t)-1?0:metadata_min),(unsigned long long)metadata_max,(unsigned long long)sizeof(plan),(unsigned long long)sizeof(overlay),(unsigned long long)sizeof(patches)); + for(int i=1;i1?",":"",reasons[i],reject_reason[i]); + fputs("}}\n",f);fclose(f); +} diff --git a/tests/manual/specialized_replay_experiment.py b/tests/manual/specialized_replay_experiment.py new file mode 100644 index 0000000..ded2e1b --- /dev/null +++ b/tests/manual/specialized_replay_experiment.py @@ -0,0 +1,260 @@ +"""R288-only typed replay cost-floor experiment; no production source changes.""" +from pathlib import Path +import argparse,hashlib,json,os,re,shutil,subprocess,statistics,struct +import real_skip_experiment as base + +ROOT=base.ROOT;HERE=Path(__file__).parent;OUT=ROOT/'test/r288-typed-replay-20260917' +OLD=base.OUT +original_core=base.core + +def replace(s,a,b):return base.replace(s,a,b) +def body(s,name,change):return base.shadow.access.change_function(s,name,change) +def function_replace(s,name,new): + a,b,e=base.ex.function_span(s,name);return s[:a]+new+s[e:] + + +def profile_core(): + s=original_core(False) + pro=''' +static uint64_t prof_ticks[24],prof_calls[24]; +typedef struct {uint64_t start;int category;} Prof; +static void prof_end(Prof *s){prof_ticks[s->category]+=lp_tick()-s->start;prof_calls[s->category]++;} +#define PROF(c) Prof ps __attribute__((cleanup(prof_end)))={lp_tick(),c} +''' + s=s.replace('static Plan plan;',pro+'\nstatic Plan plan;') + for name,index in [('first_match',10),('map_slot',11),('scalar_lookup',12),('state_read_equal',13),('from_live',14)]:s=body(s,name,lambda b,i=index:'PROF('+str(i)+');'+b) + s=s.replace('const Event *e=&plan.events[i];int slot=-1;','const Event *e=&plan.events[i];PROF(e->type);int slot=-1;') + s=body(s,'replay_overlay',lambda b:replace('uint64_t header_start=lp_tick();'+b,'for(int i=0;istates,0xa5,sizeof(dst->states));'), + (19,'dst->context=*src;memcpy(dst->states,src->states,src->count*sizeof(*src->states));'), + (20,'memcpy(dst->pipes,pipes,sizeof(dst->pipes));'), + (21,'dst->memo=*src->jacobian;memcpy(dst->entries,src->jacobian->entries,src->jacobian->capacity*sizeof(*dst->entries));')] + for tag,text in portions:b=replace(b,text,'{PROF('+str(tag)+');'+text+'}') + start=b.index('dst->memo.entries=');b=b[:start]+'{PROF(22);'+b[start:]+'}' + return b + s=body(s,'from_live',split_copy) + return s + + +def profile_runtime(): + s=(HERE/'real_skip_runtime.c').read_text() + s=s.replace('static int force_every;','static int force_every;\nstatic FILE *plans_file;\nstatic uint64_t timer_ticks,dispatch_ticks;static volatile unsigned dispatch_sink;') + s=body(s,'rr_start',lambda b:b+'\nplans_file=fopen("plans.bin","wb");if(!plans_file)fatal("plans");\nfor(int i=0;i<100000;i++){uint64_t t=lp_tick();timer_ticks+=lp_tick()-t;}\n') + s=body(s,'rr_record_end',lambda b:b+''' +int saved=errno;SREnvironment env;save_environment(&env); +fwrite(&plan,1,sizeof(plan),plans_file); +uint64_t dispatch_start=lp_tick();unsigned checksum=0; +for(int repeat=0;repeat<100;repeat++)for(int i=0;itype){ +case QUERY:checksum+=(unsigned)e->slot;break;case SCALAR:checksum+=(unsigned)e->size;break; +case ALLOCATE:checksum++;break;case WRITE_STATE:case WRITE_PIPE:checksum+=(unsigned)e->offset;break; +case READ_STATE:case READ_PIPE:checksum+=(unsigned)e->size;break;case WRITE_OR:case VALID:checksum+=e->mask;break;default:checksum++; +}} +dispatch_ticks+=lp_tick()-dispatch_start;dispatch_sink=checksum; +restore_environment(&env);errno=saved; +''') + s=body(s,'build_patch',lambda b:'PROF(15);'+b) + s=body(s,'commit',lambda b:'PROF(16);'+b) + s=body(s,'rr_finish',lambda b:b+''' +fclose(plans_file);FILE *pf=fopen("attribution.json","wb"); +fprintf(pf,"{\\"frequency\\":%llu,\\"emptyTimerTicks\\":%llu,\\"emptyTimerCalls\\":100000,\\"dispatchTicks\\":%llu,\\"dispatchRepeats\\":100,\\"categories\\":[",(unsigned long long)frequency,(unsigned long long)timer_ticks,(unsigned long long)dispatch_ticks); +for(int i=0;i<24;i++)fprintf(pf,"%s{\\"id\\":%d,\\"ticks\\":%llu,\\"calls\\":%llu}",i?",":"",i,(unsigned long long)prof_ticks[i],(unsigned long long)prof_calls[i]); +fputs("]}",pf);fclose(pf); +''') + return s + + +def minimal_sources(audit): + # The audit build adds the minimal capture hooks to the certified access + # source, allowing one physical baseline execution to feed BOTH schemas. + directory=base.ACCESS if audit else base.BASE + codes={n:(directory/n).read_text(encoding='utf-8') for n in ['properties.c','pipe.c']} + s=codes['properties.c'] + if audit: + # Native access hooks feed the compact recorder through ax_* wrappers. + return codes + s=body(s,'property_pt',lambda b:replace(replace(b,'return s;','{fp_query(cache,m,p,T,s);return s;}'),'return property_new(cache,m,p,T,scratch);','fp_query(cache,m,p,T,NULL);return property_new(cache,m,p,T,scratch);')) + s=body(s,'property_new',lambda b:replace(b,'return s;','fp_allocate(cache,s,valid);return s;')) + s=body(s,'native_temperature_ph_context',lambda b:'fp_unknown();'+b) + s=body(s,'native_jacobian_scalar_get',lambda b:replace('fp_scalar_key(kind,medium_kind,inputs,count);'+b,'return 1;','fp_scalar_value(kind,medium_kind,inputs,count,*value);return 1;')) + s=body(s,'native_jacobian_scalar_put',lambda b:'fp_scalar_value(kind,medium_kind,inputs,count,value);'+b) + codes['properties.c']=s + return codes + + +def fast_core(audit): + if audit: + s=original_core(True) + s=s.replace('static SRContext overlay;','') + s=body(s,'ax_query',lambda b:b+'\nif(phase==RECORD && strcmp(kind,"PT"))fp_unknown();') + s=body(s,'ax_match',lambda b:b+''' +if(phase==RECORD && !strcmp(kind,"PT")){ +Event *q=&plan.events[plan.query_index];double key[9];memcpy(key,q->data,sizeof(key)); +NativeMedium m={q->offset,key[2],key[3],key[4],key[5],key[6],key[7],key[8]};fp_query(bound,&m,key[0],key[1],s); +} +''') + s=body(s,'ax_new',lambda b:'if(phase==RECORD)fp_allocate(cache,s,valid);'+b) + s=body(s,'ax_scalar',lambda b:b+''' +if(phase==RECORD){ + if(!strcmp(action,"get")){fp_scalar_key(kind,medium,keys,n);if(hit && value)fp_scalar_value(kind,medium,keys,n,*value);} + else if(!strcmp(action,"put_attempt") && value)fp_scalar_value(kind,medium,keys,n,*value); +} +''') + return s + s=(HERE/'context_shadow_replay.c').read_text() + s=s[:s.index('typedef struct {\n int type,slot')] + s=s.replace('static void save_environment','static __attribute__((unused)) void save_environment').replace('static void restore_environment','static __attribute__((unused)) void restore_environment') + s+=''' +static unsigned long long jac; +static NativePropertyCache *bound; +static NativePipeCache *bound_pipes; +static void fatal(const char *s){fprintf(stderr,"typed replay fatal: %s\\n",s);abort();} +''' + return s + + +def fast_runtime(audit,direct=False): + s=(HERE/'real_skip_runtime.c').read_text() + s=s.replace('static int force_every;','static __attribute__((unused)) int force_every;\n#include "fast_replay.inc"') + start=s.index('/* The translated write set');end=s.index('double rr_execute(',start) + s=s[:start]+s[end:] + execute=(HERE/'typed_replay_runtime.inc').read_text() + s=function_replace(s,'rr_execute',execute) + if audit:s=s.replace('void rr_start(void){','#include "typed_contract.inc"\n#include "typed_oracle.inc"\nvoid rr_start(void){') + s=function_replace(s,'rr_jacobian','void rr_jacobian(void){jac++;fast.ready=0;\n#if RR_AUDIT\nplan.ready=0;\n#endif\n}') + s=function_replace(s,'rr_record_begin','''void rr_record_begin(NativePropertyCache *p,NativePipeCache *pipes,const double *x){ +baseline_start=lp_tick();bound=p;bound_pipes=pipes;fp_begin(p,pipes,x); +#if RR_AUDIT +ax_bind(p,pipes);ax_begin(jac,-1,16,0,x,4); +#endif +}''') + s=function_replace(s,'rr_record_end','''void rr_record_end(double output){ +fp_end(output); +#if RR_AUDIT +ax_end(&output,1);fp_contract_check(); +#endif +baseline_ticks+=lp_tick()-baseline_start; +}''') + if direct: + s=s.replace('static __attribute__((unused)) int force_every;','static FILE *archived_plans;\nstatic __attribute__((unused)) int force_every;') + s=body(s,'rr_start',lambda b:b+'\narchived_plans=fopen(getenv("RR_PLAN_ORACLE"),"rb");if(!archived_plans)fatal("archived plan oracle");\n') + s=s.replace('ax_end(&output,1);fp_contract_check();',''' +int saved_errno=errno;SREnvironment saved_env;save_environment(&saved_env); +if(fread(&plan,1,sizeof(plan),archived_plans)!=sizeof(plan))fatal("archived plan EOF"); +uintptr_t previous_owner=plan.memo_binding; +for(int i=0;itype==READ_STATE || e->type==WRITE_STATE) && e->offset==(int)offsetof(NativePropertyState,jacobian)){ +uintptr_t pointer;memcpy(&pointer,e->data,sizeof(pointer));if(pointer!=previous_owner)fatal("archived memo binding");memcpy(e->data,&fast.owner,sizeof(fast.owner)); +}} +plan.memo_binding=(uintptr_t)fast.owner;phase=OFF; +restore_environment(&saved_env);errno=saved_errno;fp_contract_check(); +''') + s=body(s,'rr_finish',lambda b:b+'\nif(fgetc(archived_plans)!=EOF){fatal("remaining archived plans");}\nfclose(archived_plans);\n') + s=s.replace('(unsigned long long)sizeof(plan),(unsigned long long)sizeof(overlay),(unsigned long long)sizeof(patches)', '(unsigned long long)sizeof(fast),(unsigned long long)sizeof(Pending),(unsigned long long)sizeof(Pending)') + s=body(s,'rr_finish',lambda b:b+''' +FILE *fextra=fopen("typed-summary.json","wb"); +fprintf(fextra,"{\\"metadataBytes\\":%llu,\\"captureScratchBytes\\":%llu,\\"pendingBytes\\":%llu,\\"contractChecks\\":%llu,\\"oracleChecks\\":%llu,\\"negativeChecks\\":%llu}",(unsigned long long)sizeof(fast),(unsigned long long)sizeof(capture),(unsigned long long)sizeof(Pending), +#if RR_AUDIT +contract_checks,oracle_checks,negative_checks +#else +0ULL,0ULL,0ULL +#endif +);fclose(fextra); +''') + return s + + +def contract_source(): + data=(OUT/'P/perf-skip-run/plans.bin').read_bytes();schemas={} + for off in range(0,len(data),90224): + count=struct.unpack_from('p,T=up->T; +pd=fmax(fmin(pd,p),0); +const NativeMedium *m=&up->medium; +double cp=m->cp+m->slope*(T-m->Tref); +double factor=m->real_helium?isentropic(cache,up,pd):(cp-m->R)/cp; +double g=fmax(1e-9,fmin(1-1e-9,factor)); +double rho=fmax(property_density(up),1e-12); +``` + +这包括PH/PT查询、首次匹配、entry创建、count/valid变化、上游和下游等熵准备、density获取及温度observer行为。orifice后续的q计算、方向/opening处理、有限性检查和返回值仍执行。目标调用是orifice,没有额外跳过任何pipe逻辑;整个求解中的其他pipe/context路径保持原执行。 + +### B. 已有memo/valid覆盖的工作 + +- `local_isentropic` 先观察温度,valid命中才直接返回;不能把观察和valid语义一起memo掉。 +- `property_density`、PH context获取中的已有缓存/Jacobian scalar memo继续工作。 +- 先前fallback函数统计中,昂贵PH反算、密度求解、pipe resistance求根实际调用为0;本轮不把它们当作新可省工作。 + +### C. 本次计时的数值尾部 + +**起点:原源码第272行 `double r=...`;终点:第276行平滑修正完成。** `subsonic_cm` 的调用也计入该尾部。 + +```c +double r=fmax(pd/p,0),critical=pow(2*g/(g+1),1/(1-g)),eff; +if(r<=critical) { + eff=critical; + *cm=sqrt(2/(1+g)*rho*T/p)*pow(2*g/(g+1),g/(1-g)); + *vel=sqrt(2/(1+g)*p/rho); +} else { + eff=r; + *cm=subsonic_cm(r,g,rho,T,p); + *vel=sqrt(fmax(2/(1-g)*p/rho*(1-pow(r,1-g)),0)); +} +double ref=subsonic_cm(.9999,g,rho,T,p); +if(*cm>0 && ref>0) { + double smooth=tanh(fmax(12*fabs(*cm/ref)*log(eff)/log(.9999),0)); + *cm*=smooth; *vel*=smooth; +} +``` + +- 完整数值输入:**`p, T, pd, g, rho`**,五个double的原始位,共40 B。pd已经限幅,g和rho已经完成上述准备/限幅。不能用原operation的p/h/opening作为替代key。 +- 数值输出:**`cm, vel`**,两个double原始位。 +- 不再读取medium、entry、valid、count或pipe,不写property context;原输出指针的既有值不是尾部输入,两条分支均先写cm/vel。 +- 这只表示“数值计算不依赖context”,**不表示没有机器浮点状态副作用**。数学库和x87运算会改变部分状态字位,因此不能把整个state_valve或这个尾部无条件当作可直接缓存返回的纯函数。 + +## 2. 诊断方法 + +独立worker复制既有local probe实现,只在目标position设置诊断scope;普通residual及其他position不进入记录。原whole-context guard及其成功恢复路径保留。 + +- 每次目标尾部仍执行一次原数学代码。诊断lookup只把可能的缓存结果写到临时记录,不用于operation输出,**没有真实skip,也没有双路Reference执行**。 +- 使用invariant TSC,`lfence/rdtsc/lfence`,全轨迹对QPC校准;原编译参数仍为O3、禁止fast-math与FP contraction。两端标记包围原尾部,没有循环重复同一个热key来替代实际轨迹。 +- 所有详细记录先写预分配内存,积分结束后写盘;单记录128 B。候选baseline记录仅72 B:40 B key、16 B输出、12 B环境、4 B ready。 +- 五轮完整诊断,同时直接测K、候选hit/miss lookup、baseline key/环境准备、结果捕获及每Jacobian重置。G包含外层调用、key构造、环境读取、完整比较及写回临时结果,不只测memcmp。 +- 另三轮 **K-only** 对照:尾部之前只放计时标记,key/环境采样全部移到尾部之后。这三轮的环境不是入口环境,**其hits/G/H字段不参与任何命中率或盈利判断**。 +- 原始单次均值、中位数和min/max全部保留,没有剔除抢占/缺页长尾。空标记中位约12–14 ns;预算只从可省K中扣除空标记,G/H保留原始计时,避免把微小收益做大。 + +归档初版构建与交付诊断版本的 `model.o`、`properties.o`、`valve_tail_diag.o` 字节一致;清理仅移除了未启用的预留接口,没有引入真实skip版本。 + +已检查两版 `state_valve` 目标码控制流:density/g准备先于起始标记,`pd/p`、critical及尾部数学调用位于计时范围内,输出写入完成后才取结束标记。汇编分别保存在构建目录的 `state-valve.asm`。 + +## 3. 全轨迹进入次数与尾部时间 + +所有轮次都完成896个Jacobian。目标物理operation进入和尾部执行次数均为 **24,192**: + +| 类别 | 次数 | +|---|---:| +| baseline | 896 | +| probe group0–25 | 23,296 | +| group26 | 0:已有whole-context复用使目标operation未执行 | +| 真实skip | 0 | + +分母是Jacobian内实际执行的目标probe,不含普通residual,也不把group26已有的复用计成新memo收益。 + +### K-only低扰动对照(每轮24,192次) + +| 轮次 | 均值µs | 单次中位µs | 单次min–max µs | 原始累计ms | 扣空标记均值µs | +|---|---:|---:|---:|---:|---:| +| tail-only-0 | 0.803447 | 0.610863 | 0.377976–3595.053 | 19.436981 | 0.791542 | +| tail-only-1 | 0.691729 | 0.617559 | 0.369792–459.269 | 16.734302 | 0.679824 | +| tail-only-2 | 0.917657 | 0.627976 | 0.369047–3031.652 | 22.199961 | 0.904264 | + +三轮逐次中位数的中位数 **0.617559µs**,每轮均值的中位数 **0.803447µs**,累计时间中位数 **19.436981ms**。数千µs的max明显混有系统调度长尾,不代表一次数学计算通常要这么久;这些样本没有被删除。 + +### 与G/H同时采集的五轮K(盈亏使用同轮配对数据) + +| 轮次 | 均值µs | 单次中位µs | 单次min–max µs | 原始累计ms | +|---|---:|---:|---:|---:| +| diagnostic-0 | 0.600212 | 0.575893 | 0.360863–33.676 | 14.520341 | +| diagnostic-1 | 0.759084 | 0.680059 | 0.396577–201.298 | 18.363768 | +| diagnostic-2 | 0.843411 | 0.677083 | 0.367559–1362.520 | 20.403801 | +| diagnostic-3 | 1.176682 | 0.691964 | 0.368303–8174.892 | 28.466296 | +| diagnostic-4 | 0.642986 | 0.577381 | 0.416666–243.518 | 15.555124 | + +这些结果支持尾部是亚微秒级计算,不能支持“所有测得墙钟长尾都可通过memo省掉”。K-only与完整诊断的批次/代码布局不同,不跨轮拿最贵K减最便宜G来拼净收益。 + +## 4. 完整数值bit-key的真实重复率 + +每个Jacobian的27次物理进入(1 baseline + 26 probes)均有 **5种不同key**: + +- baseline与 **group0–5、10–25** 共用同一key;每Jacobian22次重复。 +- group6、7、8、9各有一个不同key,彼此及baseline不同。 +- 896个Jacobian全部如此;baseline-only单槽候选已经覆盖所有数值重复,增加多条probe memo不会再增加理论hit。 + +| 统计 | 结果 | +|---|---:| +| 理论memo hit | 19,712 | +| 理论probe miss | 3,584 | +| 数值key hit rate | 19,712 / 23,296 = **84.6153846%** | +| 相同key的cm/vel逐位不同 | **0** | +| 各组与baseline相同次数 | 上述22组分别896次 | + +[每个Jacobian的baseline完整十六进制key、各组key和分组关系](../../test/position379-tail-20260917/diagnostic-0/key-groups.json)。其他四轮也各自保存清单和原始记录,并得到相同分组计数。 + +### 数值重复不等于可以直接返回 + +本次读取:errno、MXCSR全寄存器、x87 control word、x87 status word。尾部前后: + +- errno、MXCSR、x87 control word变化次数均为0。 +- **x87 status word变化13,374次**,其中baseline变化499/896次。 +- 变化位是x87条件状态位,例如 `0x120 → 0x320` 和 `0x320 → 0x120`;本轨迹没有观察到标准浮点异常标志或舍入控制变化。不能把“标准异常标志没变”表述为“完整状态字没变”。 + +首版诊断候选保持保守边界:baseline key/output必须有限,所有异常被mask;baseline尾部前后上述环境完全相同,当前probe环境又与baseline一致,且完整40 B key相同,才计算为可直接返回的hit。其余情况仍原计算。没有尝试重放/修补x87状态字,也没有把条件位从比较中移除。 + +| 严格候选结果(每轮一致) | 次数 | +|---|---:| +| 可直接返回候选hit | **8,734 = 397 × 22** | +| 候选hit rate | **37.4914148%** | +| baseline改变x87状态,故记录不可直接复用 | 12,974次probe | +| 在可复用baseline下,probe环境不同 | 901 | +| 环境相同但key不同 | 687 | +| 总候选miss / 原执行 | 14,562 | + +拒绝原因按实际检查优先级归类,环境不同和key不同可能重叠,不能将此表与3,584次纯数值miss相加。上述hit均进一步检查了真实原执行的出口环境和cm/vel:与baseline一致;但本轮没有把它接入真实skip。 + +这里的完整x87状态保护比仅核对C标准fenv的异常/舍入控制更严格。它明确对应本轮不放宽FP状态语义的候选;不能用其37.49%结果断言所有可能的FP兼容memo设计都只有这个命中率。 + +## 5. Break-even:必须收费的miss与baseline + +本轮的H包括每Jacobian重置、baseline key/入口环境构造、出口环境检查、完整key/output捕获和ready判定。原baseline尾部计算本身未计为H,因为无优化时也必须执行。 + +```text +net = sum(K_i for accepted hits) + − sum(G_hit_i) + − sum(G_miss_i) + − sum(H_j) +``` + +仅用 `K > G_hit + H/22` 会出错:严格候选平均每Jacobian只有 **9.747768次hit**,同时每Jacobian有 **16.252232次miss lookup**。miss不省任何尾部工作,却仍要支付查找开销。 + +### 同轮测量的开销及诊断预算 + +G为逐次成本的均值,H为总baseline新增除以896。下表净值使用该轮可接受hit对应的原尾部时间,扣一个空标记后计算;不是根据全体K均值估算命中部分。 + +| 轮次 | G_hit µs | G_miss µs | H µs/J | 可省尾部ms | 全部probe lookup ms | baseline新增ms | 诊断净预算ms | +|---|---:|---:|---:|---:|---:|---:|---:| +| 0 | 0.151522 | 0.157994 | 2.161951 | 5.193716 | 3.624103 | 1.937108 | **−0.367495** | +| 1 | 0.385920 | 0.575816 | 3.331597 | 6.472028 | 11.755655 | 2.985111 | **−8.268738** | +| 2 | 0.415912 | 0.480126 | 3.563161 | 8.496013 | 10.624167 | 3.192592 | **−5.320746** | +| 3 | 0.475128 | 0.507744 | 6.618877 | 15.133467 | 11.543533 | 5.930514 | **−2.340579** | +| 4 | 0.152557 | 0.197271 | 2.087834 | 5.390453 | 4.205092 | 1.870699 | **−0.685338** | + +五轮G_hit均值中位数 **0.385920µs**,H中位数 **3.331597µs/J**。H不仅是几个孤立长尾:不含重置的逐baseline成本中位数也为 **1.794–2.944µs**。没有测出H≤1µs/J。 + +G_hit逐次中位数较低(约0.071–0.133µs),但不能只收hit的中位数费用,再把miss和baseline账单省略。要计算轨迹净收益,必须使用累计成本。 + +### 具体break-even门槛 + +固定同轮实测H和全部miss成本后: + +```text +G_hit_max = (saved_tail_work − baseline_cost − miss_lookup_cost) / hits +``` + +五轮上限分别为 **0.109446、−0.560810、−0.193287、0.207143、0.074089µs**。负数表示即使hit完全免费,该轮H+miss也已用完预算;其余轮的实际G_hit同样超过上限。 + +以较平稳的第0轮举例,命中尾部扣标记均值0.594655µs: + +- 若H=1µs/J,hit与miss统一收费G,需要 **G<0.184483µs/每次probe尝试** 才盈利。因此“G≤0.25µs且H≤1µs/J”在37.49%可接受率下也不自动足够。 +- 实测H=2.161951µs/J时,所有probe的平均查找预算约 **0.13979µs**;实测约0.15557µs,仍超过。 +- 这是具体候选的盈亏边界,不是kernel memo的理论最低实现成本。没有测量未知的新FP状态重放设计,也不把这种未实现方案当作已有收益。 + +所有净预算是诊断阶段的候选费用估计,**不是实测real-skip加速**。系统负载和计时长尾较大,无法证明纯硬件成本存在绝对负收益下界;但是没有一轮形成完整账单上的正收益,故不满足“诊断显示可盈利才实现”的条件。 + +## 6. 数值验收与停止决定 + +8轮(5轮完整诊断、3轮K-only)全部与归档未修改local probe基线一致:states、outputs、events二进制、最终状态、warning和solver counters。diagnostic-0与tail-only-0另分别核对全部896个132×132 Jacobian及对应t/y,逐位一致。 + +| 计数 | 各轮结果 | +|---|---:| +| accepted / rejected | 10840 / 918 | +| Newton iterations / convergence failures | 19371 / 798 | +| nfev / njev / nlu | 44467 / 896 / 3106 | +| solverStarts / stateTransitions | 4 / 1 | +| 目标原尾部实际执行 | 24,192 / 24,192 | + +本轮没有真实skip版本,因此没有声称完成memo版的逐evaluator property/pipe context与memo生命周期双路验收。当前验证证明诊断插桩保持了上述完整轨迹数值;将来若提出另一种盈利方案,仍须重新完成用户要求的全context/FP等真实skip验收,不能借用本轮数值一致性代替。 + +**停止决定:保留position379原尾部计算;不接入生产、不扩展其他position、不继续调小这套候选以追求过线。** + +## 产物与复现 + +- [诊断构建/执行脚本](diagnose_valve_tail.py)、[只读汇总脚本](analyze_valve_tail.py)。 +- [诊断接口](valve_tail_diag.h)、[诊断记录与计时](valve_tail_diag.c)。代码没有真实skip入口。 +- [全部轮次统计、预算及FP变化](../../test/position379-tail-20260917/diagnostic-analysis.json)。 +- [最终核验记录](../../test/position379-tail-20260917/verification.json):五轮key/output/FP记录投影逐字节相同,源文件/构建哈希及数值检查通过。 +- `test/position379-tail-20260917/diagnostic-0..4/`:原始128 B记录、每Jacobian key分组、空时钟标记、逐轮校验结果。 +- `tail-only-0..2/`:独立K对照;其后置环境采样不作语义证据。 +- `diagnostic-build-v1/`:原五轮使用的构建及哈希;`diag-trace/`为交付诊断构建,关键目标码一致;`tail-only-trace/`为K对照构建。 + +```powershell +.venv-win/Scripts/python.exe -B tests/manual/diagnose_valve_tail.py prepare +.venv-win/Scripts/python.exe -B tests/manual/diagnose_valve_tail.py run --label diagnostic-new --matrices +.venv-win/Scripts/python.exe -B tests/manual/diagnose_valve_tail.py prepare --tail-only +.venv-win/Scripts/python.exe -B tests/manual/diagnose_valve_tail.py run --worker tail-only-trace --label tail-only-new --matrices +.venv-win/Scripts/python.exe -B tests/manual/analyze_valve_tail.py +``` + +路径均从脚本所在仓库解析;此实验使用Windows归档工具链和TSC/FP寄存器读数,没有进行Linux性能测试。 diff --git a/tests/manual/summarize_mechanical_events.py b/tests/manual/summarize_mechanical_events.py new file mode 100644 index 0000000..2cb9bb8 --- /dev/null +++ b/tests/manual/summarize_mechanical_events.py @@ -0,0 +1,63 @@ +"""Summarize accuracy, contact phases and repeated cost for isolated variants.""" +import argparse +import json +from pathlib import Path +import statistics + +import numpy as np + + +def read(path):return json.loads(path.read_bytes()) + + +def main(): + parser=argparse.ArgumentParser(description=__doc__) + parser.add_argument('directory',type=Path) + args=parser.parse_args();out=args.directory + runs=read(out/'summary.json');rows=[];branches=[];groups=[] + for profile,variants in runs.items(): + baseline=variants['off'] + base_force=baseline['groups']['force']['worstAbsolute']['maxAbsoluteError'] + base_time=baseline['timing']['medianSeconds'] + base_curves=np.load(out/profile/'off/curves.npz') + for mode,r in variants.items(): + data=np.load(out/profile/mode/'curves.npz') + contacts=[] + for key in data.files: + if key.startswith('platform|') and key.endswith('.gap'): + expected='amesim|'+key.split('|',1)[1] + mismatch=data['phaseMatched'] & ((data[key]<0)!=(data[expected]<0)) + contacts.extend(dict(key=key.split('|',1)[1],time=float(data['time'][i])) for i in np.flatnonzero(mismatch)) + equal=all(np.array_equal(data[k],base_curves[k],equal_nan=True) + for k in data.files if k.startswith('platform|')) + worst=r['groups']['force']['worstAbsolute'];timing=r['timing'];native=r['nativeRun'] + report=read(out/profile/mode/'comparison.json') + branches.extend(dict(profile=profile,mode=mode,**c) for c in report['curves'] if c['quantity']=='force') + groups.extend(dict(profile=profile,mode=mode,quantity=q,**g) for q,g in r['groups'].items()) + counter_keys=('nfev','acceptedSteps','rejectedSteps','solverStarts','stateTransitions','njev','nlu','experimentalEvents') + counters=[{k:t.get(k) for k in counter_keys} for t in timing['runs']] + row=dict(profile=profile,mode=mode,medianSolveSeconds=timing['medianSeconds'], + minimumSolveSeconds=timing['minimumSeconds'],maximumSolveSeconds=timing['maximumSeconds'], + timingRepeats=timing['repeats'],repeatCountersStable=all(c==counters[0] for c in counters), + medianCpuSeconds=statistics.median(t['solveCpuSeconds'] for t in timing['runs']), + solveChangePercent=100*(timing['medianSeconds']/base_time-1), + forceMaxError=worst['maxAbsoluteError'],forceWorstTime=worst['worstTime'],forceWorstKey=worst['key'], + forceErrorChangePercent=100*(worst['maxAbsoluteError']/base_force-1), + forceQuietMax=max(c['quietMaxAbsolute'] for c in report['curves'] if c['quantity']=='force'), + pressureMaxError=r['groups']['pressure']['worstAbsolute']['maxAbsoluteError'], + temperatureMaxError=r['groups']['temperature']['worstAbsolute']['maxAbsoluteError'], + above5PercentCount=r['above5PercentCount'],phaseUnpairedCount=r['phaseUnpairedGridCount'], + contactPhaseMismatches=contacts,gridTrajectoryEqualsOff=equal, + rawPeakContactForce=r['extraEventPointMaxContactForce'], + acceptedSteps=native['acceptedSteps'],rejectedSteps=native['rejectedSteps'], + nfev=native['nfev'],solverStarts=native['solverStarts'],njev=native['njev'],nlu=native['nlu'], + stateTransitions=native['stateTransitions'], + experimentalEvents=native.get('experimentalEvents',{}),jacobianMode=native['jacobianMode']) + rows.append(row) + print(json.dumps(row,ensure_ascii=False)) + (out/'effect-summary.json').write_text(json.dumps(rows,ensure_ascii=False,indent=2)+'\n',encoding='utf-8') + (out/'branch-force-summary.json').write_text(json.dumps(branches,ensure_ascii=False,indent=2)+'\n',encoding='utf-8') + (out/'group-summary.json').write_text(json.dumps(groups,ensure_ascii=False,indent=2)+'\n',encoding='utf-8') + + +if __name__=='__main__':main() diff --git a/tests/manual/summarize_output_semantics.py b/tests/manual/summarize_output_semantics.py new file mode 100644 index 0000000..8510890 --- /dev/null +++ b/tests/manual/summarize_output_semantics.py @@ -0,0 +1,71 @@ +"""Audit the controlled output-only eight-branch experiment without hiding raw errors.""" +import argparse +import json +from pathlib import Path + +import numpy as np + + +def load(path): + return json.loads(path.read_bytes()) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('directory', type=Path) + args = parser.parse_args() + out, result = args.directory, {} + for name in ('full', 'noncyclic'): + before, after = (out / stage / name for stage in ('before', 'after')) + a, b = (load(p/'native/result.json') for p in (before, after)) + at, bt = (np.asarray(r['series']['time']) for r in (a, b)) + indices = np.searchsorted(bt, at) + assert np.array_equal(bt[indices], at), 'Every old sample must remain at its real timestamp' + differences = [] + for key, values in a['series'].items(): + old, new = np.asarray(values), np.asarray(b['series'][key])[indices] + if not np.array_equal(old, new): + differences.append(dict(key=key, maxAbsoluteDifference=float(np.max(np.abs(new-old))))) + counters = ('acceptedSteps', 'rejectedSteps', 'nfev', 'njev', 'nlu', 'solverStarts', 'stateTransitions') + counter_pairs = {key: [a[key], b[key]] for key in counters} + paired = np.load(after/'curves.npz') + contact_phase = {} + for key in paired.files: + if key.startswith('platform|') and key.endswith('.gap'): + other = 'amesim|' + key.split('|', 1)[1] + mismatch = paired['phaseMatched'] & ((paired[key]<0) != (paired[other]<0)) + contact_phase[key.split('|', 1)[1]] = dict(mismatchCount=int(mismatch.sum()), + times=paired['time'][mismatch].tolist()) + old_metrics = load(before/'raw-time-comparison.json') + new_raw = load(after/'raw-time-comparison.json') + aligned = load(after/'comparison.json') + quantities = {} + for quantity in ('signal', 'force', 'pressure', 'temperature', 'mass_flow', 'enthalpy_flow', 'velocity'): + groups = [] + for rows in (old_metrics['curves'], new_raw['curves'], aligned['curves']): + rows = [r for r in rows if r['quantity'] == quantity] + worst = max(rows, key=lambda r: r['maxAbsoluteError']) + groups.append(dict(maxAbsoluteError=worst['maxAbsoluteError'], key=worst['key'], + time=worst['worstTime'], above5PercentCount=sum(r['above5PercentCount'] for r in rows))) + quantities[quantity] = dict(beforeRaw=groups[0], afterRaw=groups[1], afterSamePhase=groups[2]) + result[name] = dict(finalStateExactlyEqual=a['finalState']==b['finalState'], + solverCountersEqual=all(x==y for x,y in counter_pairs.values()), counters=counter_pairs, + originalSampleCount=len(at), newSampleCount=len(bt), extraSamples=len(bt)-len(at), + allOriginalSamplesPreserved=True, changedOriginalOutputs=differences, + solveSeconds=[a['solveSeconds'], b['solveSeconds']], + processWallSeconds=[load(before/'native-summary.json')['processWallSeconds'], + load(after/'native-summary.json')['processWallSeconds']], + phasePairing=load(after/'phase-pairing.json'), contactPhase=contact_phase, + quantities=quantities, + above5PercentCount=[old_metrics['above5PercentCount'], new_raw['above5PercentCount'], aligned['above5PercentCount']], + above5PercentOutsideEvents=aligned['above5PercentOutsideEvents'], + rawPeakContactForce=[load(before/'comparison.json')['extraEventPointMaxContactForce'], aligned['extraEventPointMaxContactForce']], + sourceVerification={s: load(out/s/'source-verification.json') for s in ('before','after')}) + (out/'effect-summary.json').write_text(json.dumps(result, ensure_ascii=False, indent=2, allow_nan=False)+'\n', encoding='utf-8') + for name, row in result.items(): + print(name, json.dumps({k:v for k,v in row.items() if k not in ('phasePairing','quantities','sourceVerification')}, ensure_ascii=False)) + print(json.dumps(row['quantities'], ensure_ascii=False)) + + +if __name__ == '__main__': + main() diff --git a/tests/manual/typed_contract_check.inc b/tests/manual/typed_contract_check.inc new file mode 100644 index 0000000..06ec7e1 --- /dev/null +++ b/tests/manual/typed_contract_check.inc @@ -0,0 +1,57 @@ +/* Audit only: every observed primitive access is accounted for. Values read + * from newly created entries are proved against the ordered writes, rather + * than dropping the consumed-field or valid-test conditions in production. */ +static void contract_failure(int event_index,const char *name){ + fprintf(stderr,"contract Jacobian=%llu event=%d %s\n",jac,event_index,name);fatal("typed effect schema"); +} +static void fp_contract_check(void){ + if(plan.error || fast.error || plan.path!=fast.path || plan.entry_count!=fast.entry_count || plan.jac!=fast.jacobian || plan.memo_binding!=(uintptr_t)fast.owner || memcmp(plan.inputs,fast.inputs,32) || memcmp(&plan.output,&fast.output,8) || plan.rounding!=fast.rounding || plan.required_flags!=fast.flags || plan.required_sse_flags!=fast.sse_flags || plan.sse_mode!=fast.sse_mode || plan.baseline_errno!=fast.saved_errno){ + fprintf(stderr,"generic error=%d fast error=%d\n",plan.error,fast.error);contract_failure(-1,"metadata"); + } + const Contract *schema=fast.path?schema1:schema0; + int expected_n=fast.path?132:12; + if(plan.n!=expected_n)contract_failure(-1,"length"); + NativePropertyState local[2],zero={0};NativePipeCache pipe={0};memset(local,0,sizeof(local)); + Pending pending; + if(fp_prepare(bound,bound_pipes,&pending))contract_failure(-1,"pending construction"); + int queries=0,scalars=0,allocations=0; + for(int i=0;islot; + if(e->type==READ_STATE || e->type==WRITE_STATE || e->type==WRITE_OR || e->type==VALID || e->type==ALLOCATE)slot-=(int)fast.entry_count; + if(e->type!=c->type || slot!=c->slot || e->offset!=c->offset || e->size!=c->size || e->aux!=c->aux || e->mask!=c->mask)contract_failure(i,"ordered access signature"); + if(e->type==QUERY){ + const double *v=queries++?fast.down:fast.up;double key[9]={v[0],v[1]};memcpy(key+2,fp_medium_key,56); + if(memcmp(e->data,key,72))contract_failure(i,"query key"); + }else if(e->type==ALLOCATE){ + if(slot!=allocations++)contract_failure(i,"append order"); + }else if(e->type==SCALAR){ + int k=scalars++;double key[9],value; + if(k<2){const double *v=k?fast.down:fast.up;memcpy(key,v,16);memcpy(key+2,fp_medium_key,56);value=v[2];} + else {memcpy(key,fast.pipe_key,24);value=fast.pipe_value;} + if(memcmp(e->data,key,e->size*8) || memcmp(&e->result,&value,8) || fp_hash(e->slot,e->offset,key,e->size)!=fast.hashes[k])contract_failure(i,"memo key/value"); + }else if(e->type==WRITE_STATE){ + unsigned char expected_data[sizeof(NativePropertyState)]={0}; + if(e->size==(int)sizeof(NativePropertyState))memcpy(expected_data,&zero,sizeof(zero)); + else if(e->offset==(int)offsetof(NativePropertyState,valid)){unsigned valid=NATIVE_PROPERTY_PT;memcpy(expected_data,&valid,sizeof(valid));} + else memcpy(expected_data,(char*)&pending.entries[slot]+e->offset,e->size); + if(memcmp(e->data,expected_data,e->size))contract_failure(i,"property store"); + memcpy((char*)&local[slot]+e->offset,expected_data,e->size); + }else if(e->type==WRITE_OR){ + local[slot].valid|=e->mask; + if(memcmp(e->data,&local[slot].valid,4))contract_failure(i,"valid update"); + }else if(e->type==READ_STATE){ + if(memcmp(e->data,(char*)&local[slot]+e->offset,e->size))contract_failure(i,"consumed field"); + }else if(e->type==VALID){ + unsigned value=local[slot].valid&e->mask; + if(memcmp(e->data,&value,4))contract_failure(i,"valid test"); + }else if(e->type==READ_PIPE){ + if(memcmp(e->data,(char*)&pipe+e->offset,e->size))contract_failure(i,"pipe read"); + }else if(e->type==WRITE_PIPE){ + if(e->offset==(int)offsetof(NativePipeCache,valid)){int valid=i==plan.n-1;pipe.valid=valid;} + else memcpy((char*)&pipe+e->offset,(char*)&pending.pipe+e->offset,e->size); + if(memcmp(e->data,(char*)&pipe+e->offset,e->size))contract_failure(i,"pipe store"); + }else contract_failure(i,"uncovered effect"); + } + if(memcmp(&pipe,&pending.pipe,sizeof(pipe)) || (fast.path && memcmp(local,pending.entries,sizeof(local))))contract_failure(-1,"final effects"); + contract_checks++; +} diff --git a/tests/manual/typed_oracle.inc b/tests/manual/typed_oracle.inc new file mode 100644 index 0000000..313ceda --- /dev/null +++ b/tests/manual/typed_oracle.inc @@ -0,0 +1,62 @@ +/* Audit only. Generic interpretation is an oracle, never a native Reference. */ +static SRContext oracle_generic,oracle_typed,negative_context; +static void check_oracle(NativePropertyCache *p,NativePipeCache *pipes,const double *x,int typed_reason,const Pending *patch){ + int saved_errno=errno;SREnvironment env;save_environment(&env); + NativePropertyCache *saved_bound=bound;bound=p;probe_entry_count=p->count; + from_live(&oracle_generic,p,pipes);from_live(&oracle_typed,p,pipes); + int mapping[SR_STATES],appends;phase=CANDIDATE; + int generic_reason=replay_overlay(&oracle_generic,x,mapping,&appends);phase=OFF; + if(generic_reason!=typed_reason){fprintf(stderr,"decision Jacobian=%llu generic=%s typed=%s\n",jac,reasons[generic_reason],reasons[typed_reason]);fatal("guard decisions differ");} + if(!generic_reason){ + /* replay_overlay already applied the generic effects in its copy. */ + Pending adjusted=*patch; + for(int i=0;ioutput,&plan.output,8))fatal("generic/typed patch differs"); + for(int i=0;icount+i))fatal("logical relocation differs"); + } + bound=saved_bound;restore_environment(&env);errno=saved_errno; +} +static void negative_test(int which,NativePropertyCache *live,NativePipeCache *pipes,const double *inputs){ + FastMetadata saved=fast;Plan *saved_plan=malloc(sizeof(plan));if(!saved_plan)fatal("negative metadata allocation");*saved_plan=plan; + int saved_errno=errno;SREnvironment env;save_environment(&env); + from_live(&negative_context,live,pipes);NativePropertyCache *p=&negative_context.context; + fast.owner=p->jacobian;plan.memo_binding=(uintptr_t)p->jacobian; + double x[4];memcpy(x,inputs,sizeof(x)); + switch(which){ + case 0:p->temperatures=&negative_context.observer;break; + case 1:p->capacity=p->count;break; + case 2:p->capacity=p->count+1;break; + case 3:negative_context.pipes[0].valid=1;break; + case 4:p->jacobian->recording=1;break; + case 5:memset(p->jacobian->entries,0,sizeof(negative_context.entries));break; + case 6:for(size_t i=0;ijacobian->capacity;i++)if(p->jacobian->entries[i].hash)p->jacobian->entries[i].value=nextafter(p->jacobian->entries[i].value,INFINITY);break; + case 7:x[0]=nextafter(x[0],INFINITY);break; + case 8:fast.output=NAN;plan.output=NAN;break; + case 9:fast.jacobian++;plan.jac++;break; + case 10:feclearexcept(FE_ALL_EXCEPT);break; + case 11:fesetround(fast.rounding==FE_DOWNWARD?FE_UPWARD:FE_DOWNWARD);break; + case 12: +#if defined(__SSE__) + _mm_setcsr(env.sse^0x2000u); +#endif + break; + case 13:if(p->count)p->states[0].jacobian=NULL;break; + case 14:if(p->count){p->states[0].medium=fp_medium;p->states[0].p=fast.up[0];p->states[0].T=fast.up[1];p->states[0].valid|=NATIVE_PROPERTY_PT;}break; + case 15:if(p->count){p->states[0].medium=fp_medium;p->states[0].p=fast.down[0];p->states[0].T=fast.down[1];p->states[0].valid|=NATIVE_PROPERTY_PT;}break; + case 16:fast.error=CONSUMED;plan.error=CONSUMED;break; + case 17:fast.error=VALID_BITS;plan.error=VALID_BITS;break; + case 18:fast.error=EXISTING_UPDATE;plan.error=EXISTING_UPDATE;break; + case 19:fast.error=UNKNOWN_EFFECT;plan.error=UNKNOWN_EFFECT;break; + } + static SRContext unchanged;memcpy(&unchanged,&negative_context,sizeof(unchanged)); + Pending patch;int reason=fp_validate(p,negative_context.pipes,x);if(!reason)reason=fp_prepare(p,negative_context.pipes,&patch); + if(!reason)fatal("negative test accepted"); + check_oracle(p,negative_context.pipes,x,reason,&patch); + if(memcmp(&unchanged,&negative_context,sizeof(unchanged)))fatal("negative validate modified context"); + negative_checks++;fast=saved;plan=*saved_plan;free(saved_plan);restore_environment(&env);errno=saved_errno; +} +static void fp_oracle(NativePropertyCache *p,NativePipeCache *pipes,const double *x,int reason,const Pending *pending){ + check_oracle(p,pipes,x,reason,pending);oracle_checks++; + if(jac==200 && !force_every)for(int i=0;i<20;i++)negative_test(i,p,pipes,x); +} diff --git a/tests/manual/typed_replay_core.inc b/tests/manual/typed_replay_core.inc new file mode 100644 index 0000000..ca23871 --- /dev/null +++ b/tests/manual/typed_replay_core.inc @@ -0,0 +1,184 @@ +/* Static effect contract: R288, group6, position16 only. No native calls. */ +static const NativeMedium fp_medium={1,2077.2643940499802,5193.1609851249505,293.15,0,1.96e-5,293.15,79.4}; +static const double fp_medium_key[]={2077.2643940499802,5193.1609851249505,293.15,0,1.96e-5,293.15,79.4}; +typedef struct { + unsigned long long jacobian; + NativeJacobianScalars *owner; + size_t entry_count; + double inputs[4],output; + double up[6]; /* p,T,rho,mu,isentropic_factor,isentropic_exponent */ + double down[5]; /* p,T,rho,isentropic_factor,isentropic_exponent */ + double pipe_key[3],pipe_value; + uint64_t hashes[3]; + int flags,rounding,saved_errno,error,ready,path; + unsigned sse_flags,sse_mode; +} FastMetadata; +typedef struct { + double queries[2][2],density_keys[2][2],density_values[2]; + int queries_count,allocations,scalars,values; +} Capture; +typedef struct { + NativePropertyState entries[2]; + NativePipeCache pipe; + size_t count; + unsigned long reuse[3]; + double output; + int append_count; +} Pending; +static FastMetadata fast; +static Capture capture; + +static uint64_t fp_hash(int kind,int medium,const double *keys,size_t n){ + uint64_t h=UINT64_C(14695981039346656037)^(unsigned)kind; + h=(h^(unsigned)medium)*UINT64_C(1099511628211); + for(size_t i=0;i>32;} + return h?h:1; +} +static int fp_medium_equal(const NativeMedium *m){ + return m->real_helium==fp_medium.real_helium && m->R==fp_medium.R && m->cp==fp_medium.cp && + m->Tref==fp_medium.Tref && m->slope==fp_medium.slope && m->mu==fp_medium.mu && m->muT==fp_medium.muT && m->S==fp_medium.S; +} +void fp_unknown(void){fast.error=NONFINITE;} +static void fp_begin(NativePropertyCache *p,NativePipeCache *pipes,const double *x){ + memset(&fast,0,sizeof(fast));memset(&capture,0,sizeof(capture)); + fast.jacobian=jac;fast.owner=p->jacobian;fast.entry_count=p->count;memcpy(fast.inputs,x,32); + fast.rounding=fegetround();fast.sse_mode=sse_control()&~63u; + if(p->temperatures)fast.error=OBSERVER; + if(pipes[0].valid)fast.error=PIPE_BRANCH; + if(!p->jacobian || p->count>p->capacity || p->capacity>SR_STATES)fast.error=CAPACITY; +} +void fp_query(NativePropertyCache *p,const NativeMedium *m,double pressure,double temperature,NativePropertyState *hit){ + int i=capture.queries_count++; + if(p!=bound || i>=2 || hit || !fp_medium_equal(m)){fast.error=QUERY_PATH;return;} + if(!isfinite(pressure) || !isfinite(temperature)){fast.error=NONFINITE;return;} + capture.queries[i][0]=pressure;capture.queries[i][1]=temperature; +} +void fp_allocate(NativePropertyCache *p,NativePropertyState *s,int valid){ + int i=capture.allocations++; + if(i>=2 || !valid || p!=bound || capture.queries_count!=i+1 || p->count!=fast.entry_count+(size_t)i+1 || s!=p->states+fast.entry_count+i)fast.error=CAPACITY; +} +void fp_scalar_key(int kind,int medium,const double *keys,size_t n){ + int i=capture.scalars++; + if(i>=3 || (i<2?(kind!=NATIVE_JACOBIAN_DENSITY || medium!=1 || n!=9):(kind!=NATIVE_JACOBIAN_PIPE || medium!=0 || n!=3))){fast.error=UNKNOWN_EFFECT;return;} + for(size_t k=0;k=3 || capture.values!=i || (i<2?(kind!=NATIVE_JACOBIAN_DENSITY || medium!=1 || n!=9):(kind!=NATIVE_JACOBIAN_PIPE || medium!=0 || n!=3))){fast.error=UNKNOWN_EFFECT;return;} + if((i<2 && (memcmp(keys,capture.density_keys[i],16) || memcmp(keys+2,fp_medium_key,56))) || (i==2 && memcmp(keys,fast.pipe_key,24)))fast.error=UNKNOWN_EFFECT; + if(!isfinite(value))fast.error=NONFINITE; + if(i<2)capture.density_values[i]=value;else fast.pipe_value=value; + capture.values++; +} +static void fp_end(double output){ + fast.flags=fetestexcept(FE_ALL_EXCEPT);fast.sse_flags=sse_control()&63u;fast.saved_errno=errno;fast.output=output; + if(!isfinite(output))fast.error=NONFINITE; + if(capture.queries_count==0 && capture.allocations==0 && capture.scalars==0 && capture.values==0 && bound->count==fast.entry_count)fast.path=0; + else if(capture.queries_count==2 && capture.allocations==2 && capture.scalars==3 && capture.values==3 && bound->count==fast.entry_count+2){ + fast.path=1;const NativePropertyState *u=&bound->states[fast.entry_count],*d=u+1; + if(u->valid!=29 || d->valid!=21 || u->temperatures || d->temperatures || u->jacobian!=fast.owner || d->jacobian!=fast.owner || !fp_medium_equal(&u->medium) || !fp_medium_equal(&d->medium))fast.error=UNKNOWN_EFFECT; + if(memcmp(&u->p,capture.queries[0],16) || memcmp(&d->p,capture.queries[1],16) || memcmp(&u->p,capture.density_keys[0],16) || memcmp(&d->p,capture.density_keys[1],16) || memcmp(&u->rho,&capture.density_values[0],8) || memcmp(&d->rho,&capture.density_values[1],8))fast.error=CONSUMED; + const double zero=0; + if(memcmp(&u->h,&zero,8) || memcmp(&d->h,&zero,8) || memcmp(&d->mu,&zero,8))fast.error=UNKNOWN_EFFECT; + double up[]={u->p,u->T,u->rho,u->mu,u->isentropic_factor,u->isentropic_exponent}; + double down[]={d->p,d->T,d->rho,d->isentropic_factor,d->isentropic_exponent}; + memcpy(fast.up,up,sizeof(up));memcpy(fast.down,down,sizeof(down)); + for(int i=0;i<6;i++)if(!isfinite(up[i]))fast.error=NONFINITE; + for(int i=0;i<5;i++)if(!isfinite(down[i]))fast.error=NONFINITE; + }else fast.error=NONFINITE; + const NativePipeCache *pipe=&bound_pipes[0]; + const double diameter=.014,length=1,roughness=.0032142857142857142; + if(pipe->valid!=1 || pipe->kind!=1 || !fp_medium_equal(&pipe->medium) || memcmp(&pipe->p1,&fast.inputs[3],8) || memcmp(&pipe->p2,&fast.inputs[1],8) || memcmp(&pipe->T,&fast.inputs[0],8) || memcmp(&pipe->diameter,&diameter,8) || memcmp(&pipe->length,&length,8) || memcmp(&pipe->roughness,&roughness,8) || memcmp(&pipe->flow,&output,8))fast.error=UNKNOWN_EFFECT; + fast.ready=1; +} + +/* Exact ordered PT matching against the live prefix. The second query also + * sees the first virtual append; it may not silently ignore it. */ +static int fp_match(const NativePropertyState *s,double p,double T){ + return (s->valid&NATIVE_PROPERTY_PT) && s->p==p && s->T==T && fp_medium_equal(&s->medium); +} +static int fp_query_miss(const NativePropertyCache *p,double pressure,double temperature){ + for(size_t i=0;icount;i++)if(fp_match(&p->states[i],pressure,temperature))return 0; + return 1; +} +static int fp_memo(const NativeJacobianScalars *m,int index){ + size_t capacity=m->capacity; + if(!capacity || capacity>MODEL_JACOBIAN_SCALAR_COUNT || (capacity&(capacity-1)) || m->recording || !m->entries)return MEMO_BINDING; + int kind=index<2?NATIVE_JACOBIAN_DENSITY:NATIVE_JACOBIAN_PIPE,medium=index<2?1:0; + size_t n=index<2?9:3,limit=capacity<32?capacity:32; + const double *key=index==0?fast.up:index==1?fast.down:fast.pipe_key; + double value=index==0?fast.up[2]:index==1?fast.down[2]:fast.pipe_value; + uint64_t hash=fast.hashes[index]; + for(size_t i=0;ientries[(hash+i)&(capacity-1)]; + if(!e->hash)return MEMO_MISS; + if(e->hash==hash && e->kind==kind && e->medium_kind==medium && e->input_count==n && + (index<2?(!memcmp(e->inputs,key,16) && !memcmp(e->inputs+2,fp_medium_key,56)):!memcmp(e->inputs,key,24))){ + if(!isfinite(e->value) || memcmp(&e->value,&value,8))return MEMO_VALUE; + return OK; + } + } + return MEMO_MISS; +} +static int fp_validate(NativePropertyCache *p,NativePipeCache *pipes,const double *inputs){ + if(!fast.ready || fast.jacobian!=jac)return NO_RECORD; + if(fast.error)return fast.error; + if(fegetround()!=fast.rounding || (fetestexcept(FE_ALL_EXCEPT)&fast.flags)!=fast.flags || + (fast.saved_errno && errno!=fast.saved_errno) || (sse_control()&~63u)!=fast.sse_mode || + (sse_control()&fast.sse_flags)!=fast.sse_flags)return UNKNOWN_EFFECT; + if(memcmp(inputs,fast.inputs,32))return INPUTS; + for(int i=0;i<4;i++)if(!isfinite(inputs[i]))return NONFINITE; + if(p->temperatures)return OBSERVER; + if(p->capacity>SR_STATES || p->count>p->capacity)return CAPACITY; + if(!p->jacobian || p->jacobian!=fast.owner || !p->jacobian->entries || p->jacobian->recording || p->jacobian->capacity>MODEL_JACOBIAN_SCALAR_COUNT)return MEMO_BINDING; + for(size_t i=0;icount;i++)if(p->states[i].temperatures || p->states[i].jacobian!=p->jacobian)return MEMO_BINDING; + if(pipes[0].valid)return PIPE_BRANCH; + if(fast.path==1){ + if(!fp_query_miss(p,fast.up[0],fast.up[1]))return QUERY_PATH; + if(p->count>=p->capacity)return CAPACITY; + int reason=fp_memo(p->jacobian,0);if(reason)return reason; + if(!fp_query_miss(p,fast.down[0],fast.down[1]))return QUERY_PATH; + /* The virtual U entry already has PT at the time query D runs. */ + if(fast.up[0]==fast.down[0] && fast.up[1]==fast.down[1])return QUERY_PATH; + if(p->count+1>=p->capacity)return CAPACITY; + reason=fp_memo(p->jacobian,1);if(reason)return reason; + reason=fp_memo(p->jacobian,2);if(reason)return reason; + }else if(fast.path!=0)return NONFINITE; + return OK; +} +static int fp_prepare(NativePropertyCache *p,NativePipeCache *pipes,Pending *pending){ + pending->append_count=fast.path?2:0;pending->count=p->count+(size_t)pending->append_count; + pending->reuse[0]=0;pending->reuse[1]=fast.path?2:0;pending->reuse[2]=fast.path?1:0; + if(fast.path){ + NativePropertyState *u=&pending->entries[0],*d=&pending->entries[1]; + memset(pending->entries,0,sizeof(pending->entries)); + u->medium=fp_medium;u->p=fast.up[0];u->T=fast.up[1];u->rho=fast.up[2];u->mu=fast.up[3];u->isentropic_factor=fast.up[4];u->isentropic_exponent=fast.up[5];u->valid=29;u->jacobian=p->jacobian; + d->medium=fp_medium;d->p=fast.down[0];d->T=fast.down[1];d->rho=fast.down[2];d->isentropic_factor=fast.down[3];d->isentropic_exponent=fast.down[4];d->valid=21;d->jacobian=p->jacobian; + } + /* Copy ONE pipe so even its unwritten padding is preserved. */ + pending->pipe=pipes[0];pending->pipe.medium=fp_medium; + pending->pipe.p1=fast.inputs[3];pending->pipe.p2=fast.inputs[1];pending->pipe.T=fast.inputs[0]; + pending->pipe.diameter=.014;pending->pipe.length=1;pending->pipe.roughness=.0032142857142857142;pending->pipe.kind=1; + pending->pipe.flow=fast.output;pending->pipe.valid=1;pending->output=fast.output; + if(!isfinite(fast.output))return NONFINITE; + return OK; +} +static void fp_commit(NativePropertyCache *p,NativePipeCache *pipes,const Pending *pending){ + if(pending->append_count){ + memcpy(&p->states[p->count],pending->entries,sizeof(pending->entries));p->count=pending->count; + p->jacobian->reuses[1]+=pending->reuse[1];p->jacobian->reuses[2]+=pending->reuse[2]; + } + pipes[0].valid=0;pipes[0]=pending->pipe; +} + +#if RR_AUDIT +/* Full generic guard and event contracts exist only in correctness builds. */ +static unsigned long long contract_checks,oracle_checks,negative_checks; +static void fp_contract_check(void); +static void fp_oracle(NativePropertyCache*,NativePipeCache*,const double*,int,const Pending*); +#endif diff --git a/tests/manual/typed_replay_hooks.h b/tests/manual/typed_replay_hooks.h new file mode 100644 index 0000000..3c68221 --- /dev/null +++ b/tests/manual/typed_replay_hooks.h @@ -0,0 +1,6 @@ +/* Only the separately named baseline recording kernels call these hooks. */ +void fp_unknown(void); +void fp_query(NativePropertyCache*,const NativeMedium*,double,double,NativePropertyState*); +void fp_allocate(NativePropertyCache*,NativePropertyState*,int); +void fp_scalar_key(int,int,const double*,size_t); +void fp_scalar_value(int,int,const double*,size_t,double); diff --git a/tests/manual/typed_replay_runtime.inc b/tests/manual/typed_replay_runtime.inc new file mode 100644 index 0000000..0c43761 --- /dev/null +++ b/tests/manual/typed_replay_runtime.inc @@ -0,0 +1,40 @@ +double rr_execute(NativePropertyCache *p,NativePipeCache *pipes,const double *x,SROperation original){ + visits++; +#if RR_AUDIT + audit_context(1,0,NULL,NULL,NULL,1,0,p,pipes,NULL);entry_audits++; + from_live(&before_reject,p,pipes); +#endif + uint64_t start=lp_tick(),tick;double output;Pending pending;size_t initial_count=p->count; + attempts++;rr_replaying=1;count_different+=(p->count!=fast.entry_count); + double saved_output=fast.output; +#if RR_AUDIT + double old_plan_output=plan.output; + if(force_every && (attempts-1)%(unsigned)force_every==0){fast.output=NAN;plan.output=NAN;} +#endif + tick=lp_tick();int reason=fp_validate(p,pipes,x);validation_ticks+=lp_tick()-tick; + if(!reason){tick=lp_tick();reason=fp_prepare(p,pipes,&pending);overlay_ticks+=lp_tick()-tick;} +#if RR_AUDIT + fp_oracle(p,pipes,x,reason,&pending); + plan.output=old_plan_output; +#endif + fast.output=saved_output; + if(!reason){ + tick=lp_tick();fp_commit(p,pipes,&pending);output=pending.output;commit_ticks+=lp_tick()-tick; + commits++;successes++;appends_total+=pending.append_count;paths[fast.path]++; + relocations+=(fast.path && fast.entry_count!=initial_count); + metadata_min=metadata_max=sizeof(fast); + }else{ + rejects++;reject_reason[reason]++;rr_replaying=0; +#if RR_AUDIT + from_live(&after_reject,p,pipes); + if(compare_contexts(&before_reject,&after_reject))fatal("reject altered live context"); + rollback_checks++; +#endif + tick=lp_tick();rr_original_scope=1;output=original(p,pipes,x);rr_original_scope=0;fallback_ticks+=lp_tick()-tick;fallbacks++; + } + rr_replaying=0;path_ticks+=lp_tick()-start; +#if RR_AUDIT + audit_context(2,0,NULL,NULL,NULL,1,output,p,pipes,NULL);exit_audits++; +#endif + return output; +} diff --git a/tests/manual/valve_tail_diag.c b/tests/manual/valve_tail_diag.c new file mode 100644 index 0000000..9ad281f --- /dev/null +++ b/tests/manual/valve_tail_diag.c @@ -0,0 +1,63 @@ +/* Diagnostic worker only: original tail executes on every visit. */ +#include "valve_tail_diag.h" +#include +#include +#include +#include +#include +#include +#include +int kd_scope; +uint64_t kd_jac; +KdMemo kd_memo; +static KdRecord records[26000]; +static size_t count; +static uint64_t entries,hits,executions,lookup_ticks,capture_ticks,reset_ticks,tail_ticks; +static uint64_t base_lookup_ticks,miss_lookup_ticks,hit_lookup_ticks; +static uint64_t qpc_frequency,tsc_start,qpc_start; +static FILE *matrix_file; +static int disabled; +static void fatal(const char *s){fprintf(stderr,"valve tail: %s\n",s);abort();} +void kd_eval_exit(double t,const double*y,const double*dy,const double*w,int r,NativePropertyCache*p,NativePipeCache*c,ModelJacobianWorkspace*m){(void)t;(void)y;(void)dy;(void)w;(void)r;(void)p;(void)c;(void)m;} +void kd_start(void){ + unsigned a,b,c,d;if(!__get_cpuid(0x80000007,&a,&b,&c,&d) || !(d&(1u<<8)))fatal("invariant TSC required"); + LARGE_INTEGER f;QueryPerformanceFrequency(&f);qpc_frequency=(uint64_t)f.QuadPart; + qpc_start=lp_tick();tsc_start=kd_clock();disabled=getenv("KD_DISABLED")!=NULL; + if(getenv("KD_MATRICES")){matrix_file=fopen("jacobians.bin","wb");if(!matrix_file)fatal("matrix file");setvbuf(matrix_file,NULL,_IOFBF,1024*1024);} +} +void kd_new_jac(void){uint64_t t=kd_clock();kd_memo.ready=0;reset_ticks+=kd_clock()-t;kd_jac++;} +void kd_before(KdRecord *r,double p,double T,double pd,double g,double rho){ + double key[5]={p,T,pd,g,rho};memcpy(r->key,key,40);r->before=kd_env(); + r->hit=lp_color>=0 && !disabled && kd_lookup(r->key,r->before,&r->cm,&r->vel); + r->jac=kd_jac;r->group=lp_color; +} +void kd_after(KdRecord *r,double cm,double vel,uint64_t elapsed,int executed){ + uint64_t capture_start=lp_color<0?kd_clock():0; + r->after=kd_env();r->cm=cm;r->vel=vel;r->tail=elapsed;r->executed=executed;r->capture=0;r->pad=0; + if(lp_color<0){ + memcpy(kd_memo.key,r->key,40);kd_memo.cm=cm;kd_memo.vel=vel;kd_memo.env=r->before; + kd_memo.ready=kd_finite(r->key) && isfinite(cm) && isfinite(vel) && kd_env_equal(r->before,r->after) && (r->before.cw&63)==63 && (r->before.mxcsr&0x1f80)==0x1f80; + r->capture=kd_clock()-capture_start;capture_ticks+=r->capture; + } + entries++;lookup_ticks+=r->lookup;if(lp_color<0)base_lookup_ticks+=r->lookup;else if(r->hit)hit_lookup_ticks+=r->lookup;else miss_lookup_ticks+=r->lookup; + hits+=r->hit;executions+=executed;tail_ticks+=elapsed; +#if KD_TRACE + if(count>=sizeof(records)/sizeof(*records)){fatal("record capacity");} + records[count++]=*r; +#endif +} +void kd_matrix(double t,const double*y,const double*m){ + if(!matrix_file){return;} + int saved=errno;fenv_t env;fegetenv(&env);unsigned sse=_mm_getcsr(); + fwrite(&t,8,1,matrix_file);fwrite(y,8,NSTATES,matrix_file);fwrite(m,8,NSTATES*NSTATES,matrix_file); + fesetenv(&env);_mm_setcsr(sse);errno=saved; +} +void kd_finish(void){ + uint64_t t=kd_clock(),q=lp_tick();double frequency=(double)(t-tsc_start)/(q-qpc_start)*qpc_frequency; + uint64_t empty[10001];for(int i=0;i<10001;i++){uint64_t a=kd_clock();empty[i]=kd_clock()-a;} + FILE *f=fopen("tail-records.bin","wb");if(!f)fatal("records");fwrite(records,sizeof(KdRecord),count,f);fclose(f); + f=fopen("empty-clock.bin","wb");fwrite(empty,8,10001,f);fclose(f); + f=fopen("tail.json","wb");if(!f)fatal("summary"); + fprintf(f,"{\"frequency\":%.9f,\"recordBytes\":%llu,\"memoBytes\":%llu,\"jacobians\":%llu,\"entries\":%llu,\"hits\":%llu,\"executions\":%llu,\"lookupTicks\":%llu,\"hitLookupTicks\":%llu,\"missLookupTicks\":%llu,\"baseLookupTicks\":%llu,\"captureTicks\":%llu,\"resetTicks\":%llu,\"tailTicks\":%llu,\"skipEnabled\":%d,\"disabled\":%d}\n",frequency,(unsigned long long)sizeof(KdRecord),(unsigned long long)sizeof(KdMemo),(unsigned long long)kd_jac,(unsigned long long)entries,(unsigned long long)hits,(unsigned long long)executions,(unsigned long long)lookup_ticks,(unsigned long long)hit_lookup_ticks,(unsigned long long)miss_lookup_ticks,(unsigned long long)base_lookup_ticks,(unsigned long long)capture_ticks,(unsigned long long)reset_ticks,(unsigned long long)tail_ticks,0,disabled);fclose(f); + if(matrix_file)fclose(matrix_file); +} diff --git a/tests/manual/valve_tail_diag.h b/tests/manual/valve_tail_diag.h new file mode 100644 index 0000000..4d166cb --- /dev/null +++ b/tests/manual/valve_tail_diag.h @@ -0,0 +1,35 @@ +#ifndef VALVE_TAIL_DIAG_H +#define VALVE_TAIL_DIAG_H +#ifndef _WIN32_WINNT +#define _WIN32_WINNT 0x0600 +#endif +#include "local_probe.h" +#include +#include +#include +#include +#ifndef KD_TRACE +#define KD_TRACE 1 +#endif +typedef struct {unsigned mxcsr;unsigned short cw,sw;int err;} KdEnv; +typedef struct {uint64_t key[5];double cm,vel;KdEnv env;int ready;} KdMemo; +typedef struct {uint64_t jac;int group,hit;uint64_t key[5];double cm,vel;KdEnv before,after;uint64_t tail,lookup,capture;int executed,pad;} KdRecord; +extern int kd_scope; +extern uint64_t kd_jac; +extern KdMemo kd_memo; +static inline uint64_t kd_clock(void){unsigned lo,hi;__asm__ __volatile__("lfence\n\trdtsc\n\tlfence":"=a"(lo),"=d"(hi)::"memory");return ((uint64_t)hi<<32)|lo;} +static inline KdEnv kd_env(void){KdEnv e;__asm__ __volatile__("stmxcsr %0;fnstcw %1;fnstsw %2":"=m"(e.mxcsr),"=m"(e.cw),"=m"(e.sw)::"memory");e.err=errno;return e;} +static inline int kd_env_equal(KdEnv a,KdEnv b){return a.mxcsr==b.mxcsr && a.cw==b.cw && a.sw==b.sw && a.err==b.err;} +static inline int kd_finite(const uint64_t *key){for(int i=0;i<5;i++)if((key[i]&UINT64_C(0x7ff0000000000000))==UINT64_C(0x7ff0000000000000))return 0;return 1;} +/* Diagnostic-only lookup: its answer NEVER bypasses the original tail. + * Only a baseline entry is considered. Probe misses never populate the slot. */ +static inline int kd_lookup(const uint64_t *key,KdEnv e,double *cm,double *vel){ + if(!kd_memo.ready || !kd_env_equal(e,kd_memo.env) || memcmp(key,kd_memo.key,40))return 0; + *cm=kd_memo.cm;*vel=kd_memo.vel;return 1; +} +void kd_start(void);void kd_finish(void);void kd_new_jac(void); +void kd_before(KdRecord*,double,double,double,double,double); +void kd_after(KdRecord*,double,double,uint64_t,int); +void kd_matrix(double,const double*,const double*); +void kd_eval_exit(double,const double*,const double*,const double*,int,NativePropertyCache*,NativePipeCache*,ModelJacobianWorkspace*); +#endif diff --git a/tests/manual/verify_context_access_evidence.py b/tests/manual/verify_context_access_evidence.py new file mode 100644 index 0000000..9acf9f3 --- /dev/null +++ b/tests/manual/verify_context_access_evidence.py @@ -0,0 +1,56 @@ +"""Negative checks of the evidence validator (not a production reuse guard).""" +from copy import deepcopy +import json +import analyze_context_access as ax + + +def rejected(events): + try: + ax.analyze_operation(events) + except AssertionError: + return True + return False + + +def main(): + examples=json.loads((ax.OUT/'jacobian-200.json').read_text(encoding='utf-8')) + original=next(x['events'] for x in examples if x['summary']['group']==6) + assert ax.analyze_operation(original)['replayExact'] + results={} + + events=deepcopy(original) + index=next(i for i,e in enumerate(events) if e['event']=='write' and e.get('domain')=='pipes' and e.get('field')=='cache->valid') + del events[index] + results['omitted_equal_value_pipe_valid_store']=rejected(events) + + events=deepcopy(original) + event=next(e for e in events if e['event']=='read' and e.get('field')=='s->rho') + event['value']='0000000000000000' + results['wrong_consumed_rho']=rejected(events) + + events=deepcopy(original) + event=next(e for e in events if e['event']=='allocate') + event['slot']-=1 + results['baseline_slot_used_for_shifted_append']=rejected(events) + + events=deepcopy(original) + event=next(e for e in events if e['event']=='write' and e.get('field')=='cache->count') + event['value']=(13).to_bytes(8,'little').hex() + results['restored_baseline_count']=rejected(events) + + events=deepcopy(original) + event=next(e for e in events if e['event']=='match') + event.update(hit=True,slot=0) + results['wrong_first_match']=rejected(events) + + events=deepcopy(original) + event=next(e for e in events if e['event']=='valid_test' and e['mask']==8) + event['value']=8 + results['wrong_valid_bit_result']=rejected(events) + + assert all(results.values()), results + (ax.OUT/'negative-checks.json').write_text(json.dumps(results,indent=2)+'\n',encoding='utf-8') + print(json.dumps(results,indent=2)) + + +if __name__=='__main__':main() diff --git a/tests/test_local_probe_experiment.py b/tests/test_local_probe_experiment.py new file mode 100644 index 0000000..98363c6 --- /dev/null +++ b/tests/test_local_probe_experiment.py @@ -0,0 +1,45 @@ +"""Conservative-graph contracts of the opt-in manual local-probe experiment.""" +import importlib.util +from pathlib import Path +import unittest +from types import SimpleNamespace +from app.simulation.native_codegen.jacobian import StateDependencies + +spec=importlib.util.spec_from_file_location('local_probe_experiment',Path(__file__).parent/'manual/local_probe_experiment.py') +experiment=importlib.util.module_from_spec(spec) +spec.loader.exec_module(experiment) + +class LocalProbeDependenciesTests(unittest.TestCase): + def test_unknown_leaf_taints_every_state_and_transitive_consumer(self): + d=StateDependencies(3);d.assign('p[0]',{'unregistered_context'});d.assign('q[0]',{'p[0]'}) + masks,unknown=experiment.dependency_masks(d) + self.assertEqual(unknown,{'unregistered_context'}) + self.assertEqual(masks['q[0]'],0b111) + + def test_union_of_branch_writes_is_preserved(self): + d=StateDependencies(3);d.assign('w[0]',{'y[0]'});d.assign('w[0]',{'y[2]'}) + d.assign('q[0]',{'w[0]'}) + masks,_=experiment.dependency_masks(d) + self.assertEqual(masks['q[0]'],0b101) + + def test_projected_state_coupling_reaches_downstream_equations(self): + d=StateDependencies(3);d.project_states([0,1]);d.assign('p[0]',{'y[0]'}) + d.assign('dy[2]',{'p[0]'}) + masks,_=experiment.dependency_masks(d) + self.assertEqual(masks['dy[2]'],0b011) + + def test_dependency_cycle_conservatively_reaches_fixed_point(self): + d=StateDependencies(3);d.assign('p[0]',{'q[0]','y[0]'}) + d.assign('q[0]',{'p[0]','y[2]'}) + masks,_=experiment.dependency_masks(d) + self.assertEqual(masks['p[0]'],0b101) + self.assertEqual(masks['q[0]'],0b101) + + def test_experiment_refuses_to_slice_cyclic_schedule(self): + d=StateDependencies(2);d.expression('dy[0]','y[0]');d.expression('dy[1]','y[1]') + program=SimpleNamespace(manifest=lambda:{'jacobianStructure':{'reuse':{'enabled':True}}}) + saved={'dependencies':d,'schedule':SimpleNamespace(blocks=[SimpleNamespace(cyclic=True)])} + with self.assertRaisesRegex(ValueError,'rejects SCCs'): + experiment.generate(program,saved) + +if __name__=='__main__':unittest.main() diff --git a/tests/test_mechanical_event_experiment.py b/tests/test_mechanical_event_experiment.py new file mode 100644 index 0000000..c3552b8 --- /dev/null +++ b/tests/test_mechanical_event_experiment.py @@ -0,0 +1,246 @@ +"""Independent analytic and Amesim-reference checks for opt-in event variants.""" +import json +import math +import time +import subprocess +import sys +from pathlib import Path +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.extended import compile_extended_program +from app.simulation.native_codegen.runner import execute_native +from tests.manual.mechanical_event_variant import event_program, prepare_runtime +from tests.test_native_catalog import Circuit + +ROOT = Path(__file__).resolve().parents[1] +OUT = ROOT/'test/mechanical-events-20260917/isolated' +CONTROL = ROOT/'test/mechanical-events-20260917/source-control/native' +if not CONTROL.exists(): + CONTROL = ROOT/'native' + + +def free_ports(b, mass, connected=()): + for port in ('port_1','port_2'): + if port not in connected: + free=b.add('amesim_f000',mass.name+'_'+port+'_free') + b.connect(mass,port,free,'port_1') + + +def elastic_mass(damping=1, signed=1, pdis=0): + b=Circuit() + mass=b.add('amesim_mecmas21','mass',mass=1,useFriction=1,stoptype=2,x0=.5,v0=1, + xmin=0,xmax=1,Kbmin=100,Kbmax=100,Dbmin=damping,Dbmax=damping, + Pdmin=pdis,Pdmax=pdis,discContactOption=signed) + free_ports(b,mass) + return b + + +def contact_pair(damping=0,signed=1,pdis=0,x2=.2,v1=1,v2=0): + b=Circuit() + a=b.add('amesim_mecmas21','left',mass=1,useFriction=1,stoptype=4,x0=0,v0=v1, + fcoul=0,fstick=0,rvisc=0,wind=0,theta=0) + z=b.add('amesim_mecmas21','right',mass=1,useFriction=1,stoptype=4,x0=x2,v0=v2, + fcoul=0,fstick=0,rvisc=0,wind=0,theta=0) + c=b.add('amesim_lstp00a','contact',gap0=0,stiffmode=1,kcont=100,rcont=damping, + Pdis=pdis,discContactOption=signed) + b.connect(a,'port_2',c,'port_1');b.connect(z,'port_1',c,'port_2') + free_ports(b,a,('port_2',));free_ports(b,z,('port_1',)) + return b + + +class MechanicalEventExperimentTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + OUT.mkdir(parents=True,exist_ok=True) + cls.runtime=OUT/'experimental-native' + prepare_runtime(CONTROL,cls.runtime) + cls.run_tag=str(time.time_ns()) + + def run_case(self,b,mode,method,*,stop=1,max_step=.05,label='',compiler=compile_native_program,sample_step=.002): + original=compiler(b.net) + program,metadata=event_program(original,b.net,mode) + self.assertEqual(program.state_keys,original.state_keys) + self.assertEqual(program.jacobian_structure,original.jacobian_structure) + self.assertEqual(program.evaluation_schedule,original.evaluation_schedule) + self.assertTrue(program.source.startswith(original.source)) + directory=OUT/self._testMethodName/(mode+'-'+method+label)/self.run_tag + directory.mkdir(parents=True,exist_ok=True) + (directory/'event-descriptors.json').write_text(json.dumps(metadata,indent=2)+'\n',encoding='utf-8') + with patch.object(builder,'NATIVE',CONTROL if mode=='off' else self.runtime), \ + patch.object(builder,'CACHE',OUT/'build-cache'),patch.object(result_storage,'RESULT_ROOT',OUT/'results'): + build=builder.build_native(program) + try: + result=execute_native(build,SolveIVPConfig(t_stop=stop,method=method,max_step=max_step,rtol=1e-9), + sample_step,run_dir=directory,timeout=60) + finally: + build.close() + self.assertTrue(result['success'],result['message']) + self.assertEqual(result['simulatedUntil'],stop) + times=result['series']['time'] + self.assertTrue(all(a0 for e in clips)) + key='mass.Fmax' if label=='mass' else 'contact.force' + self.assertGreaterEqual(min(r['series'][key]),0) + if label=='mass': + expected=.5+(math.pi-math.atan(math.sqrt(3)))/math.sqrt(75) + self.assertAlmostEqual(clips[0]['time'],expected,delta=3e-6) + + def test_no_contact_initial_touch_and_continuous_contact_do_not_restart(self): + cases=[('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)] + for label,b,stop in cases: + a,_=self.run_case(b,'off','BDF',stop=stop,label=label) + z,events=self.run_case(b,'lstp','BDF',stop=stop,label=label) + self.assertEqual(events,[]) + self.assertEqual(a['finalState'],z['finalState']) + self.assertEqual(a['series'],z['series']) + self.assertEqual(a['solverStarts'],z['solverStarts']) + + def test_exponential_damping_modes_and_extended_lowering(self): + for signed in (1,2): + for mode,b in (('mass',elastic_mass(2,signed,.01)),('lstp',contact_pair(2,signed,.01))): + for method in ('BDF','RK45'): + r,events=self.run_case(b,mode,method,stop=1.1,label=str(signed),compiler=compile_extended_program) + self.assertGreaterEqual(len(events),2) + self.assertLess(len(events),15) + self.assertEqual(r['experimentalEvents']['releaseRhsCalls'],0) + + def test_existing_amesim_mechanical_reference_circuits(self): + cases=json.loads((ROOT/'tests/fixtures/amesim-component-semantics-2404.json').read_text(encoding='utf-8'))['cases'] + for case in cases: + if case['name'] not in ('friction_ideal','friction_elastic','joint_mechanical'):continue + 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) + modes=('mass',) if case['name']!='joint_mechanical' else ('lstp','all') + for mode in modes: + r,_=self.run_case(b,mode,'BDF',stop=3,max_step=.002,label=case['name'],sample_step=.001) + for key,expected in case['series'].items(): + actual=np.interp(case['time'],r['series']['time'],r['series'][key]) + field=key.split('.')[-1] + atol=.002 if field in ('a','Ffric','Fmin','Fmax','force') else 3e-6 + np.testing.assert_allclose(actual,expected,rtol=0,atol=atol,err_msg=case['name']+'/'+mode+'/'+key) + + def test_same_sign_gap_excursion_and_tangent_with_analytic_dense_output(self): + directory=OUT/self._testMethodName/self.run_tag;directory.mkdir(parents=True) + header='''#include "kernels.h" +#define NSTATES 2 +#define NOUTPUTS 1 +#define NSTOPS 0 +#define NEXPERIMENT_CONTACTS 1 +#define NEXPERIMENT_RELEASES 0 +typedef struct { int kind,v1,v2;double boundary,stiffness,damping,pdis;int signed_force; } ExperimentContact; +extern const ExperimentContact experiment_contacts[1]; +''' + (directory/'model.h').write_text(header,encoding='ascii') + harness=r''' +#include "runtime.h" +#include +#include +#include "experimental_events.c" +const ExperimentContact experiment_contacts[1]={{2,0,-1,0,100,1,0,1}}; +static int polynomial(void *context,double t,double *y) { + y[0]=1-2*t;y[1]=(*(int*)context?0:.01)-(t-.5)*(t-.5);return 1; +} +int main(void) { + for(int tangent=0;tangent<2;tangent++) { + NativeRun r={0};double a[2],b[2],when[8],t=0;int indices[8],kinds[8],count=0; + polynomial(&tangent,t,a);polynomial(&tangent,1,b); + if(!experiment_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);experiment_commit(&r,t,a,when,indices,kinds,count); + count=0; + if(!experiment_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; +} +''' + (directory/'check.c').write_text(harness,encoding='ascii') + cc,_,_=builder.toolchain();exe=directory/('check.exe' if sys.platform=='win32' else 'check') + builder._command([cc,*builder.COMPILER_FLAGS,'-I',str(directory),'-I',str(self.runtime/'include'), + '-I',str(self.runtime/'runtime'),str(directory/'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) + + +if __name__=='__main__':unittest.main() diff --git a/tests/test_native_contact_events.py b/tests/test_native_contact_events.py new file mode 100644 index 0000000..f08eb54 --- /dev/null +++ b/tests/test_native_contact_events.py @@ -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 +#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() diff --git a/tests/test_native_output_semantics.py b/tests/test_native_output_semantics.py new file mode 100644 index 0000000..903f9b8 --- /dev/null +++ b/tests/test_native_output_semantics.py @@ -0,0 +1,99 @@ +"""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() diff --git a/tests/test_native_signal_boundaries.py b/tests/test_native_signal_boundaries.py new file mode 100644 index 0000000..5d077d4 --- /dev/null +++ b/tests/test_native_signal_boundaries.py @@ -0,0 +1,188 @@ +"""Independent stage/one-ULP checks and isolated cyclic UD00 execution.""" +from __future__ import annotations + +from bisect import bisect_right +import math +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest +from unittest.mock import patch + +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.test_native_catalog import Circuit + +ROOT = Path(__file__).resolve().parents[1] +HARNESS = r''' +#include "kernels.h" +#include +#include +int main(void) { + char mode; double t,start,end,data[24]; int stages,cyclic,count; + while(scanf(" %c %lf %lf %lf %d %d %d",&mode,&t,&start,&end,&stages,&cyclic,&count)==7) { + for(int i=0;i<24;i++) if(scanf("%lf",&data[i])!=1) return 2; + if(mode=='V') { + printf("%.17g %.17g\n",native_signal(t,start,stages,cyclic,data), + native_signal_break(t,end,start,stages,cyclic,data)); + } else { + for(int i=0;it)) return 3; + t=b; + } + } + } + return 0; +} +''' + + +def stage_data(starts, ends, durations): + return [*starts, *([0.] * (8-len(starts))), *ends, *([0.] * (8-len(ends))), + *durations, *([0.] * (8-len(durations)))] + + +class NativeSignalBoundaryTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.tmp = tempfile.TemporaryDirectory(prefix='native-signal-boundaries-') + cls.addClassCleanup(cls.tmp.cleanup) + cls.root = Path(cls.tmp.name) + compiler, _, _ = builder.toolchain() + source = cls.root / 'probe.c' + source.write_text(HARNESS, encoding='ascii') + cls.exe = cls.root / ('probe.exe' if sys.platform == 'win32' else 'probe') + builder._command([compiler, *builder.COMPILER_FLAGS, '-I', str(ROOT / 'native/include'), + str(source), str(ROOT / 'native/components/modules/signal.c'), '-lm', '-o', str(cls.exe)], log=[]) + + def probe(self, data, stages, queries, *, start=0., cyclic=True, end=1e12, events=0): + lines = [' '.join(map(str, ['E' if events else 'V', t, start, end, stages, + int(cyclic), events or 1, *data])) for t in queries] + run = subprocess.run([str(self.exe)], input='\n'.join(lines)+'\n', + capture_output=True, text=True, timeout=15, check=True) + rows = [list(map(float, line.split())) for line in run.stdout.splitlines()] + self.assertEqual(len(rows), len(queries) * (events or 1)) + return rows + + def test_mql_cyclic_boundaries_and_adjacent_floats(self): + # Freeze the known event times independently of signal evaluation. + boundaries = [.8, 10.8, 11.600000000000001, 21.6, 22.400000000000002, + 32.400000000000006, 33.2, 43.2, 44., 54.] + data = stage_data([1e17, 49000.], [1e17, 49000.], [.8, 10.]) + queries = [t for boundary in boundaries for t in + (math.nextafter(boundary, -math.inf), boundary, math.nextafter(boundary, math.inf))] + rows = self.probe(data, 2, [*queries, 32.4]) + for i, boundary in enumerate(boundaries): + before, after = (1e17, 49000.) if i % 2 == 0 else (49000., 1e17) + left, at, right = rows[3*i:3*i+3] + with self.subTest(boundary=boundary): + self.assertEqual([left[0], at[0], right[0]], [before, after, after]) + self.assertEqual(left[1], boundary) + self.assertGreater(at[1], boundary) + if i+1 < len(boundaries): + self.assertEqual(at[1], boundaries[i+1]) + # A genuinely earlier sample must not be pulled onto the event. + self.assertEqual(rows[-1][0], 49000.) + + def test_multistage_long_cycle_sequence_has_no_skipped_or_duplicate_events(self): + data = stage_data(list(range(1, 9)), list(range(1, 9)), [.1, .2, .3, .4, .5, .6, .7, .8]) + rows = self.probe(data, 8, [0.], events=8000) + last = 0. + for i, (boundary, left, at, right) in enumerate(rows): + old, new = i % 8 + 1, (i+1) % 8 + 1 + self.assertGreater(boundary, last, (i, boundary)) + self.assertEqual((left, at, right), (old, new, new), (i, boundary)) + last = boundary + self.assertAlmostEqual(last, 3600., places=9) + + def test_offset_start_and_small_period_boundaries(self): + for start, scale in ((.37, 1.), (-1.25, 1.), (1e6, 1e-5)): + data = stage_data([1., 2., 3.], [1., 2., 3.], [.1*scale, .2*scale, .3*scale]) + rows = self.probe(data, 3, [start], start=start, events=900) + for i, (boundary, left, at, right) in enumerate(rows): + with self.subTest(start=start, scale=scale, event=i): + self.assertEqual((left, at, right), (i % 3 + 1, (i+1) % 3 + 1, (i+1) % 3 + 1), boundary) + + def test_ramps_delayed_start_and_noncyclic_final_value(self): + data = stage_data([0., 10., -10.], [2., 12., -7.], [.25, .5, .75]) + times = [0., 1.25, 1.375, 1.5, 1.75, 2., 2.375, 2.75, 2.875, 3.] + expected = [0., 0., 1., 10., 11., -10., -8.5, 0., 1., 10.] + actual = self.probe(data, 3, times, start=1.25) + self.assertEqual([row[0] for row in actual], expected) + once = self.probe(data, 3, [2.75, 5.], start=1.25, cyclic=False, end=10.) + self.assertEqual(once, [[-7., 10.], [-7., 10.]]) + # A simulation beginning after the signal's start can enter any cycle. + shifted = self.probe(data, 3, [-2., -1.875, -1.75, -.5], start=-2.) + self.assertEqual([row[0] for row in shifted], [0., 1., 10., 0.]) + + def test_zero_duration_stages_and_zero_total_duration_keep_existing_semantics(self): + data = stage_data([-100., 10., -300., 20., -500.], [-100., 10., -300., 20., -500.], + [0., .5, 0., .5, 0.]) + rows = self.probe(data, 5, [-1., 0., .5, 1., 1.5], end=10.) + self.assertEqual(rows, [[-100., 0.], [10., .5], [20., 1.], [10., 1.5], [20., 2.]]) + self.assertEqual(self.probe(data, 5, [1.], cyclic=False, end=10.), [[-500., 10.]]) + zero = stage_data([2., 9.], [4., 11.], [0., 0.]) + self.assertEqual(self.probe(zero, 2, [-1., 0., 50.], end=100.), + [[2., 0.], [11., 100.], [11., 100.]]) + # A trailing zero stage must not create a second, differently rounded + # representation of the cycle endpoint or leak its instantaneous value. + fractional = stage_data([-100., 10., -300., 20., -500.], [-100., 10., -300., 20., -500.], + [0., .8, 0., 10., 0.]) + for i, (boundary, left, at, right) in enumerate(self.probe(fractional, 5, [0.], events=2000)): + old, new = (10., 20.) if i % 2 == 0 else (20., 10.) + self.assertEqual((left, at, right), (old, new, new), (i, boundary)) + + def test_single_stage_sawtooth_and_queries_out_of_order(self): + data = stage_data([2.], [6.], [2.]) + times = [50., 2., -1., 1., 3., 4., 1.] + expected = [2., 2., 2., 4., 4., 2., 4.] + self.assertEqual([row[0] for row in self.probe(data, 1, times)], expected) + + def test_isolated_cyclic_component_runs_both_solvers_and_replays_boundaries(self): + b = Circuit() + b.add('amesim_ud00', 'signal', start1=1e17, end1=1e17, t1=.8, + start2=49000., end2=49000., t2=10., nstages=2, iscyclic=1) + with patch.object(builder, 'CACHE', self.root / 'build-cache'), \ + patch.object(result_storage, 'RESULT_ROOT', self.root / 'results'): + build = builder.build_native(compile_native_program(b.net)) + self.addCleanup(build.close) + outputs = [] + for method in ('BDF', 'RK45'): + result = execute_native(build, SolveIVPConfig(t_stop=60., method=method, max_step=1e30, rtol=1e-8), + .01, run_dir=self.root / method) + self.assertTrue(result['success'], result['message']) + self.assertEqual(result['simulatedUntil'], 60.) + series = result['series'] + self.assertGreater(len(series['time']), 6001) + self.assertTrue(set(i*.01 for i in range(6001)).issubset(series['time'])) + self.assertTrue(all(a < b for a, b in zip(series['time'], series['time'][1:]))) + values = series['signal.out.signal'] + self.assertEqual(set(values), {1e17, 49000.}) + self.assertEqual(values[series['time'].index(44.)], 49000.) + self.assertEqual(values[series['time'].index(54.)], 1e17) + known_boundaries = [.8, 10.8, 11.600000000000001, 21.6, 22.400000000000002, + 32.400000000000006, 33.2, 43.2, 44., 54., 54.8] + # A correct replay alone would not prove that integration + # stopped and restarted at each of the eleven time events. + self.assertEqual(result['solverStarts'], len(known_boundaries) + 1) + expected = [49000. if bisect_right(known_boundaries, t) % 2 else 1e17 + for t in series['time']] + self.assertEqual(values, expected) + for boundary in known_boundaries: + self.assertIn(boundary, series['time']) + self.assertIn(math.nextafter(boundary, -math.inf), series['time']) + self.assertEqual(result['outputSemantics']['version'], 2) + outputs.append(values) + self.assertEqual(*outputs) + + +if __name__ == '__main__': + unittest.main()