优化端口传递物性参数方式,减少上下游元件重复计算量

This commit is contained in:
ljz committed 2026-09-11 14:42:19 +08:00
1 parent 91bd9fb252
commit 5d5a2e1843
12 files changed
+773 -65

No files matched your search

+6 -3
View File
@@ -260,7 +260,7 @@ def _compile_storage_anchored_program(network: SimulationNetwork) -> NativeProgr
initial_volume = max(base, c.cvol0 / 100) if c.model_type == "amesim_pnch012" else base
init.append(f"if (!native_gas_init({_number(c.p0)}, {_number(c.T0)}, {_number(initial_volume)}, &y[{si(c, 'm')}])) return 0;")
lines.append(f"NativeGas gas_{gi};")
lines.append(f"if (!native_gas(y[{si(c, 'm')}], y[{si(c, 'U')}], {volume}, &gas_{gi})) return 0;")
lines.append(f"if (!native_gas_context(properties, y[{si(c, 'm')}], y[{si(c, 'U')}], {volume}, &gas_{gi})) return 0;")
for field in ("m", "U"):
put(c, field, f"y[{si(c, field)}]")
for field in ("p", "T", "rho", "u", "h"):
@@ -277,7 +277,7 @@ def _compile_storage_anchored_program(network: SimulationNetwork) -> NativeProgr
continue
a, b = chamber_at(c, "port_2"), chamber_at(c, "port_3")
put(c, "xv", f"fmax(0.0, fmin(1.0, {get(c, 'res.signal')}))")
lines.append(f"if (!native_orifice({get(a, 'p')}, {get(b, 'p')}, {get(a, 'h')}, {get(b, 'h')}, {_number(c.effective_cq * c.maximum_area)}, {get(c, 'xv')}, &{get(c, 'port_2.m_flow')}, &{get(c, 'cm')}, &{get(c, 'gasvel')})) return 0;")
lines.append(f"if (!native_orifice_context(properties, {get(a, 'p')}, {get(b, 'p')}, {get(a, 'h')}, {get(b, 'h')}, {_number(c.effective_cq * c.maximum_area)}, {get(c, 'xv')}, &{get(c, 'port_2.m_flow')}, &{get(c, 'cm')}, &{get(c, 'gasvel')})) return 0;")
assigned.update((key(c, "cm"), key(c, "gasvel")))
put(c, "port_3.m_flow", f"-{get(c, 'port_2.m_flow')}")
put(c, "port_2.h_outflow", get(b, "h"))
@@ -368,7 +368,10 @@ def _compile_storage_anchored_program(network: SimulationNetwork) -> NativeProgr
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; }",
"int model_eval(double t, const double *y, double *dy, double *w) {", "(void)t;", *lines,
"int model_eval(double t, const double *y, double *dy, double *w) {", "(void)t;",
f"NativePropertyState property_states[{min(256,max(16,4*len(components)))}];",
"NativePropertyCache property_cache, *properties=&property_cache;",
"native_properties_init(properties,property_states,sizeof(property_states)/sizeof(property_states[0]));", *lines,
"for (int i=0;i<NSTATES;i++) if (!isfinite(dy[i])) return 0;",
"for (int i=0;i<NOUTPUTS;i++) if (!isfinite(w[i])) return 0;",
"return 1; }",
+14 -11
View File
@@ -268,7 +268,7 @@ def compile_extended_program(network):
if kind=='amesim_pnch012' else
c.cvol if kind=='amesim_pnch023' else c.V if kind in ('cylinder','tank') else c.volume)
gas_initializers.append(f'if(!native_medium_init({medium(c)},{num(p0)},{num(T0)},{num(initial_volume)},{int(kind in ("cylinder","tank"))},&y[{states[c.name,"m"+suffix]}])) return 0;')
lines.append(f'if(!native_medium_gas({medium(c)}, {y(c,"m"+suffix)}, {y(c,"U"+suffix)}, {V}, &{gas})) return 0;')
lines.append(f'if(!native_medium_gas_context(properties,{medium(c)}, {y(c,"m"+suffix)}, {y(c,"U"+suffix)}, {V}, &{gas})) return 0;')
for field in ('m', 'U'):
put(c, field+suffix, y(c, field+suffix))
for field in ('p','T','rho','u','h'):
@@ -312,7 +312,7 @@ def compile_extended_program(network):
pipe_cache_count = 0
def pipe_flow(expression):
nonlocal pipe_cache_count
result = f'native_pipe_flow_cached(&pipe_cache[{pipe_cache_count}],{expression})'
result = f'native_pipe_flow_cached_context(properties,&pipe_cache[{pipe_cache_count}],{expression})'
pipe_cache_count += 1
return result
def flow(c, name, expr):
@@ -339,9 +339,9 @@ def compile_extended_program(network):
flow(c,a,f'{num(c.K_eff)}*copysign(sqrt(fabs({pa}-{pb})),{pa}-{pb})')
elif kind=='pipe':
resistance=c.lambda_darcy*c.L/c.D
flow(c,a,f'copysign(sqrt(fabs({pa}-{pb})*2*fmax(native_density({medium(c)},fmax(.5*({pa}+{pb}),1),{num(c.T0)}),1e-12)*{num(c.area*c.area/resistance)}),{pa}-{pb})')
flow(c,a,f'copysign(sqrt(fabs({pa}-{pb})*2*fmax(native_density_context(properties,{medium(c)},fmax(.5*({pa}+{pb}),1),{num(c.T0)}),1e-12)*{num(c.area*c.area/resistance)}),{pa}-{pb})')
elif kind=='amesim_pnl00r':
T=f'native_temperature_ph({medium(c)},fmax(fmax({pa},{pb}),1),{pa}>={pb}?{hin(c,a,True)}:{hin(c,b,True)})'
T=f'native_temperature_ph_context(properties,{medium(c)},fmax(fmax({pa},{pb}),1),{pa}>={pb}?{hin(c,a,True)}:{hin(c,b,True)})'
flow(c,a,pipe_flow(f'{medium(c)},{pa},{pb},{T},{num(c.diam)},{num(c.le)},{num(c.rr)},0'))
else:
opening = w(c,'xv') if kind!='amesim_pnor001' else '1.0'
@@ -351,7 +351,7 @@ def compile_extended_program(network):
area = c.effective_cq*(c.effective_area if kind=='amesim_pnor001' else c.maximum_area)
inputs=f'{medium(c)},{pa},{pb},{hin(c,a)},{hin(c,b)},{num(area)},{opening}'
outputs=(q(c,a),w(c,'cm'),w(c,'gasvel'))
code=f'if(!native_medium_orifice({inputs},&{outputs[0]},&{outputs[1]},&{outputs[2]})) return 0;'
code=f'if(!native_medium_orifice_context(properties,{inputs},&{outputs[0]},&{outputs[1]},&{outputs[2]})) return 0;'
operations.append(Computation(f'flow:{c.name}.{a}',outputs,references(inputs),(code,)))
flow_known.add(q(c,a));assigned.update((c.name+'.cm',c.name+'.gasvel'))
flow(c,b,f'-{q(c,a)}')
@@ -360,7 +360,7 @@ def compile_extended_program(network):
for name in (['port_1'] if kind.endswith('1') else names):
# Resistance uses the gas arriving from the upstream side,
# including inflow into a PNL0001 storage volume.
T=f'({p(c,name)}>{gas}.p?native_temperature_ph({medium(c)},fmax({p(c,name)},1),{hin(c,name,True)}):{gas}.T)'
T=f'({p(c,name)}>{gas}.p?native_temperature_ph_context(properties,{medium(c)},fmax({p(c,name)},1),{hin(c,name,True)}):{gas}.T)'
flow(c,name,pipe_flow(f'{medium(c)},{p(c,name)},{gas}.p,{T},{num(c.diam)},{num(c.le/(2 if kind.endswith("2") else 1))},{num(c.rr)},1'))
for edge in network.connections:
if edge.domain=='pneumatic':
@@ -513,7 +513,7 @@ def compile_extended_program(network):
if kind=='amesim_pnl0003':
a,b=gases[c.name,1],gases[c.name,2]
center=w(c,'dmctr')
put(c,'dmctr',f'native_pipe_flow({medium(c)},{a}.p,{b}.p,{a}.p>={b}.p?{a}.T:{b}.T,{num(c.diam)},{num(c.le)},{num(c.rr)},3)')
put(c,'dmctr',f'native_pipe_flow_context(properties,{medium(c)},{a}.p,{b}.p,{a}.p>={b}.p?{a}.T:{b}.T,{num(c.diam)},{num(c.le)},{num(c.rr)},3)')
for half in halves:
gas=gases[c.name,half];suffix=str(half) if half else ''
names=['port_'+suffix] if half else pnames(c)
@@ -536,7 +536,7 @@ def compile_extended_program(network):
gas=gases[c.name,0]
for name in pnames(c):
flow=q(c,name);pp=f'({flow}>=0?fmax({p(c,name)},1):fmax({gas}.p,1))'
temp=f'({flow}>=0?fmax(native_temperature_ph({medium(c)},{pp},{hin(c,name,True)}),1):{gas}.T)'
temp=f'({flow}>=0?fmax(native_temperature_ph_context(properties,{medium(c)},{pp},{hin(c,name,True)}),1):{gas}.T)'
diag.append((flow,pp,temp,c.le/2,0))
elif kind=='amesim_pnl0003':
a,b=gases[c.name,1],gases[c.name,2];flow=w(c,'dmctr')
@@ -545,13 +545,13 @@ def compile_extended_program(network):
pa,pb=p(c,'port_1'),p(c,'port_2');pp=f'fmax(fmax({pa},{pb}),1)'
if kind=='amesim_pnl0001':
gas=gases[c.name,0]
temp=f'({pa}>{gas}.p?fmax(native_temperature_ph({medium(c)},{pp},{hin(c,"port_1",True)}),1):{gas}.T)'
temp=f'({pa}>{gas}.p?fmax(native_temperature_ph_context(properties,{medium(c)},{pp},{hin(c,"port_1",True)}),1):{gas}.T)'
else:
temp=f'fmax(native_temperature_ph({medium(c)},{pp},{pa}>={pb}?{hin(c,"port_1",True)}:{hin(c,"port_2",True)}),1)'
temp=f'fmax(native_temperature_ph_context(properties,{medium(c)},{pp},{pa}>={pb}?{hin(c,"port_1",True)}:{hin(c,"port_2",True)}),1)'
diag=[(q(c,'port_1'),pp,temp,c.le,0)]
lines.append('{ double d[4],acc[4]={0};')
for flow,pp,temp,length,diagnostic in diag:
lines.append(f'native_pipe_diagnostics({medium(c)},{flow},{pp},{temp},{num(c.diam)},{num(length)},{num(c.rr)},{diagnostic},d);')
lines.append(f'native_pipe_diagnostics_context(properties,{medium(c)},{flow},{pp},{temp},{num(c.diam)},{num(length)},{num(c.rr)},{diagnostic},d);')
if len(diag)>1: lines.append('d[2]=fabs(d[2]);')
lines.append('for(int i=0;i<4;i++) acc[i]+=d[i];')
for i,field in enumerate(('re','cm','v','ff')):
@@ -574,6 +574,9 @@ def compile_extended_program(network):
*schedule_helpers,
'int model_init(double *y) {',*[f'y[{i}]={num(v)};' for i,v in enumerate(initial)],*gas_initializers,'return 1;}',
'int model_eval(double t,const double *y,double *dy,double *w) {',
f'NativePropertyState property_states[{min(256,max(16,4*gas_count+2*len(components)))}];',
'NativePropertyCache property_cache, *properties=&property_cache;',
'native_properties_init(properties,property_states,sizeof(property_states)/sizeof(property_states[0]));',
*(['double projected[NSTATES];for(int i=0;i<NSTATES;i++) projected[i]=y[i];',*project,'y=projected;'] if project else []),
f'double p[{np}]={{0}},h[{nq}]={{0}},q[{nq}]={{0}};NativeGas g[{ng}];',
f'double fb[{max(1,flow_temporary_count)}]={{0}};(void)fb;',
+3 -3
View File
@@ -185,9 +185,9 @@ class EvaluationSchedule:
closure; a pressure trial further restricts work to its residual inputs.
"""
helpers, lines = [], []
args = 'p,h,g,w,q,fb,pipe_cache'
args = 'p,h,g,w,q,fb,pipe_cache,properties'
signature = ('double *p,double *h,const NativeGas *g,double *w,double *q,'
'double *fb,NativePipeCache *pipe_cache')
'double *fb,NativePipeCache *pipe_cache,NativePropertyCache *properties')
def code(indices):
return [line for i in indices for line in
@@ -195,7 +195,7 @@ class EvaluationSchedule:
def helper(name, indices):
helpers.extend([f'static int {name}({signature}) {{',
'(void)p;(void)h;(void)g;(void)w;(void)q;(void)fb;(void)pipe_cache;',
'(void)p;(void)h;(void)g;(void)w;(void)q;(void)fb;(void)pipe_cache;(void)properties;',
*code(indices), 'return 1;', '}'])
return f'if(!{name}({args})) return 0;'
@@ -0,0 +1,203 @@
# 物性与管流分段性能分析(2026-09-11)
后续实现已完成:见 [物性复用与管流求根实现及验证](物性复用与管流求根实现及验证-2026-09-11.md)。本文保留为实施前的剖析与隔离实验记录;最新生产实现、计时和回归结论以实现报告为准。
本次沿用网页端已验证的 `test-mql-4-corrected.json`,直接测试网页后端使用的 C 求解程序。结论是:两个方向都值得优化,但物性应优先减少重复反算,管流则应改进收敛方法并检查残差。把迭代次数直接调小,不能可靠地解决问题。
隔离试验中,完整 10 秒仿真的纯求解耗时从 **3.119 秒降至 2.102 秒,中位耗时缩短 32.6%**。这些是 `/test` 内的实验结果;本次没有更改网页后端的生产内核,也没有修改系统雅各比矩阵。
**测试对象与测量口径**
- 输入:`test/mql4-20260910/test-mql-4-corrected.json`,与上一轮网页端对照使用同一模型。
- 配置:真实氦气 Peng–Robinson 物性;CVODE/BDF;仿真 0~10 s;相对误差限 `1e-7`;最大积分步长 `1e30`;状态绝对误差限沿用生产配置。
- 规模:64 个状态,882 个输出;本模型的编译计算顺序已经形成,无代数环。当前检查的是这个顺序内部的成本。
- 代码:`system-optimization` 分支,构建标识 `a23de3b825eaacc2df5b61f524df66ec3f92da943acb5a9a98aaf4ba479fb02f`。试验脚本首先核验生产 C 源文件与构建清单的哈希一致。
- 性能比较:每个方案独立进程运行三轮,交错执行,报告中位数。只统计 C 求解阶段,使用 `--solve-only`,不含 Python 前处理、编译、进程启动、曲线输出、网页交互和绘图。
- 正确性比较另跑完整输出,保存 0.01 s 网格及事件点;对组合方案另外记录碰撞附近内部积分点。
- 分段测试是在一次连续求解中计数,按方程试算的时间归入区间,**没有在分段边界重启求解器**。试算可能回退、拒绝或越过区间边界,因此这些数据表示“为该区间试算花了多少成本”,不是各段独立启动的时间。
- 计时探针和详细计数、输入采集分两次执行;二者的全部输出序列均与未插桩基准逐点完全一致。插桩运行总耗时为 3.600 s,同批完整输出基准为 3.211 s,探针约增加 12% 耗时。因此下面函数时间用于定位成本,最终加速比使用无探针程序测量。
本次没有重跑 Amesim;数值对照读取此前官方重新编译运行的 `amesim-fresh/test_mql_4.ame`。不将历史 Amesim 耗时与本次机器负载不同的测量混合计算加速比。
**1.时间花在哪个阶段**
基准共执行 24,941 次系统方程计算,接受 5,137 个积分步,发生 2 次状态切换。下表中的时间是探针测得的方程计算时间,不包括方程外的求解器工作。
| 仿真时间区间 / s | 方程计算次数 | 方程计算耗时 / s | 占全部方程计算时间 |
|---|---:|---:|---:|
| 0~0.01 | 760 | 0.068 | 1.9% |
| 0.01~0.8 | 12,042 | 1.757 | 49.9% |
| 0.8~1.20 | 2,165 | 0.272 | 7.7% |
| 1.20~1.202,包含首次碰撞 | 1,186 | 0.146 | 4.1% |
| 1.202~1.23,包含第二次限位事件 | 2,451 | 0.313 | 8.9% |
| 1.23~2 | 4,333 | 0.668 | 19.0% |
| 2~10 | 2,004 | 0.299 | 8.5% |
| 合计 | 24,941 | 3.522 | 100% |
接近一半的工作发生在 0.01~0.8 s。最后 8 秒仿真只占约 8.5% 的方程计算时间。碰撞附近确实需要处理很快的变化,但本次测量不支持“主要时间都花在碰撞瞬间”的判断。
另有 16,768 次方程计算来自线性求解器估算数值导数,占全部方程调用的 67.2%。这部分已经包含在表内,不能另加一次。它意味着物性和管流计算会随系统试算反复执行。本轮保留现有雅各比矩阵处理方式,只降低每次试算成本。
**2.把物性和管流迭代分开后,结论发生了什么变化**
下表按互不重复的函数耗时分类:例如管路内部调用的可压缩流动计算,记在“可压缩流动系数”中,不再记入“管流摩擦迭代”。
| 计算内容 | 插桩耗时 / s | 占方程计算时间 | 调用次数 |
|---|---:|---:|---:|
| 可压缩流动系数:等熵修正、密度、临界/亚音速关系等 | 1.009 | 28.6% | 728,296 |
| 管流摩擦迭代 | 0.945 | 26.8% | 483,678 次进入循环 |
| 用压力、焓反求温度 | 0.728 | 20.7% | 646,040 |
| 从质量、内能恢复气体状态 | 0.328 | 9.3% | 648,466 |
| 管路诊断量:雷诺数、速度、摩擦系数等 | 0.252 | 7.2% | 548,702 |
| 管路其余准备工作 | 0.093 | 2.6% | 548,702 次管流调用 |
| 系统组装、阀口剩余计算、接触力等 | 0.166 | 4.7% | — |
因此,不能把整个 `native_pipe_flow` 函数的时间都解释成“内部摩擦迭代太慢”。它首先还要计算黏度、真实气体修正以及可压缩流动系数。表中 28.6% 的项目也包含流量公式中的幂函数等,不是全部都属于物性查询。
本次计数还发现:一次完整求解累计调用约 **620 万次气体状态方程求根**、348 万次焓偏差计算和829万次摩擦系数计算。现在这些均运行在 C 内核中,当前问题已经是物理计算量和重复调用量,不能继续归因于 Python 语言开销。
**3.物性为什么贵,应该怎样优化**
当前真实氦气的 `p,h → T` 过程大致如下:先用理想气体关系猜温度,再代入真实气体关系计算焓,看是否吻合,然后更新温度。每次更新又要解一次气体状态方程,并计算对数等函数。
| 反算对象 | 次数 | 平均迭代次数 | 观察到的最多次数 | 达到16次上限且未收敛 |
|---|---:|---:|---:|---:|
| 质量、内能恢复温度 | 648,466 | 4.41 | 5 | 0 |
| 压力、焓恢复温度 | 646,040 | 4.38 | 5 | 0 |
物性反算在这个工况下并不存在迭代失控;主要问题是一次略贵的计算被调用了很多遍。C 输入重放试验中,`p,h → T` 平均约 **1.065 μs/次**,几十万次累加后就成为明显成本。
更重要的是,这 646,040 次调用,在同一次方程计算中,都能找到压力、焓完全相同的上游气体状态。温度此前已经算出来了。其中 369,553 次还是对相同压力、焓的重复请求,占 57.2%。
通俗地说,目前有些路径在做:“先算出温度,把它换成焓传给下一个部件,下一个部件又把同一压力下的焓反算成温度。”这个模型中,优先消除这条往返路径,比单独把反算公式加快更有价值。
已验证的两个层次:
1. **同一次试算中,相同输入只反算一次。** 完全相同的介质、压力、焓使用同一个结果。纯求解耗时减少 11.0%,全部882个输出及时间序列逐点完全一致,方程调用数也不变。
2. **传递与压力、焓对应的已知温度。** 如果气体状态已经提供了同一组压力、焓,直接使用其温度;找不到时仍调用原来的反算。单独使用这一方案,纯求解耗时减少 16.7%。采集的17,527个气体状态中,源温度与反算温度最大只差 `1.08e-10 K`,但微小数值变化仍会改变 BDF 的后续试算路径,所以不能承诺逐位相同。
通用实现建议是建立一个“本次方程计算的物性状态”结构,保存介质身份、压力、焓、温度及已经算出的密度、黏度等。编译器知道量来自哪个状态时,直接传递状态引用;不知道时再查询或反算。缓存由求解上下文持有,不依赖整个进程中的可变全局状态。
必须遵守物理含义:**相同焓并不代表相同温度**。节流后的压力改变了,或者支路发生焓混合,就不能直接沿用原来的温度。缓存必须核对介质与完整输入,未来两相介质还需要区分物性区域。每次试算更新或失效缓存;本轮实验不采用“数值差不多就复用”,以免把求解器正在估算的细小变化抹掉。
还有两个次级机会:
- `valve → isentropic → local_isentropic` 已经求过上游密度,返回阀口后又按相同 `p,T` 求了一遍。可以让一次物性更新同时提供密度、热容和所需导数,把公共中间量复用。该方向还未纳入本轮完整系统加速结果。
- 已知内能、压力、密度时,可以利用 `h = u + p/ρ` 得到焓,避免再从 `p,T` 求一遍真实气体焓。对采集状态的微基准,气体状态恢复从 **0.484 降至 0.247 μs/次**,约减少49%;与原式的最大焓差为 `1.67e-7 J/kg`。这是物性定义层面的通用机会,但必须先核对各介质的能量参考点与实现一致性。本轮只完成了单项验证,没有把该收益算进完整系统的32.6%。
对于确实需要反算的状态,后续再考虑使用上一次温度作初始猜测、利用局部热容改善更新、加区间保护与失败回退。初始猜测只能帮助收敛,不能绕过结果残差检查。查表、插值等近似方案暂不作为第一步,因为本轮已经找到无需近似的重复计算。
**4.管流为什么反复迭代,而且到上限仍没有收敛**
当前过程是:猜流量 → 算雷诺数 → 算摩擦系数 → 用新摩擦系数重新算流量 → 新旧流量各取一半 → 重复。每次调用又从“摩擦系数等于0.02”的固定猜测开始。
| 管路类别 | 调用次数 | 未进入循环的次数 | 达到上限且未收敛 | 占全部调用 |
|---|---:|---:|---:|---:|
| PNL00R(kind 0) | 49,882 | 49,644 | 0 | 0% |
| PNL0001/2(kind 1) | 399,056 | 8,028 | 390,268 | 97.8% |
| PNL0003(kind 3) | 99,764 | 7,352 | 92,412 | 92.6% |
PNL00R 大多走层流公式,确实进入迭代的少量调用最多需要62次,但都在其64次上限内收敛。PNL0003 的92,412次循环全部走满16次;PNL0001/2 的非零流动调用也几乎如此。
代码达到上限后直接返回当前流量,没有要求最终残差合格。这不只是速度问题,还意味着当前局部流量并未都达到代码宣称的收敛精度。
“新旧各取一半”虽然温和,却会限制收敛速度。例如层流关系下,在解附近一次更新大约只把误差缩至原来的75%;16次后仍可能剩下初始误差的约1%。充分粗糙的湍流区,即使摩擦系数已经近似恒定,仍然反复取平均,也会继续消耗循环。真实过渡区更不能依靠固定次数保证精度。
对实际仿真每37次调用抽取一组输入,重放得到14,830组管流输入,覆盖正流7,362组、反流6,951组、等压517组。压力约0.1~15.3 MPa,温度约173~526 K。
本次把相同的管流关系写成 `Re² × f(Re) = K`,以左右两边的相对差衡量残差。`K` 来自原来的物性、压力、温度及几何关系,没有换用新的物理流量公式。
| 类别 | 旧方法残差中位数 | 旧方法残差90分位 | 旧方法残差99分位 | 新方法最大残差 |
|---|---:|---:|---:|---:|
| PNL0001/2 | `7.51e-6` | 3.07% | 15.9% | `9.59e-10` |
| PNL0003 | `2.82e-6` | `1.85e-5` | 2.19% | `6.38e-11` |
相对残差在流量接近零时会很大,不能直接当成系统流量误差。即使只看新流量绝对值大于 `1e-8 kg/s` 的输入,旧方法的最大方程残差仍分别为21.6%和12.3%。在全部重放输入上,新旧流量的最大绝对差分别为 **`2.91e-6 kg/s`、`1.93e-6 kg/s`**。因此既要看残差,也要看流量的绝对变化和系统曲线。
针对这一点,实验采用带区间保护的局部 Newton 求根:根据当前偏差决定下一次改多少;如果下一次估计越界,则用区间二分回退。保留原有摩擦系数公式、层流分支、流向、可压缩修正及近平衡平滑。这里计算的是管路内部一个量的斜率,没有修改系统雅各比矩阵。
| 同一批输入的 C 微基准 | 旧方法 / μs每次 | 实验方法 / μs每次 | 时间减少 |
|---|---:|---:|---:|
| 只计算摩擦迭代,物性和几何系数提前准备 | 1.619 | 0.555 | 65.7% |
| 整个管流计算,包含物性准备 | 3.369 | 2.043 | 39.4% |
PNL0001/2 真正进入新迭代时平均4.40次,PNL0003平均4.08次,最多6次。用独立的100次二分求解同一方程作参照,新流量最大差约 `1.1e-13 kg/s`。
另测906组独立工况,覆盖雷诺数 `1e-6~1e9`、相对粗糙度 `0~0.1`:无非有限结果,最多7次迭代,与二分结果最大相对差 `3.62e-11`。部分工况确实触发了区间回退,最多2次。这个标量求根部分通过 `K` 与介质物性分离,因此可以复用于采用同一阻力方程的不同气体与几何参数。其他阻力模型应提供自己的残差函数和物理适用区间,不能直接套用氦气物性公式。
正式实现还应返回 `converged / iterations / residual / fallback` 等状态;超过上限时继续可靠回退或报告失败,不能静默返回未经检查的数值。
**5.完整系统能节省多少时间**
下表全部使用无性能探针的可执行程序,三轮交错运行。每个方案都运行到10 s。
| 方案 | 求解墙钟中位数 / s | 三轮范围 / s | CPU中位数 / s | 比基准减少 | 方程计算次数 |
|---|---:|---|---:|---:|---:|
| 当前生产算法基准 | 3.119 | 3.111~3.165 | 3.172 | — | 24,941 |
| 只复用完全相同输入的物性反算结果 | 2.775 | 2.772~2.807 | 2.844 | 11.0% | 24,941 |
| 直接使用对应气体状态的已知温度 | 2.599 | 2.573~2.607 | 2.672 | 16.7% | 25,493 |
| 只替换管流内部求根方法 | 2.500 | 2.496~2.508 | 2.578 | 19.8% | 24,626 |
| 已知温度复用+管流求根 | 2.102 | 2.091~2.108 | 2.172 | 32.6% | 28,164 |
组合方案虽然单次计算明显加快,方程调用次数却增加约12.9%。它改变了局部流量精度与微小舍入,BDF在这个敏感模型上的步长选择随之变化。因此,“单次管流快39%”不等于“整个系统快39%”,两个单项收益也不能直接相加。真实的完整系统收益应以上表为准。
相同输入复用方案和直接传递温度的方案是两个层次,不应叠加计算11.0%与16.7%。组合实验包含的是后一种物性方案。
**6.是否牺牲了仿真结果**
只复用相同反算输入的方案,包含事件点在内的全部输出完全一致。其余方案保持同一物理方程,但不会逐位相同,必须检查结果差异。
组合方案与基准在0.01 s公共网格上的比较,针对此前对照的72条压力、温度、位移、速度和阀口流量曲线:
| 量 | 最大绝对差 |
|---|---:|
| 压力 | 19.3 Pa |
| 温度 | 0.00586 K |
| 位移 | `2.06e-7 m` |
| 速度 | `1.78e-6 m/s` |
| 阀口质量流量 | `9.36e-5 kg/s` |
流量最大差可能发生在短暂变化附近,不能只拿最后时刻或少量输出点作结论。上述72条量的完整公共网格已经比较;与存档Amesim的回归对照另外采用原有57个指定时间点,不能将其称为每个内部积分点均一致。
与Amesim在这57个指定时间点的对照中,组合方案最大压力差约199.8 Pa、最大温度差0.01154 K;基准对应约198.3 Pa、0.00963 K。位移、速度、阀口流量差仍在同一数量级。新方案没有消除原有模型/数值差异,也没有显示出数量级上的恶化。详情按曲线存入 `accuracy.json`。
此前关注的第一个 LSTP 接口力单独比较:
- 1001个公共网格点,与Amesim存档力曲线的最大差:基准0.5770 N,组合方案0.5766 N。
- 第一次碰撞时间相对基准变化约 `−4.32e-8 s`,第二次约 `−1.52e-7 s`。
- 第一次事件的瞬时峰值从 `1.498588675e11 N` 变为 `1.498588441e11 N`,相对变化约 `−1.56e-7`。
- 另行记录内部接受的积分点后,峰值降至一半的时间均约0.348 ns,降至1%的时间均约2.336 ns。记录探针也未改变组合方案原有输出。
所以本轮加速没有跳过或删除此前发现的纳秒级力脉冲。Amesim存档在这附近没有相同密度的内部采样,仍不能据此判断其内部是否存在同样脉冲。这项既有对照限制继续保留。
这些验证支持方案继续进入正式开发,并不等价于所有模型、所有介质均已通过生产回归。
**7.建议的通用实施顺序**
1. **先补可观测性和收敛判据。** 把本轮的调用次数、迭代分布、未收敛次数、最大残差做成可选诊断,默认关闭详细计时。每条管路分别累计,便于新增模型后直接定位。改变流量算法前保存基准。
2. **将物性复用做进编译与运行上下文。** 第一步采用完整输入一致的缓存,获得本轮已证明的低风险收益;随后让编译器沿供需关系传递物性状态引用,跳过已有温度的反算。焓混合、压力改变、介质改变时重新构造状态。未来介质各自实现 `pT / ph / rho-u` 更新及可复用的派生量。
3. **统一管路标量求根器。** 将介质与几何计算形成的系数交给局部求根器,保留各管路已有的物理分支;以流量绝对误差和相对残差共同验收。区间保护、失败回退和统计接口作为公共能力,不按具体实例编号写特例。局部斜率可由阻力模型提供,缺少时仍有不依赖斜率的区间方法。
4. **合并物性中间量,再分离仅用于显示的诊断量。** 复用上游密度、热容和等熵计算中间量;验证 `h=u+p/ρ` 快路径。对雷诺数、显示速度等,编译器检查是否被其他模型或控制器使用;只有确认为纯输出时才移到采样阶段。不能一律关闭,因为新增模型可能把这些量接入反馈。
5. **逐项做系统回归,再合并收益。** 检查不同气体、正反流、等压、层流、过渡区、粗糙管、限位事件和小体积储气模型;同时保存稳态输出和事件附近内部点。每个方案单独测,再测组合,接受与拒绝步数也要记录。保持本轮所用系统雅各比矩阵策略,避免混入其他变化。
优先顺序上,建议尽快处理管流“达到上限仍返回”的行为,同时先落地完全一致输入的物性复用。前者关系到局部方程是否真的解好,后者已有逐点一致和约11%完整系统收益的证据。更进一步的状态传递与管流求根组合,已有约32.6%的隔离试验收益,可以作为第一轮正式优化的目标;不把这一百分比保证给其他模型。
**复现与文件**
所有原始记录与实验程序位于:`F:/Master/SystemSimulationApp/test/property-pipe-profile-20260911/`。
| 文件 | 内容 |
|---|---|
| `profile.py` | 核验源码哈希,构建基准、分段计时、独立计数和输入采集程序 |
| `timed/profile.json` | 各阶段独占耗时、调用次数、迭代直方图 |
| `counters/profile.json`、`counters/replay.txt` | 缓存命中机会及49,818条气体/反算/管流采集输入 |
| `experiments.py` | 完全相同输入缓存、已知温度复用、管流求根及组合方案生成器 |
| `microbench.py`、`micro-summary.json` | C重放微基准、残差、906组独立工况检查 |
| `benchmark.py`、`benchmark.json` | 无探针的三轮交错纯求解测量及逐次结果 |
| `analyze.py`、`accuracy.json` | 公共网格、Amesim存档及事件点的数值比较 |
| `trace_combined.py`、`event-validation.json` | 组合方案碰撞附近内部积分点检查 |
| `baseline/`、`cache_exact/`、`known_temperature/`、`pipe_root/`、`combined/` | 各自的C源文件、可执行程序、完整结果及日志 |
在仓库根目录,用 `.venv-win/Scripts/python.exe` 依次运行上述 `profile.py`、`experiments.py`、`microbench.py`、`benchmark.py`、`analyze.py` 可复现主结果。需要本机现有GCC及SUNDIALS开发环境。事件记录脚本会拒绝向既有记录追加,避免混合两次结果;重跑时应指定新的实验输出目录。
`/test` 按现有仓库规则被Git忽略;本报告进入文档目录。正式合入时需把通用实现和必要回归测试迁入对应源码目录,本轮实验代码不作为已经上线的功能。
@@ -0,0 +1,143 @@
# 物性复用与管流求根:实现及验证
已在 `system-optimization` 工作区将三项物性优化接入正式 C 内核:共享同一次试算的物性状态、复用等熵计算中的上游密度,以及用已有内能、压力和密度求焓。两条代码生成路径均已接入。此前验证过的管流求根方案也已一并落实。
最终版本中,MQL4 的纯求解中位耗时从 **3.138 s 降至1.391 s,减少55.7%**;skill-test 从 **0.255 s 降至0.139 s,减少45.6%**。本机后台已启动新版本,通过真实 HTTP 流式仿真请求验证,结果与独立EXE逐点一致。本次没有提交或推送Git。
**物性状态如何共享**
每次生成的 `model_eval` 都在自己的栈上建立 `NativePropertyCache`,随后将同一个上下文传给部件和局部压力、焓闭合计算。记录保存介质参数副本、压力、温度、焓、密度、黏度,以及已算出的等熵修正系数和指数;标志位区分哪些量已经有效。
气体容腔恢复状态后,先登记已有的温度、焓和密度。管路、阀口需要温度时,按介质与精确的压力、焓查找。相同状态直接读取温度,未知状态仍使用原来的反算。需要密度、黏度或等熵修正时,仅补算缺少的量,后续计算继续复用。
这一版采用完整输入查询和共享状态引用,尚未把所有端口都静态替换成无需查询的状态指针;它已经覆盖当前两条生成路径的相关气体、阀口和管路调用。
有效性规则如下:
- 压力和焓必须精确匹配,压力、温度查询也必须精确匹配。没有“差不多就沿用”的近似缓存。
- 介质按全部数值参数核对,并保存参数副本;改变压力、焓、介质参数,都不会误用旧状态。
- 节流后的压力变化、气流混合产生的新焓,会触发新状态查询或反算。同焓不能直接推断同温度。
- 每次系统试算重新初始化缓存;不同仿真、线程和独立调用不共享可变全局缓存。没有把上一时刻的物性直接拿来代替本次试算。
- 编译器按模型规模分配16~256条记录,控制栈空间。容量满后转为正常计算,不丢失正确性。未来大规模模型可进一步改善索引与容量策略。
- 真实氦气在临界温度以下及临界温度处,保留原有气相根和PH反算路径;不把本次优化当作两相物性支持。
**两个次级机会的实现**
第一项已落实到可压缩流动函数内部。等熵修正计算出的上游密度,直接用于随后流量、流速计算。上下游状态分别保存;下游压力、温度不同就有自己的记录。同一个状态的等熵系数也可以重复使用。管路诊断中的重复黏度计算同时合并。
第二项在当前真实氦气单根区域使用 `h = u + p/ρ`。这里的 `u` 为已有比内能,`p` 为绝对压力,`ρ` 为已有密度,单位分别为J/kg、Pa、kg/m³。与本内核的内能和焓参考点一致,避免再次从压力、温度求真实气体焓。低温的原有气相根选择保留回退。理想气体已有的简单焓表达式继续使用。
这些优化没有引入物性查表、插值或额外近似。
**管流收敛修正为什么一并接入**
最初只接入全组物性优化时,MQL4仍能运行到10 s,但PNL0002_10的一条温度曲线在存档参考点的最大偏差为0.019965 K,超过现有0.015 K门槛。分项试验表明,极小的物性数值变化会改变BDF的试算路径;结合前一轮“管流几乎总达到16次上限”的证据,不能把这个完整系统偏差简单解释为物性公式算错,也不能只保留更快的计时。
因此一并接入此前试验的管流求根方案,并保持原有曲线门槛。最终完整1001个公共时间点上的最大温度差为0.009901 K,通过验收。上述分项结果保留在 `decomposition.json` 和 `final-benchmark.json`,未以放宽误差门槛代替修正。
管流仍使用原有可压缩关系、摩擦系数混合公式、流向和近平衡平滑。内部将流量关系整理为 `Re² × f(Re) = K`,使用带区间保护的局部Newton求根;越界则二分回退。PNL00R原有的直接层流分支保留。
返回前会在实际返回点检查方程相对残差,要求不超过 `1e-9`,同时使用局部流量变化判据。接口 `native_pipe_resistance` 可返回收敛状态、迭代次数、二分回退次数和残差;异常输入或达到上限仍不合格时返回非有限失败值,交由现有模型有效性检查拒绝,不再静默接受未收敛的流量。
这里仅使用管流内部一个标量关系的斜率。系统雅各比矩阵策略、BDF/RK45积分器和模型误差设置均未调整。
**实测耗时:完整系统与分项对照**
性能测试使用独立EXE,关闭性能探针,三轮交错运行并取中位数。只统计C求解过程,不含Python前处理、编译、进程启动、结果输出、网页传输或绘图。全部运行至10 s。
MQL4:BDF,相对误差限 `1e-7`,最大积分步长 `1e30`,其余状态绝对误差限沿用生产配置。
| 方案 | 求解中位耗时 / s | 三轮范围 / s | 方程调用次数 | 说明 |
|---|---:|---|---:|---|
| 修改前基准 | 3.138 | 3.126~3.175 | 24,941 | 原有物性和管流迭代 |
| 仅局部密度、黏度去重 | 3.027 | 3.021~3.108 | 24,941 | 全部时间及输出序列逐点完全一致 |
| 全组物性优化,保留旧管流迭代 | 1.974 | 1.973~2.030 | 24,612 | 温度参考门槛未通过,不作为最终版本 |
| 最终组合,但关闭焓直接计算 | 1.518 | 1.514~1.611 | 24,481 | 用于评估第二个次级机会 |
| 最终组合:全部物性优化+管流求根 | **1.391** | **1.382~1.456** | **24,262** | 完整曲线验收通过 |
局部密度、黏度去重单独减少约3.5%完整系统耗时;在最终组合中加入焓直接计算,耗时从1.518降至1.391 s,进一步减少约8.4%。后一项也伴随试算路径变化,因此不是单纯的函数微基准。两项与主物性复用、管流求根存在重叠,不能将各百分比直接相加。
skill-test采用仓库 `tests/data/native-skill-test.xml`,这是之前skill-test模型的回归输入;RK45、最大步长0.001 s、相对误差限 `1e-7`:
| 方案 | 求解中位耗时 / s | 方程调用次数 | 接受步数 |
|---|---:|---:|---:|
| 修改前 | 0.255 | 60,278 | 10,022 |
| 最终版本 | 0.139 | 60,278 | 10,022 |
skill-test的计算次数和步数相同,这一组能更直接地反映每次物性计算变轻带来的收益。它没有MQL4中的管路网络,因而也验证了物性优化在另一种模型和积分器下的效果。
**计算次数确实减少了多少**
另行构建只加计数、不用于性能比较的程序,限定在系统方程回调内计数,排除初始化和输出重算。计数版的全部序列分别与对应无探针版本完全一致。
| MQL4求解中的计算 | 修改前 | 最终版本 |
|---|---:|---:|
| 真实气体状态方程求根 | 6,202,660 | 690,869 |
| 压力、焓反求温度 | 646,040 | **0** |
| 黏度计算 | 1,626,172 | 333,213 |
| 摩擦系数普通计算函数 | 8,285,496 | 1,360,592 |
最后一行只统计普通摩擦系数函数。新Newton步骤还会在“摩擦系数及其斜率”联合函数中计算摩擦关系,因此不能据此声称全部摩擦计算工作减少了同样比例;管流性能应以上面的完整计时及独立求根验证为准。
PH反算降至0,仅说明本模型的请求都能找到本次试算的已知状态。对压力变化、焓混合、其他模型和不适用快路径的低温工况,反算功能仍然存在。
**曲线、守恒和事件验证**
与此前官方重新编译的四支路Amesim存档比较,72条压力、温度、位移、速度和阀口流量曲线使用完整1001个共同输出时间点。没有重跑Amesim或重新解读其内部算法。
| 量 | 最大绝对差 | 原有验收门槛 |
|---|---:|---:|
| 压力 | 199.863 Pa | 250 Pa |
| 温度 | 0.009901 K | 0.015 K |
| 位移 | `7.49e-7 m` | `2e-6 m` |
| 速度 | `2.64e-6 m/s` | `1e-5 m/s` |
| 阀口质量流量 | `1.55e-5 kg/s` | `3e-5 kg/s` |
闭合气路总质量最大漂移约 `1.33e-14 kg`。
LSTP四个接口力另行比较。第一个接口力在共同网格上与Amesim的最大差为0.6135 N;四个接口中最大差为1.123 N。新增事件点未并入普通网格误差,以免把瞬时峰值插值成虚假的长三角形。
首次碰撞时间为1.201137461445 s,峰值约 `1.49858577e11 N`,相对修改前的峰值变化约 `−1.94e-6`。另行记录内部接受的积分点:峰值降至一半约0.348 ns,降至1%约2.336 ns。事件记录不改变最终输出序列,先前发现的极短脉冲没有被省略。Amesim存档缺少同等密度的内部采样,仍不能由本次验证断言其内部是否存在同一脉冲。
skill-test的输出时间序列一致,最大压力差约 `1.70e-7 Pa`,最大温度差约 `2.39e-12 K`;所检查的位移、速度和力序列完全一致。
**后台已生效**
检查时本机8000端口无运行中的服务,随后以当前工作区启动后台。启动日志确认数值引擎为 `native`。真实请求:
`POST http://127.0.0.1:8000/api/system-xml/simulate-stream`
此次请求完成0~10 s,C纯求解1.507 s,HTTP全程3.967 s,收到21个进度事件。HTTP时间包含前处理、进程启动、输出序列化及传输,不能与纯求解时间等同,也未包含浏览器绘图测量。
后台返回的构建标识为 `9fdd91da210e189d1e44de686d6109bbdb5be56c284450b41364486160bfe494`,与最终独立EXE相同;全部时间和输出序列逐点一致。后台日志、启动进程号及HTTP结果均保存在实验目录。
**回归与通用性边界**
最终完整复核命令的25项相关测试在23.898 s内全部通过(`regression-final.log`),包含:
- 50组元件网络的冻结物性参考、守恒及未改变模型的完整参考对照;两条代码生成路径。
- BDF、RK45、原生后端默认路径,以及不依赖NumPy/SciPy的编译导入路径。
- 缓存跨试算失效、独立上下文、容量耗尽回退、一个浮点单位的压力变化、焓变化及每个介质参数变化。
- 真实氦气与含温度相关热容的理想气体,在100~1000 K、`1e4~1e8 Pa`的状态往返检查,以及低温氦气保留原有路径的检查。
- 管流求根的2718组工况:雷诺数 `1e-6~1e9`、相对粗糙度0~0.1,三个不同流量尺度;均与独立100次二分求解比较。检查返回残差、区间回退、非法输入失败、零流量和PNL00R层流分支。
- 独立压力回路与直接支路组合前后的结果隔离、流向反转、焓混合回路,以及MQL4的已有曲线验收。
旧冻结参考中的PNL流量包含原先迭代达到上限的值。现在所有PNL共用检查残差的求根器,因此保留这些网络的独立物性与守恒参考,并由独立二分、管路物理测试和Amesim系统曲线验证流量;没有要求新算法重现旧的未收敛迭代值。
一次目录复测曾有两个短进程的 `--init` 返回空标准输出;随后这两个进程各自连续5次核验正常,50组目录整套复测也通过。未为此增加忽略错误或自动重试逻辑,原始与复测日志均保留。
**代码与复现记录**
- C接口与实现:`native/include/kernels.h`、`native/components/kernels.c`。
- 生成与上下文传递:`app/simulation/native_codegen/compiler.py`、`extended.py`、`schedule.py`。
- 新增及更新测试:`tests/test_native_properties.py`、`test_native_pipe_physics.py`、`test_native_catalog.py`。
- 本次数据:`test/property-implementation-20260911/`,包括 `final-benchmark.json`、`counter-audit.json`、`http-and-curves.json`、`event-audit.json` 及完整结果、源文件、EXE和日志。
- 前一轮剖析与原型:`test/property-pipe-profile-20260911/`,原报告保留为历史测试记录。
复核命令:
```powershell
./.venv-win/Scripts/python.exe -m unittest tests.test_native_properties tests.test_native_pipe_cache tests.test_native_pipe_physics tests.test_native_codegen tests.test_native_schedule tests.test_native_catalog tests.test_native_only_backend -v
```
实验记录和生成EXE遵循现有 `/test` 忽略规则;正式实现、测试和文档位于受版本管理的目录。本次未修改用户输入JSON、Amesim文件或前端模型参数。
+7 -1
View File
@@ -34,7 +34,9 @@
- 压力与焓同时互相依赖时,在这个局部块内保留嵌套求解。迭代到上限仍未满足条件,或出现非有限数值,返回求值失败;不把未完成的局部闭合当作成功。
- 条件表达式记录所有可能分支的依赖,覆盖反向与零流量。此做法较为保守:某个特定工况下能进一步简化的关系仍可能留在小循环内。本版没有实现运行时分支特化。
管路自身的摩擦/流量求根属于元件内部计算。本轮没有改其算法或 16/64/48 轮上限,也没有改积分器或雅可比策略。系统循环的来源检查和局部划分不代表任意新非线性方程都已得到数值求解支持。
管路自身的摩擦/流量求根属于元件内部计算。2026-09-11起,PNL管流使用带区间保护的局部Newton求根,保留既有物理分支,并在实际返回点要求方程相对残差不超过 `1e-9`;不再把达到16/64次上限的迭代值直接当成解。上面的系统压力、焓闭合策略,以及积分器和系统雅可比策略仍然保留。系统循环的来源检查和局部划分不代表任意新非线性方程都已得到数值求解支持。
每次 `model_eval` 还建立调用者持有的 `NativePropertyCache`,并显式传给局部辅助函数。气体状态准备后登记已有温度、焓和密度;阀口、管路及诊断计算共享相同状态中的有效物性。压力、焓或介质参数改变时按完整输入重新查询,缓存不跨系统试算复用。记录容量满时使用正常计算回退。
## 新模型如何接入
@@ -49,6 +51,10 @@ operations.append(Computation.assignment(
没有依赖流量的参考值复制必须单独列出,不能与需要流量的能量计算合并为一个条目。共享状态、机械/信号准备和气体物性阶段仍需要在现有 C 接入中实现;仅注册 `PORTS/PARAMETERS` 不会自动产生方程。
新增气动部件应传递现有 `properties` 上下文,使用 `native_medium_gas_context`、`native_temperature_ph_context`、`native_density_context` 及相应阀口/管路的 `_context` 接口。记录和有效标志由内核维护,不能由部件绕过检查直接改写。新增物性字段时应在内核中定义其输入条件与有效标志;不能因焓相同就沿用不同压力下的温度,也不能在多次试算之间建立未核对输入的全局缓存。旧无上下文接口用于兼容或独立计算,不作为新生成代码的默认路径。
新增能力至少检查:来源缺失、多个提供者、长参考链、组件与连接顺序打乱、正反流和零压差、两个独立循环互不重算、耦合压力/混合循环的质量能量守恒,以及独立数值基准。当前自动排序对象是已接入的内置方程,不接受任意外部 C 代码。
对应测试:`tests/test_native_schedule.py`、`tests/test_native_catalog.py`。本轮模型对照记录见 [计算排序验证记录](../other/C计算依赖排序验证-2026-09-10.md)。
物性复用和管流求根还由 `tests/test_native_properties.py`、`tests/test_native_pipe_physics.py` 覆盖。实现边界与实测结果见 [物性复用与管流求根实现及验证](../other/物性复用与管流求根实现及验证-2026-09-11.md)。
@@ -1,5 +1,7 @@
# 2026-09-11:C 管路验证版合入网页后端
当天后续更新:已将物性状态复用、等熵计算中的密度复用、焓直接计算,以及检查残差的管流求根接入正式C内核和两条生成路径。最终25项回归通过;MQL4纯求解中位耗时3.138→1.391 s,skill-test 0.255→0.139 s。已启动更新后的本机后台并以真实HTTP请求验证。详见 [实现与验证报告](../other/物性复用与管流求根实现及验证-2026-09-11.md)。下文为当天较早一次合入的历史记录。
后续网页检查补充:LSTP00A 接口力在新增碰撞事件点存在极短高峰,原有共同采样点回归没有覆盖该现象。详情及对原曲线一致性结论的限定见 [接口力对比报告](../other/test-mql-4网页仿真与LSTP00A接口力对比-2026-09-11.md)。
在 `system-optimization` 工作区,将 test-mql-4 验证版的修正合入正式原生编译和求解路径,并重启本机 `127.0.0.1:8000` 后端。网页通过 `5173` 端口代理调用的新仿真已经使用这些修正。未提交或推送 Git。
+168 -42
View File
@@ -16,6 +16,71 @@ static const double pr_a=.457235583*RU*RU*TC*TC/PC;
static const double pr_b=.07779607*RU*TC/PC;
static const double kappa=.37464+1.54226*OMEGA-.26992*OMEGA*OMEGA;
static const double rg=RU/MOLAR_MASS;
static const NativeMedium helium_medium={1,RU/MOLAR_MASS,2.5*RU/MOLAR_MASS,293.15,0,1.96e-5,293.15,79.4};
static int same_medium(const NativeMedium *a,const NativeMedium *b) {
return a->real_helium==b->real_helium && a->R==b->R && a->cp==b->cp &&
a->Tref==b->Tref && a->slope==b->slope && a->mu==b->mu && a->muT==b->muT && a->S==b->S;
}
void native_properties_init(NativePropertyCache *cache,NativePropertyState *states,size_t capacity) {
cache->states=states;cache->count=0;cache->capacity=states?capacity:0;
}
static NativePropertyState *property_new(NativePropertyCache *cache,const NativeMedium *m,
double p,double T,NativePropertyState *scratch) {
int valid=p>0 && T>0 && isfinite(p) && isfinite(T);
NativePropertyState *s=valid && cache && cache->count<cache->capacity ? &cache->states[cache->count++] : scratch;
*s=(NativePropertyState){0};s->medium=*m;s->p=p;s->T=T;s->valid=valid?NATIVE_PROPERTY_PT:0;
return s;
}
static NativePropertyState *property_pt(NativePropertyCache *cache,const NativeMedium *m,
double p,double T,NativePropertyState *scratch) {
if(cache)for(size_t i=0;i<cache->count;i++) {
NativePropertyState *s=&cache->states[i];
if((s->valid&NATIVE_PROPERTY_PT) && s->p==p && s->T==T && same_medium(&s->medium,m))return s;
}
return property_new(cache,m,p,T,scratch);
}
static double property_density(NativePropertyState *s) {
if(!(s->valid&NATIVE_PROPERTY_RHO)) {
s->rho=native_density(&s->medium,s->p,s->T);
if(s->rho>0 && isfinite(s->rho))s->valid|=NATIVE_PROPERTY_RHO;
}
return s->rho;
}
static double property_viscosity(NativePropertyState *s) {
if(!(s->valid&NATIVE_PROPERTY_MU)) {
s->mu=native_viscosity(&s->medium,s->T,0);
if(s->mu>0 && isfinite(s->mu))s->valid|=NATIVE_PROPERTY_MU;
}
return s->mu;
}
static void remember_gas(NativePropertyCache *cache,const NativeMedium *m,const NativeGas *gas) {
/* Below helium's critical temperature retain the existing vapor-root/PH
selection. A future phase-aware medium contract can carry that state. */
if(!cache || (m->real_helium && gas->T<=TC))return;
NativePropertyState scratch,*s=property_pt(cache,m,gas->p,gas->T,&scratch);
if(!(s->valid&NATIVE_PROPERTY_PT) || !isfinite(gas->h))return;
if((s->valid&NATIVE_PROPERTY_H) && s->h!=gas->h)
s=property_new(cache,m,gas->p,gas->T,&scratch);
s->h=gas->h;s->valid|=NATIVE_PROPERTY_H;
if(gas->rho>0 && isfinite(gas->rho)){s->rho=gas->rho;s->valid|=NATIVE_PROPERTY_RHO;}
}
double native_temperature_ph_context(NativePropertyCache *cache,const NativeMedium *m,double p,double h) {
if(cache)for(size_t i=0;i<cache->count;i++) {
NativePropertyState *s=&cache->states[i];
if((s->valid&NATIVE_PROPERTY_H) && s->p==p && s->h==h && same_medium(&s->medium,m))return s->T;
}
double T=native_temperature_ph(m,p,h);
if(cache && isfinite(h) && T>0 && isfinite(T) && p>0 && isfinite(p)) {
NativePropertyState scratch,*s=property_pt(cache,m,p,T,&scratch);
if((s->valid&NATIVE_PROPERTY_H) && s->h!=h)s=property_new(cache,m,p,T,&scratch);
s->h=h;s->valid|=NATIVE_PROPERTY_H;
}
return T;
}
double native_density_context(NativePropertyCache *cache,const NativeMedium *m,double p,double T) {
NativePropertyState scratch;return property_density(property_pt(cache,m,p,T,&scratch));
}
static double cube_root(double x) { return x==0 ? 0 : copysign(pow(fabs(x),1.0/3.0),x); }
static void attraction(double T, double *a, double *da, double *dda) {
@@ -68,39 +133,52 @@ static NativeGas gas_properties(double m,double U,double V) {
double T=fmax(temperature_u(g.u),2.2);
for(int i=0;i<16;i++) { double next=fmax(temperature_u(g.u-u_departure(T,g.rho)),2.2);
int done=fabs(next-T)<=1e-10*fmax(T,1);T=next;if(done) break; }
g.T=T;g.p=pressure_rho(T,g.rho);g.h=h_ideal(T)+h_departure(g.p,T);return g;
g.T=T;g.p=pressure_rho(T,g.rho);
/* Same energy reference as u_ideal/h_ideal. Avoid a redundant cubic solve
in the single-root region; preserve vapor-root semantics below TC. */
g.h=T>TC ? g.u+g.p/g.rho : h_ideal(T)+h_departure(g.p,T);return g;
}
static double temperature_ph(double p,double h) {
double T=fmax(temperature_h(h),2.2);
for(int i=0;i<16;i++) { double next=fmax(temperature_h(h-h_departure(p,T)),2.2);
int done=fabs(next-T)<=1e-10*fmax(T,1);T=next;if(done) break; }return T;
}
static void local_isentropic(double p,double T,double *factor,double *exponent) {
double rho=density(p,T),v=MOLAR_MASS/rho,a,da,dda;attraction(T,&a,&da,&dda);
static void local_isentropic(NativePropertyState *s) {
if(s->valid&NATIVE_PROPERTY_ISENTROPIC)return;
double p=s->p,T=s->T,rho=property_density(s),v=MOLAR_MASS/rho,a,da,dda;attraction(T,&a,&da,&dda);
double d=v*(v+pr_b)+pr_b*(v-pr_b);
double dpT=RU/(v-pr_b)-da/d;
double dpR=(-RU*T/pow(v-pr_b,2)+a*2*(v+pr_b)/(d*d))*(-MOLAR_MASS/(rho*rho));
double cv=1.5*rg+T*dda*log_volume(rho);
double cp=cv+T*dpT*dpT/(rho*rho*dpR),gamma=cp/cv;
*factor=p/(rho*dpR*gamma);*exponent=p*(gamma-1)/(gamma*T*dpT);
s->isentropic_factor=p/(rho*dpR*gamma);s->isentropic_exponent=p*(gamma-1)/(gamma*T*dpT);
if(isfinite(s->isentropic_factor) && isfinite(s->isentropic_exponent))s->valid|=NATIVE_PROPERTY_ISENTROPIC;
}
static double isentropic(double p,double T,double pd) {
double f,e,fd,ed;local_isentropic(p,T,&f,&e);if(pd>=p) return f;
double Td=fmax(T*pow(fmax(pd/p,1e-12),e),2.2);
local_isentropic(fmax(pd,1),Td,&fd,&ed);return .5*(f+fd);
static double isentropic(NativePropertyCache *cache,NativePropertyState *up,double pd) {
local_isentropic(up);if(pd>=up->p)return up->isentropic_factor;
double Td=fmax(up->T*pow(fmax(pd/up->p,1e-12),up->isentropic_exponent),2.2);
NativePropertyState scratch,*down=property_pt(cache,&up->medium,fmax(pd,1),Td,&scratch);
local_isentropic(down);return .5*(up->isentropic_factor+down->isentropic_factor);
}
static double subsonic_cm(double r,double gamma,double rho,double T,double p) {
return sqrt(fmax(2/(1-gamma)*rho*T/p*(pow(r,2*gamma)-pow(r,1+gamma)),0));
}
static void valve(double p,double pd,double T,double *cm,double *vel) {
p=fmax(p,1);pd=fmax(fmin(pd,p),0);T=fmax(T,1);
double g=fmax(1e-9,fmin(1-1e-9,isentropic(p,T,pd))),rho=fmax(density(p,T),1e-12);
static void state_valve(NativePropertyCache *cache,NativePropertyState *up,double pd,double *cm,double *vel) {
double p=up->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)),rho=fmax(property_density(up),1e-12);
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; }
}
static void medium_valve(NativePropertyCache *cache,const NativeMedium *m,double p,double pd,double T,double *cm,double *vel) {
NativePropertyState scratch,*up=property_pt(cache,m,fmax(p,1),fmax(T,1),&scratch);
state_valve(cache,up,pd,cm,vel);
}
int native_gas_init(double p, double T, double volume, double *mU) {
if (!(p > 0 && T >= 2.2 && volume > 0)) return 0;
@@ -115,14 +193,21 @@ int native_gas(double m, double U, double volume, NativeGas *gas) {
*gas = gas_properties(m, U, volume);
return gas->p > 0 && isfinite(gas->p) && isfinite(gas->h);
}
int native_gas_context(NativePropertyCache *cache,double m,double U,double V,NativeGas *gas) {
int ok=native_gas(m,U,V,gas);if(ok)remember_gas(cache,&helium_medium,gas);return ok;
}
int native_orifice(double p1, double p2, double h1, double h2,
double cq_area, double opening, double *flow,
double *cm, double *velocity) {
return native_orifice_context(NULL,p1,p2,h1,h2,cq_area,opening,flow,cm,velocity);
}
int native_orifice_context(NativePropertyCache *cache,double p1,double p2,double h1,double h2,
double cq_area,double opening,double *flow,double *cm,double *velocity) {
int forward = p1 >= p2;
double p = forward ? p1 : p2, pd = forward ? p2 : p1;
double T = temperature_ph(fmax(p, 1), forward ? h1 : h2);
valve(p, pd, T, cm, velocity);
double T = native_temperature_ph_context(cache,&helium_medium,fmax(p,1),forward?h1:h2);
medium_valve(cache,&helium_medium,p,pd,T,cm,velocity);
double sign = forward ? 1 : -1;
*velocity *= sign;
*flow = opening == 0 || fabs(p1-p2) <= 1e-8 ? 0 :
@@ -218,22 +303,18 @@ int native_medium_gas(const NativeMedium *medium, double m, double U, double V,
g->h=medium->cp*g->T+.5*medium->slope*dt*dt;
return g->T>0 && isfinite(g->p) && isfinite(g->h);
}
static void medium_valve(const NativeMedium *m, double p, double pd, double T, double *cm, double *vel) {
if (m->real_helium) { valve(p,pd,T,cm,vel); return; }
p=fmax(p,1); pd=fmax(fmin(pd,p),0); T=fmax(T,1);
double cp=m->cp+m->slope*(T-m->Tref);
double g=fmax(1e-9,fmin(1-1e-9,(cp-m->R)/cp)),rho=fmax(native_density(m,p,T),1e-12);
double r=pd/p,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; }
int native_medium_gas_context(NativePropertyCache *cache,const NativeMedium *m,double mass,double U,double V,NativeGas *g) {
int ok=native_medium_gas(m,mass,U,V,g);if(ok)remember_gas(cache,m,g);return ok;
}
int native_medium_orifice(const NativeMedium *m, double p1, double p2, double h1, double h2,
double area, double opening, double *q, double *cm, double *v) {
return native_medium_orifice_context(NULL,m,p1,p2,h1,h2,area,opening,q,cm,v);
}
int native_medium_orifice_context(NativePropertyCache *cache,const NativeMedium *m,double p1,double p2,double h1,double h2,
double area,double opening,double *q,double *cm,double *v) {
double p=fmax(p1,p2),pd=fmin(p1,p2),sign=p1>=p2?1:-1;
double T=fmax(native_temperature_ph(m,fmax(p,1),p1>=p2?h1:h2),1);
medium_valve(m,p,pd,T,cm,v);
double T=fmax(native_temperature_ph_context(cache,m,fmax(p,1),p1>=p2?h1:h2),1);
medium_valve(cache,m,p,pd,T,cm,v);
*q=fabs(p1-p2)<=1e-8?0:sign*area*opening*fmax(p,1)*(*cm)/sqrt(T);
*v*=fabs(opening)<=1e-12?0:sign;
return isfinite(*q) && isfinite(*cm) && isfinite(*v);
@@ -253,38 +334,77 @@ static double pipe_friction_prepared(double re, double rr, double rough_limit) {
static double pipe_friction(double re, double rr) {
return pipe_friction_prepared(re,rr,pipe_rough_limit(rr));
}
/* Derivative of the existing friction blend, local to this scalar solve. */
static double pipe_friction_derivative(double re,double rr,double rough,double *df) {
double lam=64/re,dl=-lam/re;
if(re<=89.96829989){*df=dl;return lam;}
double a=-1.8*log10(6.9/re),smooth=1/(a*a);
double ds=-2*smooth/a*1.8/(log(10.0)*re),turb=smooth,dt=ds;
if(rr>0){double r=re*rr,w=r*r/(r*r+180*180),dw=2*w*(1-w)/re;
turb+=w*(rough-smooth);dt=ds*(1-w)+dw*(rough-smooth);}
double z=pow((re-89.96829989)/2741.96700831,8.37293695),b=z/(1+z);
double db=8.37293695*b*(1-b)/(re-89.96829989);
*df=dl*(1-b)+b*dt+db*(turb-lam);return lam+b*(turb-lam);
}
double native_pipe_resistance(double K,double rr,double flow_per_re,NativePipeSolve *status) {
NativePipeSolve local={0,0,0,INFINITY};if(!status)status=&local;
*status=local;
if(!(K>=0 && rr>=0 && flow_per_re>0) || !isfinite(K) || !isfinite(rr) || !isfinite(flow_per_re))return NAN;
if(K/64<=89.96829989){status->converged=1;status->relative_residual=0;return K/64;}
double rough=pipe_rough_limit(rr),lo=0,hi=fmax(sqrt(K/.02),1),df;
for(int i=0;i<128 && hi*hi*pipe_friction_prepared(hi,rr,rough)<K;i++)hi*=2;
if(!isfinite(hi))return NAN;
double re=fmin(sqrt(K/.02),hi);
for(int i=0;i<80;i++) {
status->iterations=i+1;
double f=pipe_friction_derivative(re,rr,rough,&df),F=re*re*f-K;
double target=sqrt(K/f);
if(fabs(target-re)*flow_per_re<=fmax(1e-13,re*flow_per_re*1e-10)) {
/* Check the returned point, not just the change of successive guesses. */
status->relative_residual=fabs(target*target*pipe_friction_prepared(target,rr,rough)/K-1);
if(isfinite(target) && status->relative_residual<=1e-9){status->converged=1;return target;}
}
if(F>0)hi=re;else lo=re;
double next=re-F/(2*re*f+re*re*df);
if(!isfinite(next) || next<=lo || next>=hi){next=.5*(lo+hi);status->bisections++;}
re=next;
}
return NAN;
}
double native_pipe_flow(const NativeMedium *m, double p1, double p2, double T,
double d, double length, double rr, int kind) {
return native_pipe_flow_context(NULL,m,p1,p2,T,d,length,rr,kind);
}
double native_pipe_flow_context(NativePropertyCache *cache,const NativeMedium *m,double p1,double p2,double T,
double d,double length,double rr,int kind) {
if(fabs(p1-p2)<=1e-8) return 0;
double p=fmax(fmax(p1,p2),1),pd=fmin(p1,p2),sign=p1>p2?1:-1;
T=fmax(T,1);
double area=PI*d*d/4,mu=native_viscosity(m,T,0),den=PI*d*mu;
NativePropertyState scratch,*up=property_pt(cache,m,p,T,&scratch);
double area=PI*d*d/4,mu=property_viscosity(up),den=PI*d*mu;
/* PNL0001/2/3 share compressible flow and its near-equilibrium smoothing.
PNL0003 differs in storage placement, not in the resistance law. */
double cm,vel;medium_valve(m,p,pd,T,&cm,&vel);
double cm,vel;state_valve(cache,up,pd,&cm,&vel);
if(kind==0) {
double lam=pow(area*p*cm,2)/(16*PI*mu*length*T);
if(4*lam/den<=1000) return sign*lam;
}
double rough_limit=pipe_rough_limit(rr);
double base=area*p*cm/sqrt(T),q=sqrt(d/(length*.02))*base;
for(int i=0;i<(kind==0?64:16);i++) {
double next=sqrt(d/(length*pipe_friction_prepared(4*fabs(q)/den,rr,rough_limit)))*base;
if(fabs(next-q)<=fmax(1e-12,fabs(q)*1e-9)) return sign*next;
q=.5*(q+next);
}
return sign*q;
double base=area*p*cm/sqrt(T),K=pow(4*base/den,2)*d/length;
return sign*native_pipe_resistance(K,rr,den/4,NULL)*den/4;
}
double native_pipe_flow_cached(NativePipeCache *cache, const NativeMedium *m,
double p1, double p2, double T, double d,
double length, double rr, int kind) {
return native_pipe_flow_cached_context(NULL,cache,m,p1,p2,T,d,length,rr,kind);
}
double native_pipe_flow_cached_context(NativePropertyCache *properties,NativePipeCache *cache,const NativeMedium *m,
double p1,double p2,double T,double d,double length,double rr,int kind) {
if(cache->valid && cache->p1==p1 && cache->p2==p2 && cache->T==T &&
cache->diameter==d && cache->length==length && cache->roughness==rr && cache->kind==kind &&
cache->medium.real_helium==m->real_helium && cache->medium.R==m->R &&
cache->medium.cp==m->cp && cache->medium.Tref==m->Tref && cache->medium.slope==m->slope &&
cache->medium.mu==m->mu && cache->medium.muT==m->muT && cache->medium.S==m->S)
same_medium(&cache->medium,m))
return cache->flow;
double result=native_pipe_flow(m,p1,p2,T,d,length,rr,kind);
double result=properties?native_pipe_flow_context(properties,m,p1,p2,T,d,length,rr,kind):
native_pipe_flow(m,p1,p2,T,d,length,rr,kind);
cache->valid=0;
if(isfinite(result)) {
cache->medium=*m;cache->p1=p1;cache->p2=p2;cache->T=T;
@@ -295,10 +415,16 @@ double native_pipe_flow_cached(NativePipeCache *cache, const NativeMedium *m,
}
void native_pipe_diagnostics(const NativeMedium *m, double q, double p, double T,
double d, double length, double rr, int diagnostic, double *r) {
double area=PI*d*d/4,re=4*fabs(q)/(PI*d*native_viscosity(m,T,0)),ff=pipe_friction(re,rr);
r[0]=4*fabs(q)/(PI*d*native_viscosity(m,T,diagnostic));
native_pipe_diagnostics_context(NULL,m,q,p,T,d,length,rr,diagnostic,r);
}
void native_pipe_diagnostics_context(NativePropertyCache *cache,const NativeMedium *m,double q,double p,double T,
double d,double length,double rr,int diagnostic,double *r) {
(void)diagnostic;
NativePropertyState scratch,*state=property_pt(cache,m,p,T,&scratch);
double area=PI*d*d/4,re=4*fabs(q)/(PI*d*property_viscosity(state)),ff=pipe_friction(re,rr);
r[0]=re;
r[1]=fabs(q)*sqrt(T)/fmax(sqrt(d/(length*ff))*area*p,1e-18);
r[2]=q/(fmax(native_density(m,p,T),1e-12)*area);r[3]=ff;
r[2]=q/(fmax(property_density(state),1e-12)*area);r[3]=ff;
}
double native_limit_force(double penetration, double velocity, double stiffness,
double damping, double depth, int signed_force) {
+35
View File
@@ -1,5 +1,6 @@
#ifndef NATIVE_KERNELS_H
#define NATIVE_KERNELS_H
#include <stddef.h>
typedef struct { double p, T, rho, u, h; } NativeGas;
typedef struct {
int velocity_index;
@@ -8,6 +9,30 @@ typedef struct {
} NativeStop;
/* Constants are emitted per medium instance by the model compiler. */
typedef struct { int real_helium; double R, cp, Tref, slope, mu, muT, S; } NativeMedium;
/* Caller-owned scratch for ONE model_eval. Keys use exact values and a copy of
* every medium constant. No process/thread global cache or approximate reuse. */
enum {
NATIVE_PROPERTY_PT=1, NATIVE_PROPERTY_H=2, NATIVE_PROPERTY_RHO=4,
NATIVE_PROPERTY_MU=8, NATIVE_PROPERTY_ISENTROPIC=16
};
typedef struct {
NativeMedium medium;
double p, T, h, rho, mu, isentropic_factor, isentropic_exponent;
unsigned valid;
} NativePropertyState;
typedef struct {
NativePropertyState *states;
size_t count, capacity;
} NativePropertyCache;
void native_properties_init(NativePropertyCache *, NativePropertyState *, size_t capacity);
int native_gas_context(NativePropertyCache *, double m, double U, double V, NativeGas *);
int native_medium_gas_context(NativePropertyCache *, const NativeMedium *, double m, double U, double V, NativeGas *);
double native_temperature_ph_context(NativePropertyCache *, const NativeMedium *, double p, double h);
double native_density_context(NativePropertyCache *, const NativeMedium *, double p, double T);
int native_orifice_context(NativePropertyCache *, double p1, double p2, double h1, double h2,
double area, double opening, double *q, double *cm, double *v);
int native_medium_orifice_context(NativePropertyCache *, const NativeMedium *, double p1, double p2, double h1, double h2,
double area, double opening, double *q, double *cm, double *v);
/* One entry per pipe branch, zero-initialized for each model_eval. Never shared
* across solver trials. Exact inputs, including medium constants, form the key. */
typedef struct {
@@ -15,6 +40,16 @@ typedef struct {
double p1, p2, T, diameter, length, roughness, flow;
int kind, valid;
} NativePipeCache;
typedef struct { int converged, iterations, bisections; double relative_residual; } NativePipeSolve;
/* Solve Re^2*f(Re)=K for the shared pipe resistance law. On failure returns
* NAN, with converged=0; flow_per_re converts the local stopping test to kg/s. */
double native_pipe_resistance(double K, double roughness, double flow_per_re, NativePipeSolve *status);
double native_pipe_flow_context(NativePropertyCache *, const NativeMedium *, double p1, double p2, double T,
double diameter, double length, double roughness, int kind);
double native_pipe_flow_cached_context(NativePropertyCache *, NativePipeCache *, const NativeMedium *,
double p1, double p2, double T, double diameter, double length, double roughness, int kind);
void native_pipe_diagnostics_context(NativePropertyCache *, const NativeMedium *, double q, double p, double T,
double diameter, double length, double roughness, int diagnostic, double *result);
int native_medium_init(const NativeMedium *, double p, double T, double V, int legacy_ideal_initial, double *mU);
double native_density(const NativeMedium *, double p, double T);
double native_temperature_ph(const NativeMedium *, double p, double h);
+7 -5
View File
@@ -12,11 +12,13 @@ from tests.native_reference import reference_data, reference_network
def has_revised_pipe_law(case):
"""The frozen Python flow laws predate the September 2026 corrections."""
return any(c['type'] in ('amesim_pnl0001', 'amesim_pnl0003') or
(c['type'].startswith('amesim_pnl') and
c['medium']['type'] == 'AmesimHeliumPengRobinsonMedium')
for c in case['components'])
"""The frozen flow values include capped pipe iterates, not checked roots.
All PNL media now share the residual-checked scalar solver. Keep the frozen
thermodynamic oracle; the pipe suite checks the actual resistance equation
against independent bisection as well as system-level Amesim curves.
"""
return any(c['type'].startswith('amesim_pnl') for c in case['components'])
class Circuit:
+49
View File
@@ -68,6 +68,55 @@ int main(void) {
run=subprocess.run([str(exe)],capture_output=True,text=True,timeout=15)
self.assertEqual(run.returncode,0,run.stderr)
def test_scalar_pipe_solver_checks_residual_and_bracket_fallback(self):
harness = r'''
#include <stdio.h>
#define CHECK(x) do { if(!(x)){fprintf(stderr,"line %d\n",__LINE__);return 1;} } while(0)
static double reference(double K,double rr) {
double lo=0,hi=fmax(sqrt(K/.02),1);
while(hi*hi*pipe_friction(hi,rr)<K)hi*=2;
for(int i=0;i<100;i++) {
double mid=.5*(lo+hi);
if(mid*mid*pipe_friction(mid,rr)>K)hi=mid;else lo=mid;
}
return .5*(lo+hi);
}
int main(void) {
NativePipeSolve s;int fallbacks=0;
for(int i=0;i<=150;i++)for(int r=0;r<6;r++)for(int scale=0;scale<3;scale++) {
double re=pow(10,-6+.1*i),rr=r==0?0:pow(10,-6+r);
double K=re*re*pipe_friction(re,rr),q_per_re=pow(10,-12+6*scale);
double actual=native_pipe_resistance(K,rr,q_per_re,&s),expected=reference(K,rr);
CHECK(s.converged && isfinite(actual) && s.relative_residual<=1e-9);
CHECK(s.iterations<20 && fabs(actual-expected)<=1e-12+expected*1e-9);
CHECK(fabs(actual*actual*pipe_friction(actual,rr)/K-1)<=1e-9);
fallbacks+=s.bisections;
}
CHECK(fallbacks>0);
CHECK(native_pipe_resistance(0,0,1,&s)==0 && s.converged);
CHECK(isnan(native_pipe_resistance(-1,0,1,&s)) && !s.converged);
CHECK(isnan(native_pipe_resistance(NAN,0,1,&s)) && !s.converged);
CHECK(isnan(native_pipe_resistance(1,0,0,&s)) && !s.converged);
/* Direct laminar handling preserves the distinct PNL00R branch. */
NativeMedium medium={0,287,1005,300,0,1.8e-5,300,110.4};
double p=2e5,T=300,d=.01,L=1,cm,v;
medium_valve(NULL,&medium,p,p-1,T,&cm,&v);
double expected=pow(PI*d*d/4*p*cm,2)/(16*PI*native_viscosity(&medium,T,0)*L*T);
CHECK(4*expected/(PI*d*native_viscosity(&medium,T,0))<1000);
CHECK(native_pipe_flow(&medium,p,p-1,T,d,L,1e-5,0)==expected);
return 0;
}
'''
with tempfile.TemporaryDirectory(prefix='native-pipe-root-') as tmp:
directory=Path(tmp);source=directory/'check.c';exe=directory/'check.exe'
source.write_text((ROOT/'native/components/kernels.c').read_text()+harness)
build=subprocess.run([self.compiler,'-std=c11','-O3','-Wall','-Wextra','-Werror',
'-ffp-contract=off','-fno-fast-math','-static-libgcc','-I',str(ROOT/'native/include'),
str(source),'-lm','-o',str(exe)],capture_output=True,text=True,timeout=60)
self.assertEqual(build.returncode,0,build.stderr)
run=subprocess.run([str(exe)],capture_output=True,text=True,timeout=30)
self.assertEqual(run.returncode,0,run.stderr)
def test_pnl0001_uses_upstream_temperature_in_both_directions(self):
for reverse in (False,True):
with self.subTest(reverse=reverse):
+136
View File
@@ -0,0 +1,136 @@
"""Property reuse must preserve state identity, trial isolation and EOS physics."""
from pathlib import Path
import subprocess
import tempfile
import unittest
from app.simulation.native_codegen.build import toolchain
ROOT = Path(__file__).resolve().parents[1]
class NativePropertyTests(unittest.TestCase):
def test_shared_properties_and_invalidation(self):
try:
compiler = toolchain()[0]
except (OSError, RuntimeError, subprocess.SubprocessError) as exc:
self.skipTest(f"Native toolchain unavailable: {exc}")
source = (ROOT / 'native/components/kernels.c').read_text()
for signature, counter in (
('static double temperature_ph(double p,double h) {', 'ph_calls'),
('static double z_factor(double p,double T) {', 'z_calls'),
('double native_viscosity(const NativeMedium *m, double T, int diagnostic) {', 'mu_calls'),
):
self.assertEqual(source.count(signature), 1)
source = source.replace(signature, signature + f'++{counter};')
source = 'static int ph_calls,z_calls,mu_calls;\n' + source
harness = r'''
#include <stdio.h>
#define CHECK(x) do { if(!(x)){fprintf(stderr,"line %d\n",__LINE__);return 1;} } while(0)
static int close_to(double a,double b,double atol,double rtol) {
return isfinite(a) && isfinite(b) && fabs(a-b)<=atol+rtol*fmax(fabs(a),fabs(b));
}
int main(void) {
NativeMedium m=helium_medium;
NativePropertyState states[32],other_states[2];NativePropertyCache cache,other;
native_properties_init(&cache,states,32);native_properties_init(&other,other_states,2);
double y[2];NativeGas gas;
CHECK(native_medium_init(&m,1.5e7,293.15,.01,0,y));
int before=z_calls;
CHECK(native_medium_gas_context(&cache,&m,y[0],y[1],.01,&gas));
CHECK(z_calls==before); /* h=u+p/rho does not solve the cubic again. */
CHECK(gas.h==gas.u+gas.p/gas.rho);
before=ph_calls;
CHECK(native_temperature_ph_context(&cache,&m,gas.p,gas.h)==gas.T && ph_calls==before);
NativeMedium copy=m;
CHECK(native_temperature_ph_context(&cache,&copy,gas.p,gas.h)==gas.T && ph_calls==before);
double q,cm,v;before=z_calls;
CHECK(native_medium_orifice_context(&cache,&m,gas.p,.6*gas.p,gas.h,gas.h,.001,.5,&q,&cm,&v));
CHECK(z_calls==before+1); /* Only the different downstream state needs rho. */
CHECK(native_medium_orifice_context(&cache,&m,gas.p,.6*gas.p,gas.h,gas.h,.001,.5,&q,&cm,&v));
CHECK(z_calls==before+1);
before=mu_calls;
q=native_pipe_flow_context(&cache,&m,gas.p,.6*gas.p,gas.T,.01,1,1e-5,1);
CHECK(isfinite(q) && mu_calls==before+1);
double diag[4];native_pipe_diagnostics_context(&cache,&m,q,gas.p,gas.T,.01,1,1e-5,1,diag);
CHECK(mu_calls==before+1);
/* Changed pressure (even one ULP), mixed enthalpy and every medium field
must force a fresh PH evaluation. Equal h alone is not a state key. */
double pressures[]={nextafter(gas.p,INFINITY),.2*gas.p,gas.p};
for(int i=0;i<3;i++) {
double h=gas.h+(i==2?1000:0),expected=native_temperature_ph(&m,pressures[i],h);
before=ph_calls;
CHECK(native_temperature_ph_context(&cache,&m,pressures[i],h)==expected);
CHECK(ph_calls==before+1);
CHECK(native_temperature_ph_context(&cache,&m,pressures[i],h)==expected && ph_calls==before+1);
}
CHECK(fabs(native_temperature_ph(&m,.2*gas.p,gas.h)-gas.T)>1e-3);
double positive_h=gas.h+2e6;
(void)native_temperature_ph_context(&cache,&m,gas.p,positive_h);
for(int i=0;i<8;i++) {
copy=m;
double *fields[]={&copy.R,&copy.cp,&copy.Tref,&copy.slope,&copy.mu,&copy.muT,&copy.S};
if(i==7)copy.real_helium=0;else *fields[i]+=fmax(fabs(*fields[i])*.001,.00001);
double expected=native_temperature_ph(&copy,gas.p,positive_h);
size_t count=cache.count;
CHECK(native_temperature_ph_context(&cache,&copy,gas.p,positive_h)==expected);
CHECK(cache.count>count);
}
/* Separate contexts, a new trial, and exhausted capacity remain correct. */
before=ph_calls;
CHECK(isfinite(native_temperature_ph_context(&other,&m,gas.p,gas.h)) && ph_calls==before+1);
native_properties_init(&cache,states,1);
CHECK(native_medium_gas_context(&cache,&m,y[0],y[1],.01,&gas));
for(int i=1;i<30;i++) {
double p=gas.p*(1+i*.01),h=gas.h+i;
CHECK(native_temperature_ph_context(&cache,&m,p,h)==native_temperature_ph(&m,p,h));
CHECK(cache.count==1);
}
native_properties_init(&cache,states,32);before=ph_calls;
CHECK(isfinite(native_temperature_ph_context(&cache,&m,gas.p,gas.h)) && ph_calls==before+1);
size_t count=cache.count;
(void)native_temperature_ph_context(&cache,&m,NAN,gas.h);
CHECK(cache.count==count);
/* Thermodynamic round trips use an independent PT enthalpy formula.
Ideal gases include temperature-dependent heat capacity. */
for(int ideal=0;ideal<2;ideal++) {
NativeMedium fluid=ideal?(NativeMedium){0,287,1005,300,.2,1.8e-5,300,110.4}:m;
for(double T=100;T<=1000;T+=100)for(double p=1e4;p<=1e8;p*=10) {
native_properties_init(&cache,states,32);
CHECK(native_medium_init(&fluid,p,T,.01,0,y));
CHECK(native_medium_gas_context(&cache,&fluid,y[0],y[1],.01,&gas));
double ref_h=ideal?fluid.cp*T+.5*fluid.slope*pow(T-fluid.Tref,2):h_ideal(T)+h_departure(p,T);
CHECK(close_to(gas.h,ref_h,2e-5,1e-9));
CHECK(close_to(gas.p,p,.001,1e-9));
CHECK(close_to(gas.T,T,1e-7,1e-9));
CHECK(close_to(native_temperature_ph(&fluid,gas.p,gas.h),gas.T,1e-7,1e-9));
double oldq,oldcm,oldv;
CHECK(native_medium_orifice(&fluid,gas.p,.8*gas.p,gas.h,gas.h,.001,.5,&oldq,&oldcm,&oldv));
CHECK(native_medium_orifice_context(&cache,&fluid,gas.p,.8*gas.p,gas.h,gas.h,.001,.5,&q,&cm,&v));
CHECK(close_to(q,oldq,1e-10,2e-9) && close_to(v,oldv,1e-7,2e-9));
}
}
/* Subcritical helium retains the pre-existing vapor-root selection. */
native_properties_init(&cache,states,32);
CHECK(native_gas_init(1e4,3,.01,y));
CHECK(native_medium_gas_context(&cache,&m,y[0],y[1],.01,&gas));
CHECK(gas.T<TC && cache.count==0);
CHECK(gas.h==h_ideal(gas.T)+h_departure(gas.p,gas.T));
return 0;
}
'''
with tempfile.TemporaryDirectory(prefix='native-properties-') as tmp:
folder=Path(tmp);c_file=folder/'properties.c';exe=folder/'properties.exe'
c_file.write_text(source+harness)
compiled=subprocess.run([compiler,'-std=c11','-O3','-Wall','-Wextra','-Werror',
'-ffp-contract=off','-fno-fast-math','-static-libgcc','-I',str(ROOT/'native/include'),
str(c_file),'-lm','-o',str(exe)],capture_output=True,text=True,timeout=60)
self.assertEqual(compiled.returncode,0,compiled.stderr)
run=subprocess.run([str(exe)],capture_output=True,text=True,timeout=30)
self.assertEqual(run.returncode,0,run.stderr)
if __name__ == '__main__':
unittest.main()