前端进度条性能优化、仿真结束后后处理优化;后端C代码生成流程优化:先识别来源,再按照已知未知量需求排序,最后局部求解
This commit is contained in:
1 parent
0dcb465d84
commit
91bd9fb252
77 files changed
+11223
-551
No files matched your search
@@ -12,6 +12,7 @@ from math import isfinite
|
||||
from app.simulation.core.metadata import ResultVariableMetadata
|
||||
from app.simulation.systems.network import SimulationNetwork
|
||||
from .contracts import SUPPORTED_TYPES, SUPPORTED_VERSIONS
|
||||
from .tolerances import state_absolute_tolerance
|
||||
|
||||
|
||||
class NativeCapabilityError(ValueError):
|
||||
@@ -33,6 +34,7 @@ class NativeProgram:
|
||||
state_keys: tuple[str, ...]
|
||||
variables: tuple[ResultVariableMetadata, ...]
|
||||
component_types: tuple[str, ...]
|
||||
evaluation_schedule: dict | None = None
|
||||
|
||||
def manifest(self) -> dict:
|
||||
return {
|
||||
@@ -41,6 +43,7 @@ class NativeProgram:
|
||||
"componentTypes": self.component_types,
|
||||
"componentVersions": {name: SUPPORTED_VERSIONS[name] for name in self.component_types},
|
||||
"jacobianPolicy": "CVODE default; no custom Jacobian",
|
||||
"evaluationSchedule": self.evaluation_schedule or {"strategy": "storage-anchored", "cyclicBlockCount": 0},
|
||||
}
|
||||
|
||||
|
||||
@@ -65,6 +68,7 @@ def _number(value):
|
||||
|
||||
|
||||
def compile_native_program(network: SimulationNetwork) -> NativeProgram:
|
||||
network.validate_port_supplies()
|
||||
from .extended import catalog_contracts
|
||||
contracts = catalog_contracts()
|
||||
for component in network.components.values():
|
||||
@@ -361,7 +365,7 @@ def _compile_storage_anchored_program(network: SimulationNetwork) -> NativeProgr
|
||||
source = '\n'.join([
|
||||
'#include "model.h"', '#include <math.h>', *declarations,
|
||||
f"const NativeStop model_stops[{max(1,len(stops))}] = {{{stop_c}}};",
|
||||
f"const double model_atol[NSTATES] = {{{','.join('1e-12' if k.rsplit('.',1)[1] in ('v','x') else '1e-8' for k in state_keys)}}};",
|
||||
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,
|
||||
|
||||
@@ -6,10 +6,13 @@ All thermodynamics, flow/stream closure and derivatives execute in the EXE.
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from importlib import import_module
|
||||
|
||||
from .compiler import NativeCapabilityError, NativeProgram, _Groups, _number as num
|
||||
from .contracts import SUPPORTED_VERSIONS
|
||||
from .schedule import Computation, EvaluationSchedule, references
|
||||
from .tolerances import state_absolute_tolerance
|
||||
|
||||
|
||||
GAS_TYPES = {'amesim_pnch023', 'amesim_pnch012', 'amesim_pnl0001',
|
||||
@@ -30,7 +33,7 @@ def catalog_contracts():
|
||||
return result
|
||||
|
||||
|
||||
def linear_schedule(equations, unknowns, free_flows=()):
|
||||
def linear_assignments(equations, unknowns, free_flows=()):
|
||||
"""Eliminate constant coefficients; retain RHS expressions as C temporaries.
|
||||
|
||||
Exact pivot elimination avoids a numeric pseudoinverse and its tiny spurious
|
||||
@@ -68,15 +71,21 @@ def linear_schedule(equations, unknowns, free_flows=()):
|
||||
else:
|
||||
target[k] = value
|
||||
pivots.append(key)
|
||||
lines = [f'double b{i} = {expr};' for i, (_, expr) in enumerate(equations)
|
||||
lines = [(f'b{i}', expr) for i, (_, expr) in enumerate(equations)
|
||||
if any(i in rows[j][1] for j in range(len(pivots)))]
|
||||
for j, key in enumerate(pivots):
|
||||
expr = ' + '.join(f'({num(v)})*b{i}' for i, v in rows[j][1].items()) or '0.0'
|
||||
lines.append(f'{key} = {expr};')
|
||||
lines.append((key, expr))
|
||||
return lines
|
||||
|
||||
|
||||
def linear_schedule(equations, unknowns, free_flows=()):
|
||||
return [f'{"double " if key.startswith("b") else ""}{key} = {expr};'
|
||||
for key, expr in linear_assignments(equations, unknowns, free_flows)]
|
||||
|
||||
|
||||
def compile_extended_program(network):
|
||||
network.validate_port_supplies()
|
||||
components = list(network.components.values())
|
||||
contracts = catalog_contracts()
|
||||
for c in components:
|
||||
@@ -297,17 +306,19 @@ def compile_extended_program(network):
|
||||
coupled.append((root,offsets,volumes))
|
||||
for root, gas in anchor.items():
|
||||
lines.append(f'p[{pgi[root]}]={gas}.p;')
|
||||
default_h = next(iter(gases.values()))+'.h' if gases else '0.0'
|
||||
for endpoint, idx in pi.items():
|
||||
lines.append(f'h[{idx}]={port_gas.get(endpoint,default_h.removesuffix(".h"))}.h;' if endpoint in port_gas else f'h[{idx}]={default_h};')
|
||||
for c in components:
|
||||
if c.model_type in {'amesim_pnpl01','amesim_pnrp17'}:
|
||||
# Closed-end ports retain their declared zero outflow enthalpy.
|
||||
lines.append(f'{h(c,"port_1")}=0.0;')
|
||||
h_initial = {f'h[{pi[endpoint]}]': gas+'.h' for endpoint,gas in port_gas.items()}
|
||||
|
||||
flow_lines, flow_known, flow_eq = [], set(), []
|
||||
operations, flow_known, flow_eq = [], set(), []
|
||||
pipe_cache_count = 0
|
||||
def pipe_flow(expression):
|
||||
nonlocal pipe_cache_count
|
||||
result = f'native_pipe_flow_cached(&pipe_cache[{pipe_cache_count}],{expression})'
|
||||
pipe_cache_count += 1
|
||||
return result
|
||||
def flow(c, name, expr):
|
||||
target=q(c,name);flow_lines.append(f'{target}={expr};');flow_known.add(target)
|
||||
target=q(c,name)
|
||||
operations.append(Computation.assignment(f'flow:{c.name}.{name}',target,expr))
|
||||
flow_known.add(target)
|
||||
def flow_equation(terms):
|
||||
flow_eq.append(({q(c,name):coef for c,name,coef in terms},'0.0'))
|
||||
for c in components:
|
||||
@@ -331,23 +342,26 @@ def compile_extended_program(network):
|
||||
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})')
|
||||
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)})'
|
||||
flow(c,a,f'native_pipe_flow({medium(c)},{pa},{pb},{T},{num(c.diam)},{num(c.le)},{num(c.rr)},0)')
|
||||
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'
|
||||
if kind!='amesim_pnor001':
|
||||
value = f'fmax(0,fmin(1,{w(c,"res.signal")}))' if kind=='amesim_pnvo001' else num(c.opening)
|
||||
put(c,'xv',value)
|
||||
area = c.effective_cq*(c.effective_area if kind=='amesim_pnor001' else c.maximum_area)
|
||||
flow_lines.append(f'if(!native_medium_orifice({medium(c)},{pa},{pb},{hin(c,a)},{hin(c,b)},{num(area)},{opening},&{q(c,a)},&{w(c,"cm")},&{w(c,"gasvel")})) return 0;')
|
||||
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;'
|
||||
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)}')
|
||||
elif kind in ('amesim_pnl0001','amesim_pnl0002'):
|
||||
gas=gases[c.name,0]
|
||||
for name in (['port_1'] if kind.endswith('1') else names):
|
||||
T=gas+'.T'
|
||||
if kind.endswith('2'):
|
||||
T=f'({p(c,name)}>{gas}.p?native_temperature_ph({medium(c)},fmax({p(c,name)},1),{hin(c,name,True)}):{gas}.T)'
|
||||
flow(c,name,f'native_pipe_flow({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)')
|
||||
# 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)'
|
||||
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':
|
||||
flow_eq.append(({f'q[{pi[e.key]}]':1 for e in edge.endpoints},'0.0'))
|
||||
@@ -357,46 +371,41 @@ def compile_extended_program(network):
|
||||
known=''.join(f'-({num(v)})*{k}' for k,v in terms.items() if k in flow_known)
|
||||
reduced.append(({k:v for k,v in terms.items() if k not in flow_known},rhs+known))
|
||||
free_flows={f'q[{pi[e]}]' for root,_,_ in coupled for e in pneu if groups.find(e)==root}
|
||||
flow_lines += linear_schedule(reduced,unknownq,free_flows)
|
||||
flow_bindings=linear_assignments(reduced,unknownq,free_flows)
|
||||
flow_temporary_count=max((int(key[1:])+1 for key,_ in flow_bindings if key.startswith('b')),default=0)
|
||||
for key,expr in flow_bindings:
|
||||
target=re.sub(r'\bb(\d+)\b',r'fb[\1]',key)
|
||||
value=re.sub(r'\bb(\d+)\b',r'fb[\1]',expr)
|
||||
operations.append(Computation.assignment('connection:'+key,target,value,'linear'))
|
||||
|
||||
unknownp=[root for root in pgroups if root not in anchor]
|
||||
residuals=[]
|
||||
for root in unknownp:
|
||||
terms=[f'q[{pi[e]}]' for e in pneu if groups.find(e)==root and f'q[{pi[e]}]' in flow_known]
|
||||
if not terms:
|
||||
raise NativeCapabilityError('Unanchored pneumatic pressure group has no constitutive flow relation')
|
||||
residuals.append(' + '.join(terms))
|
||||
# A monotone nodal mass-balance solve. This is an algebraic connection
|
||||
# closure; the integration solver's Jacobian policy is unchanged.
|
||||
pressure_lines=[]
|
||||
if unknownp:
|
||||
if not anchor:
|
||||
raise NativeCapabilityError('Pneumatic pressure network has no storage pressure anchor')
|
||||
pressure_lines += ['double plo=INFINITY,phi=0;', *[f'plo=fmin(plo,{g}.p);phi=fmax(phi,{g}.p);' for g in anchor.values()]]
|
||||
pressure_lines += [f'p[{pgi[root]}]=.5*(plo+phi);' for root in unknownp]
|
||||
pressure_lines += ['int pressure_ok=0;', 'for(int sweep=0;sweep<256;sweep++) {']
|
||||
for root,expr in zip(unknownp,residuals):
|
||||
pressure_lines += ['{ double lo=plo,hi=phi;', 'for(int bisect=0;bisect<48;bisect++) {',f'p[{pgi[root]}]=.5*(lo+hi);',
|
||||
'if(!model_flows(p,h,g,w,q)) return 0;', f'if(({expr})>0) hi=p[{pgi[root]}];else lo=p[{pgi[root]}];','}}']
|
||||
pressure_lines += ['if(!model_flows(p,h,g,w,q)) return 0;', 'double residual=0;', *[f'residual=fmax(residual,fabs({expr}));' for expr in residuals],
|
||||
'if(residual<1e-11) { pressure_ok=1;break; }','}', 'if(!pressure_ok) return 0;']
|
||||
else:
|
||||
pressure_lines=['if(!model_flows(p,h,g,w,q)) return 0;']
|
||||
expr=' + '.join(terms)
|
||||
operations.append(Computation('pressure:'+str(root),(f'p[{pgi[root]}]',),references(expr),(),'pressure',expr))
|
||||
|
||||
stream_lines=[]
|
||||
for c in components:
|
||||
names=pnames(c);kind=c.model_type
|
||||
if kind in {'amesim_pnpl01','amesim_pnrp17'}:
|
||||
stream_lines.append(f'{h(c,"port_1")}={hin(c,"port_1")};')
|
||||
operations.append(Computation.assignment(f'alias:{c.name}.port_1',h(c,'port_1'),hin(c,'port_1'),'alias'))
|
||||
elif kind in RESISTORS:
|
||||
a,b=names
|
||||
stream_lines += [f'{h(c,a)}={hin(c,b)};',f'{h(c,b)}={hin(c,a)};']
|
||||
for name,other in ((a,b),(b,a)):
|
||||
operations.append(Computation.assignment(f'alias:{c.name}.{name}',h(c,name),hin(c,other),'alias'))
|
||||
elif kind in NODES:
|
||||
stream_lines += ['{ double total=0,energy=0,average=0;', *[f'if({q(c,name)}>1e-12) {{total+={q(c,name)};energy+={q(c,name)}*{hin(c,name)};}} average+={hin(c,name)};' for name in names]]
|
||||
if kind!='tee':
|
||||
# Reference copies have no flow dependency. Keep them separate
|
||||
# from the reference port's returned energy/mixing calculation.
|
||||
for name in names:
|
||||
if name!='port_2':
|
||||
operations.append(Computation.assignment(f'alias:{c.name}.{name}',h(c,name),hin(c,'port_2'),'alias'))
|
||||
stream_lines = ['{ double total=0,energy=0,average=0;', *[f'if({q(c,name)}>1e-12) {{total+={q(c,name)};energy+={q(c,name)}*{hin(c,name)};}} average+={hin(c,name)};' for name in names]]
|
||||
if kind=='tee':
|
||||
stream_lines += [f'double mixed=total>1e-12?energy/total:average/{len(names)};', *[f'{h(c,name)}=mixed;' for name in names]]
|
||||
else:
|
||||
stream_lines += [f'double ref={hin(c,"port_2")},mixed=total>1e-12?energy/total:ref;', *[f'{h(c,name)}=ref;' for name in names if name!='port_2'],f'{h(c,"port_2")}=mixed;',f'if({q(c,"port_2")}<0) {{ double e=0,scale=0;',
|
||||
stream_lines += [f'double ref={hin(c,"port_2")},mixed=total>1e-12?energy/total:ref;', f'{h(c,"port_2")}=mixed;',f'if({q(c,"port_2")}<0) {{ double e=0,scale=0;',
|
||||
*[f'e+={q(c,name)}*({q(c,name)}>1e-12?{hin(c,name)}:ref);scale+=fabs({q(c,name)});' for name in names if name!='port_2'],
|
||||
f'double flow={q(c,"port_2")},transition=fmax(.05*scale,1e-12);',
|
||||
'double inv=-flow>=transition?1/flow:flow*(2*transition*transition-flow*flow)/pow(transition,4);',
|
||||
@@ -405,10 +414,34 @@ def compile_extended_program(network):
|
||||
if c.name+'.T' in slots:
|
||||
raise NativeCapabilityError('Unexpected node temperature output contract')
|
||||
stream_lines += ['}']
|
||||
if pneu:
|
||||
lines += ['int closure_ok=0;',f'for(int closure=0;closure<{max(64,4*len(pneu))};closure++) {{',f'double previous[{len(pneu)}];',f'for(int i=0;i<{len(pneu)};i++) previous[i]=h[i];',*pressure_lines,*stream_lines,
|
||||
'double change=0;',f'for(int i=0;i<{len(pneu)};i++) change=fmax(change,fabs(h[i]-previous[i])/fmax(1,fabs(h[i])));',
|
||||
'if(change<1e-12) {closure_ok=1;break;}','}', 'if(!closure_ok) return 0;', 'if(!model_flows(p,h,g,w,q)) return 0;']
|
||||
outputs=tuple(h(c,name) for name in (names if kind=='tee' else ['port_2']))
|
||||
inputs=frozenset(q(c,name) for name in names)|frozenset(hin(c,name) for name in names)
|
||||
operations.append(Computation(f'stream:{c.name}',outputs,inputs,tuple(stream_lines),'stream'))
|
||||
|
||||
# Current state/parameter/signal values are available before this phase.
|
||||
# Default h guesses are deliberately absent from the known-source map.
|
||||
labels={f'w[{i}]': key for key,i in slots.items()}
|
||||
known={f'w[{slots[key]}]': 'prepared:'+key for key in assigned}
|
||||
gas_origins={gas: 'state:'+','.join(f'{name}.{field}{str(half) if half else ""}' for field in ('m','U'))
|
||||
for (name,half),gas in gases.items()}
|
||||
for gas,origin in gas_origins.items():
|
||||
for field in ('p','T','rho','u','h'):
|
||||
known[gas+'.'+field]=origin
|
||||
for root,gas in anchor.items():
|
||||
known[f'p[{pgi[root]}]']=gas_origins[gas]
|
||||
for endpoint,gas in port_gas.items():
|
||||
known[f'h[{pi[endpoint]}]']=gas_origins[gas]
|
||||
for endpoint,index in pi.items():
|
||||
labels[f'h[{index}]']='.'.join(endpoint)+'.h_outflow'
|
||||
labels[f'q[{index}]']='.'.join(endpoint)+'.m_flow'
|
||||
for root,index in pgi.items():
|
||||
labels[f'p[{index}]']='pressure:'+','.join('.'.join(ep) for ep in pneu if groups.find(ep)==root)
|
||||
schedule=EvaluationSchedule(operations,known,labels)
|
||||
for target,expr in h_initial.items():
|
||||
if target not in schedule.producers:
|
||||
lines.append(f'{target}={expr};')
|
||||
schedule_helpers, scheduled_lines=schedule.emit()
|
||||
lines += scheduled_lines
|
||||
for c in components:
|
||||
for name in pnames(c):
|
||||
for field,expr in [('p',p(c,name)),('m_flow',q(c,name)),('h_outflow',h(c,name))]:
|
||||
@@ -510,7 +543,11 @@ def compile_extended_program(network):
|
||||
diag=[(flow,f'fmax(fmax({a}.p,{b}.p),1)',f'({flow}>=0?{a}.T:{b}.T)',c.le,1)]
|
||||
else:
|
||||
pa,pb=p(c,'port_1'),p(c,'port_2');pp=f'fmax(fmax({pa},{pb}),1)'
|
||||
temp=gases[c.name,0]+'.T' if kind=='amesim_pnl0001' else f'fmax(native_temperature_ph({medium(c)},{pp},{pa}>={pb}?{hin(c,"port_1",True)}:{hin(c,"port_2",True)}),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)'
|
||||
else:
|
||||
temp=f'fmax(native_temperature_ph({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:
|
||||
@@ -532,15 +569,16 @@ def compile_extended_program(network):
|
||||
np,ng,nq=max(1,len(pgroups)),max(1,gas_count),max(1,len(pneu))
|
||||
source='\n'.join(['#include "model.h"','#include <math.h>',*declarations,
|
||||
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}')+'};',
|
||||
'const double model_atol[NSTATES] = {'+','.join('1e-12' if k.endswith(('.v','.x')) else '1e-8' for k in state_keys or ['dummy'])+'};',
|
||||
'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 '""')+'};',
|
||||
'static int model_flows(const double *p,const double *h,const NativeGas *g,double *w,double *q) {',
|
||||
'(void)p;(void)h;(void)g;(void)w;(void)q;',*flow_lines,'return 1;}',
|
||||
*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) {',
|
||||
*(['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}];',
|
||||
'(void)t;(void)y;(void)w;(void)p;(void)h;(void)q;(void)g;(void)model_flows;',*lines,
|
||||
f'double fb[{max(1,flow_temporary_count)}]={{0}};(void)fb;',
|
||||
*([f'NativePipeCache pipe_cache[{max(1,pipe_cache_count)}]={{0}};(void)pipe_cache;'] if pneu else []),
|
||||
'(void)t;(void)y;(void)w;(void)p;(void)h;(void)q;(void)g;',*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;}',
|
||||
'double model_next_break(double t,double end) { double result=end;(void)t;',*breaks,'return result;}',''])
|
||||
@@ -558,4 +596,4 @@ int model_eval(double t,const double *y,double *dy,double *w);
|
||||
double model_next_break(double t,double end);
|
||||
#endif
|
||||
'''
|
||||
return NativeProgram(source,header,tuple(state_keys),variables,tuple(sorted({c.model_type for c in components})))
|
||||
return NativeProgram(source,header,tuple(state_keys),variables,tuple(sorted({c.model_type for c in components})),schedule.report())
|
||||
@@ -22,7 +22,7 @@ def execute_native(build: NativeBuild, config: SolveIVPConfig, sample_step: floa
|
||||
if config.method not in ("RK45", "BDF"):
|
||||
raise NativeCapabilityError(f"Native v1 does not support method {config.method}.")
|
||||
if not isinstance(config.atol, (int, float)) or config.atol != 1e-8 or config.first_step is not None:
|
||||
raise NativeCapabilityError("Native v1 uses the existing default gas/mechanical absolute tolerances and automatic initial step.")
|
||||
raise NativeCapabilityError("Native uses generated per-state absolute tolerances and automatic initial step; custom config.atol/first_step are not supported.")
|
||||
run_dir.mkdir(parents=True, exist_ok=True)
|
||||
output = run_dir / "result.json"
|
||||
cancel_path = run_dir / "cancel.request"
|
||||
@@ -123,6 +123,6 @@ def simulate_native(network, config, *, sample_step, progress_callback=None,
|
||||
simulated_until=data["simulatedUntil"], requested_stop_time=config.t_stop,
|
||||
variables=program.variables, series=data["series"], final=data["final"],
|
||||
diagnostics={"backend": "native-c", "native": {k: v for k, v in data.items()
|
||||
if k not in ("series", "final", "finalState")}, "integration": {"method": config.method, "totals": totals},
|
||||
if k not in ("series", "final", "finalState")}, "integration": {"method": config.method, "rtol": config.rtol, "totals": totals},
|
||||
"stateCount": len(program.state_keys), "sampleCount": len(data["series"]["time"])},
|
||||
)
|
||||
@@ -0,0 +1,257 @@
|
||||
"""Dependency ordering and local algebraic blocks for generated C expressions.
|
||||
|
||||
This module only arranges reviewed C computations. It never evaluates a model
|
||||
numerically and has no dependency on the retired Python numerical backend.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import heapq
|
||||
import re
|
||||
|
||||
from .compiler import NativeCapabilityError
|
||||
|
||||
|
||||
# Only compiler-owned array expressions are inspected, never arbitrary user C.
|
||||
_REFERENCE = re.compile(r"\b(?:[phqw]|fb)\[\d+\]|\bg\[\d+\]\.[A-Za-z_]\w*")
|
||||
|
||||
|
||||
def references(expression: str) -> frozenset[str]:
|
||||
return frozenset(_REFERENCE.findall(expression))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Computation:
|
||||
key: str
|
||||
outputs: tuple[str, ...]
|
||||
inputs: frozenset[str]
|
||||
code: tuple[str, ...]
|
||||
kind: str = "flow"
|
||||
residual: str | None = None
|
||||
|
||||
@classmethod
|
||||
def assignment(cls, key, target, expression, kind="flow"):
|
||||
return cls(key, (target,), references(expression), (f'{target}={expression};',), kind)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Block:
|
||||
members: tuple[int, ...]
|
||||
cyclic: bool
|
||||
|
||||
|
||||
class EvaluationSchedule:
|
||||
def __init__(self, computations, known, labels=None):
|
||||
self.computations = tuple(computations)
|
||||
self.labels = labels or {}
|
||||
self.producers = {}
|
||||
for i, op in enumerate(self.computations):
|
||||
for output in op.outputs:
|
||||
if output in self.producers:
|
||||
raise NativeCapabilityError(f'Multiple native producers for {self.label(output)}')
|
||||
self.producers[output] = i
|
||||
# An iterative initial guess is not a known source if an equation owns it.
|
||||
used = set().union(*(op.inputs for op in self.computations)) if self.computations else set()
|
||||
self.known = {key: value for key, value in known.items() if key not in self.producers and key in used}
|
||||
self.dependencies = []
|
||||
for op in self.computations:
|
||||
missing = op.inputs - self.producers.keys() - self.known.keys()
|
||||
if missing:
|
||||
raise NativeCapabilityError(f'{op.key}: missing native input sources: {sorted(map(self.label, missing))}')
|
||||
self.dependencies.append({self.producers[key] for key in op.inputs if key in self.producers})
|
||||
self.blocks = self._blocks()
|
||||
|
||||
def label(self, key):
|
||||
return self.labels.get(key, key)
|
||||
|
||||
def _blocks(self):
|
||||
"""Iterative SCC discovery followed by deterministic topological order."""
|
||||
count = len(self.computations)
|
||||
consumers = [set() for _ in range(count)]
|
||||
for target, sources in enumerate(self.dependencies):
|
||||
for source in sources:
|
||||
consumers[source].add(target)
|
||||
visited, finish = set(), []
|
||||
for start in range(count):
|
||||
if start in visited:
|
||||
continue
|
||||
visited.add(start)
|
||||
stack = [(start, iter(sorted(consumers[start])))]
|
||||
while stack:
|
||||
node, edges = stack[-1]
|
||||
child = next(edges, None)
|
||||
if child is None:
|
||||
finish.append(node)
|
||||
stack.pop()
|
||||
elif child not in visited:
|
||||
visited.add(child)
|
||||
stack.append((child, iter(sorted(consumers[child]))))
|
||||
groups, owner = [], {}
|
||||
for start in reversed(finish):
|
||||
if start in owner:
|
||||
continue
|
||||
index = len(groups)
|
||||
owner[start] = index
|
||||
members, stack = [], [start]
|
||||
while stack:
|
||||
node = stack.pop()
|
||||
members.append(node)
|
||||
for child in sorted(self.dependencies[node]):
|
||||
if child not in owner:
|
||||
owner[child] = index
|
||||
stack.append(child)
|
||||
groups.append(tuple(sorted(members)))
|
||||
incoming = [set() for _ in groups]
|
||||
outgoing = [set() for _ in groups]
|
||||
for target, sources in enumerate(self.dependencies):
|
||||
for source in sources:
|
||||
a, b = owner[source], owner[target]
|
||||
if a != b:
|
||||
incoming[b].add(a)
|
||||
outgoing[a].add(b)
|
||||
ready = [(min(groups[i]), i) for i, inputs in enumerate(incoming) if not inputs]
|
||||
heapq.heapify(ready)
|
||||
blocks = []
|
||||
while ready:
|
||||
_, index = heapq.heappop(ready)
|
||||
members = groups[index]
|
||||
cyclic = len(members) > 1 or members[0] in self.dependencies[members[0]]
|
||||
blocks.append(Block(members, cyclic))
|
||||
for child in sorted(outgoing[index]):
|
||||
incoming[child].remove(index)
|
||||
if not incoming[child]:
|
||||
heapq.heappush(ready, (min(groups[child]), child))
|
||||
return tuple(blocks)
|
||||
|
||||
def ordered_subset(self, members):
|
||||
"""Order a trial's computations while pressure/stream guesses are fixed."""
|
||||
pending = set(members)
|
||||
result = []
|
||||
while pending:
|
||||
ready = sorted(i for i in pending if not self.dependencies[i] & pending)
|
||||
if not ready:
|
||||
raise NativeCapabilityError('Unsupported cycle within native flow expressions')
|
||||
result.extend(ready)
|
||||
pending.difference_update(ready)
|
||||
return result
|
||||
|
||||
def ancestors(self, inputs, allowed):
|
||||
pending = [self.producers[key] for key in inputs if key in self.producers]
|
||||
found = set()
|
||||
while pending:
|
||||
index = pending.pop()
|
||||
if index in found or index not in allowed:
|
||||
continue
|
||||
found.add(index)
|
||||
pending.extend(self.dependencies[index])
|
||||
return self.ordered_subset(found)
|
||||
|
||||
def report(self):
|
||||
result = []
|
||||
sources = {key: {str(origin)} for key, origin in self.known.items()}
|
||||
for block in self.blocks:
|
||||
outputs = {key for i in block.members for key in self.computations[i].outputs}
|
||||
inputs = {key for i in block.members for key in self.computations[i].inputs} - outputs
|
||||
origins = set().union(*(sources[key] for key in inputs)) if inputs else set()
|
||||
for key in outputs:
|
||||
sources[key] = origins
|
||||
result.append({
|
||||
'cyclic': block.cyclic,
|
||||
'inputs': sorted(map(self.label, inputs)),
|
||||
'outputs': sorted(map(self.label, outputs)),
|
||||
'origins': sorted(origins),
|
||||
'operations': [self.computations[i].key for i in block.members],
|
||||
'pressureUnknowns': [self.label(key) for i in block.members
|
||||
if self.computations[i].kind == 'pressure'
|
||||
for key in self.computations[i].outputs],
|
||||
})
|
||||
return {
|
||||
'strategy': 'dependency-blocks',
|
||||
'operationCount': len(self.computations),
|
||||
'cyclicBlockCount': sum(b.cyclic for b in self.blocks),
|
||||
'knownSources': {self.label(key): value for key, value in self.known.items()},
|
||||
'blocks': result,
|
||||
'operations': [{'key': op.key, 'kind': op.kind,
|
||||
'inputs': sorted(map(self.label, op.inputs)),
|
||||
'outputs': list(map(self.label, op.outputs))}
|
||||
for op in self.computations],
|
||||
}
|
||||
|
||||
def emit(self):
|
||||
"""Return C helper definitions and a straight-line/local-block schedule.
|
||||
|
||||
Existing scalar pressure bisection and stream convergence tolerances are
|
||||
retained. Only the computations in the relevant SCC participate in each
|
||||
closure; a pressure trial further restricts work to its residual inputs.
|
||||
"""
|
||||
helpers, lines = [], []
|
||||
args = 'p,h,g,w,q,fb,pipe_cache'
|
||||
signature = ('double *p,double *h,const NativeGas *g,double *w,double *q,'
|
||||
'double *fb,NativePipeCache *pipe_cache')
|
||||
|
||||
def code(indices):
|
||||
return [line for i in indices for line in
|
||||
(f'/* schedule operation {i}: {self.computations[i].kind} */', *self.computations[i].code)]
|
||||
|
||||
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;',
|
||||
*code(indices), 'return 1;', '}'])
|
||||
return f'if(!{name}({args})) return 0;'
|
||||
|
||||
for number, block in enumerate(self.blocks):
|
||||
if not block.cyclic:
|
||||
if self.computations[block.members[0]].kind == 'pressure':
|
||||
raise NativeCapabilityError('Pressure balance has no pressure-dependent flow relation')
|
||||
lines.extend(code(block.members))
|
||||
continue
|
||||
pressures = [i for i in block.members if self.computations[i].kind == 'pressure']
|
||||
streams = [i for i in block.members if self.computations[i].kind in ('stream', 'alias')]
|
||||
flows = set(block.members) - set(pressures) - set(streams)
|
||||
if all(self.computations[i].kind == 'alias' for i in block.members):
|
||||
raise NativeCapabilityError('Enthalpy reference cycle has no thermodynamic source: ' +
|
||||
', '.join(self.computations[i].key for i in block.members))
|
||||
refresh = helper(f'model_block_{number}_flows', self.ordered_subset(flows)) if flows else ''
|
||||
lines.append(f'{{ /* local algebraic block {number} */')
|
||||
hvars = [key for i in streams for key in self.computations[i].outputs]
|
||||
outputs = {key for i in block.members for key in self.computations[i].outputs}
|
||||
inputs = sorted({key for i in block.members for key in self.computations[i].inputs} - outputs)
|
||||
if hvars:
|
||||
seeds = [key for key in inputs if key.startswith('h[') or key.endswith('.h')]
|
||||
if not seeds:
|
||||
raise NativeCapabilityError('Local stream loop has no supplied thermodynamic state')
|
||||
lines.extend([*[f'{key}={seeds[0]};' for key in hvars], 'int closure_ok=0;',
|
||||
f'for(int closure=0;closure<{max(64,4*len(hvars))};closure++) {{',
|
||||
'double previous[]={' + ','.join(hvars) + '};'])
|
||||
if pressures:
|
||||
pressure_bounds = [key for key in inputs if key.startswith('p[') or key.endswith('.p')]
|
||||
if not pressure_bounds:
|
||||
raise NativeCapabilityError('Local pressure block has no pressure boundary')
|
||||
lines.extend(['double plo=INFINITY,phi=0;',
|
||||
*[f'plo=fmin(plo,{p});phi=fmax(phi,{p});' for p in pressure_bounds],
|
||||
*[f'{self.computations[i].outputs[0]}=.5*(plo+phi);' for i in pressures],
|
||||
'int pressure_ok=0;', 'for(int sweep=0;sweep<256;sweep++) {'])
|
||||
for i in pressures:
|
||||
op = self.computations[i]
|
||||
p = op.outputs[0]
|
||||
trial = helper(f'model_block_{number}_pressure_{i}', self.ancestors(op.inputs, flows))
|
||||
lines.extend(['{ double lo=plo,hi=phi;', 'for(int bisect=0;bisect<48;bisect++) {',
|
||||
f'{p}=.5*(lo+hi);', trial,
|
||||
f'double balance={op.residual};if(!isfinite(balance)) return 0;',
|
||||
f'if(balance>0) hi={p};else lo={p};', '}}'])
|
||||
lines.extend([refresh, 'double residual=0;',
|
||||
*[f'{{double balance={self.computations[i].residual};if(!isfinite(balance)) return 0;residual=fmax(residual,fabs(balance));}}' for i in pressures],
|
||||
'if(residual<1e-11) {pressure_ok=1;break;}', '}',
|
||||
'if(!pressure_ok) return 0;'])
|
||||
elif refresh:
|
||||
lines.append(refresh)
|
||||
if hvars:
|
||||
# Gauss-Seidel within a genuine stream loop, in stable emission order.
|
||||
lines.extend([*code(streams), 'double change=0;',
|
||||
*[line for j,key in enumerate(hvars) for line in
|
||||
(f'if(!isfinite({key})) return 0;',
|
||||
f'change=fmax(change,fabs({key}-previous[{j}])/fmax(1,fabs({key})));')],
|
||||
'if(change<1e-12) {closure_ok=1;break;}', '}',
|
||||
'if(!closure_ok) return 0;', refresh])
|
||||
lines.append('}')
|
||||
return helpers, lines
|
||||
@@ -0,0 +1,16 @@
|
||||
"""SI absolute error floors shared by both native code generators.
|
||||
|
||||
Mass must not inherit the energy floor: 1e-8 kg overwhelmed relative error
|
||||
control in small pipe volumes. These conservative defaults were validated on
|
||||
the pipe and chamber regressions; model-scale/user tolerance settings remain
|
||||
a separate protocol change.
|
||||
"""
|
||||
|
||||
|
||||
def state_absolute_tolerance(key: str) -> str:
|
||||
field = key.rsplit('.', 1)[-1]
|
||||
if field in ('m', 'm1', 'm2'):
|
||||
return '1e-14' # kg
|
||||
if field in ('v', 'x'):
|
||||
return '1e-12' # m/s or m
|
||||
return '1e-8' # J (or the internal constant state of an algebraic model)
|
||||
Reference in new issue
Block a user