Replace Python numerical kernels with native C execution
This commit is contained in:
1 parent
48da6be21c
commit
3b38f73fe0
227 files changed
+16801
-75499
No files matched your search
@@ -1,14 +1,11 @@
|
||||
"""Component parameters, ports and output definitions; numerical equations execute in C."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
|
||||
from app.simulation.core.base import AlgebraicComponent
|
||||
from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
|
||||
from app.simulation.core.equations import EquationResidual
|
||||
from app.simulation.core.medium import IdealGasMedium
|
||||
from app.simulation.core.ports import PortDefinition
|
||||
|
||||
|
||||
class AmesimPnpl01(AlgebraicComponent):
|
||||
"""AMESim PNPL01 zero pneumatic flow source.
|
||||
|
||||
@@ -16,53 +13,19 @@ class AmesimPnpl01(AlgebraicComponent):
|
||||
solver: it does not prescribe pressure, and only constrains its port mass
|
||||
flow to zero.
|
||||
"""
|
||||
|
||||
MODEL_TYPE = "amesim_pnpl01"
|
||||
MODEL_VERSION = "0.1.0"
|
||||
PRESSURE_FLOW_DEPENDS_ON_STREAM = False
|
||||
PORTS = (PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),)
|
||||
MODEL_TYPE = 'amesim_pnpl01'
|
||||
MODEL_VERSION = '0.1.0'
|
||||
PORTS = (PortDefinition.pneumatic('port_1', nominal_role='bidirectional'),)
|
||||
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,
|
||||
)
|
||||
DISPLAY = ComponentDisplaySpec(label='PNPL01 零气动流边界', library_id='amesim', category_id='boundary', symbol='amesim_pnpl01', ports=(PortDisplaySpec('port_1', 'left', order=10),), order=10)
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
super().__init__(name=name)
|
||||
self.set_parameter_values({})
|
||||
self.port_1 = self.register_declared_port("port_1")
|
||||
self.port_1 = self.register_declared_port('port_1')
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> AmesimPnpl01:
|
||||
def create(cls, *, name: str, medium: IdealGasMedium, parameters: Mapping[str, float]) -> AmesimPnpl01:
|
||||
return cls(name=name)
|
||||
|
||||
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
||||
return (self.port_1.m_flow,)
|
||||
|
||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||
return (
|
||||
EquationResidual(
|
||||
id=f"{self.name}:zero_mass_flow",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="constitutive",
|
||||
variables=(f"{self.name}.port_1.m_flow",),
|
||||
role="flow",
|
||||
value=self.port_1.m_flow,
|
||||
),
|
||||
)
|
||||
|
||||
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
|
||||
if "port_1" in connected_h:
|
||||
self.port_1.h_outflow = connected_h["port_1"]
|
||||
EQUATIONS = ({'id': '__MODEL__:zero_mass_flow', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'constitutive', 'variables': ['__MODEL__.port_1.m_flow'], 'role': 'flow'},)
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -1,31 +1,10 @@
|
||||
"""Component parameters, ports and output definitions; numerical equations execute in C."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
|
||||
from app.simulation.core.base import AlgebraicComponent
|
||||
from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
|
||||
from app.simulation.core.equations import EquationResidual
|
||||
from app.simulation.core.medium import IdealGasMedium
|
||||
from app.simulation.core.ports import PortDefinition, PortState
|
||||
|
||||
|
||||
_REFERENCE_OUTFLOW_REGULARIZATION_RATIO = 0.05
|
||||
|
||||
|
||||
def _regularized_inverse_outflow(flow: float, transition_flow: float) -> float:
|
||||
"""Return a C1 inverse that tends to zero as a negative flow vanishes."""
|
||||
|
||||
if flow >= 0.0:
|
||||
return 0.0
|
||||
transition_flow = max(float(transition_flow), 1.0e-12)
|
||||
if -flow >= transition_flow:
|
||||
return 1.0 / flow
|
||||
return (
|
||||
flow
|
||||
* (2.0 * transition_flow * transition_flow - flow * flow)
|
||||
/ transition_flow**4
|
||||
)
|
||||
|
||||
from app.simulation.core.ports import PortDefinition
|
||||
|
||||
class _AmesimPneumaticNode(AlgebraicComponent):
|
||||
"""Shared implementation for AMESim pneumatic junction submodels.
|
||||
@@ -35,9 +14,7 @@ class _AmesimPneumaticNode(AlgebraicComponent):
|
||||
an outlet, its enthalpy is the residual that closes the junction energy
|
||||
balance, matching the AMESim dh2 causality.
|
||||
"""
|
||||
|
||||
PRESSURE_FLOW_DEPENDS_ON_STREAM = False
|
||||
REFERENCE_PORT = "port_2"
|
||||
REFERENCE_PORT = 'port_2'
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
super().__init__(name=name)
|
||||
@@ -46,194 +23,30 @@ class _AmesimPneumaticNode(AlgebraicComponent):
|
||||
for definition in self.PORTS:
|
||||
setattr(self, definition.name, self.register_declared_port(definition.name))
|
||||
|
||||
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
||||
reference = self.get_port(self.REFERENCE_PORT)
|
||||
return tuple(
|
||||
self.get_port(definition.name).p - reference.p
|
||||
for definition in self.PORTS
|
||||
if definition.name != self.REFERENCE_PORT
|
||||
) + (
|
||||
sum(
|
||||
self.get_port(definition.name).m_flow
|
||||
for definition in self.PORTS
|
||||
),
|
||||
)
|
||||
|
||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||
reference = self.get_port(self.REFERENCE_PORT)
|
||||
residuals: list[EquationResidual] = []
|
||||
for definition in self.PORTS:
|
||||
if definition.name == self.REFERENCE_PORT:
|
||||
continue
|
||||
port = self.get_port(definition.name)
|
||||
residuals.append(
|
||||
EquationResidual(
|
||||
id=f"{self.name}:{definition.name}_pressure_reference",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="equal",
|
||||
variables=(
|
||||
f"{self.name}.{definition.name}.p",
|
||||
f"{self.name}.{self.REFERENCE_PORT}.p",
|
||||
),
|
||||
role="effort",
|
||||
value=port.p - reference.p,
|
||||
)
|
||||
)
|
||||
residuals.append(
|
||||
EquationResidual(
|
||||
id=f"{self.name}:mass_flow_balance",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="sumToZero",
|
||||
variables=tuple(
|
||||
f"{self.name}.{definition.name}.m_flow"
|
||||
for definition in self.PORTS
|
||||
),
|
||||
role="flow",
|
||||
value=sum(self.get_port(definition.name).m_flow for definition in self.PORTS),
|
||||
)
|
||||
)
|
||||
return tuple(residuals)
|
||||
|
||||
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
|
||||
self.temperature_reference_h = connected_h.get(
|
||||
self.REFERENCE_PORT,
|
||||
sum(connected_h.values()) / len(connected_h) if connected_h else 0.0,
|
||||
)
|
||||
incoming = [
|
||||
(port.m_flow, connected_h[name])
|
||||
for name, port in self.ports.items()
|
||||
if port.m_flow > 1e-12
|
||||
]
|
||||
total_flow = sum(m_flow for m_flow, _ in incoming)
|
||||
if total_flow > 1e-12:
|
||||
mixed_h = sum(m_flow * h for m_flow, h in incoming) / total_flow
|
||||
else:
|
||||
mixed_h = self.temperature_reference_h
|
||||
|
||||
reference_port = self.get_port(self.REFERENCE_PORT)
|
||||
for name, port in self.ports.items():
|
||||
port.h_outflow = (
|
||||
mixed_h
|
||||
if name == self.REFERENCE_PORT
|
||||
else self.temperature_reference_h
|
||||
)
|
||||
|
||||
if reference_port.m_flow < 0.0:
|
||||
energy_without_reference = sum(
|
||||
port.m_flow
|
||||
* (
|
||||
connected_h[name]
|
||||
if port.m_flow > 1e-12
|
||||
else self.temperature_reference_h
|
||||
)
|
||||
for name, port in self.ports.items()
|
||||
if name != self.REFERENCE_PORT
|
||||
)
|
||||
non_reference_flow_scale = sum(
|
||||
abs(port.m_flow)
|
||||
for name, port in self.ports.items()
|
||||
if name != self.REFERENCE_PORT
|
||||
)
|
||||
transition_flow = (
|
||||
_REFERENCE_OUTFLOW_REGULARIZATION_RATIO
|
||||
* non_reference_flow_scale
|
||||
)
|
||||
# Port 2 carries AMESim's residual-energy causality. Exact
|
||||
# division is singular when its outflow reverses through zero, so
|
||||
# use a C1 band that matches the exact balance at its boundary and
|
||||
# tends to the mixed enthalpy at zero flow.
|
||||
inverse_flow = _regularized_inverse_outflow(
|
||||
reference_port.m_flow,
|
||||
transition_flow,
|
||||
)
|
||||
energy_residual_at_mixed_h = (
|
||||
energy_without_reference
|
||||
+ reference_port.m_flow * mixed_h
|
||||
)
|
||||
reference_port.h_outflow = (
|
||||
mixed_h - energy_residual_at_mixed_h * inverse_flow
|
||||
)
|
||||
|
||||
|
||||
class AmesimPn3Node2(_AmesimPneumaticNode):
|
||||
"""AMESim PN3NODE2 pneumatic three-port junction."""
|
||||
|
||||
MODEL_TYPE = "amesim_pn3node2"
|
||||
MODEL_VERSION = "0.3.0"
|
||||
PRESSURE_FLOW_DEPENDS_ON_STREAM = False
|
||||
PRESSURE_FLOW_EXACT_SUM_TO_ZERO_EQUATION_SUFFIXES = frozenset(
|
||||
("mass_flow_balance",)
|
||||
)
|
||||
PORTS = (
|
||||
PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),
|
||||
PortDefinition.pneumatic("port_2", nominal_role="bidirectional"),
|
||||
PortDefinition.pneumatic("port_3", nominal_role="bidirectional"),
|
||||
)
|
||||
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'))
|
||||
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,
|
||||
)
|
||||
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)
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> AmesimPn3Node2:
|
||||
def create(cls, *, name: str, medium: IdealGasMedium, parameters: Mapping[str, float]) -> AmesimPn3Node2:
|
||||
return cls(name=name)
|
||||
|
||||
EQUATIONS = ({'id': '__MODEL__:port_1_pressure_reference', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'equal', 'variables': ['__MODEL__.port_1.p', '__MODEL__.port_2.p'], 'role': 'effort'}, {'id': '__MODEL__:port_3_pressure_reference', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'equal', 'variables': ['__MODEL__.port_3.p', '__MODEL__.port_2.p'], 'role': 'effort'}, {'id': '__MODEL__:mass_flow_balance', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'sumToZero', 'variables': ['__MODEL__.port_1.m_flow', '__MODEL__.port_2.m_flow', '__MODEL__.port_3.m_flow'], 'role': 'flow'})
|
||||
|
||||
class AmesimP4Node2(_AmesimPneumaticNode):
|
||||
"""AMESim P4NODE2 pneumatic four-port junction."""
|
||||
|
||||
MODEL_TYPE = "amesim_p4node2"
|
||||
MODEL_VERSION = "0.3.0"
|
||||
PRESSURE_FLOW_DEPENDS_ON_STREAM = False
|
||||
PRESSURE_FLOW_EXACT_SUM_TO_ZERO_EQUATION_SUFFIXES = frozenset(
|
||||
("mass_flow_balance",)
|
||||
)
|
||||
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"),
|
||||
)
|
||||
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'))
|
||||
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,
|
||||
)
|
||||
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)
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> AmesimP4Node2:
|
||||
def create(cls, *, name: str, medium: IdealGasMedium, parameters: Mapping[str, float]) -> AmesimP4Node2:
|
||||
return cls(name=name)
|
||||
EQUATIONS = ({'id': '__MODEL__:port_1_pressure_reference', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'equal', 'variables': ['__MODEL__.port_1.p', '__MODEL__.port_2.p'], 'role': 'effort'}, {'id': '__MODEL__:port_3_pressure_reference', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'equal', 'variables': ['__MODEL__.port_3.p', '__MODEL__.port_2.p'], 'role': 'effort'}, {'id': '__MODEL__:port_4_pressure_reference', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'equal', 'variables': ['__MODEL__.port_4.p', '__MODEL__.port_2.p'], 'role': 'effort'}, {'id': '__MODEL__:mass_flow_balance', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'sumToZero', 'variables': ['__MODEL__.port_1.m_flow', '__MODEL__.port_2.m_flow', '__MODEL__.port_3.m_flow', '__MODEL__.port_4.m_flow'], 'role': 'flow'})
|
||||
@@ -1,36 +1,15 @@
|
||||
"""Component parameters, ports and output definitions; numerical equations execute in C."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from math import isfinite, pi
|
||||
|
||||
from app.simulation.components.amesim.gases import (
|
||||
AMESIM_GAS_INDEX_PARAMETER,
|
||||
normalize_amesim_gas_index,
|
||||
)
|
||||
from collections.abc import Mapping
|
||||
from math import pi
|
||||
from app.simulation.components.amesim.gases import AMESIM_GAS_INDEX_PARAMETER, normalize_amesim_gas_index
|
||||
from app.simulation.core.base import AlgebraicComponent
|
||||
from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
|
||||
from app.simulation.core.equations import EquationResidual
|
||||
from app.simulation.core.metadata import ParameterDefinition, ResultVariableDefinition
|
||||
from app.simulation.core.medium import GasMedium
|
||||
from app.simulation.core.ports import PortDefinition
|
||||
|
||||
|
||||
AMESIM_REFERENCE_PRESSURE_PA = 101300.0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Pnrp17Linearization:
|
||||
volume: float
|
||||
volume_flow: float
|
||||
pressure_force: float
|
||||
volume_tangent: tuple[float, ...]
|
||||
volume_flow_tangent: tuple[float, ...]
|
||||
pressure_force_tangent: tuple[float, ...]
|
||||
valid: bool = True
|
||||
reason: str | None = None
|
||||
|
||||
|
||||
class AmesimPnrp17(AlgebraicComponent):
|
||||
"""AMESim PNRP17 pneumatic piston with two mechanical faces.
|
||||
|
||||
@@ -38,268 +17,32 @@ class AmesimPnrp17(AlgebraicComponent):
|
||||
cylinder-side motion. The pneumatic port contributes its swept volume and
|
||||
volume rate to the connected variable-volume chamber.
|
||||
"""
|
||||
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'))
|
||||
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)
|
||||
|
||||
MODEL_TYPE = "amesim_pnrp17"
|
||||
MODEL_VERSION = "0.1.0"
|
||||
PRESSURE_FLOW_DEPENDS_ON_STREAM = False
|
||||
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"),
|
||||
)
|
||||
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,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
medium: GasMedium,
|
||||
*,
|
||||
gi: float = 0.0,
|
||||
dp: float = 0.2,
|
||||
dr: float = 0.001,
|
||||
x0: float = 0.0,
|
||||
) -> None:
|
||||
def __init__(self, name: str, medium: GasMedium, *, gi: float=0.0, dp: float=0.2, dr: float=0.001, x0: float=0.0) -> None:
|
||||
super().__init__(name=name)
|
||||
self.set_parameter_values({"gi": gi, "dp": dp, "dr": dr, "x0": x0})
|
||||
self.set_parameter_values({'gi': gi, 'dp': dp, 'dr': dr, 'x0': x0})
|
||||
self.medium = medium
|
||||
self.gi = normalize_amesim_gas_index(gi)
|
||||
self.dp = float(dp)
|
||||
self.dr = float(dr)
|
||||
self.x0 = float(x0)
|
||||
if self.dr > self.dp:
|
||||
raise ValueError("PNRP17 rod diameter dr must not exceed piston diameter dp.")
|
||||
raise ValueError('PNRP17 rod diameter dr must not exceed piston diameter dp.')
|
||||
for definition in self.PORTS:
|
||||
port = self.register_declared_port(definition.name)
|
||||
setattr(self, definition.name, port)
|
||||
self.port_1.h_outflow = medium.specific_enthalpy(medium.T_ref)
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: GasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> "AmesimPnrp17":
|
||||
def create(cls, *, name: str, medium: GasMedium, parameters: Mapping[str, float]) -> 'AmesimPnrp17':
|
||||
return cls(name=name, medium=medium, **dict(parameters))
|
||||
|
||||
@property
|
||||
def effective_area(self) -> float:
|
||||
return pi * (self.dp * self.dp - self.dr * self.dr) / 4.0
|
||||
|
||||
@property
|
||||
def chamber_length(self) -> float:
|
||||
return self.x0 + self.port_5.x - self.port_4.x
|
||||
|
||||
@property
|
||||
def chamber_volume(self) -> float:
|
||||
return self.effective_area * self.chamber_length
|
||||
|
||||
@property
|
||||
def chamber_volume_flow(self) -> float:
|
||||
return self.effective_area * (self.port_5.v - self.port_4.v)
|
||||
|
||||
@property
|
||||
def pressure_force(self) -> float:
|
||||
return (self.port_1.p - AMESIM_REFERENCE_PRESSURE_PA) * self.effective_area
|
||||
|
||||
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
||||
values = [self.port_1.m_flow]
|
||||
effort_pairs = (("port_2", "port_5"), ("port_3", "port_4"))
|
||||
for first_name, second_name in effort_pairs:
|
||||
first = self.get_port(first_name)
|
||||
second = self.get_port(second_name)
|
||||
values.extend((first.x - second.x, first.v - second.v))
|
||||
force = self.pressure_force
|
||||
values.extend(
|
||||
(
|
||||
self.port_2.f + self.port_5.f + force,
|
||||
self.port_3.f + self.port_4.f - force,
|
||||
)
|
||||
)
|
||||
return tuple(values)
|
||||
|
||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||
effort_pairs = (("port_2", "port_5"), ("port_3", "port_4"))
|
||||
residuals: list[EquationResidual] = [
|
||||
EquationResidual(
|
||||
id=f"{self.name}:pneumatic_zero_mass_flow",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="constitutive",
|
||||
variables=(f"{self.name}.port_1.m_flow",),
|
||||
role="flow",
|
||||
value=self.port_1.m_flow,
|
||||
)
|
||||
]
|
||||
for first_name, second_name in effort_pairs:
|
||||
first = self.get_port(first_name)
|
||||
second = self.get_port(second_name)
|
||||
for variable in ("x", "v"):
|
||||
residuals.append(
|
||||
EquationResidual(
|
||||
id=f"{self.name}:{first_name}_{second_name}_{variable}_equal",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="equal",
|
||||
variables=(
|
||||
f"{self.name}.{first_name}.{variable}",
|
||||
f"{self.name}.{second_name}.{variable}",
|
||||
),
|
||||
role="effort",
|
||||
value=getattr(first, variable) - getattr(second, variable),
|
||||
)
|
||||
)
|
||||
force = self.pressure_force
|
||||
residuals.extend(
|
||||
(
|
||||
EquationResidual(
|
||||
id=f"{self.name}:piston_side_force_balance",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="constitutive",
|
||||
variables=(f"{self.name}.port_2.f", f"{self.name}.port_5.f", f"{self.name}.port_1.p"),
|
||||
role="flow",
|
||||
value=self.port_2.f + self.port_5.f + force,
|
||||
),
|
||||
EquationResidual(
|
||||
id=f"{self.name}:cylinder_side_force_balance",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="constitutive",
|
||||
variables=(f"{self.name}.port_3.f", f"{self.name}.port_4.f", f"{self.name}.port_1.p"),
|
||||
role="flow",
|
||||
value=self.port_3.f + self.port_4.f - force,
|
||||
),
|
||||
)
|
||||
)
|
||||
return tuple(residuals)
|
||||
|
||||
def pneumatic_volume_outputs(self) -> Mapping[str, tuple[float, float]]:
|
||||
return {"port_1": (self.chamber_volume, self.chamber_volume_flow)}
|
||||
|
||||
def linearize_geometry_and_force(
|
||||
self,
|
||||
port_4_x_tangent: Sequence[float],
|
||||
port_5_x_tangent: Sequence[float],
|
||||
port_4_v_tangent: Sequence[float],
|
||||
port_5_v_tangent: Sequence[float],
|
||||
port_1_pressure_tangent: Sequence[float],
|
||||
) -> Pnrp17Linearization:
|
||||
"""Return exact piston geometry and pressure-force tangents."""
|
||||
|
||||
vectors = tuple(
|
||||
tuple(float(value) for value in values)
|
||||
for values in (
|
||||
port_4_x_tangent,
|
||||
port_5_x_tangent,
|
||||
port_4_v_tangent,
|
||||
port_5_v_tangent,
|
||||
port_1_pressure_tangent,
|
||||
)
|
||||
)
|
||||
widths = {len(values) for values in vectors}
|
||||
if len(widths) != 1:
|
||||
raise ValueError("PNRP17 tangent vectors must have equal lengths.")
|
||||
valid = all(isfinite(value) for values in vectors for value in values)
|
||||
area = self.effective_area
|
||||
volume_tangent = tuple(
|
||||
area * (right - left)
|
||||
for left, right in zip(vectors[0], vectors[1], strict=True)
|
||||
)
|
||||
volume_flow_tangent = tuple(
|
||||
area * (right - left)
|
||||
for left, right in zip(vectors[2], vectors[3], strict=True)
|
||||
)
|
||||
pressure_force_tangent = tuple(
|
||||
area * value for value in vectors[4]
|
||||
)
|
||||
return Pnrp17Linearization(
|
||||
volume=self.chamber_volume,
|
||||
volume_flow=self.chamber_volume_flow,
|
||||
pressure_force=self.pressure_force,
|
||||
volume_tangent=volume_tangent,
|
||||
volume_flow_tangent=volume_flow_tangent,
|
||||
pressure_force_tangent=pressure_force_tangent,
|
||||
valid=valid,
|
||||
reason=None if valid else "non_finite_tangent_input",
|
||||
)
|
||||
|
||||
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
|
||||
self.port_1.h_outflow = connected_h.get(
|
||||
"port_1",
|
||||
self.medium.specific_enthalpy(self.medium.T_ref),
|
||||
)
|
||||
|
||||
def component_result_values(self) -> Mapping[str, float]:
|
||||
return {
|
||||
"volume": self.chamber_volume,
|
||||
"volume_flow": self.chamber_volume_flow,
|
||||
"length": self.chamber_length,
|
||||
"pressure_force": self.pressure_force,
|
||||
}
|
||||
EQUATIONS = ({'id': '__MODEL__:pneumatic_zero_mass_flow', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'constitutive', 'variables': ['__MODEL__.port_1.m_flow'], 'role': 'flow'}, {'id': '__MODEL__:port_2_port_5_x_equal', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'equal', 'variables': ['__MODEL__.port_2.x', '__MODEL__.port_5.x'], 'role': 'effort'}, {'id': '__MODEL__:port_2_port_5_v_equal', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'equal', 'variables': ['__MODEL__.port_2.v', '__MODEL__.port_5.v'], 'role': 'effort'}, {'id': '__MODEL__:port_3_port_4_x_equal', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'equal', 'variables': ['__MODEL__.port_3.x', '__MODEL__.port_4.x'], 'role': 'effort'}, {'id': '__MODEL__:port_3_port_4_v_equal', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'equal', 'variables': ['__MODEL__.port_3.v', '__MODEL__.port_4.v'], 'role': 'effort'}, {'id': '__MODEL__:piston_side_force_balance', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'constitutive', 'variables': ['__MODEL__.port_2.f', '__MODEL__.port_5.f', '__MODEL__.port_1.p'], 'role': 'flow'}, {'id': '__MODEL__:cylinder_side_force_balance', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'constitutive', 'variables': ['__MODEL__.port_3.f', '__MODEL__.port_4.f', '__MODEL__.port_1.p'], 'role': 'flow'})
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,488 +1,42 @@
|
||||
"""Gas identities and constants passed to the native compiler."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Sequence
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from math import exp, isfinite, log
|
||||
from typing import ClassVar
|
||||
|
||||
from app.simulation.core.errors import RecoverableTrialStateError
|
||||
from app.simulation.core.medium import (
|
||||
GasMedium,
|
||||
IdealGasMedium,
|
||||
ThermodynamicProperties,
|
||||
ThermodynamicPropertiesLinearization,
|
||||
ThermodynamicPropertyTangents,
|
||||
)
|
||||
from app.simulation.core.peng_robinson import HELIUM_PR, PengRobinsonFluid
|
||||
from app.simulation.performance import profile_property, record_property_iterations
|
||||
from app.simulation.property_cache import cache_property_calculation
|
||||
|
||||
from app.simulation.core.medium import GasMedium, IdealGasMedium
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AmesimIdealAirMedium(IdealGasMedium):
|
||||
"""AMESim air properties evaluated with the ideal-gas method.
|
||||
|
||||
Substance identity and property method are part of the concrete Python
|
||||
type. A future air correlation or helium Peng-Robinson implementation can
|
||||
therefore coexist as a sibling type without turning ``gi`` into a fluid
|
||||
enumeration.
|
||||
"""
|
||||
|
||||
SUBSTANCE_ID: ClassVar[str] = "air"
|
||||
PROPERTY_METHOD_ID: ClassVar[str] = "ideal_gas"
|
||||
|
||||
name: str = "AMESimAirIdealGas"
|
||||
SUBSTANCE_ID: ClassVar[str] = 'air'
|
||||
PROPERTY_METHOD_ID: ClassVar[str] = 'ideal_gas'
|
||||
name: str = 'AMESimAirIdealGas'
|
||||
R_gas: float = 287.0
|
||||
cp_ref: float = 1005.0
|
||||
T_ref: float = 300.0
|
||||
cp_slope: float = 0.0
|
||||
viscosity_ref: float = 1.82e-5
|
||||
viscosity_ref: float = 1.82e-05
|
||||
viscosity_T_ref: float = 293.15
|
||||
sutherland_constant: float = 110.4
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AmesimHeliumPengRobinsonMedium(IdealGasMedium):
|
||||
"""AMESim helium with a Peng-Robinson mechanical equation of state.
|
||||
|
||||
The pressure-density-temperature relation is evaluated by the shared
|
||||
``HELIUM_PR`` fluid. The caloric reference follows the constant NASA
|
||||
polynomial from Simcenter Amesim 2404 ``helium_cp_h_s.data``.
|
||||
"""
|
||||
|
||||
SUBSTANCE_ID: ClassVar[str] = "helium"
|
||||
PROPERTY_METHOD_ID: ClassVar[str] = "peng_robinson"
|
||||
fluid: ClassVar[PengRobinsonFluid] = HELIUM_PR
|
||||
SUBSTANCE_ID: ClassVar[str] = 'helium'
|
||||
PROPERTY_METHOD_ID: ClassVar[str] = 'peng_robinson'
|
||||
nasa_cp_over_R: ClassVar[float] = 2.5
|
||||
nasa_enthalpy_constant_K: ClassVar[float] = -745.375
|
||||
nasa_viscosity_coefficients: ClassVar[tuple[float, float, float, float]] = (
|
||||
0.7501594,
|
||||
35.76324,
|
||||
-2212.129,
|
||||
0.9212635,
|
||||
)
|
||||
|
||||
name: str = "AMESimHeliumPengRobinson"
|
||||
R_gas: float = HELIUM_PR.specific_gas_constant
|
||||
cp_ref: float = nasa_cp_over_R * HELIUM_PR.specific_gas_constant
|
||||
nasa_viscosity_coefficients: ClassVar[tuple[float, float, float, float]] = (0.7501594, 35.76324, -2212.129, 0.9212635)
|
||||
name: str = 'AMESimHeliumPengRobinson'
|
||||
R_gas: float = 8.31446261815324 / 0.004002602
|
||||
cp_ref: float = 2.5 * (8.31446261815324 / 0.004002602)
|
||||
T_ref: float = 293.15
|
||||
cp_slope: float = 0.0
|
||||
viscosity_ref: float = 1.96e-5
|
||||
viscosity_ref: float = 1.96e-05
|
||||
viscosity_T_ref: float = 293.15
|
||||
sutherland_constant: float = 79.4
|
||||
|
||||
@property
|
||||
def cv(self) -> float:
|
||||
return (self.nasa_cp_over_R - 1.0) * self.R_gas
|
||||
|
||||
def cv_at_temperature(self, T: float) -> float:
|
||||
del T
|
||||
return self.cv
|
||||
|
||||
def diagnostic_dynamic_viscosity(self, T: float) -> float:
|
||||
"""Return the AMESim NASA-table viscosity used by pipe diagnostics.
|
||||
|
||||
pn2pipefr reports Reynolds number with sagum viscosity. Keep this
|
||||
separate from dynamic_viscosity so matching that diagnostic cannot
|
||||
alter the already-validated pipe flow or friction dynamics.
|
||||
"""
|
||||
|
||||
if T <= 0.0:
|
||||
raise ValueError("Temperature must be positive.")
|
||||
a, b, c, d = self.nasa_viscosity_coefficients
|
||||
return 1.0e-7 * exp(a * log(T) + b / T + c / (T * T) + d)
|
||||
|
||||
@profile_property("density")
|
||||
@cache_property_calculation("density")
|
||||
def density(self, p: float, T: float) -> float:
|
||||
return self.fluid.density(p, T)
|
||||
|
||||
def _real_heat_capacities(
|
||||
self,
|
||||
p: float,
|
||||
T: float,
|
||||
) -> tuple[float, float, float, float, float]:
|
||||
density = self.density(p, T)
|
||||
pressure_density_derivative = (
|
||||
self.fluid.pressure_density_derivative_at_temperature(
|
||||
T,
|
||||
density,
|
||||
)
|
||||
)
|
||||
pressure_temperature_derivative = (
|
||||
self.fluid.pressure_temperature_derivative_at_density(
|
||||
T,
|
||||
density,
|
||||
)
|
||||
)
|
||||
cv = (
|
||||
self.cv_at_temperature(T)
|
||||
+ self.fluid.residual_isochoric_heat_capacity_at_density(T, density)
|
||||
)
|
||||
cp = (
|
||||
cv
|
||||
+ T
|
||||
* pressure_temperature_derivative
|
||||
* pressure_temperature_derivative
|
||||
/ (density * density * pressure_density_derivative)
|
||||
)
|
||||
if cp <= 0.0 or cv <= 0.0:
|
||||
raise ValueError("Real-gas heat capacities must be positive.")
|
||||
return (
|
||||
cp,
|
||||
cv,
|
||||
density,
|
||||
pressure_density_derivative,
|
||||
pressure_temperature_derivative,
|
||||
)
|
||||
|
||||
def _local_isentropic_density_pressure_factor(
|
||||
self,
|
||||
p: float,
|
||||
T: float,
|
||||
) -> tuple[float, float]:
|
||||
cp, cv, density, pressure_density_derivative, pressure_temperature_derivative = (
|
||||
self._real_heat_capacities(p, T)
|
||||
)
|
||||
heat_capacity_ratio = cp / cv
|
||||
factor = p / (
|
||||
density * pressure_density_derivative * heat_capacity_ratio
|
||||
)
|
||||
exponent = (
|
||||
p
|
||||
* (heat_capacity_ratio - 1.0)
|
||||
/ (
|
||||
heat_capacity_ratio
|
||||
* T
|
||||
* pressure_temperature_derivative
|
||||
)
|
||||
)
|
||||
return factor, exponent
|
||||
|
||||
@profile_property("isentropic_density_pressure_factor")
|
||||
@cache_property_calculation("isentropic_density_pressure_factor")
|
||||
def isentropic_density_pressure_factor(
|
||||
self,
|
||||
p: float,
|
||||
T: float,
|
||||
downstream_pressure: float | None = None,
|
||||
) -> float:
|
||||
upstream_factor, isentropic_temperature_exponent = (
|
||||
self._local_isentropic_density_pressure_factor(p, T)
|
||||
)
|
||||
if downstream_pressure is None or downstream_pressure >= p:
|
||||
return upstream_factor
|
||||
|
||||
pressure_ratio = max(downstream_pressure / p, 1.0e-12)
|
||||
isentropic_temperature = max(
|
||||
T * pressure_ratio**isentropic_temperature_exponent,
|
||||
2.2,
|
||||
)
|
||||
downstream_factor, _unused_exponent = (
|
||||
self._local_isentropic_density_pressure_factor(
|
||||
max(downstream_pressure, 1.0),
|
||||
isentropic_temperature,
|
||||
)
|
||||
)
|
||||
# AMESim 2404 saggs_ evaluates the local factor at the upstream
|
||||
# state and at an approximate isentropic downstream state.
|
||||
return 0.5 * (upstream_factor + downstream_factor)
|
||||
|
||||
def pressure(self, m: float, T: float, V: float) -> float:
|
||||
if V <= 0.0:
|
||||
raise ValueError("Volume must stay positive.")
|
||||
return self.fluid.pressure_from_density(T, m / V)
|
||||
|
||||
@profile_property("specific_internal_energy")
|
||||
def specific_internal_energy(self, T: float) -> float:
|
||||
return self.R_gas * (
|
||||
(self.nasa_cp_over_R - 1.0) * T
|
||||
+ self.nasa_enthalpy_constant_K
|
||||
)
|
||||
|
||||
@profile_property("specific_internal_energy_at_pressure")
|
||||
def specific_internal_energy_at_pressure(self, p: float, T: float) -> float:
|
||||
density = self.density(p, T)
|
||||
return (
|
||||
self.specific_internal_energy(T)
|
||||
+ self.fluid.residual_specific_internal_energy_at_density(T, density)
|
||||
)
|
||||
|
||||
@profile_property("specific_enthalpy")
|
||||
def specific_enthalpy(self, T: float) -> float:
|
||||
return self.R_gas * (
|
||||
self.nasa_cp_over_R * T
|
||||
+ self.nasa_enthalpy_constant_K
|
||||
)
|
||||
|
||||
@profile_property("specific_enthalpy_at_pressure")
|
||||
def specific_enthalpy_at_pressure(self, p: float, T: float) -> float:
|
||||
return self.specific_enthalpy(T) + self.fluid.residual_specific_enthalpy(p, T)
|
||||
|
||||
def temperature_from_internal_energy(self, u: float) -> float:
|
||||
return (
|
||||
u / self.R_gas - self.nasa_enthalpy_constant_K
|
||||
) / (self.nasa_cp_over_R - 1.0)
|
||||
|
||||
def temperature_from_enthalpy(self, h: float) -> float:
|
||||
return (
|
||||
h / self.R_gas - self.nasa_enthalpy_constant_K
|
||||
) / self.nasa_cp_over_R
|
||||
|
||||
@profile_property("temperature_from_pressure_enthalpy")
|
||||
@cache_property_calculation("temperature_from_pressure_enthalpy")
|
||||
def temperature_from_pressure_enthalpy(self, p: float, h: float) -> float:
|
||||
temperature = max(self.temperature_from_enthalpy(h), 2.2)
|
||||
for _iteration in range(16):
|
||||
residual_enthalpy = self.fluid.residual_specific_enthalpy(p, temperature)
|
||||
next_temperature = max(
|
||||
self.temperature_from_enthalpy(h - residual_enthalpy),
|
||||
2.2,
|
||||
)
|
||||
if abs(next_temperature - temperature) <= 1.0e-10 * max(
|
||||
temperature,
|
||||
1.0,
|
||||
):
|
||||
record_property_iterations(
|
||||
"temperature_from_pressure_enthalpy",
|
||||
_iteration + 1,
|
||||
True,
|
||||
)
|
||||
return next_temperature
|
||||
temperature = next_temperature
|
||||
record_property_iterations(
|
||||
"temperature_from_pressure_enthalpy",
|
||||
16,
|
||||
False,
|
||||
)
|
||||
return temperature
|
||||
|
||||
def temperature_from_mass_internal_energy(self, m: float, U: float) -> float:
|
||||
if m <= 0.0:
|
||||
raise RecoverableTrialStateError(
|
||||
"Mass must stay positive when recovering temperature."
|
||||
)
|
||||
return self.temperature_from_internal_energy(U / m)
|
||||
|
||||
@profile_property("properties_from_mU")
|
||||
@cache_property_calculation("properties_from_mU")
|
||||
def properties_from_mU(
|
||||
self,
|
||||
m: float,
|
||||
U: float,
|
||||
V: float,
|
||||
) -> ThermodynamicProperties:
|
||||
"""Recover a real-gas state, reusing exact repeated evaluations.
|
||||
|
||||
Implicit integration asks several component interfaces for the same
|
||||
``(m, U, V)`` state while closing one RHS evaluation and while building
|
||||
finite-difference Jacobians. The calculation is pure and its result is
|
||||
immutable, so an exact-key bounded cache avoids repeating the
|
||||
Peng-Robinson temperature iteration without changing model semantics.
|
||||
"""
|
||||
if m <= 0.0:
|
||||
raise RecoverableTrialStateError(
|
||||
"Mass must stay positive when recovering temperature."
|
||||
)
|
||||
if V <= 0.0:
|
||||
raise ValueError("Volume must stay positive.")
|
||||
density = m / V
|
||||
target_internal_energy = U / m
|
||||
temperature = max(
|
||||
self.temperature_from_internal_energy(target_internal_energy),
|
||||
2.2,
|
||||
)
|
||||
converged = False
|
||||
for _iteration in range(16):
|
||||
residual_internal_energy = (
|
||||
self.fluid.residual_specific_internal_energy_at_density(
|
||||
temperature,
|
||||
density,
|
||||
)
|
||||
)
|
||||
next_temperature = max(
|
||||
self.temperature_from_internal_energy(
|
||||
target_internal_energy - residual_internal_energy
|
||||
),
|
||||
2.2,
|
||||
)
|
||||
if abs(next_temperature - temperature) <= 1.0e-10 * max(
|
||||
temperature,
|
||||
1.0,
|
||||
):
|
||||
temperature = next_temperature
|
||||
converged = True
|
||||
break
|
||||
temperature = next_temperature
|
||||
record_property_iterations(
|
||||
"properties_from_mU",
|
||||
_iteration + 1,
|
||||
converged,
|
||||
)
|
||||
pressure = self.fluid.pressure_from_density(temperature, density)
|
||||
return ThermodynamicProperties(
|
||||
p=pressure,
|
||||
T=temperature,
|
||||
rho=density,
|
||||
u=target_internal_energy,
|
||||
h=self.specific_enthalpy_at_pressure(
|
||||
pressure,
|
||||
temperature,
|
||||
),
|
||||
)
|
||||
|
||||
def linearize_properties_from_mU(
|
||||
self,
|
||||
m: float,
|
||||
U: float,
|
||||
V: float,
|
||||
dm: Sequence[float],
|
||||
dU: Sequence[float],
|
||||
dV: Sequence[float],
|
||||
*,
|
||||
properties: ThermodynamicProperties | None = None,
|
||||
) -> ThermodynamicPropertiesLinearization:
|
||||
"""Implicitly differentiate the Peng-Robinson m/U/V recovery."""
|
||||
|
||||
dm_values = tuple(float(value) for value in dm)
|
||||
dU_values = tuple(float(value) for value in dU)
|
||||
dV_values = tuple(float(value) for value in dV)
|
||||
if not (len(dm_values) == len(dU_values) == len(dV_values)):
|
||||
raise ValueError("Thermodynamic tangent vectors must have equal lengths.")
|
||||
props = properties or self.properties_from_mU(m, U, V)
|
||||
width = len(dm_values)
|
||||
|
||||
def invalid(reason: str) -> ThermodynamicPropertiesLinearization:
|
||||
return ThermodynamicPropertiesLinearization(
|
||||
properties=props,
|
||||
tangents=ThermodynamicPropertyTangents.zeros(width),
|
||||
valid=False,
|
||||
reason=reason,
|
||||
)
|
||||
|
||||
expected_density = m / V
|
||||
expected_internal_energy = U / m
|
||||
if (
|
||||
abs(props.rho - expected_density)
|
||||
> 1.0e-12 * max(abs(expected_density), 1.0)
|
||||
or abs(props.u - expected_internal_energy)
|
||||
> 1.0e-12 * max(abs(expected_internal_energy), 1.0)
|
||||
):
|
||||
return invalid("properties_primal_mismatch")
|
||||
if not all(
|
||||
isfinite(value)
|
||||
for values in (dm_values, dU_values, dV_values)
|
||||
for value in values
|
||||
):
|
||||
return invalid("non_finite_tangent_input")
|
||||
if props.T <= 2.2 * (1.0 + 1.0e-10):
|
||||
return invalid("temperature_floor_boundary")
|
||||
|
||||
pressure_temperature_derivative = (
|
||||
self.fluid.pressure_temperature_derivative_at_density(
|
||||
props.T,
|
||||
props.rho,
|
||||
)
|
||||
)
|
||||
pressure_density_derivative = (
|
||||
self.fluid.pressure_density_derivative_at_temperature(
|
||||
props.T,
|
||||
props.rho,
|
||||
)
|
||||
)
|
||||
cv = (
|
||||
self.cv_at_temperature(props.T)
|
||||
+ self.fluid.residual_isochoric_heat_capacity_at_density(
|
||||
props.T,
|
||||
props.rho,
|
||||
)
|
||||
)
|
||||
recovered_internal_energy = (
|
||||
self.specific_internal_energy(props.T)
|
||||
+ self.fluid.residual_specific_internal_energy_at_density(
|
||||
props.T,
|
||||
props.rho,
|
||||
)
|
||||
)
|
||||
recovery_scale = max(
|
||||
abs(props.u),
|
||||
abs(cv * props.T) if isfinite(cv) else 0.0,
|
||||
1.0,
|
||||
)
|
||||
if (
|
||||
not all(
|
||||
isfinite(value)
|
||||
for value in (
|
||||
pressure_temperature_derivative,
|
||||
pressure_density_derivative,
|
||||
cv,
|
||||
recovered_internal_energy,
|
||||
)
|
||||
)
|
||||
or cv <= 0.0
|
||||
):
|
||||
return invalid("invalid_peng_robinson_derivative")
|
||||
if abs(recovered_internal_energy - props.u) > 1.0e-8 * recovery_scale:
|
||||
return invalid("properties_recovery_not_converged")
|
||||
|
||||
internal_energy_density_derivative = (
|
||||
props.p - props.T * pressure_temperature_derivative
|
||||
) / (props.rho * props.rho)
|
||||
drho: list[float] = []
|
||||
du: list[float] = []
|
||||
dT: list[float] = []
|
||||
dp: list[float] = []
|
||||
dh: list[float] = []
|
||||
for mass_tangent, energy_tangent, volume_tangent in zip(
|
||||
dm_values,
|
||||
dU_values,
|
||||
dV_values,
|
||||
strict=True,
|
||||
):
|
||||
density_tangent = (
|
||||
mass_tangent / V - m * volume_tangent / (V * V)
|
||||
)
|
||||
internal_energy_tangent = (
|
||||
energy_tangent / m - U * mass_tangent / (m * m)
|
||||
)
|
||||
temperature_tangent = (
|
||||
internal_energy_tangent
|
||||
- internal_energy_density_derivative * density_tangent
|
||||
) / cv
|
||||
pressure_tangent = (
|
||||
pressure_temperature_derivative * temperature_tangent
|
||||
+ pressure_density_derivative * density_tangent
|
||||
)
|
||||
enthalpy_tangent = (
|
||||
internal_energy_tangent
|
||||
+ pressure_tangent / props.rho
|
||||
- props.p * density_tangent / (props.rho * props.rho)
|
||||
)
|
||||
drho.append(density_tangent)
|
||||
du.append(internal_energy_tangent)
|
||||
dT.append(temperature_tangent)
|
||||
dp.append(pressure_tangent)
|
||||
dh.append(enthalpy_tangent)
|
||||
|
||||
tangent_values = (*drho, *du, *dT, *dp, *dh)
|
||||
if not all(isfinite(value) for value in tangent_values):
|
||||
return invalid("non_finite_property_tangent")
|
||||
return ThermodynamicPropertiesLinearization(
|
||||
properties=props,
|
||||
tangents=ThermodynamicPropertyTangents(
|
||||
p=tuple(dp),
|
||||
T=tuple(dT),
|
||||
rho=tuple(drho),
|
||||
u=tuple(du),
|
||||
h=tuple(dh),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AmesimGasPropertyModelSpec:
|
||||
"""A selectable calculation method for one AMESim gas substance."""
|
||||
|
||||
value: int
|
||||
label: str
|
||||
method_id: str
|
||||
@@ -491,27 +45,7 @@ class AmesimGasPropertyModelSpec:
|
||||
|
||||
def build_medium(self) -> GasMedium:
|
||||
return self.factory()
|
||||
|
||||
|
||||
AMESIM_AIR_IDEAL_GAS_PROPERTY_MODEL = 0
|
||||
AMESIM_AIR_PROPERTY_MODELS = (
|
||||
AmesimGasPropertyModelSpec(
|
||||
value=AMESIM_AIR_IDEAL_GAS_PROPERTY_MODEL,
|
||||
label="理想气体",
|
||||
method_id=AmesimIdealAirMedium.PROPERTY_METHOD_ID,
|
||||
factory=AmesimIdealAirMedium,
|
||||
eos_type=1,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
AMESIM_AIR_PROPERTY_MODELS = (AmesimGasPropertyModelSpec(value=AMESIM_AIR_IDEAL_GAS_PROPERTY_MODEL, label='理想气体', method_id=AmesimIdealAirMedium.PROPERTY_METHOD_ID, factory=AmesimIdealAirMedium, eos_type=1),)
|
||||
AMESIM_HELIUM_PENG_ROBINSON_PROPERTY_MODEL = 0
|
||||
AMESIM_HELIUM_PROPERTY_MODELS = (
|
||||
AmesimGasPropertyModelSpec(
|
||||
value=AMESIM_HELIUM_PENG_ROBINSON_PROPERTY_MODEL,
|
||||
label="Peng–Robinson",
|
||||
method_id=AmesimHeliumPengRobinsonMedium.PROPERTY_METHOD_ID,
|
||||
factory=AmesimHeliumPengRobinsonMedium,
|
||||
eos_type=6,
|
||||
),
|
||||
)
|
||||
AMESIM_HELIUM_PROPERTY_MODELS = (AmesimGasPropertyModelSpec(value=AMESIM_HELIUM_PENG_ROBINSON_PROPERTY_MODEL, label='Peng–Robinson', method_id=AmesimHeliumPengRobinsonMedium.PROPERTY_METHOD_ID, factory=AmesimHeliumPengRobinsonMedium, eos_type=6),)
|
||||
@@ -1,355 +1,79 @@
|
||||
"""Component parameters, ports and output definitions; numerical equations execute in C."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from math import floor
|
||||
|
||||
from app.simulation.core.base import AlgebraicComponent
|
||||
from app.simulation.core.catalog import (
|
||||
ComponentDisplaySpec,
|
||||
ParameterGroupDisplaySpec,
|
||||
PortDisplaySpec,
|
||||
)
|
||||
from app.simulation.core.metadata import (
|
||||
ParameterCondition,
|
||||
ParameterDefinition,
|
||||
ParameterOption,
|
||||
ResultVariableDefinition,
|
||||
)
|
||||
from app.simulation.core.catalog import ComponentDisplaySpec, ParameterGroupDisplaySpec, PortDisplaySpec
|
||||
from app.simulation.core.metadata import ParameterCondition, ParameterDefinition, ParameterOption, ResultVariableDefinition
|
||||
from app.simulation.core.medium import IdealGasMedium
|
||||
from app.simulation.core.ports import PortDefinition
|
||||
|
||||
|
||||
def _ud00_stage_parameters(index: int) -> tuple[ParameterDefinition, ...]:
|
||||
visible_when = (
|
||||
()
|
||||
if index == 1
|
||||
else (
|
||||
ParameterCondition(
|
||||
"nstages",
|
||||
tuple(float(stage_count) for stage_count in range(index, 9)),
|
||||
),
|
||||
)
|
||||
)
|
||||
return (
|
||||
ParameterDefinition(
|
||||
f"start{index}",
|
||||
0.0 if index == 1 else 1.0,
|
||||
label=f"第 {index} 段起点",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
description=f"第 {index} 段开始时的输出值。",
|
||||
visible_when=visible_when,
|
||||
),
|
||||
ParameterDefinition(
|
||||
f"end{index}",
|
||||
1.0,
|
||||
label=f"第 {index} 段终点",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
description=f"第 {index} 段结束时的输出值。",
|
||||
visible_when=visible_when,
|
||||
),
|
||||
ParameterDefinition(
|
||||
f"t{index}",
|
||||
1.0 if index == 1 else 0.0,
|
||||
label=f"第 {index} 段时长",
|
||||
quantity="time",
|
||||
unit="s",
|
||||
minimum=0.0,
|
||||
description=f"第 {index} 段的持续时间。",
|
||||
visible_when=visible_when,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
_UD00_STAGE_PARAMETERS = tuple(
|
||||
parameter
|
||||
for stage_index in range(1, 9)
|
||||
for parameter in _ud00_stage_parameters(stage_index)
|
||||
)
|
||||
visible_when = () if index == 1 else (ParameterCondition('nstages', tuple((float(stage_count) for stage_count in range(index, 9)))),)
|
||||
return (ParameterDefinition(f'start{index}', 0.0 if index == 1 else 1.0, label=f'第 {index} 段起点', quantity='dimensionless', unit='', description=f'第 {index} 段开始时的输出值。', visible_when=visible_when), ParameterDefinition(f'end{index}', 1.0, label=f'第 {index} 段终点', quantity='dimensionless', unit='', description=f'第 {index} 段结束时的输出值。', visible_when=visible_when), ParameterDefinition(f't{index}', 1.0 if index == 1 else 0.0, label=f'第 {index} 段时长', quantity='time', unit='s', minimum=0.0, description=f'第 {index} 段的持续时间。', visible_when=visible_when))
|
||||
|
||||
_UD00_STAGE_PARAMETERS = tuple((parameter for stage_index in range(1, 9) for parameter in _ud00_stage_parameters(stage_index)))
|
||||
|
||||
class AmesimStep0(AlgebraicComponent):
|
||||
"""AMESim STEP0 scalar step signal source."""
|
||||
MODEL_TYPE = 'amesim_step0'
|
||||
MODEL_VERSION = '0.1.0'
|
||||
PORTS = (PortDefinition.signal('out', nominal_role='output'),)
|
||||
PARAMETERS = (ParameterDefinition('initial', 0.0, label='初始值', quantity='dimensionless', unit=''), ParameterDefinition('final', 1.0, label='阶跃后值', quantity='dimensionless', unit=''), ParameterDefinition('time', 0.0, label='阶跃时间', quantity='time', unit='s'))
|
||||
RESULT_VARIABLES = (ResultVariableDefinition('y', '输出', 'dimensionless', '', 'signal', 10),)
|
||||
DISPLAY = ComponentDisplaySpec(label='STEP0 阶跃信号', library_id='amesim', category_id='signals', symbol='amesim_step0', ports=(PortDisplaySpec('out', 'right', order=10),), order=10)
|
||||
|
||||
MODEL_TYPE = "amesim_step0"
|
||||
MODEL_VERSION = "0.1.0"
|
||||
PORTS = (PortDefinition.signal("out", nominal_role="output"),)
|
||||
PARAMETERS = (
|
||||
ParameterDefinition("initial", 0.0, label="初始值", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("final", 1.0, label="阶跃后值", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("time", 0.0, label="阶跃时间", quantity="time", unit="s"),
|
||||
)
|
||||
RESULT_VARIABLES = (
|
||||
ResultVariableDefinition("y", "输出", "dimensionless", "", "signal", 10),
|
||||
)
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="STEP0 阶跃信号",
|
||||
library_id="amesim",
|
||||
category_id="signals",
|
||||
symbol="amesim_step0",
|
||||
ports=(PortDisplaySpec("out", "right", order=10),),
|
||||
order=10,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
*,
|
||||
initial: float = 0.0,
|
||||
final: float = 1.0,
|
||||
time: float = 0.0,
|
||||
) -> None:
|
||||
def __init__(self, name: str, medium: IdealGasMedium, *, initial: float=0.0, final: float=1.0, time: float=0.0) -> None:
|
||||
super().__init__(name=name)
|
||||
self.set_parameter_values({"initial": initial, "final": final, "time": time})
|
||||
self.set_parameter_values({'initial': initial, 'final': final, 'time': time})
|
||||
self.initial = float(initial)
|
||||
self.final = float(final)
|
||||
self.time = float(time)
|
||||
self.out = self.register_declared_port("out")
|
||||
self.out.signal = self.output_at(0.0)
|
||||
self.out = self.register_declared_port('out')
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> "AmesimStep0":
|
||||
return cls(
|
||||
name=name,
|
||||
medium=medium,
|
||||
initial=parameters["initial"],
|
||||
final=parameters["final"],
|
||||
time=parameters["time"],
|
||||
)
|
||||
|
||||
def output_at(self, time: float) -> float:
|
||||
return self.final if time >= self.time else self.initial
|
||||
|
||||
def signal_output_values(self, time: float) -> dict[str, float]:
|
||||
return {"out": self.output_at(time)}
|
||||
|
||||
def signal_event_times(
|
||||
self,
|
||||
start_time: float,
|
||||
stop_time: float,
|
||||
) -> tuple[float, ...]:
|
||||
"""Expose the exact STEP0 switch time as an integration split point."""
|
||||
|
||||
return (self.time,) if start_time < self.time < stop_time else ()
|
||||
|
||||
def component_result_values(self) -> Mapping[str, float]:
|
||||
return {"y": self.out.signal}
|
||||
|
||||
def create(cls, *, name: str, medium: IdealGasMedium, parameters: Mapping[str, float]) -> 'AmesimStep0':
|
||||
return cls(name=name, medium=medium, initial=parameters['initial'], final=parameters['final'], time=parameters['time'])
|
||||
EQUATIONS = ()
|
||||
|
||||
class AmesimUd00(AlgebraicComponent):
|
||||
"""AMESim UD00 piecewise-linear scalar signal source."""
|
||||
MODEL_TYPE = 'amesim_ud00'
|
||||
MODEL_VERSION = '0.2.0'
|
||||
PORTS = (PortDefinition.signal('out', nominal_role='output'),)
|
||||
PARAMETERS = (ParameterDefinition('tstart', 0.0, label='启动时间', quantity='time', unit='s', description='分段信号开始输出第一段之前的等待时间。'), *_UD00_STAGE_PARAMETERS, ParameterDefinition('nstages', 1.0, label='段数', quantity='dimensionless', unit='', minimum=1.0, maximum=8.0, editor='choice', options=tuple((ParameterOption(float(stage_count), str(stage_count)) for stage_count in range(1, 9))), description='参与输出计算的有效线性分段数量。'), ParameterDefinition('iscyclic', 0.0, label='循环', quantity='dimensionless', unit='', minimum=0.0, maximum=1.0, editor='choice', options=(ParameterOption(0.0, '否'), ParameterOption(1.0, '是')), description='当前公共协议编码:0 表示单次输出,1 表示循环输出。'))
|
||||
RESULT_VARIABLES = (ResultVariableDefinition('y', '输出', 'dimensionless', '', 'signal', 10),)
|
||||
DISPLAY = ComponentDisplaySpec(label='UD00 分段线性信号', library_id='amesim', category_id='signals', symbol='amesim_ud00', ports=(PortDisplaySpec('out', 'right', order=10),), order=20, parameter_groups=(ParameterGroupDisplaySpec(id='stages', label='分段参数', parameters=tuple((parameter.name for parameter in _UD00_STAGE_PARAMETERS)), order=10),))
|
||||
|
||||
MODEL_TYPE = "amesim_ud00"
|
||||
MODEL_VERSION = "0.2.0"
|
||||
PORTS = (PortDefinition.signal("out", nominal_role="output"),)
|
||||
PARAMETERS = (
|
||||
ParameterDefinition(
|
||||
"tstart",
|
||||
0.0,
|
||||
label="启动时间",
|
||||
quantity="time",
|
||||
unit="s",
|
||||
description="分段信号开始输出第一段之前的等待时间。",
|
||||
),
|
||||
*_UD00_STAGE_PARAMETERS,
|
||||
ParameterDefinition(
|
||||
"nstages",
|
||||
1.0,
|
||||
label="段数",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
minimum=1.0,
|
||||
maximum=8.0,
|
||||
editor="choice",
|
||||
options=tuple(
|
||||
ParameterOption(float(stage_count), str(stage_count))
|
||||
for stage_count in range(1, 9)
|
||||
),
|
||||
description="参与输出计算的有效线性分段数量。",
|
||||
),
|
||||
ParameterDefinition(
|
||||
"iscyclic",
|
||||
0.0,
|
||||
label="循环",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
minimum=0.0,
|
||||
maximum=1.0,
|
||||
editor="choice",
|
||||
options=(
|
||||
ParameterOption(0.0, "否"),
|
||||
ParameterOption(1.0, "是"),
|
||||
),
|
||||
description="当前公共协议编码:0 表示单次输出,1 表示循环输出。",
|
||||
),
|
||||
)
|
||||
RESULT_VARIABLES = (
|
||||
ResultVariableDefinition("y", "输出", "dimensionless", "", "signal", 10),
|
||||
)
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="UD00 分段线性信号",
|
||||
library_id="amesim",
|
||||
category_id="signals",
|
||||
symbol="amesim_ud00",
|
||||
ports=(PortDisplaySpec("out", "right", order=10),),
|
||||
order=20,
|
||||
parameter_groups=(
|
||||
ParameterGroupDisplaySpec(
|
||||
id="stages",
|
||||
label="分段参数",
|
||||
parameters=tuple(
|
||||
parameter.name for parameter in _UD00_STAGE_PARAMETERS
|
||||
),
|
||||
order=10,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
*,
|
||||
tstart: float = 0.0,
|
||||
starts: tuple[float, ...] = (0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0),
|
||||
ends: tuple[float, ...] = (1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0),
|
||||
durations: tuple[float, ...] = (1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
|
||||
nstages: int = 1,
|
||||
iscyclic: bool = False,
|
||||
) -> None:
|
||||
def __init__(self, name: str, medium: IdealGasMedium, *, tstart: float=0.0, starts: tuple[float, ...]=(0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0), ends: tuple[float, ...]=(1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0), durations: tuple[float, ...]=(1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), nstages: int=1, iscyclic: bool=False) -> None:
|
||||
super().__init__(name=name)
|
||||
if len(starts) != 8 or len(ends) != 8 or len(durations) != 8:
|
||||
raise ValueError("UD00 requires exactly eight start, end, and duration values.")
|
||||
raise ValueError('UD00 requires exactly eight start, end, and duration values.')
|
||||
if nstages < 1 or nstages > 8:
|
||||
raise ValueError("UD00 nstages must be between 1 and 8.")
|
||||
raise ValueError('UD00 nstages must be between 1 and 8.')
|
||||
self.tstart = float(tstart)
|
||||
self.starts = tuple(float(value) for value in starts)
|
||||
self.ends = tuple(float(value) for value in ends)
|
||||
self.durations = tuple(float(value) for value in durations)
|
||||
self.starts = tuple((float(value) for value in starts))
|
||||
self.ends = tuple((float(value) for value in ends))
|
||||
self.durations = tuple((float(value) for value in durations))
|
||||
self.nstages = int(nstages)
|
||||
self.iscyclic = bool(iscyclic)
|
||||
values: dict[str, float] = {"tstart": self.tstart, "nstages": float(self.nstages), "iscyclic": float(int(self.iscyclic))}
|
||||
values: dict[str, float] = {'tstart': self.tstart, 'nstages': float(self.nstages), 'iscyclic': float(int(self.iscyclic))}
|
||||
for index in range(1, 9):
|
||||
values[f"start{index}"] = self.starts[index - 1]
|
||||
values[f"end{index}"] = self.ends[index - 1]
|
||||
values[f"t{index}"] = self.durations[index - 1]
|
||||
values[f'start{index}'] = self.starts[index - 1]
|
||||
values[f'end{index}'] = self.ends[index - 1]
|
||||
values[f't{index}'] = self.durations[index - 1]
|
||||
self.set_parameter_values(values)
|
||||
self.out = self.register_declared_port("out")
|
||||
self.out.signal = self.output_at(0.0)
|
||||
self.out = self.register_declared_port('out')
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> "AmesimUd00":
|
||||
nstages = parameters["nstages"]
|
||||
iscyclic = parameters["iscyclic"]
|
||||
def create(cls, *, name: str, medium: IdealGasMedium, parameters: Mapping[str, float]) -> 'AmesimUd00':
|
||||
nstages = parameters['nstages']
|
||||
iscyclic = parameters['iscyclic']
|
||||
definitions = {definition.name: definition for definition in cls.PARAMETERS}
|
||||
for parameter_name, value in (
|
||||
("nstages", nstages),
|
||||
("iscyclic", iscyclic),
|
||||
):
|
||||
for parameter_name, value in (('nstages', nstages), ('iscyclic', iscyclic)):
|
||||
numeric_value = float(value)
|
||||
if not numeric_value.is_integer():
|
||||
raise ValueError(f"UD00 {parameter_name} must be an integer.")
|
||||
raise ValueError(f'UD00 {parameter_name} must be an integer.')
|
||||
message = definitions[parameter_name].validation_message(numeric_value)
|
||||
if message is not None:
|
||||
raise ValueError(f"UD00 {parameter_name} {message}.")
|
||||
return cls(
|
||||
name=name,
|
||||
medium=medium,
|
||||
tstart=parameters["tstart"],
|
||||
starts=tuple(parameters[f"start{index}"] for index in range(1, 9)),
|
||||
ends=tuple(parameters[f"end{index}"] for index in range(1, 9)),
|
||||
durations=tuple(parameters[f"t{index}"] for index in range(1, 9)),
|
||||
nstages=int(nstages),
|
||||
iscyclic=bool(int(iscyclic)),
|
||||
)
|
||||
|
||||
def output_at(self, time: float) -> float:
|
||||
elapsed = max(float(time) - self.tstart, 0.0)
|
||||
active_durations = self.durations[: self.nstages]
|
||||
total_duration = sum(active_durations)
|
||||
if self.iscyclic and total_duration > 0.0:
|
||||
elapsed = elapsed % total_duration
|
||||
|
||||
stage_start_time = 0.0
|
||||
for index, duration in enumerate(active_durations):
|
||||
stage_end_time = stage_start_time + duration
|
||||
if elapsed < stage_end_time or index == self.nstages - 1:
|
||||
if duration <= 0.0:
|
||||
return self.ends[index]
|
||||
fraction = (elapsed - stage_start_time) / duration
|
||||
return self.starts[index] + fraction * (self.ends[index] - self.starts[index])
|
||||
stage_start_time = stage_end_time
|
||||
return self.ends[self.nstages - 1]
|
||||
|
||||
def signal_output_values(self, time: float) -> dict[str, float]:
|
||||
return {"out": self.output_at(time)}
|
||||
|
||||
def signal_event_times(
|
||||
self,
|
||||
start_time: float,
|
||||
stop_time: float,
|
||||
) -> tuple[float, ...]:
|
||||
"""Return UD00 start, stage, and repeated cycle boundaries.
|
||||
|
||||
The final non-cyclic stage is intentionally not given an end event:
|
||||
``output_at`` continues that stage's slope after its configured duration.
|
||||
"""
|
||||
|
||||
if stop_time <= start_time:
|
||||
return ()
|
||||
|
||||
active_durations = self.durations[: self.nstages]
|
||||
stage_offsets = [0.0]
|
||||
elapsed = 0.0
|
||||
for duration in active_durations[:-1]:
|
||||
elapsed += duration
|
||||
stage_offsets.append(elapsed)
|
||||
|
||||
if not self.iscyclic:
|
||||
return tuple(
|
||||
sorted(
|
||||
{
|
||||
event_time
|
||||
for offset in stage_offsets
|
||||
if start_time
|
||||
< (event_time := self.tstart + offset)
|
||||
< stop_time
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
cycle_duration = sum(active_durations)
|
||||
if cycle_duration <= 0.0:
|
||||
return ()
|
||||
|
||||
events: set[float] = set()
|
||||
for offset in stage_offsets:
|
||||
first_boundary = self.tstart + offset
|
||||
cycle_index = max(
|
||||
0,
|
||||
floor((start_time - first_boundary) / cycle_duration) + 1,
|
||||
)
|
||||
event_time = first_boundary + cycle_index * cycle_duration
|
||||
while event_time < stop_time:
|
||||
if event_time > start_time:
|
||||
events.add(event_time)
|
||||
cycle_index += 1
|
||||
event_time = first_boundary + cycle_index * cycle_duration
|
||||
return tuple(sorted(events))
|
||||
|
||||
def component_result_values(self) -> Mapping[str, float]:
|
||||
return {"y": self.out.signal}
|
||||
raise ValueError(f'UD00 {parameter_name} {message}.')
|
||||
return cls(name=name, medium=medium, tstart=parameters['tstart'], starts=tuple((parameters[f'start{index}'] for index in range(1, 9))), ends=tuple((parameters[f'end{index}'] for index in range(1, 9))), durations=tuple((parameters[f't{index}'] for index in range(1, 9))), nstages=int(nstages), iscyclic=bool(int(iscyclic)))
|
||||
EQUATIONS = ()
|
||||
@@ -1,38 +1,12 @@
|
||||
"""Component parameters, ports and output definitions; numerical equations execute in C."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from math import isfinite
|
||||
|
||||
from app.simulation.components.amesim.gases import (
|
||||
AMESIM_GAS_INDEX_PARAMETER,
|
||||
normalize_amesim_gas_index,
|
||||
)
|
||||
from collections.abc import Mapping
|
||||
from app.simulation.components.amesim.gases import AMESIM_GAS_INDEX_PARAMETER, normalize_amesim_gas_index
|
||||
from app.simulation.core.base import ThermodynamicVolumeComponent
|
||||
from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
|
||||
from app.simulation.core.equations import EquationResidual
|
||||
from app.simulation.core.metadata import (
|
||||
ParameterDefinition,
|
||||
ResultVariableDefinition,
|
||||
THERMODYNAMIC_VOLUME_RESULT_VARIABLES,
|
||||
)
|
||||
from app.simulation.core.medium import (
|
||||
GasMedium,
|
||||
ThermodynamicProperties,
|
||||
ThermodynamicPropertiesLinearization,
|
||||
)
|
||||
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.state import VolumeState
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Pnch012DerivativeLinearization:
|
||||
derivative: tuple[float, float]
|
||||
tangents: tuple[tuple[float, ...], tuple[float, ...]]
|
||||
properties: ThermodynamicPropertiesLinearization
|
||||
valid: bool = True
|
||||
reason: str | None = None
|
||||
|
||||
|
||||
class AmesimPnch023(ThermodynamicVolumeComponent):
|
||||
"""AMESim PNCH023 simple pneumatic chamber with heat exchange.
|
||||
@@ -42,112 +16,16 @@ class AmesimPnch023(ThermodynamicVolumeComponent):
|
||||
framework's mass/internal-energy volume state and keeps the AMESim
|
||||
heat-transfer contract `kth * sth * (extemp - T)`.
|
||||
"""
|
||||
|
||||
MODEL_TYPE = "amesim_pnch023"
|
||||
MODEL_VERSION = "0.1.0"
|
||||
PORTS = (
|
||||
PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),
|
||||
PortDefinition.pneumatic("port_2", nominal_role="bidirectional"),
|
||||
)
|
||||
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="仿真开始时气室内气体的绝对温度。",
|
||||
),
|
||||
)
|
||||
MODEL_TYPE = 'amesim_pnch023'
|
||||
MODEL_VERSION = '0.1.0'
|
||||
PORTS = (PortDefinition.pneumatic('port_1', nominal_role='bidirectional'), PortDefinition.pneumatic('port_2', nominal_role='bidirectional'))
|
||||
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,
|
||||
)
|
||||
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)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
medium: GasMedium,
|
||||
*,
|
||||
cvol: float = 0.057,
|
||||
kth: float = 0.0,
|
||||
sth: float = 0.1,
|
||||
extemp: float = 293.15,
|
||||
gi: float = 1.0,
|
||||
p0: float = 100000.0,
|
||||
T0: float = 293.15,
|
||||
) -> None:
|
||||
def __init__(self, name: str, medium: GasMedium, *, cvol: float=0.057, kth: float=0.0, sth: float=0.1, extemp: float=293.15, gi: float=1.0, p0: float=100000.0, T0: float=293.15) -> None:
|
||||
super().__init__(name=name)
|
||||
self.set_parameter_values(
|
||||
{
|
||||
"cvol": cvol,
|
||||
"kth": kth,
|
||||
"sth": sth,
|
||||
"extemp": extemp,
|
||||
"gi": gi,
|
||||
"p0": p0,
|
||||
"T0": T0,
|
||||
}
|
||||
)
|
||||
self.set_parameter_values({'cvol': cvol, 'kth': kth, 'sth': sth, 'extemp': extemp, 'gi': gi, 'p0': p0, 'T0': T0})
|
||||
self.medium = medium
|
||||
self.cvol = float(cvol)
|
||||
self.kth = float(kth)
|
||||
@@ -156,120 +34,13 @@ class AmesimPnch023(ThermodynamicVolumeComponent):
|
||||
self.gi = normalize_amesim_gas_index(gi)
|
||||
self.p0 = float(p0)
|
||||
self.T0 = float(T0)
|
||||
m0 = medium.density(self.p0, self.T0) * self.cvol
|
||||
U0 = m0 * medium.specific_internal_energy_at_pressure(self.p0, self.T0)
|
||||
self.state = VolumeState(m=m0, U=U0)
|
||||
initial_h = medium.specific_enthalpy_at_pressure(self.p0, self.T0)
|
||||
self.port_1 = self.register_declared_port("port_1")
|
||||
self.port_1.p = self.p0
|
||||
self.port_1.h_outflow = initial_h
|
||||
self.port_2 = self.register_declared_port("port_2")
|
||||
self.port_2.p = self.p0
|
||||
self.port_2.h_outflow = initial_h
|
||||
self.port_1 = self.register_declared_port('port_1')
|
||||
self.port_2 = self.register_declared_port('port_2')
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: GasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> AmesimPnch023:
|
||||
return cls(
|
||||
name=name,
|
||||
medium=medium,
|
||||
cvol=parameters["cvol"],
|
||||
kth=parameters["kth"],
|
||||
sth=parameters["sth"],
|
||||
extemp=parameters["extemp"],
|
||||
gi=parameters["gi"],
|
||||
p0=parameters["p0"],
|
||||
T0=parameters["T0"],
|
||||
)
|
||||
|
||||
def get_state_vector(self) -> list[float]:
|
||||
return self.state.as_vector()
|
||||
|
||||
def set_state_vector(self, values: list[float]) -> None:
|
||||
self.state = VolumeState.from_vector(values)
|
||||
|
||||
def properties(self) -> ThermodynamicProperties:
|
||||
props = self.medium.properties_from_mU(self.state.m, self.state.U, self.cvol)
|
||||
self.port_1.p = props.p
|
||||
self.port_1.h_outflow = props.h
|
||||
self.port_2.p = props.p
|
||||
self.port_2.h_outflow = props.h
|
||||
return props
|
||||
|
||||
def refresh_thermodynamic_ports(self) -> ThermodynamicProperties:
|
||||
return self.properties()
|
||||
|
||||
def thermal_energy_flow_w(self, temperature: float) -> float:
|
||||
return self.kth * self.sth * (self.extemp - temperature)
|
||||
|
||||
def state_derivative_from_ports(
|
||||
self,
|
||||
connected_h: Mapping[str, float],
|
||||
) -> list[float]:
|
||||
props = self.properties()
|
||||
inlet_h_1 = self.connection_inlet_enthalpy(
|
||||
port_m_flow=self.port_1.m_flow,
|
||||
connected_h=connected_h["port_1"],
|
||||
internal_h=props.h,
|
||||
)
|
||||
inlet_h_2 = self.connection_inlet_enthalpy(
|
||||
port_m_flow=self.port_2.m_flow,
|
||||
connected_h=connected_h["port_2"],
|
||||
internal_h=props.h,
|
||||
)
|
||||
derivative = VolumeState(
|
||||
m=self.port_1.m_flow + self.port_2.m_flow,
|
||||
U=(
|
||||
self.port_1.m_flow * inlet_h_1
|
||||
+ self.port_2.m_flow * inlet_h_2
|
||||
+ self.thermal_energy_flow_w(props.T)
|
||||
),
|
||||
)
|
||||
return derivative.as_vector()
|
||||
|
||||
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
||||
pressure = self.medium.properties_from_mU(
|
||||
self.state.m,
|
||||
self.state.U,
|
||||
self.cvol,
|
||||
).p
|
||||
return (
|
||||
self.port_1.p - pressure,
|
||||
self.port_2.p - pressure,
|
||||
)
|
||||
|
||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||
pressure = self.medium.properties_from_mU(
|
||||
self.state.m,
|
||||
self.state.U,
|
||||
self.cvol,
|
||||
).p
|
||||
return (
|
||||
EquationResidual(
|
||||
id=f"{self.name}:port_1_pressure_state",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="state",
|
||||
variables=(f"{self.name}.port_1.p", f"{self.name}.state"),
|
||||
role="effort",
|
||||
value=self.port_1.p - pressure,
|
||||
),
|
||||
EquationResidual(
|
||||
id=f"{self.name}:port_2_pressure_state",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="state",
|
||||
variables=(f"{self.name}.port_2.p", f"{self.name}.state"),
|
||||
role="effort",
|
||||
value=self.port_2.p - pressure,
|
||||
),
|
||||
)
|
||||
|
||||
def create(cls, *, name: str, medium: GasMedium, parameters: Mapping[str, float]) -> AmesimPnch023:
|
||||
return cls(name=name, medium=medium, cvol=parameters['cvol'], kth=parameters['kth'], sth=parameters['sth'], extemp=parameters['extemp'], gi=parameters['gi'], p0=parameters['p0'], T0=parameters['T0'])
|
||||
EQUATIONS = ({'id': '__MODEL__:port_1_pressure_state', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'state', 'variables': ['__MODEL__.port_1.p', '__MODEL__.state'], 'role': 'effort'}, {'id': '__MODEL__:port_2_pressure_state', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'state', 'variables': ['__MODEL__.port_2.p', '__MODEL__.state'], 'role': 'effort'})
|
||||
|
||||
class AmesimPnch012(ThermodynamicVolumeComponent):
|
||||
"""AMESim PNCH012 variable-volume pneumatic chamber.
|
||||
@@ -279,143 +50,16 @@ class AmesimPnch012(ThermodynamicVolumeComponent):
|
||||
parameters, while connected moving-boundary components can now add live
|
||||
volume and volume-rate values through the pneumatic connector contract.
|
||||
"""
|
||||
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'))
|
||||
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)
|
||||
|
||||
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"),
|
||||
)
|
||||
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,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
medium: GasMedium,
|
||||
*,
|
||||
cvol0: float = 0.015,
|
||||
kth: float = 0.0,
|
||||
sth: float = 0.1,
|
||||
extemp: float = 293.15,
|
||||
gi: float = 1.0,
|
||||
p0: float = 100000.0,
|
||||
T0: float = 293.15,
|
||||
vol1: float = 0.0,
|
||||
vol2: float = 0.0,
|
||||
vol3: float = 0.0,
|
||||
vol4: float = 0.0,
|
||||
dvol1: float = 0.0,
|
||||
dvol2: float = 0.0,
|
||||
dvol3: float = 0.0,
|
||||
dvol4: float = 0.0,
|
||||
) -> None:
|
||||
def __init__(self, name: str, medium: GasMedium, *, cvol0: float=0.015, kth: float=0.0, sth: float=0.1, extemp: float=293.15, gi: float=1.0, p0: float=100000.0, T0: float=293.15, vol1: float=0.0, vol2: float=0.0, vol3: float=0.0, vol4: float=0.0, dvol1: float=0.0, dvol2: float=0.0, dvol3: float=0.0, dvol4: float=0.0) -> None:
|
||||
super().__init__(name=name)
|
||||
self.set_parameter_values(
|
||||
{
|
||||
"cvol0": cvol0,
|
||||
"kth": kth,
|
||||
"sth": sth,
|
||||
"extemp": extemp,
|
||||
"gi": gi,
|
||||
"p0": p0,
|
||||
"T0": T0,
|
||||
"vol1": vol1,
|
||||
"vol2": vol2,
|
||||
"vol3": vol3,
|
||||
"vol4": vol4,
|
||||
"dvol1": dvol1,
|
||||
"dvol2": dvol2,
|
||||
"dvol3": dvol3,
|
||||
"dvol4": dvol4,
|
||||
}
|
||||
)
|
||||
self.set_parameter_values({'cvol0': cvol0, 'kth': kth, 'sth': sth, 'extemp': extemp, 'gi': gi, 'p0': p0, 'T0': T0, 'vol1': vol1, 'vol2': vol2, 'vol3': vol3, 'vol4': vol4, 'dvol1': dvol1, 'dvol2': dvol2, 'dvol3': dvol3, 'dvol4': dvol4})
|
||||
self.medium = medium
|
||||
self.cvol0 = float(cvol0)
|
||||
self.kth = float(kth)
|
||||
@@ -424,267 +68,13 @@ class AmesimPnch012(ThermodynamicVolumeComponent):
|
||||
self.gi = normalize_amesim_gas_index(gi)
|
||||
self.p0 = float(p0)
|
||||
self.T0 = float(T0)
|
||||
self.external_volumes = {
|
||||
"port_1": float(vol1),
|
||||
"port_2": float(vol2),
|
||||
"port_3": float(vol3),
|
||||
"port_4": float(vol4),
|
||||
}
|
||||
self.external_volume_rates = {
|
||||
"port_1": float(dvol1),
|
||||
"port_2": float(dvol2),
|
||||
"port_3": float(dvol3),
|
||||
"port_4": float(dvol4),
|
||||
}
|
||||
if self.total_volume() <= 0.0:
|
||||
raise ValueError("PNCH012 total volume must be positive.")
|
||||
m0 = medium.density(self.p0, self.T0) * self.total_volume()
|
||||
U0 = m0 * medium.specific_internal_energy_at_pressure(self.p0, self.T0)
|
||||
self.state = VolumeState(m=m0, U=U0)
|
||||
initial_h = medium.specific_enthalpy_at_pressure(self.p0, self.T0)
|
||||
for port_name in ("port_1", "port_2", "port_3", "port_4"):
|
||||
self.external_volumes = {'port_1': float(vol1), 'port_2': float(vol2), 'port_3': float(vol3), 'port_4': float(vol4)}
|
||||
self.external_volume_rates = {'port_1': float(dvol1), 'port_2': float(dvol2), 'port_3': float(dvol3), 'port_4': float(dvol4)}
|
||||
for port_name in ('port_1', 'port_2', 'port_3', 'port_4'):
|
||||
port = self.register_declared_port(port_name)
|
||||
port.p = self.p0
|
||||
port.h_outflow = initial_h
|
||||
setattr(self, port_name, port)
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: GasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> "AmesimPnch012":
|
||||
def create(cls, *, name: str, medium: GasMedium, parameters: Mapping[str, float]) -> 'AmesimPnch012':
|
||||
return cls(name=name, medium=medium, **dict(parameters))
|
||||
|
||||
def connected_external_volume(self) -> float:
|
||||
return sum(
|
||||
getattr(getattr(self, port_name, None), "volume", 0.0)
|
||||
for port_name in self.external_volumes
|
||||
)
|
||||
|
||||
def connected_external_volume_rate(self) -> float:
|
||||
return sum(
|
||||
getattr(getattr(self, port_name, None), "volume_flow", 0.0)
|
||||
for port_name in self.external_volume_rates
|
||||
)
|
||||
|
||||
def total_volume(self) -> float:
|
||||
minimum_volume = self.cvol0 / 100.0
|
||||
return max(
|
||||
self.cvol0 + sum(self.external_volumes.values()) + self.connected_external_volume(),
|
||||
minimum_volume,
|
||||
)
|
||||
|
||||
def total_volume_rate(self) -> float:
|
||||
if self.total_volume() <= self.cvol0 / 100.0:
|
||||
return 0.0
|
||||
return sum(self.external_volume_rates.values()) + self.connected_external_volume_rate()
|
||||
|
||||
def get_state_vector(self) -> list[float]:
|
||||
return self.state.as_vector()
|
||||
|
||||
def set_state_vector(self, values: list[float]) -> None:
|
||||
self.state = VolumeState.from_vector(values)
|
||||
|
||||
def properties(self) -> ThermodynamicProperties:
|
||||
props = self.medium.properties_from_mU(self.state.m, self.state.U, self.total_volume())
|
||||
for port_name in ("port_1", "port_2", "port_3", "port_4"):
|
||||
port = self.get_port(port_name)
|
||||
port.p = props.p
|
||||
port.h_outflow = props.h
|
||||
return props
|
||||
|
||||
def refresh_thermodynamic_ports(self) -> ThermodynamicProperties:
|
||||
return self.properties()
|
||||
|
||||
def thermal_energy_flow_w(self, temperature: float) -> float:
|
||||
return self.kth * self.sth * (self.extemp - temperature)
|
||||
|
||||
def component_result_values(self) -> Mapping[str, float]:
|
||||
props = self.properties()
|
||||
return {
|
||||
"m": self.state.m,
|
||||
"U": self.state.U,
|
||||
"p": props.p,
|
||||
"T": props.T,
|
||||
"rho": props.rho,
|
||||
"u": props.u,
|
||||
"h": props.h,
|
||||
"vol": self.total_volume(),
|
||||
"dvol": self.total_volume_rate(),
|
||||
}
|
||||
|
||||
def state_derivative_from_ports(self, connected_h: Mapping[str, float]) -> list[float]:
|
||||
props = self.properties()
|
||||
mass_derivative = 0.0
|
||||
energy_derivative = 0.0
|
||||
for port_name in ("port_1", "port_2", "port_3", "port_4"):
|
||||
port = self.get_port(port_name)
|
||||
inlet_h = self.connection_inlet_enthalpy(
|
||||
port_m_flow=port.m_flow,
|
||||
connected_h=connected_h[port_name],
|
||||
internal_h=props.h,
|
||||
)
|
||||
mass_derivative += port.m_flow
|
||||
energy_derivative += port.m_flow * inlet_h
|
||||
energy_derivative += self.thermal_energy_flow_w(props.T)
|
||||
energy_derivative -= props.p * self.total_volume_rate()
|
||||
return VolumeState(m=mass_derivative, U=energy_derivative).as_vector()
|
||||
|
||||
def linearize_state_derivative(
|
||||
self,
|
||||
connected_h: Mapping[str, float],
|
||||
*,
|
||||
state_mass_tangent: Sequence[float],
|
||||
state_energy_tangent: Sequence[float],
|
||||
external_volume_tangent: Sequence[float],
|
||||
external_volume_rate_tangent: Sequence[float],
|
||||
port_mass_flow_tangents: Mapping[str, Sequence[float]],
|
||||
connected_h_tangents: Mapping[str, Sequence[float]],
|
||||
property_linearization: ThermodynamicPropertiesLinearization | None = None,
|
||||
flow_boundary_tolerance: float = 1.0e-12,
|
||||
) -> Pnch012DerivativeLinearization:
|
||||
"""Linearize the chamber balance while keeping stream modes fixed."""
|
||||
|
||||
port_names = ("port_1", "port_2", "port_3", "port_4")
|
||||
vectors = {
|
||||
"state_mass": tuple(float(value) for value in state_mass_tangent),
|
||||
"state_energy": tuple(float(value) for value in state_energy_tangent),
|
||||
"volume": tuple(float(value) for value in external_volume_tangent),
|
||||
"volume_rate": tuple(
|
||||
float(value) for value in external_volume_rate_tangent
|
||||
),
|
||||
}
|
||||
for port_name in port_names:
|
||||
vectors[f"flow:{port_name}"] = tuple(
|
||||
float(value) for value in port_mass_flow_tangents[port_name]
|
||||
)
|
||||
vectors[f"enthalpy:{port_name}"] = tuple(
|
||||
float(value) for value in connected_h_tangents[port_name]
|
||||
)
|
||||
widths = {len(values) for values in vectors.values()}
|
||||
if len(widths) != 1:
|
||||
raise ValueError("PNCH012 tangent vectors must have equal lengths.")
|
||||
width = len(vectors["state_mass"])
|
||||
invalid_reason: str | None = None
|
||||
if not all(isfinite(value) for values in vectors.values() for value in values):
|
||||
invalid_reason = "non_finite_tangent_input"
|
||||
|
||||
raw_volume = (
|
||||
self.cvol0
|
||||
+ sum(self.external_volumes.values())
|
||||
+ self.connected_external_volume()
|
||||
)
|
||||
minimum_volume = self.cvol0 / 100.0
|
||||
volume_scale = max(abs(raw_volume), abs(minimum_volume), 1.0e-18)
|
||||
on_volume_boundary = (
|
||||
abs(raw_volume - minimum_volume) <= 1.0e-12 * volume_scale
|
||||
)
|
||||
supplied_volume_tangent = vectors["volume"]
|
||||
if raw_volume < minimum_volume or on_volume_boundary:
|
||||
used_volume_tangent = (0.0,) * width
|
||||
used_volume_rate_tangent = (0.0,) * width
|
||||
if on_volume_boundary and any(
|
||||
value != 0.0
|
||||
for value in (
|
||||
*supplied_volume_tangent,
|
||||
*vectors["volume_rate"],
|
||||
)
|
||||
):
|
||||
invalid_reason = invalid_reason or "volume_floor_boundary"
|
||||
else:
|
||||
used_volume_tangent = supplied_volume_tangent
|
||||
used_volume_rate_tangent = vectors["volume_rate"]
|
||||
|
||||
properties = property_linearization or self.medium.linearize_properties_from_mU(
|
||||
self.state.m,
|
||||
self.state.U,
|
||||
self.total_volume(),
|
||||
vectors["state_mass"],
|
||||
vectors["state_energy"],
|
||||
used_volume_tangent,
|
||||
)
|
||||
if properties.tangents.width != width:
|
||||
raise ValueError(
|
||||
"PNCH012 property tangent width must match balance tangents."
|
||||
)
|
||||
props = properties.properties
|
||||
if not properties.valid:
|
||||
invalid_reason = invalid_reason or properties.reason
|
||||
|
||||
mass_derivative = sum(
|
||||
self.get_port(port_name).m_flow for port_name in port_names
|
||||
)
|
||||
volume_rate = self.total_volume_rate()
|
||||
energy_derivative = self.thermal_energy_flow_w(props.T) - props.p * volume_rate
|
||||
mass_tangent = [0.0] * width
|
||||
energy_tangent = [
|
||||
-self.kth * self.sth * properties.tangents.T[index]
|
||||
- volume_rate * properties.tangents.p[index]
|
||||
- props.p * used_volume_rate_tangent[index]
|
||||
for index in range(width)
|
||||
]
|
||||
|
||||
for port_name in port_names:
|
||||
port = self.get_port(port_name)
|
||||
flow_tangent = vectors[f"flow:{port_name}"]
|
||||
if (
|
||||
abs(port.m_flow) <= flow_boundary_tolerance
|
||||
and any(value != 0.0 for value in flow_tangent)
|
||||
):
|
||||
invalid_reason = invalid_reason or (
|
||||
f"flow_direction_boundary:{port_name}"
|
||||
)
|
||||
if port.m_flow > 0.0:
|
||||
inlet_h = connected_h[port_name]
|
||||
inlet_h_tangent = vectors[f"enthalpy:{port_name}"]
|
||||
else:
|
||||
inlet_h = props.h
|
||||
inlet_h_tangent = properties.tangents.h
|
||||
energy_derivative += port.m_flow * inlet_h
|
||||
for index in range(width):
|
||||
mass_tangent[index] += flow_tangent[index]
|
||||
energy_tangent[index] += (
|
||||
inlet_h * flow_tangent[index]
|
||||
+ port.m_flow * inlet_h_tangent[index]
|
||||
)
|
||||
|
||||
return Pnch012DerivativeLinearization(
|
||||
derivative=(mass_derivative, energy_derivative),
|
||||
tangents=(tuple(mass_tangent), tuple(energy_tangent)),
|
||||
properties=properties,
|
||||
valid=invalid_reason is None,
|
||||
reason=invalid_reason,
|
||||
)
|
||||
|
||||
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
||||
pressure = self.medium.properties_from_mU(
|
||||
self.state.m,
|
||||
self.state.U,
|
||||
self.total_volume(),
|
||||
).p
|
||||
return tuple(
|
||||
self.get_port(port_name).p - pressure
|
||||
for port_name in ("port_1", "port_2", "port_3", "port_4")
|
||||
)
|
||||
|
||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||
pressure = self.medium.properties_from_mU(
|
||||
self.state.m,
|
||||
self.state.U,
|
||||
self.total_volume(),
|
||||
).p
|
||||
return tuple(
|
||||
EquationResidual(
|
||||
id=f"{self.name}:{port_name}_pressure_state",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="state",
|
||||
variables=(f"{self.name}.{port_name}.p", f"{self.name}.state"),
|
||||
role="effort",
|
||||
value=self.get_port(port_name).p - pressure,
|
||||
)
|
||||
for port_name in ("port_1", "port_2", "port_3", "port_4")
|
||||
)
|
||||
EQUATIONS = ({'id': '__MODEL__:port_1_pressure_state', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'state', 'variables': ['__MODEL__.port_1.p', '__MODEL__.state'], 'role': 'effort'}, {'id': '__MODEL__:port_2_pressure_state', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'state', 'variables': ['__MODEL__.port_2.p', '__MODEL__.state'], 'role': 'effort'}, {'id': '__MODEL__:port_3_pressure_state', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'state', 'variables': ['__MODEL__.port_3.p', '__MODEL__.state'], 'role': 'effort'}, {'id': '__MODEL__:port_4_pressure_state', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'state', 'variables': ['__MODEL__.port_4.p', '__MODEL__.state'], 'role': 'effort'})
|
||||
@@ -1,282 +1,14 @@
|
||||
# 元件建模规范与示例
|
||||
# 元件开发示例
|
||||
|
||||
规范的权威版本位于
|
||||
[`docs/standard/component-model-authoring-spec-v1.md`](../../../docs/standard/component-model-authoring-spec-v1.md)。
|
||||
本文档保留在组件目录中,作为离模型源码最近的完整示例;若两者不一致,应在同一次
|
||||
修改中同步,不能让示例形成另一套规则。
|
||||
权威规则见 [组件模型建模规范](../../../docs/standard/component-model-authoring-spec-v1.md)。当前模型采用 Python 声明、C 数值实现。
|
||||
|
||||
本文档是 `app/simulation/components` 下新增元件的最小开发规范。当前
|
||||
`experimental` 是用于验证规范的临时组件库;后续正式模型应建立独立组件库,
|
||||
不要继续堆放在 `experimental` 中。
|
||||
以气瓶为例:
|
||||
|
||||
目标是让元件的端口、输入参数和可展示结果都由元件类显式声明,避免 XML
|
||||
校验、求解器和前端分别维护同一份含义。
|
||||
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` 声明端口压力与气瓶状态之间的约束;只保存变量名和关系。
|
||||
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 和输出键。
|
||||
|
||||
## 一、元件类必须声明的内容
|
||||
|
||||
每个对外注册的元件类至少需要声明以下六个类属性:
|
||||
|
||||
```python
|
||||
MODEL_TYPE = "example_component"
|
||||
MODEL_VERSION = "1.0.0"
|
||||
PORTS = (...)
|
||||
PARAMETERS = (...)
|
||||
RESULT_VARIABLES = (...)
|
||||
DISPLAY = ...
|
||||
```
|
||||
|
||||
- `MODEL_TYPE`:稳定的模型类型标识,对应 System XML 中的 `Component/@type`。发布后不要随意改名。
|
||||
- `MODEL_VERSION`:模型契约版本,采用 `主版本.次版本.修订版本`。
|
||||
- `PORTS`:端口契约,包括端口名、物理域、变量和正流量方向。
|
||||
- `PARAMETERS`:用户可配置的输入参数,包括默认值、物理量、SI 单位和取值范围。
|
||||
- `RESULT_VARIABLES`:允许写入仿真结果并显示在结果页的组件级变量。端口结果由 `PORTS` 中的端口变量定义自动生成。
|
||||
- `DISPLAY`:组件库名称、分类、图标、排序和端口画布位置,不参与物理求解。
|
||||
|
||||
元件构造函数还必须:
|
||||
|
||||
1. 调用 `super().__init__(name)`。
|
||||
2. 使用 `set_parameter_values()` 保存规范化后的输入参数。
|
||||
3. 使用 `register_declared_port()` 创建已声明端口。
|
||||
4. 若声明了组件结果变量,实现 `component_result_values()` 并返回对应数值;标准热力学容腔可以直接继承 `ThermodynamicVolumeComponent` 的实现。
|
||||
5. 实现统一的类方法 `create()`,接收规范化后的 SI 参数。
|
||||
|
||||
## 二、输入参数与结果变量
|
||||
|
||||
输入参数和仿真结果必须分开声明:
|
||||
|
||||
- 输入参数描述一次仿真开始前由用户配置的量,例如 `volume`、`p0`、`T0`。
|
||||
- 结果变量描述随时间变化、允许绘图的量,例如 `p`、`T`、`m`、`m_flow`。
|
||||
- 求解器缓存、中间残差和调试字段不得自动暴露为结果变量。
|
||||
- 参数名和结果变量名使用稳定的英文机器标识;`label` 专门用于界面显示。
|
||||
|
||||
参数定义示例:
|
||||
|
||||
```python
|
||||
ParameterDefinition(
|
||||
name="volume",
|
||||
label="容积",
|
||||
quantity="volume",
|
||||
unit="m3",
|
||||
default=0.1,
|
||||
minimum=0.0,
|
||||
minimum_exclusive=True,
|
||||
)
|
||||
```
|
||||
|
||||
结果变量定义示例:
|
||||
|
||||
```python
|
||||
ResultVariableDefinition(
|
||||
name="p",
|
||||
label="压力",
|
||||
quantity="pressure",
|
||||
unit="Pa",
|
||||
category="thermodynamic",
|
||||
order=30,
|
||||
)
|
||||
```
|
||||
|
||||
## 三、命名和单位约定
|
||||
|
||||
- 模型类型、参数、端口和变量名使用 `snake_case`,已有热力学惯例 `T`、`U` 可以保留。
|
||||
- 输入参数保存和计算统一使用 SI 基准值;界面单位换算不能改变后端存储值。
|
||||
- 无量纲参数的 `unit` 使用空字符串。
|
||||
- `quantity` 表示稳定的物理量类型,例如 `pressure`、`temperature`、`mass_flow`,不能使用界面文案代替。
|
||||
- 正质量流量统一定义为流入元件,即 `positiveFlowDirection="intoComponent"`。
|
||||
- 端口变量 `p`、`m_flow`、`h_outflow` 的连接规则由 `PortDefinition.pneumatic()` 统一提供。
|
||||
|
||||
## 四、完整示例:单端口储气容腔
|
||||
|
||||
下面的示例展示一个可直接接入当前框架的动态元件。真实新增元件时应放入独立的 `.py` 文件,并补充对应测试。
|
||||
|
||||
```python
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
|
||||
from app.simulation.core.base import ThermodynamicVolumeComponent
|
||||
from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
|
||||
from app.simulation.core.equations import EquationResidual
|
||||
from app.simulation.core.metadata import (
|
||||
ParameterDefinition,
|
||||
THERMODYNAMIC_VOLUME_RESULT_VARIABLES,
|
||||
)
|
||||
from app.simulation.core.medium import IdealGasMedium, ThermodynamicProperties
|
||||
from app.simulation.core.ports import PortDefinition
|
||||
from app.simulation.core.state import VolumeState
|
||||
|
||||
|
||||
class ExampleVolume(ThermodynamicVolumeComponent):
|
||||
MODEL_TYPE = "example_volume"
|
||||
MODEL_VERSION = "1.0.0"
|
||||
PORTS = (
|
||||
PortDefinition.pneumatic("port_a", nominal_role="bidirectional"),
|
||||
)
|
||||
PARAMETERS = (
|
||||
ParameterDefinition(
|
||||
name="volume",
|
||||
label="容积",
|
||||
quantity="volume",
|
||||
unit="m3",
|
||||
default=0.1,
|
||||
minimum=0.0,
|
||||
minimum_exclusive=True,
|
||||
),
|
||||
ParameterDefinition(
|
||||
name="p0",
|
||||
label="初始压力",
|
||||
quantity="pressure",
|
||||
unit="Pa",
|
||||
default=100000.0,
|
||||
minimum=0.0,
|
||||
minimum_exclusive=True,
|
||||
),
|
||||
ParameterDefinition(
|
||||
name="T0",
|
||||
label="初始温度",
|
||||
quantity="temperature",
|
||||
unit="K",
|
||||
default=300.0,
|
||||
minimum=0.0,
|
||||
minimum_exclusive=True,
|
||||
),
|
||||
)
|
||||
RESULT_VARIABLES = THERMODYNAMIC_VOLUME_RESULT_VARIABLES
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="示例容腔",
|
||||
library_id="experimental",
|
||||
category_id="storage",
|
||||
symbol="generic",
|
||||
ports=(PortDisplaySpec("port_a", "left"),),
|
||||
order=90,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
volume: float = 0.1,
|
||||
p0: float = 100000.0,
|
||||
T0: float = 300.0,
|
||||
) -> None:
|
||||
super().__init__(name)
|
||||
self.set_parameter_values(
|
||||
{"volume": volume, "p0": p0, "T0": T0}
|
||||
)
|
||||
self.medium = medium
|
||||
self.V = volume
|
||||
initial_mass = p0 * volume / (medium.R_gas * T0)
|
||||
initial_energy = initial_mass * medium.specific_internal_energy(T0)
|
||||
self.state = VolumeState(m=initial_mass, U=initial_energy)
|
||||
self.port_a = self.register_declared_port("port_a")
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> ExampleVolume:
|
||||
return cls(
|
||||
name=name,
|
||||
medium=medium,
|
||||
volume=parameters["volume"],
|
||||
p0=parameters["p0"],
|
||||
T0=parameters["T0"],
|
||||
)
|
||||
|
||||
def get_state_vector(self) -> list[float]:
|
||||
return self.state.as_vector()
|
||||
|
||||
def set_state_vector(self, values: list[float]) -> None:
|
||||
self.state = VolumeState.from_vector(values)
|
||||
|
||||
def refresh_thermodynamic_ports(self) -> ThermodynamicProperties:
|
||||
properties = self.medium.properties_from_mU(
|
||||
self.state.m, self.state.U, self.V
|
||||
)
|
||||
self.port_a.p = properties.p
|
||||
self.port_a.h_outflow = properties.h
|
||||
return properties
|
||||
|
||||
def state_derivative_from_ports(
|
||||
self,
|
||||
connected_h: Mapping[str, float],
|
||||
) -> list[float]:
|
||||
properties = self.refresh_thermodynamic_ports()
|
||||
inlet_h = self.connection_inlet_enthalpy(
|
||||
port_m_flow=self.port_a.m_flow,
|
||||
connected_h=connected_h["port_a"],
|
||||
internal_h=properties.h,
|
||||
)
|
||||
return [self.port_a.m_flow, self.port_a.m_flow * inlet_h]
|
||||
|
||||
def pressure_flow_equation_residuals(
|
||||
self,
|
||||
) -> tuple[EquationResidual, ...]:
|
||||
pressure = self.medium.properties_from_mU(
|
||||
self.state.m, self.state.U, self.V
|
||||
).p
|
||||
return (
|
||||
EquationResidual(
|
||||
id=f"{self.name}:port_a_pressure_state",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="state",
|
||||
variables=(f"{self.name}.port_a.p", f"{self.name}.state"),
|
||||
role="effort",
|
||||
value=self.port_a.p - pressure,
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
模型文件不再直接修改全局注册表。完成模型类后,只把类路径加入所属库
|
||||
`library.py` 的 `models` 清单:
|
||||
|
||||
```python
|
||||
models=(
|
||||
# ...已有模型
|
||||
"app.simulation.components.experimental.storage.example_volume:ExampleVolume",
|
||||
)
|
||||
```
|
||||
|
||||
后端会受控导入清单中的类,校验版本、分类、端口、参数、单位、显示信息和默认实例,
|
||||
再自动建立注册表。校验通过后,`GET /api/components/catalog` 会输出该元件,
|
||||
前端刷新时即可加载。
|
||||
当前 `experimental` 仅用于规范验证;正式模型应先建立新的库声明,再把
|
||||
`library_id` 指向正式库。
|
||||
|
||||
完成仿真后,每个已声明结果都会得到一条结构化元数据。前端应按字段筛选,不能再拆解 `key` 猜测含义:
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "example_volume_1.port_a.m_flow",
|
||||
"componentId": "example_volume_1",
|
||||
"componentType": "example_volume",
|
||||
"scope": "port",
|
||||
"portName": "port_a",
|
||||
"name": "m_flow",
|
||||
"label": "质量流量",
|
||||
"quantity": "mass_flow",
|
||||
"unit": "kg/s",
|
||||
"category": "flow",
|
||||
"order": 20
|
||||
}
|
||||
```
|
||||
|
||||
## 五、新增元件检查清单
|
||||
|
||||
1. `MODEL_TYPE` 是否唯一,并与 XML 的模型类型一致。
|
||||
2. 所有构造参数是否在 `PARAMETERS` 中声明并保存。
|
||||
3. 所有端口是否在 `PORTS` 中声明并通过 `register_declared_port()` 创建。
|
||||
4. `RESULT_VARIABLES` 与 `component_result_values()` 的键是否完全一致。
|
||||
5. 结果变量是否包含明确的 `quantity`、`label`、`unit` 和显示顺序。
|
||||
6. 是否只暴露有工程意义的结果,而非内部计算变量。
|
||||
7. `MODEL_VERSION` 和 `DISPLAY` 是否完整,显示端口是否与物理端口完全一致。
|
||||
8. 是否实现统一的 `create()`,并能用默认参数创建模型。
|
||||
9. 模型类路径是否只加入所属库的 `library.py` 清单。
|
||||
10. 是否补充参数边界、端口契约、目录输出、结果元数据和最小仿真的自动测试。
|
||||
|
||||
组件库、分类和自动发现的完整规则参见
|
||||
[`组件库分类、发现与读取规范 v1`](../../../docs/standard/component-library-spec-v1.md)。
|
||||
新增模型的参考值应来自独立解析结果、外部可信结果或已有冻结基准;不恢复第二套 Python 数值实现。
|
||||
@@ -1,121 +1,34 @@
|
||||
"""Component parameters, ports and output definitions; numerical equations execute in C."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from math import sqrt
|
||||
|
||||
from app.simulation.core.base import AlgebraicComponent
|
||||
from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
|
||||
from app.simulation.core.equations import EquationResidual
|
||||
from app.simulation.core.metadata import ParameterDefinition
|
||||
from app.simulation.core.medium import IdealGasMedium
|
||||
from app.simulation.core.ports import PortDefinition
|
||||
|
||||
|
||||
class Orifice(AlgebraicComponent):
|
||||
"""Python port of ModelicaModels.Myorifice."""
|
||||
|
||||
MODEL_TYPE = "orifice"
|
||||
MODEL_VERSION = "1.0.0"
|
||||
PRESSURE_FLOW_DEPENDS_ON_STREAM = False
|
||||
PRESSURE_FLOW_EXACT_SUM_TO_ZERO_EQUATION_SUFFIXES = frozenset(
|
||||
("mass_flow_balance",)
|
||||
)
|
||||
PORTS = (
|
||||
PortDefinition.pneumatic("port_a", nominal_role="inlet"),
|
||||
PortDefinition.pneumatic("port_b", nominal_role="outlet"),
|
||||
)
|
||||
PARAMETERS = (
|
||||
ParameterDefinition(
|
||||
"K",
|
||||
1e-5,
|
||||
label="流量系数",
|
||||
quantity="flow_coefficient",
|
||||
unit="kg/(s*Pa^0.5)",
|
||||
minimum=0.0,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"opening",
|
||||
1.0,
|
||||
label="开度",
|
||||
minimum=0.0,
|
||||
maximum=1.0,
|
||||
),
|
||||
)
|
||||
MODEL_TYPE = 'orifice'
|
||||
MODEL_VERSION = '1.0.0'
|
||||
PORTS = (PortDefinition.pneumatic('port_a', nominal_role='inlet'), PortDefinition.pneumatic('port_b', nominal_role='outlet'))
|
||||
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,
|
||||
)
|
||||
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)
|
||||
|
||||
def __init__(self, name: str, opening: float = 1.0, K: float = 1e-5) -> None:
|
||||
def __init__(self, name: str, opening: float=1.0, K: float=1e-05) -> None:
|
||||
super().__init__(name=name)
|
||||
self.set_parameter_values({"K": K, "opening": opening})
|
||||
self.set_parameter_values({'K': K, 'opening': opening})
|
||||
self.opening = opening
|
||||
self.K = K
|
||||
self.port_a = self.register_declared_port("port_a")
|
||||
self.port_b = self.register_declared_port("port_b")
|
||||
self.port_a = self.register_declared_port('port_a')
|
||||
self.port_b = self.register_declared_port('port_b')
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> Orifice:
|
||||
return cls(
|
||||
name=name,
|
||||
opening=parameters["opening"],
|
||||
K=parameters["K"],
|
||||
)
|
||||
def create(cls, *, name: str, medium: IdealGasMedium, parameters: Mapping[str, float]) -> Orifice:
|
||||
return cls(name=name, opening=parameters['opening'], K=parameters['K'])
|
||||
|
||||
@property
|
||||
def K_eff(self) -> float:
|
||||
return self.K * max(self.opening, 0.001)
|
||||
|
||||
def mass_flow(self, p_a: float, p_b: float) -> float:
|
||||
dp = p_a - p_b
|
||||
if dp == 0.0:
|
||||
return 0.0
|
||||
return self.K_eff * sqrt(abs(dp)) * (1.0 if dp > 0.0 else -1.0)
|
||||
|
||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||
return (
|
||||
EquationResidual(
|
||||
id=f"{self.name}:mass_flow_balance",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="sumToZero",
|
||||
variables=(
|
||||
f"{self.name}.port_a.m_flow",
|
||||
f"{self.name}.port_b.m_flow",
|
||||
),
|
||||
role="flow",
|
||||
value=self.port_a.m_flow + self.port_b.m_flow,
|
||||
),
|
||||
EquationResidual(
|
||||
id=f"{self.name}:pressure_flow_relation",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="constitutive",
|
||||
variables=(
|
||||
f"{self.name}.port_a.p",
|
||||
f"{self.name}.port_b.p",
|
||||
f"{self.name}.port_a.m_flow",
|
||||
),
|
||||
role="flow",
|
||||
value=self.port_a.m_flow
|
||||
- self.mass_flow(self.port_a.p, self.port_b.p),
|
||||
),
|
||||
)
|
||||
|
||||
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
|
||||
self.port_a.h_outflow = connected_h["port_b"]
|
||||
self.port_b.h_outflow = connected_h["port_a"]
|
||||
EQUATIONS = ({'id': '__MODEL__:mass_flow_balance', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'sumToZero', 'variables': ['__MODEL__.port_a.m_flow', '__MODEL__.port_b.m_flow'], 'role': 'flow'}, {'id': '__MODEL__:pressure_flow_relation', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'constitutive', 'variables': ['__MODEL__.port_a.p', '__MODEL__.port_b.p', '__MODEL__.port_a.m_flow'], 'role': 'flow'})
|
||||
@@ -1,10 +0,0 @@
|
||||
"""Compatibility import for the TestModel-only dynamic pipe.
|
||||
|
||||
The public ``pipe`` catalog model is ``ResistivePipe``. New code should import
|
||||
this legacy dynamic model from ``app.simulation.examples.testmodel.dynamic_pipe``.
|
||||
"""
|
||||
|
||||
from app.simulation.examples.testmodel.dynamic_pipe import Pipe
|
||||
|
||||
|
||||
__all__ = ("Pipe",)
|
||||
@@ -1,106 +1,25 @@
|
||||
"""Component parameters, ports and output definitions; numerical equations execute in C."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from math import pi
|
||||
|
||||
from app.simulation.core.base import AlgebraicComponent
|
||||
from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
|
||||
from app.simulation.core.equations import EquationResidual
|
||||
from app.simulation.core.metadata import ParameterDefinition
|
||||
from app.simulation.core.medium import IdealGasMedium
|
||||
from app.simulation.core.ports import PortDefinition
|
||||
|
||||
|
||||
class ResistivePipe(AlgebraicComponent):
|
||||
"""Quasi-steady Darcy resistance used by topology-driven simulation."""
|
||||
|
||||
MODEL_TYPE = "pipe"
|
||||
MODEL_VERSION = "1.0.0"
|
||||
PRESSURE_FLOW_DEPENDS_ON_STREAM = False
|
||||
PRESSURE_FLOW_EXACT_SUM_TO_ZERO_EQUATION_SUFFIXES = frozenset(
|
||||
("mass_flow_balance",)
|
||||
)
|
||||
PORTS = (
|
||||
PortDefinition.pneumatic("port_a", nominal_role="inlet"),
|
||||
PortDefinition.pneumatic("port_b", nominal_role="outlet"),
|
||||
)
|
||||
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",
|
||||
1e5,
|
||||
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,
|
||||
),
|
||||
)
|
||||
MODEL_TYPE = 'pipe'
|
||||
MODEL_VERSION = '1.0.0'
|
||||
PORTS = (PortDefinition.pneumatic('port_a', nominal_role='inlet'), PortDefinition.pneumatic('port_b', nominal_role='outlet'))
|
||||
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,
|
||||
)
|
||||
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)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
L: float = 5.0,
|
||||
D: float = 0.02,
|
||||
lambda_darcy: float = 0.02,
|
||||
p0: float = 1e5,
|
||||
T0: float = 300.0,
|
||||
) -> None:
|
||||
def __init__(self, name: str, medium: IdealGasMedium, L: float=5.0, D: float=0.02, lambda_darcy: float=0.02, p0: float=100000.0, T0: float=300.0) -> None:
|
||||
super().__init__(name=name)
|
||||
self.set_parameter_values(
|
||||
{
|
||||
"length": L,
|
||||
"diameter": D,
|
||||
"lambda_darcy": lambda_darcy,
|
||||
"p0": p0,
|
||||
"T0": T0,
|
||||
}
|
||||
)
|
||||
self.set_parameter_values({'length': L, 'diameter': D, 'lambda_darcy': lambda_darcy, 'p0': p0, 'T0': T0})
|
||||
self.medium = medium
|
||||
self.L = L
|
||||
self.D = D
|
||||
@@ -108,82 +27,10 @@ class ResistivePipe(AlgebraicComponent):
|
||||
self.p0 = p0
|
||||
self.T0 = T0
|
||||
self.area = pi * D * D / 4.0
|
||||
initial_h = medium.specific_enthalpy(T0)
|
||||
|
||||
self.port_a = self.register_declared_port("port_a")
|
||||
self.port_a.p = p0
|
||||
self.port_a.h_outflow = initial_h
|
||||
|
||||
self.port_b = self.register_declared_port("port_b")
|
||||
self.port_b.p = p0
|
||||
self.port_b.h_outflow = initial_h
|
||||
self.port_a = self.register_declared_port('port_a')
|
||||
self.port_b = self.register_declared_port('port_b')
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> ResistivePipe:
|
||||
return cls(
|
||||
name=name,
|
||||
medium=medium,
|
||||
L=parameters["length"],
|
||||
D=parameters["diameter"],
|
||||
lambda_darcy=parameters["lambda_darcy"],
|
||||
p0=parameters["p0"],
|
||||
T0=parameters["T0"],
|
||||
)
|
||||
|
||||
def pressure_drop(self, m_flow_a: float, p_a: float, p_b: float) -> float:
|
||||
average_pressure = max(0.5 * (p_a + p_b), 1.0)
|
||||
density = max(self.medium.density(average_pressure, self.T0), 1e-12)
|
||||
resistance = self.lambda_darcy * (self.L / self.D)
|
||||
return (
|
||||
resistance
|
||||
* m_flow_a
|
||||
* abs(m_flow_a)
|
||||
/ (2.0 * density * self.area * self.area)
|
||||
)
|
||||
|
||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||
return (
|
||||
EquationResidual(
|
||||
id=f"{self.name}:mass_flow_balance",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="sumToZero",
|
||||
variables=(
|
||||
f"{self.name}.port_a.m_flow",
|
||||
f"{self.name}.port_b.m_flow",
|
||||
),
|
||||
role="flow",
|
||||
value=self.port_a.m_flow + self.port_b.m_flow,
|
||||
),
|
||||
EquationResidual(
|
||||
id=f"{self.name}:darcy_pressure_loss",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="constitutive",
|
||||
variables=(
|
||||
f"{self.name}.port_a.p",
|
||||
f"{self.name}.port_b.p",
|
||||
f"{self.name}.port_a.m_flow",
|
||||
),
|
||||
role="effort",
|
||||
value=(
|
||||
self.port_a.p
|
||||
- self.port_b.p
|
||||
- self.pressure_drop(
|
||||
self.port_a.m_flow,
|
||||
self.port_a.p,
|
||||
self.port_b.p,
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
|
||||
self.port_a.h_outflow = connected_h["port_b"]
|
||||
self.port_b.h_outflow = connected_h["port_a"]
|
||||
def create(cls, *, name: str, medium: IdealGasMedium, parameters: Mapping[str, float]) -> ResistivePipe:
|
||||
return cls(name=name, medium=medium, L=parameters['length'], D=parameters['diameter'], lambda_darcy=parameters['lambda_darcy'], p0=parameters['p0'], T0=parameters['T0'])
|
||||
EQUATIONS = ({'id': '__MODEL__:mass_flow_balance', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'sumToZero', 'variables': ['__MODEL__.port_a.m_flow', '__MODEL__.port_b.m_flow'], 'role': 'flow'}, {'id': '__MODEL__:darcy_pressure_loss', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'constitutive', 'variables': ['__MODEL__.port_a.p', '__MODEL__.port_b.p', '__MODEL__.port_a.m_flow'], 'role': 'effort'})
|
||||
@@ -1,270 +1,28 @@
|
||||
"""Component parameters, ports and output definitions; numerical equations execute in C."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
|
||||
from app.simulation.core.base import AlgebraicComponent
|
||||
from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
|
||||
from app.simulation.core.equations import EquationResidual
|
||||
from app.simulation.core.medium import IdealGasMedium
|
||||
from app.simulation.core.ports import PortDefinition
|
||||
|
||||
|
||||
class Tee(AlgebraicComponent):
|
||||
"""Python port of ModelicaModels.Mytee."""
|
||||
|
||||
MODEL_TYPE = "tee"
|
||||
MODEL_VERSION = "1.0.0"
|
||||
PRESSURE_FLOW_DEPENDS_ON_STREAM = False
|
||||
PRESSURE_FLOW_EXACT_SUM_TO_ZERO_EQUATION_SUFFIXES = frozenset(
|
||||
("mass_flow_balance",)
|
||||
)
|
||||
PORTS = (
|
||||
PortDefinition.pneumatic("port_in", nominal_role="bidirectional"),
|
||||
PortDefinition.pneumatic("port_out1", nominal_role="bidirectional"),
|
||||
PortDefinition.pneumatic("port_out2", nominal_role="bidirectional"),
|
||||
)
|
||||
MODEL_TYPE = 'tee'
|
||||
MODEL_VERSION = '1.0.0'
|
||||
PORTS = (PortDefinition.pneumatic('port_in', nominal_role='bidirectional'), PortDefinition.pneumatic('port_out1', nominal_role='bidirectional'), PortDefinition.pneumatic('port_out2', nominal_role='bidirectional'))
|
||||
PARAMETERS = ()
|
||||
RESULT_VARIABLES = ()
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="三通",
|
||||
library_id="experimental",
|
||||
category_id="junctions",
|
||||
symbol="tee",
|
||||
ports=(
|
||||
PortDisplaySpec("port_in", "left", order=10),
|
||||
PortDisplaySpec("port_out1", "right", order=20),
|
||||
PortDisplaySpec("port_out2", "right", order=30),
|
||||
),
|
||||
order=50,
|
||||
)
|
||||
DISPLAY = ComponentDisplaySpec(label='三通', library_id='experimental', category_id='junctions', symbol='tee', ports=(PortDisplaySpec('port_in', 'left', order=10), PortDisplaySpec('port_out1', 'right', order=20), PortDisplaySpec('port_out2', 'right', order=30)), order=50)
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
super().__init__(name=name)
|
||||
self.set_parameter_values({})
|
||||
self.port_in = self.register_declared_port("port_in")
|
||||
self.port_out1 = self.register_declared_port("port_out1")
|
||||
self.port_out2 = self.register_declared_port("port_out2")
|
||||
self.port_in = self.register_declared_port('port_in')
|
||||
self.port_out1 = self.register_declared_port('port_out1')
|
||||
self.port_out2 = self.register_declared_port('port_out2')
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> Tee:
|
||||
def create(cls, *, name: str, medium: IdealGasMedium, parameters: Mapping[str, float]) -> Tee:
|
||||
return cls(name=name)
|
||||
|
||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||
return (
|
||||
EquationResidual(
|
||||
id=f"{self.name}:common_pressure_out1",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="equal",
|
||||
variables=(f"{self.name}.port_in.p", f"{self.name}.port_out1.p"),
|
||||
role="effort",
|
||||
value=self.port_in.p - self.port_out1.p,
|
||||
),
|
||||
EquationResidual(
|
||||
id=f"{self.name}:common_pressure_out2",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="equal",
|
||||
variables=(f"{self.name}.port_in.p", f"{self.name}.port_out2.p"),
|
||||
role="effort",
|
||||
value=self.port_in.p - self.port_out2.p,
|
||||
),
|
||||
EquationResidual(
|
||||
id=f"{self.name}:mass_flow_balance",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="sumToZero",
|
||||
variables=(
|
||||
f"{self.name}.port_in.m_flow",
|
||||
f"{self.name}.port_out1.m_flow",
|
||||
f"{self.name}.port_out2.m_flow",
|
||||
),
|
||||
role="flow",
|
||||
value=(
|
||||
self.port_in.m_flow
|
||||
+ self.port_out1.m_flow
|
||||
+ self.port_out2.m_flow
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
|
||||
incoming = [
|
||||
(port.m_flow, connected_h[name])
|
||||
for name, port in self.ports.items()
|
||||
if port.m_flow > 1e-12
|
||||
]
|
||||
total_flow = sum(m_flow for m_flow, _ in incoming)
|
||||
if total_flow > 1e-12:
|
||||
mixed_h = sum(
|
||||
m_flow * enthalpy for m_flow, enthalpy in incoming
|
||||
) / total_flow
|
||||
else:
|
||||
values = list(connected_h.values())
|
||||
mixed_h = sum(values) / len(values) if values else 0.0
|
||||
for port in self.ports.values():
|
||||
port.h_outflow = mixed_h
|
||||
|
||||
def mixed_inlet_enthalpy(
|
||||
self,
|
||||
branch1_m_flow: float,
|
||||
branch1_h: float,
|
||||
branch2_m_flow: float,
|
||||
branch2_h: float,
|
||||
fallback_h: float = 0.0,
|
||||
) -> float:
|
||||
positive_1 = max(branch1_m_flow, 0.0)
|
||||
positive_2 = max(branch2_m_flow, 0.0)
|
||||
total = positive_1 + positive_2
|
||||
if total <= 1e-9:
|
||||
return fallback_h
|
||||
return (positive_1 * branch1_h + positive_2 * branch2_h) / total
|
||||
|
||||
def inlet_stream_enthalpy(
|
||||
self,
|
||||
branch1_m_flow: float,
|
||||
branch1_h: float,
|
||||
branch2_m_flow: float,
|
||||
branch2_h: float,
|
||||
fallback_h: float,
|
||||
) -> float:
|
||||
"""Approximate `inStream(port_in.h_outflow)` for the current tee topology."""
|
||||
|
||||
return self.mixed_inlet_enthalpy(
|
||||
branch1_m_flow,
|
||||
branch1_h,
|
||||
branch2_m_flow,
|
||||
branch2_h,
|
||||
fallback_h=fallback_h,
|
||||
)
|
||||
|
||||
def branch_actual_stream_enthalpy(
|
||||
self,
|
||||
branch_m_flow: float,
|
||||
branch_h: float,
|
||||
inlet_h: float,
|
||||
) -> float:
|
||||
"""Approximate `actualStream(branch.h_outflow)` for a tee branch port."""
|
||||
|
||||
return inlet_h if branch_m_flow > 0.0 else branch_h
|
||||
|
||||
@staticmethod
|
||||
def _solve_linear_2x2(
|
||||
a11: float,
|
||||
a12: float,
|
||||
a21: float,
|
||||
a22: float,
|
||||
b1: float,
|
||||
b2: float,
|
||||
) -> tuple[float, float] | None:
|
||||
determinant = a11 * a22 - a12 * a21
|
||||
if abs(determinant) <= 1e-12:
|
||||
return None
|
||||
x1 = (b1 * a22 - b2 * a12) / determinant
|
||||
x2 = (a11 * b2 - a21 * b1) / determinant
|
||||
return x1, x2
|
||||
|
||||
def solve_branch_outlet_flows_from_energy_balance(
|
||||
self,
|
||||
*,
|
||||
ratio_branch1: float,
|
||||
ratio_branch2: float,
|
||||
inlet_h_branch1: float,
|
||||
inlet_h_branch2: float,
|
||||
branch1_h: float,
|
||||
branch2_h: float,
|
||||
inlet_h: float,
|
||||
q_in_branch1: float,
|
||||
q_in_branch2: float,
|
||||
tolerance: float = 1e-12,
|
||||
) -> tuple[float, float]:
|
||||
"""Solve branch outlet flows for the current three-port downstream tee use-case."""
|
||||
|
||||
rhs_branch1 = q_in_branch1 * inlet_h_branch1
|
||||
rhs_branch2 = q_in_branch2 * inlet_h_branch2
|
||||
|
||||
def solve_both_forward() -> tuple[float, float] | None:
|
||||
return self._solve_linear_2x2(
|
||||
(1.0 + ratio_branch1) * branch1_h,
|
||||
ratio_branch1 * branch2_h,
|
||||
ratio_branch2 * branch1_h,
|
||||
(1.0 + ratio_branch2) * branch2_h,
|
||||
rhs_branch1,
|
||||
rhs_branch2,
|
||||
)
|
||||
|
||||
def solve_one_reverse(
|
||||
*,
|
||||
branch1_reverse: bool,
|
||||
) -> tuple[float, float] | None:
|
||||
if branch1_reverse:
|
||||
return self._solve_linear_2x2(
|
||||
inlet_h * (1.0 + ratio_branch1),
|
||||
ratio_branch1 * inlet_h,
|
||||
ratio_branch2 * inlet_h,
|
||||
branch2_h + ratio_branch2 * inlet_h,
|
||||
rhs_branch1,
|
||||
rhs_branch2,
|
||||
)
|
||||
|
||||
return self._solve_linear_2x2(
|
||||
branch1_h + ratio_branch1 * inlet_h,
|
||||
ratio_branch1 * inlet_h,
|
||||
ratio_branch2 * inlet_h,
|
||||
inlet_h * (1.0 + ratio_branch2),
|
||||
rhs_branch1,
|
||||
rhs_branch2,
|
||||
)
|
||||
|
||||
def solve_both_reverse() -> tuple[float, float] | None:
|
||||
return self._solve_linear_2x2(
|
||||
inlet_h * (1.0 + ratio_branch1),
|
||||
ratio_branch1 * inlet_h,
|
||||
ratio_branch2 * inlet_h,
|
||||
inlet_h * (1.0 + ratio_branch2),
|
||||
rhs_branch1,
|
||||
rhs_branch2,
|
||||
)
|
||||
|
||||
candidate_solvers = (
|
||||
(
|
||||
solve_both_forward,
|
||||
lambda q1, q2: q1 >= -tolerance and q2 >= -tolerance,
|
||||
),
|
||||
(
|
||||
lambda: solve_one_reverse(branch1_reverse=True),
|
||||
lambda q1, q2: q1 < -tolerance and q2 >= -tolerance and q1 + q2 > tolerance,
|
||||
),
|
||||
(
|
||||
lambda: solve_one_reverse(branch1_reverse=True),
|
||||
lambda q1, q2: q1 < -tolerance and q2 >= -tolerance and q1 + q2 <= tolerance,
|
||||
),
|
||||
(
|
||||
lambda: solve_one_reverse(branch1_reverse=False),
|
||||
lambda q1, q2: q2 < -tolerance and q1 >= -tolerance and q1 + q2 > tolerance,
|
||||
),
|
||||
(
|
||||
lambda: solve_one_reverse(branch1_reverse=False),
|
||||
lambda q1, q2: q2 < -tolerance and q1 >= -tolerance and q1 + q2 <= tolerance,
|
||||
),
|
||||
(
|
||||
solve_both_reverse,
|
||||
lambda q1, q2: q1 < -tolerance and q2 < -tolerance,
|
||||
),
|
||||
)
|
||||
|
||||
for solver, predicate in candidate_solvers:
|
||||
candidate = solver()
|
||||
if candidate is None:
|
||||
continue
|
||||
q_out_branch1, q_out_branch2 = candidate
|
||||
if predicate(q_out_branch1, q_out_branch2):
|
||||
return q_out_branch1, q_out_branch2
|
||||
|
||||
return solve_both_forward() or (0.0, 0.0)
|
||||
EQUATIONS = ({'id': '__MODEL__:common_pressure_out1', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'equal', 'variables': ['__MODEL__.port_in.p', '__MODEL__.port_out1.p'], 'role': 'effort'}, {'id': '__MODEL__:common_pressure_out2', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'equal', 'variables': ['__MODEL__.port_in.p', '__MODEL__.port_out2.p'], 'role': 'effort'}, {'id': '__MODEL__:mass_flow_balance', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'sumToZero', 'variables': ['__MODEL__.port_in.m_flow', '__MODEL__.port_out1.m_flow', '__MODEL__.port_out2.m_flow'], 'role': 'flow'})
|
||||
@@ -1,155 +1,29 @@
|
||||
"""Component parameters, ports and output definitions; numerical equations execute in C."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
|
||||
from app.simulation.core.base import ThermodynamicVolumeComponent
|
||||
from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
|
||||
from app.simulation.core.equations import EquationResidual
|
||||
from app.simulation.core.metadata import (
|
||||
ParameterDefinition,
|
||||
THERMODYNAMIC_VOLUME_RESULT_VARIABLES,
|
||||
)
|
||||
from app.simulation.core.medium import IdealGasMedium, ThermodynamicProperties
|
||||
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.state import VolumeState
|
||||
|
||||
|
||||
class Cylinder(ThermodynamicVolumeComponent):
|
||||
"""Python port of ModelicaModels.Mycylinder."""
|
||||
|
||||
MODEL_TYPE = "cylinder"
|
||||
MODEL_VERSION = "1.0.0"
|
||||
PORTS = (PortDefinition.pneumatic("port_b", nominal_role="outlet"),)
|
||||
PARAMETERS = (
|
||||
ParameterDefinition(
|
||||
"volume",
|
||||
0.01,
|
||||
label="容积",
|
||||
quantity="volume",
|
||||
unit="m3",
|
||||
minimum=0.0,
|
||||
minimum_exclusive=True,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"p0",
|
||||
35e6,
|
||||
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,
|
||||
),
|
||||
)
|
||||
MODEL_TYPE = 'cylinder'
|
||||
MODEL_VERSION = '1.0.0'
|
||||
PORTS = (PortDefinition.pneumatic('port_b', nominal_role='outlet'),)
|
||||
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,
|
||||
)
|
||||
DISPLAY = ComponentDisplaySpec(label='气瓶', library_id='experimental', category_id='storage', symbol='cylinder', ports=(PortDisplaySpec('port_b', 'right'),), order=10)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
V: float = 0.01,
|
||||
p0: float = 35e6,
|
||||
T0: float = 300.0,
|
||||
) -> None:
|
||||
def __init__(self, name: str, medium: IdealGasMedium, V: float=0.01, p0: float=35000000.0, T0: float=300.0) -> None:
|
||||
super().__init__(name=name)
|
||||
self.set_parameter_values({"volume": V, "p0": p0, "T0": T0})
|
||||
self.set_parameter_values({'volume': V, 'p0': p0, 'T0': T0})
|
||||
self.medium = medium
|
||||
self.V = V
|
||||
m0 = p0 * V / (medium.R_gas * T0)
|
||||
U0 = m0 * medium.specific_internal_energy(T0)
|
||||
self.state = VolumeState(m=m0, U=U0)
|
||||
self.port_b = self.register_declared_port("port_b")
|
||||
self.port_b = self.register_declared_port('port_b')
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> Cylinder:
|
||||
return cls(
|
||||
name=name,
|
||||
medium=medium,
|
||||
V=parameters["volume"],
|
||||
p0=parameters["p0"],
|
||||
T0=parameters["T0"],
|
||||
)
|
||||
|
||||
def get_state_vector(self) -> list[float]:
|
||||
return self.state.as_vector()
|
||||
|
||||
def set_state_vector(self, values: list[float]) -> None:
|
||||
self.state = VolumeState.from_vector(values)
|
||||
|
||||
def properties(self) -> ThermodynamicProperties:
|
||||
props = self.medium.properties_from_mU(self.state.m, self.state.U, self.V)
|
||||
self.port_b.p = props.p
|
||||
self.port_b.h_outflow = props.h
|
||||
return props
|
||||
|
||||
def refresh_thermodynamic_ports(self) -> ThermodynamicProperties:
|
||||
return self.properties()
|
||||
|
||||
def state_derivative_from_ports(
|
||||
self,
|
||||
connected_h: Mapping[str, float],
|
||||
) -> list[float]:
|
||||
properties = self.properties()
|
||||
derivative = self.derivatives_from_connection(
|
||||
connected_h=connected_h["port_b"],
|
||||
port_m_flow=self.port_b.m_flow,
|
||||
internal_h=properties.h,
|
||||
)
|
||||
return derivative.as_vector()
|
||||
|
||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||
pressure = self.medium.properties_from_mU(
|
||||
self.state.m,
|
||||
self.state.U,
|
||||
self.V,
|
||||
).p
|
||||
return (
|
||||
EquationResidual(
|
||||
id=f"{self.name}:port_b_pressure_state",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="state",
|
||||
variables=(f"{self.name}.port_b.p", f"{self.name}.state"),
|
||||
role="effort",
|
||||
value=self.port_b.p - pressure,
|
||||
),
|
||||
)
|
||||
|
||||
def derivatives_from_connection(
|
||||
self,
|
||||
*,
|
||||
connected_h: float,
|
||||
port_m_flow: float,
|
||||
internal_h: float,
|
||||
) -> VolumeState:
|
||||
inlet_h = self.connection_inlet_enthalpy(
|
||||
port_m_flow=port_m_flow,
|
||||
connected_h=connected_h,
|
||||
internal_h=internal_h,
|
||||
)
|
||||
return self.derivatives(inlet_h, port_m_flow)
|
||||
|
||||
def derivatives(self, inlet_h: float, m_flow: float) -> VolumeState:
|
||||
return VolumeState(m=m_flow, U=m_flow * inlet_h)
|
||||
def create(cls, *, name: str, medium: IdealGasMedium, parameters: Mapping[str, float]) -> Cylinder:
|
||||
return cls(name=name, medium=medium, V=parameters['volume'], p0=parameters['p0'], T0=parameters['T0'])
|
||||
EQUATIONS = ({'id': '__MODEL__:port_b_pressure_state', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'state', 'variables': ['__MODEL__.port_b.p', '__MODEL__.state'], 'role': 'effort'},)
|
||||
@@ -1,155 +1,29 @@
|
||||
"""Component parameters, ports and output definitions; numerical equations execute in C."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
|
||||
from app.simulation.core.base import ThermodynamicVolumeComponent
|
||||
from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
|
||||
from app.simulation.core.equations import EquationResidual
|
||||
from app.simulation.core.metadata import (
|
||||
ParameterDefinition,
|
||||
THERMODYNAMIC_VOLUME_RESULT_VARIABLES,
|
||||
)
|
||||
from app.simulation.core.medium import IdealGasMedium, ThermodynamicProperties
|
||||
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.state import VolumeState
|
||||
|
||||
|
||||
class Tank(ThermodynamicVolumeComponent):
|
||||
"""Python port of ModelicaModels.Mytank."""
|
||||
|
||||
MODEL_TYPE = "tank"
|
||||
MODEL_VERSION = "1.0.0"
|
||||
PORTS = (PortDefinition.pneumatic("port_a", nominal_role="inlet"),)
|
||||
PARAMETERS = (
|
||||
ParameterDefinition(
|
||||
"volume",
|
||||
0.1,
|
||||
label="容积",
|
||||
quantity="volume",
|
||||
unit="m3",
|
||||
minimum=0.0,
|
||||
minimum_exclusive=True,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"p0",
|
||||
1e5,
|
||||
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,
|
||||
),
|
||||
)
|
||||
MODEL_TYPE = 'tank'
|
||||
MODEL_VERSION = '1.0.0'
|
||||
PORTS = (PortDefinition.pneumatic('port_a', nominal_role='inlet'),)
|
||||
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,
|
||||
)
|
||||
DISPLAY = ComponentDisplaySpec(label='贮箱', library_id='experimental', category_id='storage', symbol='tank', ports=(PortDisplaySpec('port_a', 'left'),), order=20)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
V: float = 0.1,
|
||||
p0: float = 1e5,
|
||||
T0: float = 300.0,
|
||||
) -> None:
|
||||
def __init__(self, name: str, medium: IdealGasMedium, V: float=0.1, p0: float=100000.0, T0: float=300.0) -> None:
|
||||
super().__init__(name=name)
|
||||
self.set_parameter_values({"volume": V, "p0": p0, "T0": T0})
|
||||
self.set_parameter_values({'volume': V, 'p0': p0, 'T0': T0})
|
||||
self.medium = medium
|
||||
self.V = V
|
||||
m0 = p0 * V / (medium.R_gas * T0)
|
||||
U0 = m0 * medium.specific_internal_energy(T0)
|
||||
self.state = VolumeState(m=m0, U=U0)
|
||||
self.port_a = self.register_declared_port("port_a")
|
||||
self.port_a = self.register_declared_port('port_a')
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> Tank:
|
||||
return cls(
|
||||
name=name,
|
||||
medium=medium,
|
||||
V=parameters["volume"],
|
||||
p0=parameters["p0"],
|
||||
T0=parameters["T0"],
|
||||
)
|
||||
|
||||
def get_state_vector(self) -> list[float]:
|
||||
return self.state.as_vector()
|
||||
|
||||
def set_state_vector(self, values: list[float]) -> None:
|
||||
self.state = VolumeState.from_vector(values)
|
||||
|
||||
def properties(self) -> ThermodynamicProperties:
|
||||
props = self.medium.properties_from_mU(self.state.m, self.state.U, self.V)
|
||||
self.port_a.p = props.p
|
||||
self.port_a.h_outflow = props.h
|
||||
return props
|
||||
|
||||
def refresh_thermodynamic_ports(self) -> ThermodynamicProperties:
|
||||
return self.properties()
|
||||
|
||||
def state_derivative_from_ports(
|
||||
self,
|
||||
connected_h: Mapping[str, float],
|
||||
) -> list[float]:
|
||||
properties = self.properties()
|
||||
derivative = self.derivatives_from_connection(
|
||||
connected_h=connected_h["port_a"],
|
||||
port_m_flow=self.port_a.m_flow,
|
||||
internal_h=properties.h,
|
||||
)
|
||||
return derivative.as_vector()
|
||||
|
||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||
pressure = self.medium.properties_from_mU(
|
||||
self.state.m,
|
||||
self.state.U,
|
||||
self.V,
|
||||
).p
|
||||
return (
|
||||
EquationResidual(
|
||||
id=f"{self.name}:port_a_pressure_state",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="state",
|
||||
variables=(f"{self.name}.port_a.p", f"{self.name}.state"),
|
||||
role="effort",
|
||||
value=self.port_a.p - pressure,
|
||||
),
|
||||
)
|
||||
|
||||
def derivatives_from_connection(
|
||||
self,
|
||||
*,
|
||||
connected_h: float,
|
||||
port_m_flow: float,
|
||||
internal_h: float,
|
||||
) -> VolumeState:
|
||||
inlet_h = self.connection_inlet_enthalpy(
|
||||
port_m_flow=port_m_flow,
|
||||
connected_h=connected_h,
|
||||
internal_h=internal_h,
|
||||
)
|
||||
return self.derivatives(inlet_h, port_m_flow)
|
||||
|
||||
def derivatives(self, inlet_h: float, m_flow: float) -> VolumeState:
|
||||
return VolumeState(m=m_flow, U=m_flow * inlet_h)
|
||||
def create(cls, *, name: str, medium: IdealGasMedium, parameters: Mapping[str, float]) -> Tank:
|
||||
return cls(name=name, medium=medium, V=parameters['volume'], p0=parameters['p0'], T0=parameters['T0'])
|
||||
EQUATIONS = ({'id': '__MODEL__:port_a_pressure_state', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'state', 'variables': ['__MODEL__.port_a.p', '__MODEL__.state'], 'role': 'effort'},)
|
||||
Reference in new issue
Block a user