现有组件功能与Amesim组件比较对齐,补全功能;仿真服务启动环境检测,旧版json工程文件适配读取
This commit is contained in:
1 parent
b6c22a54f8
commit
6fc9afe41d
119 files changed
+260114
-351
No files matched your search
@@ -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];
|
||||
|
||||
Reference in new issue
Block a user