现有组件功能与Amesim组件比较对齐,补全功能;仿真服务启动环境检测,旧版json工程文件适配读取

This commit is contained in:
ljz committed 2026-09-13 20:01:56 +08:00
1 parent b6c22a54f8
commit 6fc9afe41d
119 files changed
+260114 -351

No files matched your search

+112 -22
View File
@@ -6,6 +6,7 @@ All thermodynamics, flow/stream closure and derivatives execute in the EXE.
from __future__ import annotations
import json
import math
import re
from importlib import import_module
@@ -14,6 +15,7 @@ from .contracts import SUPPORTED_VERSIONS
from .schedule import Computation, EvaluationSchedule, references
from .tolerances import state_absolute_tolerance
from .jacobian import StateDependencies, expression_inputs
from app.simulation.components.amesim.semantics import contact_stiffness, validate_numerical_semantics
GAS_TYPES = {'amesim_pnch023', 'amesim_pnch012', 'amesim_pnl0001',
@@ -23,6 +25,10 @@ RESISTORS = {'amesim_pnor001', 'amesim_pnvo001_fixed', 'amesim_pnvo001',
'amesim_pnl00r', 'pipe', 'orifice'}
def is_polytropic(c):
return c.model_type in {'amesim_pnl0001', 'amesim_pnl0002', 'amesim_pnl0003'} and int(c.mode) == 1
def catalog_contracts():
from app.simulation.components.amesim.library import LIBRARY as a
from app.simulation.components.experimental.library import LIBRARY as e
@@ -94,6 +100,10 @@ def compile_extended_program(network):
raise NativeCapabilityError(f'{c.name}: no native contract for {c.model_type} / {type(c).__name__}')
if c.MODEL_VERSION != SUPPORTED_VERSIONS.get(c.model_type):
raise NativeCapabilityError(f'{c.name}: native kernel version does not match the component contract')
try:
validate_numerical_semantics(c)
except ValueError as exc:
raise NativeCapabilityError(str(exc)) from exc
variables = tuple(v for c in components for v in c.result_variable_metadata())
if not variables:
raise NativeCapabilityError('Simulation requires a runtime component; medium definitions alone have no outputs')
@@ -131,7 +141,8 @@ def compile_extended_program(network):
mass_groups = {}
for c in components:
if c.model_type in GAS_TYPES:
fields = ('m1', 'U1', 'm2', 'U2') if c.model_type == 'amesim_pnl0003' else ('m', 'U')
thermal = 'T' if is_polytropic(c) else 'U'
fields = ('m1', thermal+'1', 'm2', thermal+'2') if c.model_type == 'amesim_pnl0003' else ('m', thermal)
add_states(c, fields, [0.0]*len(fields))
elif c.model_type == 'amesim_mecmas21':
root = groups.find((c.name, 'port_1'))
@@ -144,6 +155,17 @@ def compile_extended_program(network):
raise NativeCapabilityError('Rigidly connected masses require consistent initial x/v')
for field in ('v', 'x'):
states[c.name, field] = states[ref.name, field]
friction_groups = []
for group in mass_groups.values():
dry = [c for c in group if c.use_friction and int(c.stoptype) != 3 and c.fstick > 0]
if dry:
ref = group[0]
threshold = min(c.dvel if int(c.frictionType) == 2 else 0 for c in dry)
mode = (1 if ref.v0 > 0 else -1) if abs(ref.v0) > threshold else 0
if mode == 0:
initial[states[ref.name, 'v']] = 0.0
add_states(ref, ('_friction_mode',), (mode,))
friction_groups.append((group, dry, threshold))
if len(state_keys)>1024 or len(variables)>16384:
raise NativeCapabilityError('Native model exceeds the 1024-state / 16384-output resource limit')
# Algebraic models use an internal constant state; the public state map stays empty.
@@ -270,13 +292,25 @@ def compile_extended_program(network):
initial_volume=(max(c.cvol0+sum(c.external_volumes.values()),c.cvol0/100)
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_context(gas_properties,{medium(c)}, {y(c,"m"+suffix)}, {y(c,"U"+suffix)}, {V}, &{gas})) return 0;')
gas_inputs = expression_inputs(f'{y(c,"m"+suffix)}+{y(c,"U"+suffix)}+({V})')
if kind == 'amesim_pnch012':
def initial_x(d, port):
return mass_groups[groups.find(ep(d, port))][0].x0
initial_volume = max(c.cvol0 + sum(c.external_volumes.values()) + sum(
d.effective_area * (d.x0 + initial_x(d, 'port_5') - initial_x(d, 'port_4'))
for d in attached), c.cvol0 / 100)
thermal = ('T' if is_polytropic(c) else 'U') + suffix
if is_polytropic(c):
gas_initializers += [f'{y(c,"m"+suffix)}={num(p0*initial_volume/(c.medium.R_gas*T0))};', f'{y(c,thermal)}={num(T0)};']
gas_function = 'native_polytropic_gas_context'
else:
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;')
gas_function = 'native_medium_gas_context'
lines.append(f'if(!{gas_function}(gas_properties,{medium(c)}, {y(c,"m"+suffix)}, {y(c,thermal)}, {V}, &{gas})) return 0;')
gas_inputs = expression_inputs(f'{y(c,"m"+suffix)}+{y(c,thermal)}+({V})')
for field in ('p','T','rho','u','h'):
dependencies.assign(gas+'.'+field, gas_inputs)
for field in ('m', 'U'):
put(c, field+suffix, y(c, field+suffix))
put(c, 'm'+suffix, y(c, 'm'+suffix))
put(c, 'U'+suffix, f'{y(c,"m"+suffix)}*{gas}.u' if is_polytropic(c) else y(c,'U'+suffix))
for field in ('p','T','rho','u','h'):
put(c, field+suffix, gas+'.'+field)
anchored = pnames(c)
@@ -296,6 +330,10 @@ def compile_extended_program(network):
if len(partitions)<2: continue
if any(volume is None for _,volume in partitions.values()) or len({id(c.medium) for c,_ in partitions.values()})!=1:
raise NativeCapabilityError('Direct gas-storage coupling requires compatible fixed pipe compliances; insert a resistance between independent chambers')
laws = {(is_polytropic(c), c.k if is_polytropic(c) else None) for c,_ in partitions.values()}
if len(laws) != 1:
raise NativeCapabilityError('Directly coupled pipe compliances require the same thermodynamic mode and polytropic exponent')
poly = next(iter(laws))[0]
offsets=[];volumes=[]
for (name,half),(c,volume) in partitions.items():
offsets.append(states[name,'m'+(str(half) if half else '')]);volumes.append(volume)
@@ -305,11 +343,12 @@ def compile_extended_program(network):
if max(values)-min(values)>1e-9*max(1,*map(abs,values)):
raise NativeCapabilityError('Ideally coupled pipe compliances require consistent initial pressure and temperature')
total=sum(volumes)
project.append('{ double mass='+ '+'.join(f'y[{i}]' for i in offsets)+',energy='+ '+'.join(f'y[{i+1}]' for i in offsets)+';')
thermal_sum = '+'.join(f'y[{i+1}]*{num(v/total)}' if poly else f'y[{i+1}]' for i,v in zip(offsets,volumes))
project.append('{ double mass='+ '+'.join(f'y[{i}]' for i in offsets)+',thermal='+thermal_sum+';')
for i,volume in zip(offsets,volumes):
project += [f'projected[{i}]=mass*{num(volume/total)};projected[{i+1}]=energy*{num(volume/total)};']
project += [f'projected[{i}]=mass*{num(volume/total)};projected[{i+1}]=thermal*{num(1 if poly else volume/total)};']
project.append('}')
coupled.append((root,offsets,volumes))
coupled.append((root,offsets,volumes,poly))
dependencies.project_states(offsets)
dependencies.project_states([i+1 for i in offsets])
for root, gas in anchor.items():
@@ -379,7 +418,7 @@ def compile_extended_program(network):
for terms,rhs in flow_eq:
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}
free_flows={f'q[{pi[e]}]' for root,_,_,_ in coupled for e in pneu if groups.find(e)==root}
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:
@@ -474,7 +513,7 @@ def compile_extended_program(network):
elif kind=='amesim_lstp00a':
put(c,'gap',f'{num(c.gap0)}+{w(c,"port_2.x")}-{w(c,"port_1.x")}')
put(c,'penetration',f'fmax(-{w(c,"gap")},0)')
put(c,'force',f'native_contact({w(c,"penetration")},{w(c,"port_1.v")}-{w(c,"port_2.v")},{num(c.kcont)},{num(c.rcont)},{num(c.Pdis)},{int(c.discContactOption)})')
put(c,'force',f'native_contact({w(c,"penetration")},{w(c,"port_1.v")}-{w(c,"port_2.v")},{num(contact_stiffness(c))},{num(c.rcont)},{num(c.Pdis)},{int(c.discContactOption)})')
feq += [({w(c,'port_1.f'):1},w(c,'force')),({w(c,'port_2.f'):1},'-'+w(c,'force'))]
elif kind=='amesim_pnrp17':
put(c,'pressure_force',f'({w(c,"port_1.p")}-101300)*{num(c.effective_area)}')
@@ -484,19 +523,56 @@ def compile_extended_program(network):
elif kind=='amesim_mecmas21':
v,x=w(c,'v'),w(c,'x')
put(c,'Fvisc',f'-{num(c.rvisc)}*{v}' if c.use_friction else '0.0')
put(c,'Ffric',f'{v}>0?-{num(c.fcoul)}:({v}<0?{num(c.fcoul)}:0)' if c.use_friction else '0.0')
put(c,'Ffric','0.0')
for field,penetration,velocity,suffix in [('Fmin',num(c.xmin)+'-'+x,'-'+v,'min'),('Fmax',x+'-'+num(c.xmax),v,'max')]:
expr=f'native_limit_force({penetration},{velocity},{num(getattr(c,"Kb"+suffix))},{num(getattr(c,"Db"+suffix))},{num(getattr(c,"Pd"+suffix))},{int(c.discContactOption)})' if int(c.stoptype)==2 else '0.0'
put(c,field,expr)
ref=mass_groups[groups.find(ep(c,'port_1'))][0]
extra=f'{w(c,"Fvisc")}+{w(c,"Ffric")}+{w(c,"Fmin")}-{w(c,"Fmax")}'
if c.use_friction: extra+=f'-{num(c.wind)}*{v}*fabs({v})'
extra += f'+({num(c.mass * 9.80665 * math.sin(math.radians(c.theta)))})'
feq.append(({w(c,'port_1.f'):1,w(c,'port_2.f'):1,w(ref,'a'):-c.mass},f'-({extra})'))
unknownf=[w(c,name+'.f') for c in components for name in mnames(c)]+[w(group[0],'a') for group in mass_groups.values()]
# Keep the same elimination/order while registering its structured bindings.
for target, expression in linear_assignments(feq, unknownf):
mechanical_assignments = linear_assignments(feq, unknownf)
for target, expression in mechanical_assignments:
lines.append(f'{"double " if target.startswith("b") else ""}{target} = {expression};')
dependencies.expression(target, expression)
for index, (group, dry, threshold) in enumerate(friction_groups):
ref = group[0]
drive = f'friction_drive_{index}'
expression = f'{num(sum(c.mass for c in group))}*{w(ref,"a")}'
lines.append(f'double {drive}={expression};')
dependencies.expression(drive, expression)
# Amesim ideal stops take priority over dry friction. A blocked mass
# carries its load through the stop, not through an extra friction force.
blocked = f'friction_blocked_{index}'
ideal = [c for c in group if int(c.stoptype) == 1]
conditions = []
for c in ideal:
x, v = y(ref, 'x'), y(ref, 'v')
conditions += [f'({x}<={num(c.xmin+1e-12*max(abs(c.xmin),1))} && {v}<=1e-12 && {drive}<=0)',
f'({x}>={num(c.xmax-1e-12*max(abs(c.xmax),1))} && {v}>=-1e-12 && {drive}>=0)']
blocked_expression = ' || '.join(conditions) or '0'
lines.append(f'double {blocked}=({blocked_expression});')
dependencies.expression(blocked, blocked_expression)
lines.append(f'{drive}={blocked}?0:{drive};')
dependencies.expression(drive, f'{blocked}?0:{drive}')
lines.append(f'if(friction_drives) friction_drives[{index}]={drive};')
capacity = sum(c.fstick for c in dry)
for c in dry:
put(c, 'Ffric', f'{blocked}?0:native_dry_friction({y(ref,"v")},{drive}*{num(c.fstick/capacity)},'
f'{num(c.fcoul)},{num(c.fstick)},{num(c.astrib)},'
f'{int(int(c.frictionType)==2 and int(c.strib)==2)},{y(ref,"_friction_mode")})')
lines.append(f'dy[{states[ref.name,"_friction_mode"]}]=0;')
dependencies.expression(f'dy[{states[ref.name,"_friction_mode"]}]', '0')
if friction_groups:
# The first linear solve supplies the drive without dry friction. The
# second redistributes actual friction forces through the same network.
# No pressure/flow iteration or mutable RHS mode is introduced.
for target, expression in mechanical_assignments:
lines.append(f'{target}={expression};')
dependencies.expression(target, expression)
for c in components:
for name in mnames(c): assigned.add(c.name+'.'+name+'.f')
if c.model_type=='amesim_lmechn1':
@@ -545,11 +621,13 @@ def compile_extended_program(network):
if kind.startswith('amesim_pnch'):
heat=f'{num(c.kth*c.sth)}*({num(c.extemp)}-{gas}.T)-{gas}.p*({volume_rate[c.name]})'
elif kind.startswith('amesim_pnl') and int(c.mode)!=1:
temp=f'.5*({gases[c.name,1]}.T+{gases[c.name,2]}.T)' if half else gas+'.T'
temp=gas+'.T'
heat=f'{num(c.kth*c.exchange_area/(2 if half else 1))}*({num(c.extemp)}-({temp}))'
lines += [f'dy[{states[c.name,"m"+suffix]}]={mass};',f'dy[{states[c.name,"U"+suffix]}]={energy}+({heat});']
thermal = ('T' if is_polytropic(c) else 'U') + suffix
thermal_rhs = f'{num(c.k-1)}*{gas}.T/({y(c,"m"+suffix)})*({mass})' if is_polytropic(c) else f'{energy}+({heat})'
lines += [f'dy[{states[c.name,"m"+suffix]}]={mass};',f'dy[{states[c.name,thermal]}]={thermal_rhs};']
dependencies.expression(f'dy[{states[c.name,"m"+suffix]}]', mass)
dependencies.expression(f'dy[{states[c.name,"U"+suffix]}]', f'{energy}+({heat})')
dependencies.expression(f'dy[{states[c.name,thermal]}]', thermal_rhs)
if kind.startswith('amesim_pnl'):
diag=[]
if kind=='amesim_pnl0002':
@@ -578,10 +656,11 @@ def compile_extended_program(network):
expr=f'acc[{i}]/{len(diag)}'
put(c,field,f'fmin({expr},64000000)' if field=='ff' else expr)
lines.append('}')
for _,offsets,volumes in coupled:
lines.append('{ double mass='+ '+'.join(f'dy[{i}]' for i in offsets)+',energy='+ '+'.join(f'dy[{i+1}]' for i in offsets)+';')
for _,offsets,volumes,poly in coupled:
thermal_sum = '+'.join(f'dy[{i+1}]*{num(v/sum(volumes))}' if poly else f'dy[{i+1}]' for i,v in zip(offsets,volumes))
lines.append('{ double mass='+ '+'.join(f'dy[{i}]' for i in offsets)+',thermal='+thermal_sum+';')
for i,volume in zip(offsets,volumes):
lines.append(f'dy[{i}]=mass*{num(volume/sum(volumes))};dy[{i+1}]=energy*{num(volume/sum(volumes))};')
lines.append(f'dy[{i}]=mass*{num(volume/sum(volumes))};dy[{i+1}]=thermal*{num(1 if poly else volume/sum(volumes))};')
dependencies.assign(f'dy[{i}]', (f'dy[{j}]' for j in offsets))
dependencies.assign(f'dy[{i+1}]', (f'dy[{j+1}]' for j in offsets))
lines.append('}')
@@ -590,14 +669,21 @@ def compile_extended_program(network):
if not state_keys: lines.append('dy[0]=0;')
jacobian = dependencies.build()
np,ng,nq=max(1,len(pgroups)),max(1,gas_count),max(1,len(pneu))
# 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 <math.h>',*declarations,
f'const NativeFriction model_frictions[{max(1,len(friction_groups))}] = {{'+(','.join(
'{'+str(states[group[0].name,'v'])+','+str(states[group[0].name,'_friction_mode'])+','+num(threshold)+','+
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}')+'};',
'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(),
*schedule_helpers,
'int model_init(double *y) {',*[f'y[{i}]={num(v)};' for i,v in enumerate(initial)],*gas_initializers,'return 1;}',
'static int model_eval_internal(double t,const double *y,double *dy,double *w,int canonical) {',
'static int model_eval_internal(double t,const double *y,double *dy,double *w,int canonical,double *friction_drives) {',
'(void)friction_drives;',
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]));',
@@ -609,8 +695,9 @@ def compile_extended_program(network):
'(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;}',
'int model_eval(double t,const double *y,double *dy,double *w) {return model_eval_internal(t,y,dy,w,0);}',
'int model_eval_jacobian(double t,const double *y,double *dy,double *w) {return model_eval_internal(t,y,dy,w,1);}',
'int model_eval(double t,const double *y,double *dy,double *w) {return model_eval_internal(t,y,dy,w,0,NULL);}',
'int model_eval_jacobian(double t,const double *y,double *dy,double *w) {return model_eval_internal(t,y,dy,w,1,NULL);}',
'int model_friction_drives(double t,const double *y,double *drives) {double dy[NSTATES],w[NOUTPUTS];return model_eval_internal(t,y,dy,w,0,drives);}',
'double model_next_break(double t,double end) { double result=end;(void)t;',*breaks,'return result;}',''])
header=f'''#ifndef GENERATED_NATIVE_MODEL_H
#define GENERATED_NATIVE_MODEL_H
@@ -618,6 +705,9 @@ def compile_extended_program(network):
#define NSTATES {nstates}
#define NOUTPUTS {max(1,len(variables))}
#define NSTOPS {len(stops)}
#define NFRICTIONS {len(friction_groups)}
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))}];
extern const double model_atol[NSTATES];
extern const char *const model_output_keys[NOUTPUTS];