前端进度条性能优化、仿真结束后后处理优化;后端C代码生成流程优化:先识别来源,再按照已知未知量需求排序,最后局部求解
This commit is contained in:
1 parent
0dcb465d84
commit
91bd9fb252
77 files changed
+11223
-551
No files matched your search
@@ -22,10 +22,11 @@ def numeric_engine_name(backend: str | None = None) -> str:
|
||||
|
||||
|
||||
def simulation_config(simulation) -> SolveIVPConfig:
|
||||
# Preserve the existing XML execution accuracy. The XML
|
||||
# protocol's future tolerance fields are a separate compatibility change.
|
||||
# Match the validated native pipe/chamber accuracy. Per-state SI absolute
|
||||
# floors are emitted by native_codegen.tolerances; XML tolerance fields
|
||||
# remain a separate protocol change.
|
||||
return SolveIVPConfig(t_start=simulation.t_start, t_stop=simulation.t_stop,
|
||||
method=simulation.method, rtol=1e-6, max_step=simulation.max_step)
|
||||
method=simulation.method, rtol=1e-7, max_step=simulation.max_step)
|
||||
|
||||
|
||||
def simulate_network(network, simulation, *, progress_callback=None,
|
||||
|
||||
@@ -5,6 +5,7 @@ from app.simulation.core.base import AlgebraicComponent
|
||||
from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
|
||||
from app.simulation.core.medium import IdealGasMedium
|
||||
from app.simulation.core.ports import PortDefinition
|
||||
from app.simulation.core.port_computation import ZERO_FLOW_SUPPLY
|
||||
|
||||
class AmesimPnpl01(AlgebraicComponent):
|
||||
"""AMESim PNPL01 zero pneumatic flow source.
|
||||
@@ -15,7 +16,7 @@ class AmesimPnpl01(AlgebraicComponent):
|
||||
"""
|
||||
MODEL_TYPE = 'amesim_pnpl01'
|
||||
MODEL_VERSION = '0.1.0'
|
||||
PORTS = (PortDefinition.pneumatic('port_1', nominal_role='bidirectional'),)
|
||||
PORTS = (PortDefinition.pneumatic('port_1', computation=ZERO_FLOW_SUPPLY),)
|
||||
PARAMETERS = ()
|
||||
RESULT_VARIABLES = ()
|
||||
DISPLAY = ComponentDisplaySpec(label='PNPL01 零气动流边界', library_id='amesim', category_id='boundary', symbol='amesim_pnpl01', ports=(PortDisplaySpec('port_1', 'left', order=10),), order=10)
|
||||
|
||||
@@ -8,6 +8,7 @@ from app.simulation.core.catalog import ComponentDisplaySpec, ParameterGroupDisp
|
||||
from app.simulation.core.metadata import ParameterCondition, ParameterDefinition, ParameterOption, ResultVariableDefinition
|
||||
from app.simulation.core.medium import GasMedium
|
||||
from app.simulation.core.ports import PortDefinition
|
||||
from app.simulation.core.port_computation import FLOW_SUPPLY
|
||||
_FLOW_COEFFICIENT_OPTIONS = (ParameterOption(1.0, 'Cq'), ParameterOption(2.0, 'Cv'), ParameterOption(3.0, 'Kv'))
|
||||
_FLOWSET_USES_CQ = (ParameterCondition('flowset', (1.0,)),)
|
||||
_FLOWSET_USES_CV = (ParameterCondition('flowset', (2.0,)),)
|
||||
@@ -23,7 +24,7 @@ class AmesimPnor001(AlgebraicComponent):
|
||||
"""
|
||||
MODEL_TYPE = 'amesim_pnor001'
|
||||
MODEL_VERSION = '0.3.0'
|
||||
PORTS = (PortDefinition.pneumatic('port_1', nominal_role='bidirectional'), PortDefinition.pneumatic('port_2', nominal_role='bidirectional'))
|
||||
PORTS = (PortDefinition.pneumatic('port_1', computation=FLOW_SUPPLY), PortDefinition.pneumatic('port_2', computation=FLOW_SUPPLY))
|
||||
PARAMETERS = (AMESIM_GAS_INDEX_PARAMETER, ParameterDefinition('cq', 0.72, label='流量系数 Cq', quantity='dimensionless', unit='', minimum=1e-10, maximum=1.0, description='孔口实际质量流量相对于理想可压缩流量的无量纲修正系数。', visible_when=_FLOWSET_USES_CQ), ParameterDefinition('area', 5e-06, label='孔口面积', quantity='area', unit='m2', minimum=0.0, maximum=1.0, description='选择 Cq/面积方式时用于流量计算的有效孔口面积。', visible_when=_FLOWSET_USES_CQ), ParameterDefinition('Cv', 0.5, label='流量系数 Cv', quantity='dimensionless', unit='', minimum=0.0, description='选择 Cv 方式时使用的英制流量系数。', visible_when=_FLOWSET_USES_CV), ParameterDefinition('Kv', 0.4, label='流量系数 Kv', quantity='dimensionless', unit='', minimum=0.0, description='选择 Kv 方式时使用的公制流量系数。', visible_when=_FLOWSET_USES_KV), ParameterDefinition('flowset', 1.0, label='流量系数设置', quantity='dimensionless', unit='', minimum=1.0, maximum=3.0, editor='choice', options=_FLOW_COEFFICIENT_OPTIONS, description='流量参数方式:1 使用 Cq 和面积,2 使用 Cv,3 使用 Kv。'))
|
||||
RESULT_VARIABLES = (ResultVariableDefinition('cm', label='质量流量参数', quantity='dimensionless', unit='', category='derived', order=10), ResultVariableDefinition('gasvel', label='缩流截面气体速度', quantity='velocity', unit='m/s', category='derived', order=20))
|
||||
DISPLAY = ComponentDisplaySpec(label='PNOR001 常系数气动孔口', library_id='amesim', category_id='flow', symbol='amesim_pnor001', ports=(PortDisplaySpec('port_1', 'left', order=10), PortDisplaySpec('port_2', 'right', order=20)), order=10, parameter_groups=(_PNOR001_FLOW_COEFFICIENT_GROUP,))
|
||||
@@ -84,13 +85,13 @@ class AmesimPnor001(AlgebraicComponent):
|
||||
class AmesimPnvo001FixedOpening(AlgebraicComponent):
|
||||
"""Fixed-opening public variant of AMESim PNVO001.
|
||||
|
||||
Full PNVO001 has a signal input port. The current public component library
|
||||
does not support signal simulation, so this model exposes the pneumatic
|
||||
ports and replaces the signal with a normalized `opening` parameter.
|
||||
Full PNVO001 has a signal input port. This optional variant exposes the
|
||||
pneumatic ports and replaces that signal with a normalized `opening`
|
||||
parameter; use AmesimPnvo001SignalOpening for a time-varying control signal.
|
||||
"""
|
||||
MODEL_TYPE = 'amesim_pnvo001_fixed'
|
||||
MODEL_VERSION = '0.2.0'
|
||||
PORTS = (PortDefinition.pneumatic('port_2', nominal_role='bidirectional'), PortDefinition.pneumatic('port_3', nominal_role='bidirectional'))
|
||||
PORTS = (PortDefinition.pneumatic('port_2', computation=FLOW_SUPPLY), PortDefinition.pneumatic('port_3', computation=FLOW_SUPPLY))
|
||||
PARAMETERS = (AMESIM_GAS_INDEX_PARAMETER, ParameterDefinition('cq', 0.72, label='流量系数 Cq', quantity='dimensionless', unit='', minimum=1e-10, maximum=1.0, description='孔口实际质量流量相对于理想可压缩流量的无量纲修正系数。', visible_when=_FLOWSET_USES_CQ), ParameterDefinition('area0', 5e-06, label='最大孔口面积', quantity='area', unit='m2', minimum=0.0, maximum=1.0, description='阀门完全开启时的最大有效孔口面积。', visible_when=_FLOWSET_USES_CQ), ParameterDefinition('Cv', 0.5, label='最大流量系数 Cv', quantity='dimensionless', unit='', minimum=0.0, description='选择 Cv 方式时使用的最大英制流量系数。', visible_when=_FLOWSET_USES_CV), ParameterDefinition('Kv', 0.4, label='最大流量系数 Kv', quantity='dimensionless', unit='', minimum=0.0, description='选择 Kv 方式时使用的最大公制流量系数。', visible_when=_FLOWSET_USES_KV), ParameterDefinition('flowset', 1.0, label='流量系数设置', quantity='dimensionless', unit='', minimum=1.0, maximum=3.0, editor='choice', options=_FLOW_COEFFICIENT_OPTIONS, description='流量参数方式:1 使用 Cq 和面积,2 使用 Cv,3 使用 Kv。'), ParameterDefinition('opening', 1.0, label='固定开度', quantity='dimensionless', unit='', minimum=0.0, maximum=1.0, description='固定的归一化阀门开度;0 表示关闭,1 表示完全开启。'))
|
||||
RESULT_VARIABLES = (ResultVariableDefinition('xv', label='有效开度', quantity='dimensionless', unit='', category='derived', order=10), ResultVariableDefinition('cm', label='质量流量参数', quantity='dimensionless', unit='', category='derived', order=20), ResultVariableDefinition('gasvel', label='缩流截面气体速度', quantity='velocity', unit='m/s', category='derived', order=30))
|
||||
DISPLAY = ComponentDisplaySpec(label='PNVO001 固定开度气动孔口', library_id='amesim', category_id='flow', symbol='amesim_pnvo001_fixed', ports=(PortDisplaySpec('port_2', 'right', order=10), PortDisplaySpec('port_3', 'left', order=20)), order=30, parameter_groups=(_PNVO001_FLOW_COEFFICIENT_GROUP,))
|
||||
@@ -143,7 +144,7 @@ class AmesimPnvo001SignalOpening(AmesimPnvo001FixedOpening):
|
||||
"""AMESim PNVO001 signal-controlled pneumatic orifice."""
|
||||
MODEL_TYPE = 'amesim_pnvo001'
|
||||
MODEL_VERSION = '0.2.0'
|
||||
PORTS = (PortDefinition.signal('res', nominal_role='input'), PortDefinition.pneumatic('port_2', nominal_role='bidirectional'), PortDefinition.pneumatic('port_3', nominal_role='bidirectional'))
|
||||
PORTS = (PortDefinition.signal('res', nominal_role='input'), PortDefinition.pneumatic('port_2', computation=FLOW_SUPPLY), PortDefinition.pneumatic('port_3', computation=FLOW_SUPPLY))
|
||||
PARAMETERS = (AMESIM_GAS_INDEX_PARAMETER, ParameterDefinition('cq', 0.72, label='流量系数 Cq', quantity='dimensionless', unit='', minimum=1e-10, maximum=1.0, description='孔口实际质量流量相对于理想可压缩流量的无量纲修正系数。', visible_when=_FLOWSET_USES_CQ), ParameterDefinition('area0', 5e-06, label='最大孔口面积', quantity='area', unit='m2', minimum=0.0, maximum=1.0, description='阀门完全开启时的最大有效孔口面积。', visible_when=_FLOWSET_USES_CQ), ParameterDefinition('Cv', 0.5, label='最大流量系数 Cv', quantity='dimensionless', unit='', minimum=0.0, description='选择 Cv 方式时使用的最大英制流量系数。', visible_when=_FLOWSET_USES_CV), ParameterDefinition('Kv', 0.4, label='最大流量系数 Kv', quantity='dimensionless', unit='', minimum=0.0, description='选择 Kv 方式时使用的最大公制流量系数。', visible_when=_FLOWSET_USES_KV), ParameterDefinition('flowset', 1.0, label='流量系数设置', quantity='dimensionless', unit='', minimum=1.0, maximum=3.0, editor='choice', options=_FLOW_COEFFICIENT_OPTIONS, description='流量参数方式:1 使用 Cq 和面积,2 使用 Cv,3 使用 Kv。'), ParameterDefinition('opening0', 1.0, label='初始开度', quantity='dimensionless', unit='', minimum=0.0, maximum=1.0, description='信号尚未传播时使用的归一化初始开度;0 表示关闭,1 表示完全开启。'))
|
||||
RESULT_VARIABLES = AmesimPnvo001FixedOpening.RESULT_VARIABLES
|
||||
DISPLAY = ComponentDisplaySpec(label='PNVO001 信号开度气动孔口', library_id='amesim', category_id='flow', symbol='amesim_pnvo001', ports=(PortDisplaySpec('res', 'left', order=5), PortDisplaySpec('port_2', 'right', order=10), PortDisplaySpec('port_3', 'left', order=20)), order=35, parameter_groups=(_PNVO001_FLOW_COEFFICIENT_GROUP,))
|
||||
|
||||
@@ -8,6 +8,7 @@ from app.simulation.core.catalog import ComponentDisplaySpec, ParameterGroupDisp
|
||||
from app.simulation.core.metadata import ParameterCondition, ParameterDefinition, ParameterOption, ResultVariableDefinition, THERMODYNAMIC_VOLUME_RESULT_VARIABLES
|
||||
from app.simulation.core.medium import GasMedium
|
||||
from app.simulation.core.ports import PortDefinition
|
||||
from app.simulation.core.port_computation import FLOW_SUPPLY, THERMODYNAMIC_SUPPLY
|
||||
_DYNAMIC_PIPE_POLYTROPIC_MODE = ParameterCondition('mode', (1.0,))
|
||||
_DYNAMIC_PIPE_HEAT_EXCHANGE_MODE = ParameterCondition('mode', (2.0,))
|
||||
_DYNAMIC_PIPE_PARAMETER_GROUPS = (ParameterGroupDisplaySpec(id='thermodynamics', label='热力学', parameters=('k', 'kth', 'extemp'), order=10),)
|
||||
@@ -21,7 +22,7 @@ class AmesimPnl00r(AlgebraicComponent):
|
||||
"""
|
||||
MODEL_TYPE = 'amesim_pnl00r'
|
||||
MODEL_VERSION = '0.3.0'
|
||||
PORTS = (PortDefinition.pneumatic('port_1', nominal_role='bidirectional'), PortDefinition.pneumatic('port_2', nominal_role='bidirectional'))
|
||||
PORTS = (PortDefinition.pneumatic('port_1', computation=FLOW_SUPPLY), PortDefinition.pneumatic('port_2', computation=FLOW_SUPPLY))
|
||||
PARAMETERS = (AMESIM_GAS_INDEX_PARAMETER, ParameterDefinition('diam', 0.01, label='管径', quantity='length', unit='m', minimum=0.0, minimum_exclusive=True, description='管路的有效内径,用于计算流通面积和摩擦压降。'), ParameterDefinition('le', 1.0, label='管长', quantity='length', unit='m', minimum=0.0, minimum_exclusive=True, description='参与摩擦压降计算的管路有效长度。'), ParameterDefinition('rr', 1e-05, label='相对粗糙度', quantity='dimensionless', unit='', minimum=0.0, maximum=0.1, description='管壁绝对粗糙度与管径之比,用于计算 Darcy 摩擦因子。'))
|
||||
RESULT_VARIABLES = (ResultVariableDefinition('re', label='Reynolds 数', quantity='dimensionless', unit='', category='derived', order=10), ResultVariableDefinition('cm', label='质量流量参数', quantity='dimensionless', unit='', category='derived', order=20), ResultVariableDefinition('v', label='平均气体速度', quantity='velocity', unit='m/s', category='derived', order=30), ResultVariableDefinition('ff', label='摩擦因子', quantity='dimensionless', unit='', category='derived', order=40))
|
||||
DISPLAY = ComponentDisplaySpec(label='PNL00R 气动管路阻力', library_id='amesim', category_id='flow', symbol='amesim_pnl00r', ports=(PortDisplaySpec('port_1', 'left', order=10), PortDisplaySpec('port_2', 'right', order=20)), order=20)
|
||||
@@ -54,7 +55,7 @@ class AmesimPnl0001(ThermodynamicVolumeComponent):
|
||||
"""AMESim PNL0001 C-R pneumatic pipe with compressibility and friction."""
|
||||
MODEL_TYPE = 'amesim_pnl0001'
|
||||
MODEL_VERSION = '0.4.0'
|
||||
PORTS = (PortDefinition.pneumatic('port_1', nominal_role='bidirectional'), PortDefinition.pneumatic('port_2', nominal_role='bidirectional'))
|
||||
PORTS = (PortDefinition.pneumatic('port_1', computation=FLOW_SUPPLY), PortDefinition.pneumatic('port_2', computation=THERMODYNAMIC_SUPPLY))
|
||||
PARAMETERS = (AMESIM_GAS_INDEX_PARAMETER, ParameterDefinition('diam', 0.01, label='管径', quantity='length', unit='m', minimum=0.0, minimum_exclusive=True, description='管路的有效内径,用于计算流通面积、储气容积和摩擦压降。'), ParameterDefinition('le', 1.0, label='管长', quantity='length', unit='m', minimum=0.0, minimum_exclusive=True, description='管路的有效长度,用于计算储气容积、换热面积和摩擦压降。'), ParameterDefinition('rr', 1e-05, label='相对粗糙度', quantity='dimensionless', unit='', minimum=0.0, maximum=0.1, description='管壁绝对粗糙度与管径之比,用于计算 Darcy 摩擦因子。'), ParameterDefinition('k', 1.35, label='多方指数', quantity='dimensionless', unit='', minimum=0.0, minimum_exclusive=True, maximum=2.0, description='mode=1 多方过程使用的指数;当前公开求解器保留该 AMESim 配置,尚未实现多方指数对状态方程的修正。', visible_when=(_DYNAMIC_PIPE_POLYTROPIC_MODE,)), ParameterDefinition('kth', 0.0, label='换热系数', quantity='heat_transfer_coefficient', unit='W/(m2*K)', minimum=0.0, description='mode=2 带换热过程使用的气体与外部环境对流换热系数。', visible_when=(_DYNAMIC_PIPE_HEAT_EXCHANGE_MODE,)), ParameterDefinition('extemp', 293.15, label='外部温度', quantity='temperature', unit='K', minimum=0.0, minimum_exclusive=True, description='mode=2 带换热过程使用的外部环境绝对温度。', visible_when=(_DYNAMIC_PIPE_HEAT_EXCHANGE_MODE,)), ParameterDefinition('mode', 2.0, label='热模型', quantity='dimensionless', unit='', minimum=1.0, maximum=2.0, editor='choice', options=(ParameterOption(1.0, '多方过程'), ParameterOption(2.0, '带换热')), description='AMESim 原始编码:1 为多方过程,2 为带换热。当前公开求解器在多方模式下关闭环境换热,在带换热模式下按换热系数和外部温度计算环境换热。'), ParameterDefinition('p0', 100000.0, label='初始压力', quantity='pressure', unit='Pa', minimum=0.0, minimum_exclusive=True, description='仿真开始时管内气体的绝对压力。'), ParameterDefinition('T0', 293.15, label='初始温度', quantity='temperature', unit='K', minimum=0.0, minimum_exclusive=True, description='仿真开始时管内气体的绝对温度。'))
|
||||
RESULT_VARIABLES = THERMODYNAMIC_VOLUME_RESULT_VARIABLES + (ResultVariableDefinition('re', label='Reynolds 数', quantity='dimensionless', unit='', category='derived', order=100), ResultVariableDefinition('cm', label='质量流量参数', quantity='dimensionless', unit='', category='derived', order=110), ResultVariableDefinition('v', label='平均气体速度', quantity='velocity', unit='m/s', category='derived', order=120), ResultVariableDefinition('ff', label='摩擦因子', quantity='dimensionless', unit='', category='derived', order=130))
|
||||
DISPLAY = ComponentDisplaySpec(label='PNL0001 C-R 动态管路', library_id='amesim', category_id='flow', symbol='amesim_pnl0001', ports=(PortDisplaySpec('port_1', 'left', order=10), PortDisplaySpec('port_2', 'right', order=20)), order=30, parameter_groups=_DYNAMIC_PIPE_PARAMETER_GROUPS)
|
||||
@@ -98,7 +99,7 @@ class AmesimPnl0002(AmesimPnl0001):
|
||||
"""AMESim PNL0002 R-C-R pneumatic pipe with one center compliance."""
|
||||
MODEL_TYPE = 'amesim_pnl0002'
|
||||
MODEL_VERSION = '0.6.0'
|
||||
PORTS = (PortDefinition.pneumatic('port_1', nominal_role='bidirectional'), PortDefinition.pneumatic('port_2', nominal_role='bidirectional'))
|
||||
PORTS = (PortDefinition.pneumatic('port_1', computation=FLOW_SUPPLY), PortDefinition.pneumatic('port_2', computation=FLOW_SUPPLY))
|
||||
PARAMETERS = AmesimPnl0001.PARAMETERS
|
||||
RESULT_VARIABLES = AmesimPnl0001.RESULT_VARIABLES
|
||||
DISPLAY = ComponentDisplaySpec(label='PNL0002 R-C-R 动态管路', library_id='amesim', category_id='flow', symbol='amesim_pnl0002', ports=(PortDisplaySpec('port_1', 'left', order=10), PortDisplaySpec('port_2', 'right', order=20)), order=40, parameter_groups=_DYNAMIC_PIPE_PARAMETER_GROUPS)
|
||||
@@ -117,7 +118,7 @@ class AmesimPnl0003(DynamicComponent):
|
||||
state_size = 4
|
||||
MODEL_TYPE = 'amesim_pnl0003'
|
||||
MODEL_VERSION = '0.4.0'
|
||||
PORTS = (PortDefinition.pneumatic('port_1', nominal_role='bidirectional'), PortDefinition.pneumatic('port_2', nominal_role='bidirectional'))
|
||||
PORTS = (PortDefinition.pneumatic('port_1', computation=THERMODYNAMIC_SUPPLY), PortDefinition.pneumatic('port_2', computation=THERMODYNAMIC_SUPPLY))
|
||||
PARAMETERS = AmesimPnl0001.PARAMETERS[:-2] + (ParameterDefinition('p1_0', 100000.0, label='端口 1 初始压力', quantity='pressure', unit='Pa', minimum=0.0, minimum_exclusive=True), ParameterDefinition('T1_0', 293.15, label='端口 1 初始温度', quantity='temperature', unit='K', minimum=0.0, minimum_exclusive=True), ParameterDefinition('p2_0', 100000.0, label='端口 2 初始压力', quantity='pressure', unit='Pa', minimum=0.0, minimum_exclusive=True), ParameterDefinition('T2_0', 293.15, label='端口 2 初始温度', quantity='temperature', unit='K', minimum=0.0, minimum_exclusive=True))
|
||||
RESULT_VARIABLES = (ResultVariableDefinition('m1', '端口 1 侧质量', 'mass', 'kg', 'state', 10), ResultVariableDefinition('U1', '端口 1 侧内能', 'internal_energy', 'J', 'state', 20), ResultVariableDefinition('p1', '端口 1 侧压力', 'pressure', 'Pa', 'thermodynamic', 30), ResultVariableDefinition('T1', '端口 1 侧温度', 'temperature', 'K', 'thermodynamic', 40), ResultVariableDefinition('rho1', '端口 1 侧密度', 'density', 'kg/m³', 'thermodynamic', 50), ResultVariableDefinition('u1', '端口 1 侧比内能', 'specific_internal_energy', 'J/kg', 'thermodynamic', 60), ResultVariableDefinition('h1', '端口 1 侧比焓', 'specific_enthalpy', 'J/kg', 'thermodynamic', 70), ResultVariableDefinition('m2', '端口 2 侧质量', 'mass', 'kg', 'state', 80), ResultVariableDefinition('U2', '端口 2 侧内能', 'internal_energy', 'J', 'state', 90), ResultVariableDefinition('p2', '端口 2 侧压力', 'pressure', 'Pa', 'thermodynamic', 100), ResultVariableDefinition('T2', '端口 2 侧温度', 'temperature', 'K', 'thermodynamic', 110), ResultVariableDefinition('rho2', '端口 2 侧密度', 'density', 'kg/m³', 'thermodynamic', 120), ResultVariableDefinition('u2', '端口 2 侧比内能', 'specific_internal_energy', 'J/kg', 'thermodynamic', 130), ResultVariableDefinition('h2', '端口 2 侧比焓', 'specific_enthalpy', 'J/kg', 'thermodynamic', 140), ResultVariableDefinition('dmctr', '中心质量流量', 'mass_flow', 'kg/s', 'derived', 150), ResultVariableDefinition('re', 'Reynolds 数', 'dimensionless', '', 'derived', 160), ResultVariableDefinition('cm', '质量流量参数', 'dimensionless', '', 'derived', 170), ResultVariableDefinition('v', '平均气体速度', 'velocity', 'm/s', 'derived', 180), ResultVariableDefinition('ff', '摩擦因子', 'dimensionless', '', 'derived', 190))
|
||||
DISPLAY = ComponentDisplaySpec(label='PNL0003 C-R-C 动态管路', library_id='amesim', category_id='flow', symbol='amesim_pnl0003', ports=(PortDisplaySpec('port_1', 'left', order=10), PortDisplaySpec('port_2', 'right', order=20)), order=50, parameter_groups=_DYNAMIC_PIPE_PARAMETER_GROUPS)
|
||||
|
||||
@@ -5,6 +5,7 @@ from app.simulation.core.base import AlgebraicComponent
|
||||
from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
|
||||
from app.simulation.core.medium import IdealGasMedium
|
||||
from app.simulation.core.ports import PortDefinition
|
||||
from app.simulation.core.port_computation import NODE_BRANCH, NODE_REFERENCE
|
||||
|
||||
class _AmesimPneumaticNode(AlgebraicComponent):
|
||||
"""Shared implementation for AMESim pneumatic junction submodels.
|
||||
@@ -27,7 +28,7 @@ class AmesimPn3Node2(_AmesimPneumaticNode):
|
||||
"""AMESim PN3NODE2 pneumatic three-port junction."""
|
||||
MODEL_TYPE = 'amesim_pn3node2'
|
||||
MODEL_VERSION = '0.3.0'
|
||||
PORTS = (PortDefinition.pneumatic('port_1', nominal_role='bidirectional'), PortDefinition.pneumatic('port_2', nominal_role='bidirectional'), PortDefinition.pneumatic('port_3', nominal_role='bidirectional'))
|
||||
PORTS = (PortDefinition.pneumatic('port_1', computation=NODE_BRANCH), PortDefinition.pneumatic('port_2', computation=NODE_REFERENCE), PortDefinition.pneumatic('port_3', computation=NODE_BRANCH))
|
||||
PARAMETERS = ()
|
||||
RESULT_VARIABLES = ()
|
||||
DISPLAY = ComponentDisplaySpec(label='PN3NODE2 三端气动节点', library_id='amesim', category_id='junctions', symbol='amesim_pn3node2', ports=(PortDisplaySpec('port_1', 'left', order=10), PortDisplaySpec('port_2', 'right', order=20), PortDisplaySpec('port_3', 'right', order=30)), order=10)
|
||||
@@ -41,7 +42,7 @@ class AmesimP4Node2(_AmesimPneumaticNode):
|
||||
"""AMESim P4NODE2 pneumatic four-port junction."""
|
||||
MODEL_TYPE = 'amesim_p4node2'
|
||||
MODEL_VERSION = '0.3.0'
|
||||
PORTS = (PortDefinition.pneumatic('port_1', nominal_role='bidirectional'), PortDefinition.pneumatic('port_2', nominal_role='bidirectional'), PortDefinition.pneumatic('port_3', nominal_role='bidirectional'), PortDefinition.pneumatic('port_4', nominal_role='bidirectional'))
|
||||
PORTS = (PortDefinition.pneumatic('port_1', computation=NODE_BRANCH), PortDefinition.pneumatic('port_2', computation=NODE_REFERENCE), PortDefinition.pneumatic('port_3', computation=NODE_BRANCH), PortDefinition.pneumatic('port_4', computation=NODE_BRANCH))
|
||||
PARAMETERS = ()
|
||||
RESULT_VARIABLES = ()
|
||||
DISPLAY = ComponentDisplaySpec(label='P4NODE2 四端气动节点', library_id='amesim', category_id='junctions', symbol='amesim_p4node2', ports=(PortDisplaySpec('port_1', 'left', order=10), PortDisplaySpec('port_2', 'right', order=20), PortDisplaySpec('port_3', 'right', order=30), PortDisplaySpec('port_4', 'right', order=40)), order=20)
|
||||
|
||||
@@ -8,6 +8,7 @@ from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
|
||||
from app.simulation.core.metadata import ParameterDefinition, ResultVariableDefinition
|
||||
from app.simulation.core.medium import GasMedium
|
||||
from app.simulation.core.ports import PortDefinition
|
||||
from app.simulation.core.port_computation import FLOW_SUPPLY
|
||||
AMESIM_REFERENCE_PRESSURE_PA = 101300.0
|
||||
|
||||
class AmesimPnrp17(AlgebraicComponent):
|
||||
@@ -19,7 +20,7 @@ class AmesimPnrp17(AlgebraicComponent):
|
||||
"""
|
||||
MODEL_TYPE = 'amesim_pnrp17'
|
||||
MODEL_VERSION = '0.1.0'
|
||||
PORTS = (PortDefinition.pneumatic('port_1', nominal_role='bidirectional'), PortDefinition.mechanical_translational('port_2'), PortDefinition.mechanical_translational('port_3'), PortDefinition.mechanical_translational('port_4'), PortDefinition.mechanical_translational('port_5'))
|
||||
PORTS = (PortDefinition.pneumatic('port_1', computation=FLOW_SUPPLY), PortDefinition.mechanical_translational('port_2'), PortDefinition.mechanical_translational('port_3'), PortDefinition.mechanical_translational('port_4'), PortDefinition.mechanical_translational('port_5'))
|
||||
PARAMETERS = (AMESIM_GAS_INDEX_PARAMETER, ParameterDefinition('dp', 0.2, label='活塞直径', quantity='length', unit='m', minimum=0.0, minimum_exclusive=True, description='活塞外径;与活塞杆直径共同确定有效受压面积。'), ParameterDefinition('dr', 0.001, label='活塞杆直径', quantity='length', unit='m', minimum=0.0, description='穿过气室一侧的活塞杆直径,必须不大于活塞直径。'), ParameterDefinition('x0', 0.0, label='初始腔长', quantity='length', unit='m', description='机械端位移均为零时的气动腔长度。'))
|
||||
RESULT_VARIABLES = (ResultVariableDefinition('volume', '扫掠容积', 'volume', 'm3', 'derived', 10), ResultVariableDefinition('volume_flow', '扫掠容积变化率', 'volume_flow', 'm3/s', 'derived', 20), ResultVariableDefinition('length', '气动腔长度', 'length', 'm', 'derived', 30), ResultVariableDefinition('pressure_force', '气压力', 'force', 'N', 'derived', 40))
|
||||
DISPLAY = ComponentDisplaySpec(label='PNRP17 气动活塞', library_id='amesim', category_id='mechanical', symbol='amesim_pnrp17', ports=(PortDisplaySpec('port_1', 'left', order=10), PortDisplaySpec('port_3', 'left', order=20), PortDisplaySpec('port_2', 'left', order=30), PortDisplaySpec('port_4', 'right', order=40), PortDisplaySpec('port_5', 'right', order=50)), order=60)
|
||||
|
||||
@@ -7,6 +7,7 @@ from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
|
||||
from app.simulation.core.metadata import ParameterDefinition, ResultVariableDefinition, THERMODYNAMIC_VOLUME_RESULT_VARIABLES
|
||||
from app.simulation.core.medium import GasMedium
|
||||
from app.simulation.core.ports import PortDefinition
|
||||
from app.simulation.core.port_computation import THERMODYNAMIC_SUPPLY
|
||||
|
||||
class AmesimPnch023(ThermodynamicVolumeComponent):
|
||||
"""AMESim PNCH023 simple pneumatic chamber with heat exchange.
|
||||
@@ -18,7 +19,7 @@ class AmesimPnch023(ThermodynamicVolumeComponent):
|
||||
"""
|
||||
MODEL_TYPE = 'amesim_pnch023'
|
||||
MODEL_VERSION = '0.1.0'
|
||||
PORTS = (PortDefinition.pneumatic('port_1', nominal_role='bidirectional'), PortDefinition.pneumatic('port_2', nominal_role='bidirectional'))
|
||||
PORTS = (PortDefinition.pneumatic('port_1', computation=THERMODYNAMIC_SUPPLY), PortDefinition.pneumatic('port_2', computation=THERMODYNAMIC_SUPPLY))
|
||||
PARAMETERS = (AMESIM_GAS_INDEX_PARAMETER, ParameterDefinition('cvol', 0.057, label='气室容积', quantity='volume', unit='m3', minimum=0.0, minimum_exclusive=True, description='气室内部用于储存气体的固定有效容积。'), ParameterDefinition('kth', 0.0, label='换热系数', quantity='heat_transfer_coefficient', unit='W/(m2*K)', minimum=0.0, description='气室与环境之间的对流换热系数,与换热面积共同决定换热功率。'), ParameterDefinition('sth', 0.1, label='换热面积', quantity='area', unit='m2', minimum=0.0, description='气室与环境进行热交换的有效表面积。'), ParameterDefinition('extemp', 293.15, label='外部温度', quantity='temperature', unit='K', minimum=0.0, minimum_exclusive=True, description='气室外部环境的绝对温度,用于计算气体与环境之间的换热。'), ParameterDefinition('p0', 100000.0, label='初始压力', quantity='pressure', unit='Pa', minimum=0.0, minimum_exclusive=True, description='仿真开始时气室内气体的绝对压力。'), ParameterDefinition('T0', 293.15, label='初始温度', quantity='temperature', unit='K', minimum=0.0, minimum_exclusive=True, description='仿真开始时气室内气体的绝对温度。'))
|
||||
RESULT_VARIABLES = THERMODYNAMIC_VOLUME_RESULT_VARIABLES
|
||||
DISPLAY = ComponentDisplaySpec(label='PNCH023 固定容积气室', library_id='amesim', category_id='storage', symbol='amesim_pnch023', ports=(PortDisplaySpec('port_1', 'left', order=10), PortDisplaySpec('port_2', 'right', order=20)), order=10)
|
||||
@@ -52,7 +53,7 @@ class AmesimPnch012(ThermodynamicVolumeComponent):
|
||||
"""
|
||||
MODEL_TYPE = 'amesim_pnch012'
|
||||
MODEL_VERSION = '0.1.0'
|
||||
PORTS = (PortDefinition.pneumatic('port_1', nominal_role='bidirectional'), PortDefinition.pneumatic('port_2', nominal_role='bidirectional'), PortDefinition.pneumatic('port_3', nominal_role='bidirectional'), PortDefinition.pneumatic('port_4', nominal_role='bidirectional'))
|
||||
PORTS = tuple(PortDefinition.pneumatic(f'port_{i}', computation=THERMODYNAMIC_SUPPLY) for i in range(1, 5))
|
||||
PARAMETERS = (AMESIM_GAS_INDEX_PARAMETER, ParameterDefinition('cvol0', 0.015, label='死容积', quantity='volume', unit='m3', minimum=0.0, minimum_exclusive=True, description='变容气室在所有外部容积为零时仍保留的基础容积。'), ParameterDefinition('kth', 0.0, label='换热系数', quantity='heat_transfer_coefficient', unit='W/(m2*K)', minimum=0.0, description='气室与环境之间的对流换热系数,与换热面积共同决定换热功率。'), ParameterDefinition('sth', 0.1, label='换热面积', quantity='area', unit='m2', minimum=0.0, description='气室与环境进行热交换的有效表面积。'), ParameterDefinition('extemp', 293.15, label='外部温度', quantity='temperature', unit='K', minimum=0.0, minimum_exclusive=True, description='气室外部环境的绝对温度,用于计算气体与环境之间的换热。'), ParameterDefinition('p0', 100000.0, label='初始压力', quantity='pressure', unit='Pa', minimum=0.0, minimum_exclusive=True, description='仿真开始时气室内气体的绝对压力。'), ParameterDefinition('T0', 293.15, label='初始温度', quantity='temperature', unit='K', minimum=0.0, minimum_exclusive=True, description='仿真开始时气室内气体的绝对温度。'), ParameterDefinition('vol1', 0.0, label='端口 1 外部容积', quantity='volume', unit='m3'), ParameterDefinition('vol2', 0.0, label='端口 2 外部容积', quantity='volume', unit='m3'), ParameterDefinition('vol3', 0.0, label='端口 3 外部容积', quantity='volume', unit='m3'), ParameterDefinition('vol4', 0.0, label='端口 4 外部容积', quantity='volume', unit='m3'), ParameterDefinition('dvol1', 0.0, label='端口 1 容积变化率', quantity='volume_flow', unit='m3/s'), ParameterDefinition('dvol2', 0.0, label='端口 2 容积变化率', quantity='volume_flow', unit='m3/s'), ParameterDefinition('dvol3', 0.0, label='端口 3 容积变化率', quantity='volume_flow', unit='m3/s'), ParameterDefinition('dvol4', 0.0, label='端口 4 容积变化率', quantity='volume_flow', unit='m3/s'))
|
||||
RESULT_VARIABLES = THERMODYNAMIC_VOLUME_RESULT_VARIABLES + (ResultVariableDefinition('vol', '气室总容积', 'volume', 'm3', 'derived', 100), ResultVariableDefinition('dvol', '总容积变化率', 'volume_flow', 'm3/s', 'derived', 110))
|
||||
DISPLAY = ComponentDisplaySpec(label='PNCH012 变容气室', library_id='amesim', category_id='storage', symbol='amesim_pnch012', ports=(PortDisplaySpec('port_1', 'left', order=10), PortDisplaySpec('port_2', 'right', order=20), PortDisplaySpec('port_3', 'left', order=30), PortDisplaySpec('port_4', 'right', order=40)), order=20)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
1. 在 [cylinder.py](experimental/storage/cylinder.py) 声明 `MODEL_TYPE`、`MODEL_VERSION`、`PORTS`、`PARAMETERS`、`RESULT_VARIABLES`、`DISPLAY` 和 `create()`。
|
||||
2. 构造函数调用 `set_parameter_values()`、`register_declared_port()`,保存介质选择和容积。不要在 Python 中计算密度、内能或状态导数。
|
||||
3. 通过 `EQUATIONS` 声明端口压力与气瓶状态之间的约束;只保存变量名和关系。
|
||||
气动端口同时声明 `computation`,说明温度、压力和质量/能量流率由谁提供;固定参考口还要声明其支路的参考来源,见 [端口供需合同](../../../docs/standard/port-computation-contract.md)。
|
||||
4. 在 [extended.py](../native_codegen/extended.py) 分配状态及输出位置,生成 `native_medium_init()` 初始化调用和气瓶质量/能量导数计算。
|
||||
5. 公共物性和数值公式由 [kernels.c](../../../native/components/kernels.c) 实现,积分和事件由 `native/runtime/` 处理。
|
||||
6. 加入组件库 `library.py` 及 C 版本白名单,验证目录/XML 合同、边界输入、逆流、守恒、RK45/BDF 和输出键。
|
||||
|
||||
@@ -6,12 +6,13 @@ from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
|
||||
from app.simulation.core.metadata import ParameterDefinition
|
||||
from app.simulation.core.medium import IdealGasMedium
|
||||
from app.simulation.core.ports import PortDefinition
|
||||
from app.simulation.core.port_computation import FLOW_SUPPLY
|
||||
|
||||
class Orifice(AlgebraicComponent):
|
||||
"""Python port of ModelicaModels.Myorifice."""
|
||||
MODEL_TYPE = 'orifice'
|
||||
MODEL_VERSION = '1.0.0'
|
||||
PORTS = (PortDefinition.pneumatic('port_a', nominal_role='inlet'), PortDefinition.pneumatic('port_b', nominal_role='outlet'))
|
||||
PORTS = (PortDefinition.pneumatic('port_a', nominal_role='inlet', computation=FLOW_SUPPLY), PortDefinition.pneumatic('port_b', nominal_role='outlet', computation=FLOW_SUPPLY))
|
||||
PARAMETERS = (ParameterDefinition('K', 1e-05, label='流量系数', quantity='flow_coefficient', unit='kg/(s*Pa^0.5)', minimum=0.0), ParameterDefinition('opening', 1.0, label='开度', minimum=0.0, maximum=1.0))
|
||||
RESULT_VARIABLES = ()
|
||||
DISPLAY = ComponentDisplaySpec(label='孔板/阀门', library_id='experimental', category_id='flow', symbol='orifice', ports=(PortDisplaySpec('port_a', 'left', order=10), PortDisplaySpec('port_b', 'right', order=20)), order=40)
|
||||
|
||||
@@ -7,12 +7,13 @@ from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
|
||||
from app.simulation.core.metadata import ParameterDefinition
|
||||
from app.simulation.core.medium import IdealGasMedium
|
||||
from app.simulation.core.ports import PortDefinition
|
||||
from app.simulation.core.port_computation import FLOW_SUPPLY
|
||||
|
||||
class ResistivePipe(AlgebraicComponent):
|
||||
"""Quasi-steady Darcy resistance used by topology-driven simulation."""
|
||||
MODEL_TYPE = 'pipe'
|
||||
MODEL_VERSION = '1.0.0'
|
||||
PORTS = (PortDefinition.pneumatic('port_a', nominal_role='inlet'), PortDefinition.pneumatic('port_b', nominal_role='outlet'))
|
||||
PORTS = (PortDefinition.pneumatic('port_a', nominal_role='inlet', computation=FLOW_SUPPLY), PortDefinition.pneumatic('port_b', nominal_role='outlet', computation=FLOW_SUPPLY))
|
||||
PARAMETERS = (ParameterDefinition('length', 5.0, label='长度', quantity='length', unit='m', minimum=0.0, minimum_exclusive=True), ParameterDefinition('diameter', 0.02, label='直径', quantity='length', unit='m', minimum=0.0, minimum_exclusive=True), ParameterDefinition('lambda_darcy', 0.02, label='摩阻系数', minimum=0.0), ParameterDefinition('p0', 100000.0, label='初始压力', quantity='pressure', unit='Pa', minimum=0.0, minimum_exclusive=True), ParameterDefinition('T0', 300.0, label='初始温度', quantity='temperature', unit='K', minimum=0.0, minimum_exclusive=True))
|
||||
RESULT_VARIABLES = ()
|
||||
DISPLAY = ComponentDisplaySpec(label='管段', library_id='experimental', category_id='flow', symbol='pipe', ports=(PortDisplaySpec('port_a', 'left', order=10), PortDisplaySpec('port_b', 'right', order=20)), order=30)
|
||||
|
||||
@@ -6,12 +6,13 @@ from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
|
||||
from app.simulation.core.metadata import ParameterDefinition, THERMODYNAMIC_VOLUME_RESULT_VARIABLES
|
||||
from app.simulation.core.medium import IdealGasMedium
|
||||
from app.simulation.core.ports import PortDefinition
|
||||
from app.simulation.core.port_computation import THERMODYNAMIC_SUPPLY
|
||||
|
||||
class Cylinder(ThermodynamicVolumeComponent):
|
||||
"""Python port of ModelicaModels.Mycylinder."""
|
||||
MODEL_TYPE = 'cylinder'
|
||||
MODEL_VERSION = '1.0.0'
|
||||
PORTS = (PortDefinition.pneumatic('port_b', nominal_role='outlet'),)
|
||||
PORTS = (PortDefinition.pneumatic('port_b', nominal_role='outlet', computation=THERMODYNAMIC_SUPPLY),)
|
||||
PARAMETERS = (ParameterDefinition('volume', 0.01, label='容积', quantity='volume', unit='m3', minimum=0.0, minimum_exclusive=True), ParameterDefinition('p0', 35000000.0, label='初始压力', quantity='pressure', unit='Pa', minimum=0.0, minimum_exclusive=True), ParameterDefinition('T0', 300.0, label='初始温度', quantity='temperature', unit='K', minimum=0.0, minimum_exclusive=True))
|
||||
RESULT_VARIABLES = THERMODYNAMIC_VOLUME_RESULT_VARIABLES
|
||||
DISPLAY = ComponentDisplaySpec(label='气瓶', library_id='experimental', category_id='storage', symbol='cylinder', ports=(PortDisplaySpec('port_b', 'right'),), order=10)
|
||||
|
||||
@@ -6,12 +6,13 @@ from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
|
||||
from app.simulation.core.metadata import ParameterDefinition, THERMODYNAMIC_VOLUME_RESULT_VARIABLES
|
||||
from app.simulation.core.medium import IdealGasMedium
|
||||
from app.simulation.core.ports import PortDefinition
|
||||
from app.simulation.core.port_computation import THERMODYNAMIC_SUPPLY
|
||||
|
||||
class Tank(ThermodynamicVolumeComponent):
|
||||
"""Python port of ModelicaModels.Mytank."""
|
||||
MODEL_TYPE = 'tank'
|
||||
MODEL_VERSION = '1.0.0'
|
||||
PORTS = (PortDefinition.pneumatic('port_a', nominal_role='inlet'),)
|
||||
PORTS = (PortDefinition.pneumatic('port_a', nominal_role='inlet', computation=THERMODYNAMIC_SUPPLY),)
|
||||
PARAMETERS = (ParameterDefinition('volume', 0.1, label='容积', quantity='volume', unit='m3', minimum=0.0, minimum_exclusive=True), ParameterDefinition('p0', 100000.0, label='初始压力', quantity='pressure', unit='Pa', minimum=0.0, minimum_exclusive=True), ParameterDefinition('T0', 300.0, label='初始温度', quantity='temperature', unit='K', minimum=0.0, minimum_exclusive=True))
|
||||
RESULT_VARIABLES = THERMODYNAMIC_VOLUME_RESULT_VARIABLES
|
||||
DISPLAY = ComponentDisplaySpec(label='贮箱', library_id='experimental', category_id='storage', symbol='tank', ports=(PortDisplaySpec('port_a', 'left'),), order=20)
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
"""Compile-time variable supply contracts, separate from physical flow direction.
|
||||
|
||||
Equation ports may participate in a simultaneous solve. Fixed ports (for example
|
||||
an Amesim node's reference/branch ports) require complementary local supplies.
|
||||
These declarations do not add numerical state or Python evaluation callbacks.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Literal, Mapping
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .ports import PortDefinition
|
||||
|
||||
|
||||
VARIABLE_LABELS = {
|
||||
"p": "压力", "T": "温度", "m_flow": "质量流率", "H_flow": "能量流率",
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PortComputation:
|
||||
inputs: tuple[str, ...] = ()
|
||||
outputs: tuple[str, ...] = ()
|
||||
mode: Literal["equation", "fixed"] = "equation"
|
||||
# Output p/T aliases an input on another port of the same component.
|
||||
reference_port: str | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.mode not in {"equation", "fixed"}:
|
||||
raise ValueError(f"Unknown port computation mode: {self.mode}")
|
||||
members = (*self.inputs, *self.outputs)
|
||||
if len(set(members)) != len(members) or set(members) - VARIABLE_LABELS.keys():
|
||||
raise ValueError("Port computation variables must be unique, supported quantities.")
|
||||
if self.reference_port and not {"p", "T"}.issubset(self.outputs):
|
||||
raise ValueError("A reference alias must supply pressure and temperature.")
|
||||
|
||||
def as_dict(self) -> dict[str, object]:
|
||||
return {
|
||||
"mode": self.mode,
|
||||
"inputs": list(self.inputs),
|
||||
"outputs": list(self.outputs),
|
||||
**({"referencePort": self.reference_port} if self.reference_port else {}),
|
||||
}
|
||||
|
||||
|
||||
THERMODYNAMIC_SUPPLY = PortComputation(inputs=("m_flow", "H_flow"), outputs=("p", "T"))
|
||||
FLOW_SUPPLY = PortComputation(inputs=("p", "T"), outputs=("m_flow", "H_flow"))
|
||||
ZERO_FLOW_SUPPLY = PortComputation(outputs=("m_flow", "H_flow"))
|
||||
IMPLICIT_PNEUMATIC = PortComputation()
|
||||
NODE_REFERENCE = PortComputation(inputs=("p", "T"), outputs=("m_flow", "H_flow"), mode="fixed")
|
||||
NODE_BRANCH = PortComputation(inputs=("m_flow", "H_flow"), outputs=("p", "T"),
|
||||
mode="fixed", reference_port="port_2")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PortSupplyIssue:
|
||||
code: str
|
||||
message: str
|
||||
endpoint: tuple[str, str] | None = None
|
||||
|
||||
|
||||
class PortSupplyError(ValueError):
|
||||
def __init__(self, issue: PortSupplyIssue):
|
||||
self.issue = issue
|
||||
super().__init__(f"{issue.code}: {issue.message}")
|
||||
|
||||
|
||||
def port_supply_issue(first: PortDefinition, second: PortDefinition,
|
||||
first_label: str | None = None, second_label: str | None = None
|
||||
) -> PortSupplyIssue | None:
|
||||
"""Check fixed causality; ordinary equation-to-equation links stay legal."""
|
||||
if first.kind != second.kind or first.domain != second.domain:
|
||||
return None # Domain/type checks own their existing, more specific errors.
|
||||
a, b = first.computation, second.computation
|
||||
if not any(item and item.mode == "fixed" for item in (a, b)):
|
||||
return None
|
||||
for consumer, supplier, consumer_label, supplier_label in (
|
||||
(a, b, first_label or first.name, second_label or second.name),
|
||||
(b, a, second_label or second.name, first_label or first.name),
|
||||
):
|
||||
if consumer is None:
|
||||
continue
|
||||
missing = [name for name in consumer.inputs
|
||||
if supplier is None or name not in supplier.outputs]
|
||||
if missing:
|
||||
quantities = "、".join(VARIABLE_LABELS[name] for name in missing)
|
||||
return PortSupplyIssue(
|
||||
"CONNECTION_VARIABLE_SUPPLY_MISSING",
|
||||
f"{consumer_label} 需要对端提供{quantities},但 {supplier_label} 未提供;"
|
||||
"请检查参考口与支路口的连接。气体流向反转不会改变这一供需关系。",
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def reference_supply_issues(
|
||||
ports: Mapping[tuple[str, str], PortDefinition],
|
||||
adjacency: Mapping[tuple[str, str], tuple[str, str]],
|
||||
) -> list[PortSupplyIssue]:
|
||||
"""Follow declared aliases to reject a reference ring without an origin.
|
||||
|
||||
This is a supply check, not a whole-system execution scheduler. Reference
|
||||
chains are iterative to support deep networks without Python recursion.
|
||||
"""
|
||||
issues = []
|
||||
resolved: dict[str, set[tuple[str, str]]] = {'p': set(), 'T': set()}
|
||||
for endpoint, port in ports.items():
|
||||
contract = port.computation
|
||||
if not contract or contract.mode != "fixed" or not {"p", "T"}.issubset(contract.inputs):
|
||||
continue
|
||||
if endpoint not in adjacency:
|
||||
continue # Existing unconnected-port checks handle incomplete drawings.
|
||||
for variable in ("p", "T"):
|
||||
current = endpoint
|
||||
visited: set[tuple[str, str]] = set()
|
||||
chain: list[str] = []
|
||||
while True:
|
||||
if current in resolved[variable]:
|
||||
resolved[variable].update(visited)
|
||||
break
|
||||
if current in visited:
|
||||
issues.append(PortSupplyIssue(
|
||||
"REFERENCE_SUPPLY_CYCLE",
|
||||
f"{'.'.join(endpoint)} 的{VARIABLE_LABELS[variable]}参考形成循环,"
|
||||
f"没有实际提供者:{' → '.join(chain)} → {'.'.join(current)}。",
|
||||
endpoint,
|
||||
))
|
||||
break
|
||||
visited.add(current)
|
||||
chain.append('.'.join(current))
|
||||
supplier = adjacency.get(current)
|
||||
if supplier is None:
|
||||
issues.append(PortSupplyIssue(
|
||||
"REFERENCE_SUPPLY_UNCONNECTED",
|
||||
f"{'.'.join(endpoint)} 的{VARIABLE_LABELS[variable]}参考链在 "
|
||||
f"{'.'.join(current)} 中断:该参考输入尚未连接。", endpoint,
|
||||
))
|
||||
break
|
||||
supplied = ports.get(supplier)
|
||||
supply = supplied.computation if supplied else None
|
||||
if supply is None or variable not in supply.outputs:
|
||||
# Direct errors are already reported per connection. An
|
||||
# indirect failure is explained at that failing connection.
|
||||
break
|
||||
if supply.reference_port is None:
|
||||
resolved[variable].update(visited)
|
||||
break
|
||||
chain.append('.'.join(supplier))
|
||||
current = supplier[0], supply.reference_port
|
||||
if current not in ports:
|
||||
raise ValueError(f"Invalid reference port declaration: {current}")
|
||||
return issues
|
||||
@@ -3,6 +3,8 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Literal
|
||||
|
||||
from .port_computation import IMPLICIT_PNEUMATIC, PortComputation
|
||||
|
||||
|
||||
PortKind = Literal["physical", "signal"]
|
||||
PortNominalRole = Literal["inlet", "outlet", "bidirectional", "input", "output"]
|
||||
@@ -45,6 +47,7 @@ class PortDefinition:
|
||||
nominal_role: PortNominalRole
|
||||
positive_flow_direction: Literal["intoComponent"] | None = None
|
||||
variables: tuple[PortVariableDefinition, ...] = ()
|
||||
computation: PortComputation | None = None
|
||||
|
||||
@classmethod
|
||||
def pneumatic(
|
||||
@@ -52,6 +55,7 @@ class PortDefinition:
|
||||
name: str,
|
||||
*,
|
||||
nominal_role: Literal["inlet", "outlet", "bidirectional"] = "bidirectional",
|
||||
computation: PortComputation = IMPLICIT_PNEUMATIC,
|
||||
) -> PortDefinition:
|
||||
return cls(
|
||||
name=name,
|
||||
@@ -59,6 +63,7 @@ class PortDefinition:
|
||||
domain="pneumatic",
|
||||
nominal_role=nominal_role,
|
||||
positive_flow_direction="intoComponent",
|
||||
computation=computation,
|
||||
variables=(
|
||||
PortVariableDefinition(
|
||||
"p",
|
||||
@@ -188,6 +193,7 @@ class PortDefinition:
|
||||
"nominalRole": self.nominal_role,
|
||||
"positiveFlowDirection": self.positive_flow_direction,
|
||||
"variables": [variable.as_interface_dict() for variable in self.variables],
|
||||
**({"computation": self.computation.as_dict()} if self.computation else {}),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -24,6 +24,7 @@ from app.simulation.core.metadata import (
|
||||
)
|
||||
from app.simulation.core.medium import GasMedium, IdealGasMedium
|
||||
from app.simulation.core.ports import PortDefinition, PortVariableDefinition
|
||||
from app.simulation.core.port_computation import PortComputation
|
||||
|
||||
|
||||
ParameterSpec = ParameterDefinition
|
||||
@@ -877,8 +878,18 @@ def validate_component_model_class(
|
||||
f"Component '{model_type}' RESULT_VARIABLES must be a tuple."
|
||||
)
|
||||
|
||||
port_by_name = {port.name: port for port in ports if isinstance(port, PortDefinition)}
|
||||
for port in ports:
|
||||
_validate_port(port, model_type=model_type)
|
||||
contract = port.computation
|
||||
if contract is not None:
|
||||
if port.domain != 'pneumatic' or not isinstance(contract, PortComputation):
|
||||
raise ValueError(f"Component '{model_type}' has an invalid pneumatic computation contract.")
|
||||
if contract.reference_port:
|
||||
reference = port_by_name.get(contract.reference_port)
|
||||
if (reference is None or reference.computation is None
|
||||
or not {'p', 'T'}.issubset(reference.computation.inputs)):
|
||||
raise ValueError(f"Component '{model_type}.{port.name}' must alias a pressure/temperature input port.")
|
||||
for parameter in parameters:
|
||||
_validate_parameter(parameter, model_type=model_type)
|
||||
for variable in result_variables:
|
||||
|
||||
@@ -6,6 +6,9 @@ from app.simulation.core.base import Component, DynamicComponent
|
||||
from app.simulation.core.equations import EquationDefinition
|
||||
from app.simulation.core.metadata import ResultVariableMetadata
|
||||
from app.simulation.core.ports import PortState
|
||||
from app.simulation.core.port_computation import (
|
||||
PortSupplyError, port_supply_issue, reference_supply_issues,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -110,6 +113,10 @@ class SimulationNetwork:
|
||||
raise ValueError(
|
||||
f"Connection variable contracts do not match: {endpoint_a}, {endpoint_b}."
|
||||
)
|
||||
supply_issue = port_supply_issue(first_definition, second_definition,
|
||||
str(endpoint_a), str(endpoint_b))
|
||||
if supply_issue:
|
||||
raise PortSupplyError(supply_issue)
|
||||
if first_definition.kind == "signal" and {
|
||||
first_definition.nominal_role,
|
||||
second_definition.nominal_role,
|
||||
@@ -161,6 +168,22 @@ class SimulationNetwork:
|
||||
self.connections.append(connection)
|
||||
return connection
|
||||
|
||||
def validate_port_supplies(self) -> None:
|
||||
"""Validate again at compile time, including manually assembled networks."""
|
||||
ports = {(c.name, p.name): p for c in self.components.values()
|
||||
for p in c.active_port_definitions}
|
||||
adjacency = {}
|
||||
for edge in self.connections:
|
||||
a, b = (endpoint.key for endpoint in edge.endpoints)
|
||||
issue = port_supply_issue(ports[a], ports[b], '.'.join(a), '.'.join(b))
|
||||
if issue:
|
||||
raise PortSupplyError(issue)
|
||||
if edge.kind == 'physical':
|
||||
adjacency[a], adjacency[b] = b, a
|
||||
issues = reference_supply_issues(ports, adjacency)
|
||||
if issues:
|
||||
raise PortSupplyError(issues[0])
|
||||
|
||||
def _port_for(self, endpoint: Endpoint) -> PortState:
|
||||
try:
|
||||
component = self.components[endpoint.component]
|
||||
|
||||
Reference in new issue
Block a user