优化雅可比矩阵计算;端口转发情况下仿真结果传输方式优化

This commit is contained in:
lujingze committed 2026-09-12 03:57:31 +00:00
1 parent 3bc4be3c06
commit aa4951b14e
28 files changed
+8038 -23

No files matched your search

+7 -2
View File
@@ -13,6 +13,7 @@ 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
from .jacobian import JacobianStructure
class NativeCapabilityError(ValueError):
@@ -35,6 +36,7 @@ class NativeProgram:
variables: tuple[ResultVariableMetadata, ...]
component_types: tuple[str, ...]
evaluation_schedule: dict | None = None
jacobian_structure: dict | None = None
def manifest(self) -> dict:
return {
@@ -42,7 +44,8 @@ class NativeProgram:
"variables": [v.as_dict() for v in self.variables],
"componentTypes": self.component_types,
"componentVersions": {name: SUPPORTED_VERSIONS[name] for name in self.component_types},
"jacobianPolicy": "CVODE default; no custom Jacobian",
"jacobianPolicy": (self.jacobian_structure or {}).get("policy", "CVODE default; no custom Jacobian"),
"jacobianStructure": self.jacobian_structure,
"evaluationSchedule": self.evaluation_schedule or {"strategy": "storage-anchored", "cyclicBlockCount": 0},
}
@@ -362,6 +365,7 @@ def _compile_storage_anchored_program(network: SimulationNetwork) -> NativeProgr
next_event.append(f"if (t < {_number(c.time)}) result = fmin(result, {_number(c.time)});")
else:
next_event.append(f"result = fmin(result, native_signal_break(t, end, {_number(c.tstart)}, {c.nstages}, {int(c.iscyclic)}, signal_{index}));")
jacobian = JacobianStructure.dense(len(state_keys), 'Compact storage-anchored lowering retains default differences')
source = '\n'.join([
'#include "model.h"', '#include <math.h>', *declarations,
f"const NativeStop model_stops[{max(1,len(stops))}] = {{{stop_c}}};",
@@ -387,9 +391,10 @@ def _compile_storage_anchored_program(network: SimulationNetwork) -> NativeProgr
extern const NativeStop model_stops[{max(1,len(stops))}];
extern const double model_atol[NSTATES];
extern const char *const model_output_keys[NOUTPUTS];
{chr(10).join(jacobian.header_lines())}
int model_init(double *y);
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})),jacobian_structure=jacobian.manifest())
+32 -4
View File
@@ -13,6 +13,7 @@ from .compiler import NativeCapabilityError, NativeProgram, _Groups, _number as
from .contracts import SUPPORTED_VERSIONS
from .schedule import Computation, EvaluationSchedule, references
from .tolerances import state_absolute_tolerance
from .jacobian import StateDependencies, expression_inputs
GAS_TYPES = {'amesim_pnch023', 'amesim_pnch012', 'amesim_pnl0001',
@@ -149,6 +150,7 @@ def compile_extended_program(network):
nstates = max(len(state_keys), 1)
if not initial:
initial = [0.0]
dependencies = StateDependencies(len(state_keys))
lines, declarations, breaks = [], [], []
assigned = set()
def w(c, field):
@@ -156,6 +158,7 @@ def compile_extended_program(network):
return f'w[{slots[name+"."+field]}]'
def put(c, field, expr, dest=None):
(lines if dest is None else dest).append(f'{w(c,field)} = {expr};')
dependencies.expression(w(c,field), expr)
assigned.add((c if isinstance(c,str) else c.name)+'.'+field)
def y(c, field):
return f'y[{states[c.name,field]}]'
@@ -268,7 +271,10 @@ 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_context(properties,{medium(c)}, {y(c,"m"+suffix)}, {y(c,"U"+suffix)}, {V}, &{gas})) 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})')
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))
for field in ('p','T','rho','u','h'):
@@ -304,8 +310,11 @@ def compile_extended_program(network):
project += [f'projected[{i}]=mass*{num(volume/total)};projected[{i+1}]=energy*{num(volume/total)};']
project.append('}')
coupled.append((root,offsets,volumes))
dependencies.project_states(offsets)
dependencies.project_states([i+1 for i in offsets])
for root, gas in anchor.items():
lines.append(f'p[{pgi[root]}]={gas}.p;')
dependencies.assign(f'p[{pgi[root]}]', (gas+'.p',))
h_initial = {f'h[{pi[endpoint]}]': gas+'.h' for endpoint,gas in port_gas.items()}
operations, flow_known, flow_eq = [], set(), []
@@ -437,9 +446,12 @@ def compile_extended_program(network):
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 operation in operations:
dependencies.computation(operation)
for target,expr in h_initial.items():
if target not in schedule.producers:
lines.append(f'{target}={expr};')
dependencies.expression(target, expr)
schedule_helpers, scheduled_lines=schedule.emit()
lines += scheduled_lines
for c in components:
@@ -481,7 +493,10 @@ def compile_extended_program(network):
if c.use_friction: extra+=f'-{num(c.wind)}*{v}*fabs({v})'
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()]
lines += linear_schedule(feq,unknownf)
# Keep the same elimination/order while registering its structured bindings.
for target, expression in linear_assignments(feq, unknownf):
lines.append(f'{"double " if target.startswith("b") else ""}{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':
@@ -491,6 +506,8 @@ def compile_extended_program(network):
ref=group[0];vi=states[ref.name,'v'];xi=states[ref.name,'x']
limits=[c for c in group if int(c.stoptype) in (1,3)]
lines += [f'dy[{vi}]={w(ref,"a")};dy[{xi}]={y(ref,"v")};']
dependencies.expression(f'dy[{vi}]', w(ref,'a'))
dependencies.expression(f'dy[{xi}]', y(ref,'v'))
if limits:
lower=max(c.xmin for c in limits);upper=min(c.xmax for c in limits)
if lower>upper or ref.x0<lower-1e-12 or ref.x0>upper+1e-12:
@@ -502,6 +519,7 @@ def compile_extended_program(network):
thresholds.append(max((c.restdvel for c in active if int(c.stoptype)==3),default=0))
stops.append((vi,lower,upper,*restitution,*thresholds))
lines.append(f'native_stop_motion({y(ref,"x")},{y(ref,"v")},{num(lower)},{num(upper)},&dy[{vi}],&dy[{xi}]);')
dependencies.stop_motion(vi, xi)
for c in group: put(c,'a',f'dy[{vi}]')
for c in components:
@@ -530,6 +548,8 @@ def compile_extended_program(network):
temp=f'.5*({gases[c.name,1]}.T+{gases[c.name,2]}.T)' if half else 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});']
dependencies.expression(f'dy[{states[c.name,"m"+suffix]}]', mass)
dependencies.expression(f'dy[{states[c.name,"U"+suffix]}]', f'{energy}+({heat})')
if kind.startswith('amesim_pnl'):
diag=[]
if kind=='amesim_pnl0002':
@@ -562,21 +582,26 @@ def compile_extended_program(network):
lines.append('{ double mass='+ '+'.join(f'dy[{i}]' for i in offsets)+',energy='+ '+'.join(f'dy[{i+1}]' for i in offsets)+';')
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))};')
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('}')
missing=set(slots)-assigned
if missing: raise NativeCapabilityError(f'Native output mapping incomplete: {sorted(missing)}')
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))
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(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;}',
'int model_eval(double t,const double *y,double *dy,double *w) {',
'static int model_eval_internal(double t,const double *y,double *dy,double *w,int canonical) {',
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]));',
'NativePropertyCache *gas_properties=canonical?NULL:properties;(void)gas_properties;',
*(['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;',
@@ -584,6 +609,8 @@ 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);}',
'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
@@ -594,9 +621,10 @@ def compile_extended_program(network):
extern const NativeStop model_stops[{max(1,len(stops))}];
extern const double model_atol[NSTATES];
extern const char *const model_output_keys[NOUTPUTS];
{chr(10).join(jacobian.header_lines(canonical_rhs=True))}
int model_init(double *y);
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})),schedule.report())
return NativeProgram(source,header,tuple(state_keys),variables,tuple(sorted({c.model_type for c in components})),schedule.report(),jacobian.manifest(canonical_rhs=True))
+214
View File
@@ -0,0 +1,214 @@
"""Conservative state dependencies for compiler-owned expressions.
This is structural bookkeeping, not C parsing or numerical Jacobian evaluation.
Native multi-output operations, projections and mode-dependent writes must be
registered explicitly by the lowering path. Unresolved reachable inputs disable
coloring for the complete model.
"""
from collections import deque
from dataclasses import dataclass
import hashlib
import json
import re
# Reviewed expression leaves. Unknown arrays/scalars remain unresolved instead
# of silently becoming constants; the flow schedule supplies its own explicit IR.
_ARRAY = re.compile(r'\b[A-Za-z_]\w*\[\d+\](?:\.[A-Za-z_]\w*)?|\bgas_\d+\.[A-Za-z_]\w*')
_IDENTIFIER = re.compile(r'\b[A-Za-z_]\w*\b')
_NUMBER = re.compile(r'(?<![\w.])(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?')
_FUNCTIONS = frozenset({
'fmax', 'fmin', 'fabs', 'sqrt', 'copysign', 'pow',
'native_signal', 'native_contact', 'native_limit_force',
'native_pipe_flow_context', 'native_pipe_flow_cached_context',
'native_temperature_ph_context', 'native_density_context',
})
_CONSTANTS = frozenset({'t', 'properties', 'NAN', 'INFINITY', 'NULL', 'true', 'false'})
def expression_inputs(expression):
"""Extract leaves from a reviewed expression; unfamiliar names fail closed."""
inputs = set()
def leaf(match):
key = match.group()
# Per-evaluation cache scratch is an implementation detail of reviewed
# kernels; each value is determined by that kernel's explicit arguments.
if not re.fullmatch(r'pipe_cache\[\d+\]', key):
inputs.add(key)
return ' '
remainder = _ARRAY.sub(leaf, expression)
remainder = _NUMBER.sub(' ', remainder)
for name in _IDENTIFIER.findall(remainder):
if name in _FUNCTIONS or name in _CONSTANTS or re.fullmatch(r'(?:medium|signal)_\d+', name):
continue
inputs.add(name)
return frozenset(inputs)
@dataclass(frozen=True)
class JacobianStructure:
state_count: int
rows: tuple[tuple[int, ...], ...] = ()
colors: tuple[int, ...] = ()
reason: str | None = None
@classmethod
def dense(cls, state_count, reason):
return cls(state_count, reason=reason)
@property
def enabled(self):
return self.reason is None
@property
def color_count(self):
return max(self.colors, default=-1) + 1 if self.enabled else self.state_count
@property
def nonzeros(self):
return sum(map(len, self.rows)) if self.enabled else self.state_count ** 2
def manifest(self, *, canonical_rhs=False):
# Use proven coloring automatically when it reduces finite-difference
# groups; unsupported or unhelpful patterns retain dense differences.
runtime_eligible = self.enabled and 0 < self.color_count < self.state_count
fallback_reason = None if runtime_eligible else (
self.reason or 'Coloring does not reduce finite-difference groups')
pattern = {'rows': self.rows, 'columnColors': self.colors}
policy = ('CVODE colored forward differences; canonical-property-cache RHS' if canonical_rhs else 'CVODE colored forward differences') if runtime_eligible else 'CVODE default dense differences'
return {
'policyScope': 'default-runtime',
'defaultRuntimePolicy': policy,
'verification': '--verify-jacobian',
'policy': policy,
'rhsPolicy': 'canonical-property-cache finite differences' if canonical_rhs and runtime_eligible else 'ordinary model_eval',
'runtimeEligible': runtime_eligible, 'runtimeFallbackReason': fallback_reason,
'canonicalRhs': canonical_rhs, 'ordinaryRhsUnchanged': True,
'enabled': self.enabled, 'reason': self.reason, 'stateCount': self.state_count,
'nonzeros': self.nonzeros, 'density': self.nonzeros / self.state_count ** 2 if self.state_count else 0,
'colorCount': self.color_count,
'patternSha256': hashlib.sha256(json.dumps(pattern, separators=(',', ':')).encode()).hexdigest() if self.enabled else None,
'columnColors': list(self.colors) if self.enabled else None,
}
def header_lines(self, *, canonical_rhs=False):
lines = [f'#define MODEL_JACOBIAN_COLORED {int(self.enabled)}',
f'#define MODEL_JACOBIAN_CANONICAL_RHS {int(canonical_rhs)}',
f'#define MODEL_JACOBIAN_COLOR_COUNT {self.color_count}',
f'#define MODEL_JACOBIAN_NNZ {self.nonzeros}']
if canonical_rhs:
lines += ['int model_eval_jacobian(double t, const double *y, double *dy, double *w);']
if self.enabled:
lines += ['extern const int model_jacobian_column_color[NSTATES];',
'extern const int model_jacobian_col_ptr[NSTATES+1];',
'extern const int model_jacobian_row_index[MODEL_JACOBIAN_NNZ];']
return lines
def source_lines(self):
if not self.enabled:
return []
columns = [[] for _ in range(self.state_count)]
for row, entries in enumerate(self.rows):
for column in entries:
columns[column].append(row)
pointers = [0]
indices = []
for column in columns:
indices.extend(column)
pointers.append(len(indices))
return [f'const int {name}[{len(values)}] = {{'+','.join(map(str, values))+'};'
for name, values in [('model_jacobian_column_color', self.colors),
('model_jacobian_col_ptr', pointers),
('model_jacobian_row_index', indices)]]
class StateDependencies:
def __init__(self, state_count):
self.state_count = state_count
self.seeds = {f'y[{i}]': 1 << i for i in range(state_count)}
self.inputs = {}
def assign(self, target, inputs):
# Union repeated writes, rather than dropping dependencies from another
# branch or an earlier in-place value (stop motion is mode dependent).
self.inputs.setdefault(target, set()).update(inputs)
def expression(self, target, expression):
self.assign(target, expression_inputs(expression))
def project_states(self, offsets):
refs = {f'y[{i}]' for i in offsets}
for target in refs:
self.assign(target, refs)
def stop_motion(self, velocity_index, position_index):
refs = {f'y[{velocity_index}]', f'y[{position_index}]',
f'dy[{velocity_index}]', f'dy[{position_index}]'}
for target in (f'dy[{velocity_index}]', f'dy[{position_index}]'):
self.assign(target, refs)
def computation(self, operation):
# A schedule SCC reaches a fixed point in build(), so every member gains
# every external state dependency even through pressure/stream loops.
for output in operation.outputs:
self.assign(output, operation.inputs)
def build(self):
if self.state_count == 0:
return JacobianStructure.dense(1, 'Algebraic-only internal state uses default differences')
required = {f'dy[{i}]' for i in range(self.state_count)}
pending = list(required)
reachable = set()
missing = set()
while pending:
key = pending.pop()
if key in reachable:
continue
reachable.add(key)
if key not in self.inputs and key not in self.seeds:
missing.add(key)
pending.extend(self.inputs.get(key, ()))
if missing:
return JacobianStructure.dense(self.state_count,
'Unresolved structural inputs: '+', '.join(sorted(missing)[:16]))
consumers = {key: set() for key in reachable}
for target in reachable:
for source in self.inputs.get(target, ()):
consumers[source].add(target)
masks = dict(self.seeds)
queue = deque(key for key in self.seeds if key in reachable)
queued = set(queue)
while queue:
source = queue.popleft()
queued.remove(source)
for target in consumers[source]:
merged = masks.get(target, 0) | masks[source]
if merged != masks.get(target, 0):
masks[target] = merged
if target not in queued:
queue.append(target)
queued.add(target)
# A diagonal overestimate is safe and keeps isolated/dummy rows valid
# without introducing zero-length C arrays or uncolored state columns.
rows = tuple(tuple(column for column in range(self.state_count)
if (masks.get(f'dy[{row}]', 0) | (1 << row)) >> column & 1)
for row in range(self.state_count))
conflicts = [set() for _ in range(self.state_count)]
for entries in rows:
for column in entries:
conflicts[column].update(set(entries) - {column})
colors = {}
while len(colors) < self.state_count:
column = max((i for i in range(self.state_count) if i not in colors),
key=lambda i: (len({colors[j] for j in conflicts[i] if j in colors}),
len(conflicts[i]), -i))
used = {colors[j] for j in conflicts[column] if j in colors}
color = 0
while color in used:
color += 1
colors[column] = color
ordered = tuple(colors[i] for i in range(self.state_count))
for entries in rows:
if len({ordered[column] for column in entries}) != len(entries):
raise AssertionError('Jacobian coloring contains a row conflict')
return JacobianStructure(self.state_count, rows, ordered)
+5 -1
View File
@@ -41,6 +41,11 @@ def execute_native(build: NativeBuild, config: SolveIVPConfig, sample_step: floa
command.append("--solve-only")
creationflags = subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0
started = time.perf_counter()
# A fast worker may finish before the monitor's first iteration. Honor a
# cancellation already requested during preparation before spawning it.
cancelled_at = started if cancel_check is not None and cancel_check() else None
if cancelled_at is not None:
cancel_path.write_text("cancel\n", encoding="ascii")
process = subprocess.Popen(command, cwd=build.executable.parent, stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL, stderr=subprocess.PIPE,
text=True, encoding="utf-8", errors="replace", creationflags=creationflags)
@@ -53,7 +58,6 @@ def execute_native(build: NativeBuild, config: SolveIVPConfig, sample_step: floa
reader.start()
if activity_tracker is not None:
activity_tracker.start_integration(config.t_start)
cancelled_at = None
last_time = config.t_start
try:
with (run_dir / "worker.log").open("w", encoding="utf-8") as log: