同步仿真框架并接入AMESim气动组件
This commit is contained in:
1 parent
420bafeb4e
commit
db4bdb4b70
109 files changed
+26920
-420
No files matched your search
@@ -0,0 +1,2 @@
|
||||
"""Component implementations for the Python system model."""
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
from __future__ import annotations
|
||||
|
||||
__all__: list[str] = []
|
||||
@@ -0,0 +1 @@
|
||||
"""AMESim pneumatic boundary components."""
|
||||
@@ -0,0 +1,64 @@
|
||||
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.
|
||||
|
||||
The component behaves as a sealed pneumatic boundary in the current acausal
|
||||
solver: it does not prescribe pressure, and only constrains its port mass
|
||||
flow to zero.
|
||||
"""
|
||||
|
||||
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="generic",
|
||||
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")
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> AmesimPnpl01:
|
||||
return cls(name=name)
|
||||
|
||||
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"]
|
||||
@@ -0,0 +1 @@
|
||||
"""AMESim pneumatic flow components."""
|
||||
@@ -0,0 +1,610 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from math import isclose, 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,
|
||||
ResultVariableDefinition,
|
||||
)
|
||||
from app.simulation.core.medium import IdealGasMedium
|
||||
from app.simulation.core.ports import PortDefinition
|
||||
|
||||
|
||||
class AmesimPnor001(AlgebraicComponent):
|
||||
"""AMESim PNOR001 constant-flow-coefficient pneumatic orifice.
|
||||
|
||||
This public component preserves the PNOR001 catalog/XML contract and uses a
|
||||
finite bidirectional compressible-orifice approximation. The Siemens
|
||||
`pn2rcqfix_` details remain a later calibration target.
|
||||
"""
|
||||
|
||||
MODEL_TYPE = "amesim_pnor001"
|
||||
MODEL_VERSION = "0.1.0"
|
||||
PORTS = (
|
||||
PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),
|
||||
PortDefinition.pneumatic("port_2", nominal_role="bidirectional"),
|
||||
)
|
||||
PARAMETERS = (
|
||||
ParameterDefinition(
|
||||
"cq",
|
||||
0.72,
|
||||
label="流量系数 Cq",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
minimum=1.0e-10,
|
||||
maximum=1.0,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"area",
|
||||
5.0e-6,
|
||||
label="孔口面积",
|
||||
quantity="area",
|
||||
unit="m2",
|
||||
minimum=0.0,
|
||||
maximum=1.0,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"Cv",
|
||||
0.5,
|
||||
label="流量系数 Cv",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
minimum=0.0,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"Kv",
|
||||
0.4,
|
||||
label="流量系数 Kv",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
minimum=0.0,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"gi",
|
||||
1.0,
|
||||
label="气体类型索引",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
minimum=1.0,
|
||||
maximum=99.0,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"flowset",
|
||||
1.0,
|
||||
label="流量系数设置",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
minimum=1.0,
|
||||
maximum=3.0,
|
||||
),
|
||||
)
|
||||
RESULT_VARIABLES = (
|
||||
ResultVariableDefinition(
|
||||
"cm",
|
||||
label="质量流量参数",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
category="derived",
|
||||
order=10,
|
||||
),
|
||||
ResultVariableDefinition(
|
||||
"gasvel",
|
||||
label="缩流截面气体速度",
|
||||
quantity="velocity",
|
||||
unit="m/s",
|
||||
category="derived",
|
||||
order=20,
|
||||
),
|
||||
)
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="PNOR001 常系数气动孔口",
|
||||
library_id="amesim",
|
||||
category_id="flow",
|
||||
symbol="orifice",
|
||||
ports=(
|
||||
PortDisplaySpec("port_1", "left", order=10),
|
||||
PortDisplaySpec("port_2", "right", order=20),
|
||||
),
|
||||
order=10,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
*,
|
||||
cq: float = 0.72,
|
||||
area: float = 5.0e-6,
|
||||
Cv: float = 0.5,
|
||||
Kv: float = 0.4,
|
||||
gi: float = 1.0,
|
||||
flowset: float = 1.0,
|
||||
) -> None:
|
||||
super().__init__(name=name)
|
||||
self.set_parameter_values(
|
||||
{
|
||||
"cq": cq,
|
||||
"area": area,
|
||||
"Cv": Cv,
|
||||
"Kv": Kv,
|
||||
"gi": gi,
|
||||
"flowset": flowset,
|
||||
}
|
||||
)
|
||||
self.medium = medium
|
||||
self.cq = float(cq)
|
||||
self.area = float(area)
|
||||
self.Cv = float(Cv)
|
||||
self.Kv = float(Kv)
|
||||
self.gi = self._integer_parameter("gi", gi)
|
||||
self.flowset = self._integer_parameter("flowset", flowset)
|
||||
if self.flowset not in {1, 2, 3}:
|
||||
raise ValueError("PNOR001 flowset must be 1, 2, or 3.")
|
||||
|
||||
initial_h = medium.specific_enthalpy(medium.T_ref)
|
||||
self.port_1 = self.register_declared_port("port_1")
|
||||
self.port_1.h_outflow = initial_h
|
||||
self.port_2 = self.register_declared_port("port_2")
|
||||
self.port_2.h_outflow = initial_h
|
||||
|
||||
@staticmethod
|
||||
def _integer_parameter(name: str, value: float) -> int:
|
||||
rounded = round(value)
|
||||
if not isclose(value, rounded, rel_tol=0.0, abs_tol=1.0e-12):
|
||||
raise ValueError(f"PNOR001 parameter {name} must be an integer value.")
|
||||
return int(rounded)
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> AmesimPnor001:
|
||||
return cls(
|
||||
name=name,
|
||||
medium=medium,
|
||||
cq=parameters["cq"],
|
||||
area=parameters["area"],
|
||||
Cv=parameters["Cv"],
|
||||
Kv=parameters["Kv"],
|
||||
gi=parameters["gi"],
|
||||
flowset=parameters["flowset"],
|
||||
)
|
||||
|
||||
@property
|
||||
def effective_cq(self) -> float:
|
||||
return self.cq if self.flowset == 1 else 0.72
|
||||
|
||||
@property
|
||||
def effective_area(self) -> float:
|
||||
if self.flowset == 1:
|
||||
return self.area
|
||||
if self.flowset == 2:
|
||||
return self._area_from_cv(self.Cv, self.effective_cq)
|
||||
return self._area_from_kv(self.Kv, self.effective_cq)
|
||||
|
||||
@staticmethod
|
||||
def _area_from_cv(Cv: float, cq: float) -> float:
|
||||
water_density = 999.0
|
||||
reference_flow_m3_s = Cv * 6.30901964e-5
|
||||
reference_dp_pa = 6894.75729
|
||||
return reference_flow_m3_s / (cq * sqrt(2.0 * reference_dp_pa / water_density))
|
||||
|
||||
@staticmethod
|
||||
def _area_from_kv(Kv: float, cq: float) -> float:
|
||||
water_density = 999.0
|
||||
reference_flow_m3_s = Kv / 3600.0
|
||||
reference_dp_pa = 100000.0
|
||||
return reference_flow_m3_s / (cq * sqrt(2.0 * reference_dp_pa / water_density))
|
||||
|
||||
def _upstream_temperature(self, port_name: str) -> float:
|
||||
port = self.get_port(port_name)
|
||||
if port.h_outflow > 0.0:
|
||||
return max(port.h_outflow / self.medium.cp_ref, 1.0)
|
||||
return self.medium.T_ref
|
||||
|
||||
def mass_flow(self, p_1: float, p_2: float) -> float:
|
||||
if p_1 == p_2 or self.effective_area == 0.0:
|
||||
return 0.0
|
||||
if p_1 > p_2:
|
||||
return self._one_way_mass_flow(
|
||||
upstream_pressure=p_1,
|
||||
downstream_pressure=p_2,
|
||||
upstream_temperature=self._upstream_temperature("port_1"),
|
||||
)
|
||||
return -self._one_way_mass_flow(
|
||||
upstream_pressure=p_2,
|
||||
downstream_pressure=p_1,
|
||||
upstream_temperature=self._upstream_temperature("port_2"),
|
||||
)
|
||||
|
||||
def _one_way_mass_flow(
|
||||
self,
|
||||
*,
|
||||
upstream_pressure: float,
|
||||
downstream_pressure: float,
|
||||
upstream_temperature: float,
|
||||
) -> float:
|
||||
p_up = max(upstream_pressure, 1.0)
|
||||
p_down = max(min(downstream_pressure, p_up), 0.0)
|
||||
T_up = max(upstream_temperature, 1.0)
|
||||
gamma = max(self.medium.gamma, 1.000001)
|
||||
pressure_ratio = max(p_down / p_up, 0.0)
|
||||
critical_ratio = (2.0 / (gamma + 1.0)) ** (gamma / (gamma - 1.0))
|
||||
if pressure_ratio <= critical_ratio:
|
||||
flow_factor = sqrt(gamma / (self.medium.R_gas * T_up)) * (
|
||||
2.0 / (gamma + 1.0)
|
||||
) ** ((gamma + 1.0) / (2.0 * (gamma - 1.0)))
|
||||
else:
|
||||
expansion = pressure_ratio ** (2.0 / gamma) - pressure_ratio ** (
|
||||
(gamma + 1.0) / gamma
|
||||
)
|
||||
flow_factor = sqrt(
|
||||
max(
|
||||
2.0
|
||||
* gamma
|
||||
* expansion
|
||||
/ (self.medium.R_gas * T_up * (gamma - 1.0)),
|
||||
0.0,
|
||||
)
|
||||
)
|
||||
return self.effective_cq * self.effective_area * p_up * flow_factor
|
||||
|
||||
def component_result_values(self) -> Mapping[str, float]:
|
||||
p_1 = max(self.port_1.p, 1.0)
|
||||
p_2 = max(self.port_2.p, 1.0)
|
||||
m_flow = abs(self.mass_flow(self.port_1.p, self.port_2.p))
|
||||
upstream_pressure = max(p_1, p_2)
|
||||
upstream_temperature = self._upstream_temperature(
|
||||
"port_1" if p_1 >= p_2 else "port_2"
|
||||
)
|
||||
density = max(self.medium.density(upstream_pressure, upstream_temperature), 1.0e-12)
|
||||
area = max(self.effective_area, 1.0e-18)
|
||||
return {
|
||||
"cm": m_flow / (self.effective_cq * area * upstream_pressure),
|
||||
"gasvel": m_flow / (density * 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_1.m_flow",
|
||||
f"{self.name}.port_2.m_flow",
|
||||
),
|
||||
role="flow",
|
||||
value=self.port_1.m_flow + self.port_2.m_flow,
|
||||
),
|
||||
EquationResidual(
|
||||
id=f"{self.name}:pressure_flow_relation",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="constitutive",
|
||||
variables=(
|
||||
f"{self.name}.port_1.p",
|
||||
f"{self.name}.port_2.p",
|
||||
f"{self.name}.port_1.m_flow",
|
||||
),
|
||||
role="flow",
|
||||
value=self.port_1.m_flow
|
||||
- self.mass_flow(self.port_1.p, self.port_2.p),
|
||||
),
|
||||
)
|
||||
|
||||
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
|
||||
self.port_1.h_outflow = connected_h["port_2"]
|
||||
self.port_2.h_outflow = connected_h["port_1"]
|
||||
|
||||
|
||||
class AmesimPnvo001FixedOpening(AlgebraicComponent):
|
||||
"""Fixed-opening public variant of AMESim PNVO001.
|
||||
|
||||
Full PNVO001 has a signal input port. The current public component library
|
||||
does not support signal simulation, so this model exposes the pneumatic
|
||||
ports and replaces the signal with a normalized `opening` parameter.
|
||||
"""
|
||||
|
||||
MODEL_TYPE = "amesim_pnvo001_fixed"
|
||||
MODEL_VERSION = "0.1.0"
|
||||
PORTS = (
|
||||
PortDefinition.pneumatic("port_2", nominal_role="bidirectional"),
|
||||
PortDefinition.pneumatic("port_3", nominal_role="bidirectional"),
|
||||
)
|
||||
PARAMETERS = (
|
||||
ParameterDefinition(
|
||||
"cq",
|
||||
0.72,
|
||||
label="流量系数 Cq",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
minimum=1.0e-10,
|
||||
maximum=1.0,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"area0",
|
||||
5.0e-6,
|
||||
label="最大孔口面积",
|
||||
quantity="area",
|
||||
unit="m2",
|
||||
minimum=0.0,
|
||||
maximum=1.0,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"Cv",
|
||||
0.5,
|
||||
label="最大流量系数 Cv",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
minimum=0.0,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"Kv",
|
||||
0.4,
|
||||
label="最大流量系数 Kv",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
minimum=0.0,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"gi",
|
||||
1.0,
|
||||
label="气体类型索引",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
minimum=1.0,
|
||||
maximum=99.0,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"flowset",
|
||||
1.0,
|
||||
label="流量系数设置",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
minimum=1.0,
|
||||
maximum=3.0,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"opening",
|
||||
1.0,
|
||||
label="固定开度",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
minimum=0.0,
|
||||
maximum=1.0,
|
||||
),
|
||||
)
|
||||
RESULT_VARIABLES = (
|
||||
ResultVariableDefinition(
|
||||
"xv",
|
||||
label="有效开度",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
category="derived",
|
||||
order=10,
|
||||
),
|
||||
ResultVariableDefinition(
|
||||
"cm",
|
||||
label="质量流量参数",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
category="derived",
|
||||
order=20,
|
||||
),
|
||||
ResultVariableDefinition(
|
||||
"gasvel",
|
||||
label="缩流截面气体速度",
|
||||
quantity="velocity",
|
||||
unit="m/s",
|
||||
category="derived",
|
||||
order=30,
|
||||
),
|
||||
)
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="PNVO001 固定开度气动孔口",
|
||||
library_id="amesim",
|
||||
category_id="flow",
|
||||
symbol="orifice",
|
||||
ports=(
|
||||
PortDisplaySpec("port_2", "left", order=10),
|
||||
PortDisplaySpec("port_3", "right", order=20),
|
||||
),
|
||||
order=30,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
*,
|
||||
cq: float = 0.72,
|
||||
area0: float = 5.0e-6,
|
||||
Cv: float = 0.5,
|
||||
Kv: float = 0.4,
|
||||
gi: float = 1.0,
|
||||
flowset: float = 1.0,
|
||||
opening: float = 1.0,
|
||||
) -> None:
|
||||
super().__init__(name=name)
|
||||
self.set_parameter_values(
|
||||
{
|
||||
"cq": cq,
|
||||
"area0": area0,
|
||||
"Cv": Cv,
|
||||
"Kv": Kv,
|
||||
"gi": gi,
|
||||
"flowset": flowset,
|
||||
"opening": opening,
|
||||
}
|
||||
)
|
||||
self.medium = medium
|
||||
self.cq = float(cq)
|
||||
self.area0 = float(area0)
|
||||
self.Cv = float(Cv)
|
||||
self.Kv = float(Kv)
|
||||
self.gi = self._integer_parameter("gi", gi)
|
||||
self.flowset = self._integer_parameter("flowset", flowset)
|
||||
if self.flowset not in {1, 2, 3}:
|
||||
raise ValueError("PNVO001 fixed-opening flowset must be 1, 2, or 3.")
|
||||
self.opening = min(1.0, max(0.0, float(opening)))
|
||||
|
||||
initial_h = medium.specific_enthalpy(medium.T_ref)
|
||||
self.port_2 = self.register_declared_port("port_2")
|
||||
self.port_2.h_outflow = initial_h
|
||||
self.port_3 = self.register_declared_port("port_3")
|
||||
self.port_3.h_outflow = initial_h
|
||||
|
||||
@staticmethod
|
||||
def _integer_parameter(name: str, value: float) -> int:
|
||||
rounded = round(value)
|
||||
if not isclose(value, rounded, rel_tol=0.0, abs_tol=1.0e-12):
|
||||
raise ValueError(f"PNVO001 fixed-opening parameter {name} must be an integer value.")
|
||||
return int(rounded)
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> AmesimPnvo001FixedOpening:
|
||||
return cls(
|
||||
name=name,
|
||||
medium=medium,
|
||||
cq=parameters["cq"],
|
||||
area0=parameters["area0"],
|
||||
Cv=parameters["Cv"],
|
||||
Kv=parameters["Kv"],
|
||||
gi=parameters["gi"],
|
||||
flowset=parameters["flowset"],
|
||||
opening=parameters["opening"],
|
||||
)
|
||||
|
||||
@property
|
||||
def effective_cq(self) -> float:
|
||||
return self.cq if self.flowset == 1 else 0.72
|
||||
|
||||
@property
|
||||
def maximum_area(self) -> float:
|
||||
if self.flowset == 1:
|
||||
return self.area0
|
||||
if self.flowset == 2:
|
||||
return AmesimPnor001._area_from_cv(self.Cv, self.effective_cq)
|
||||
return AmesimPnor001._area_from_kv(self.Kv, self.effective_cq)
|
||||
|
||||
@property
|
||||
def effective_area(self) -> float:
|
||||
return self.opening * self.maximum_area
|
||||
|
||||
def _upstream_temperature(self, port_name: str) -> float:
|
||||
port = self.get_port(port_name)
|
||||
if port.h_outflow > 0.0:
|
||||
return max(port.h_outflow / self.medium.cp_ref, 1.0)
|
||||
return self.medium.T_ref
|
||||
|
||||
def mass_flow(self, p_2: float, p_3: float) -> float:
|
||||
if p_2 == p_3 or self.effective_area == 0.0:
|
||||
return 0.0
|
||||
if p_2 > p_3:
|
||||
return self._one_way_mass_flow(
|
||||
upstream_pressure=p_2,
|
||||
downstream_pressure=p_3,
|
||||
upstream_temperature=self._upstream_temperature("port_2"),
|
||||
)
|
||||
return -self._one_way_mass_flow(
|
||||
upstream_pressure=p_3,
|
||||
downstream_pressure=p_2,
|
||||
upstream_temperature=self._upstream_temperature("port_3"),
|
||||
)
|
||||
|
||||
def _one_way_mass_flow(
|
||||
self,
|
||||
*,
|
||||
upstream_pressure: float,
|
||||
downstream_pressure: float,
|
||||
upstream_temperature: float,
|
||||
) -> float:
|
||||
p_up = max(upstream_pressure, 1.0)
|
||||
p_down = max(min(downstream_pressure, p_up), 0.0)
|
||||
T_up = max(upstream_temperature, 1.0)
|
||||
gamma = max(self.medium.gamma, 1.000001)
|
||||
pressure_ratio = max(p_down / p_up, 0.0)
|
||||
critical_ratio = (2.0 / (gamma + 1.0)) ** (gamma / (gamma - 1.0))
|
||||
if pressure_ratio <= critical_ratio:
|
||||
flow_factor = sqrt(gamma / (self.medium.R_gas * T_up)) * (
|
||||
2.0 / (gamma + 1.0)
|
||||
) ** ((gamma + 1.0) / (2.0 * (gamma - 1.0)))
|
||||
else:
|
||||
expansion = pressure_ratio ** (2.0 / gamma) - pressure_ratio ** (
|
||||
(gamma + 1.0) / gamma
|
||||
)
|
||||
flow_factor = sqrt(
|
||||
max(
|
||||
2.0
|
||||
* gamma
|
||||
* expansion
|
||||
/ (self.medium.R_gas * T_up * (gamma - 1.0)),
|
||||
0.0,
|
||||
)
|
||||
)
|
||||
return self.effective_cq * self.effective_area * p_up * flow_factor
|
||||
|
||||
def component_result_values(self) -> Mapping[str, float]:
|
||||
p_2 = max(self.port_2.p, 1.0)
|
||||
p_3 = max(self.port_3.p, 1.0)
|
||||
m_flow = abs(self.mass_flow(self.port_2.p, self.port_3.p))
|
||||
upstream_pressure = max(p_2, p_3)
|
||||
upstream_temperature = self._upstream_temperature(
|
||||
"port_2" if p_2 >= p_3 else "port_3"
|
||||
)
|
||||
density = max(self.medium.density(upstream_pressure, upstream_temperature), 1.0e-12)
|
||||
area = max(self.effective_area, 1.0e-18)
|
||||
return {
|
||||
"xv": self.opening,
|
||||
"cm": m_flow / (self.effective_cq * area * upstream_pressure),
|
||||
"gasvel": m_flow / (density * 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_2.m_flow",
|
||||
f"{self.name}.port_3.m_flow",
|
||||
),
|
||||
role="flow",
|
||||
value=self.port_2.m_flow + self.port_3.m_flow,
|
||||
),
|
||||
EquationResidual(
|
||||
id=f"{self.name}:pressure_flow_relation",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="constitutive",
|
||||
variables=(
|
||||
f"{self.name}.port_2.p",
|
||||
f"{self.name}.port_3.p",
|
||||
f"{self.name}.port_2.m_flow",
|
||||
),
|
||||
role="flow",
|
||||
value=self.port_2.m_flow
|
||||
- self.mass_flow(self.port_2.p, self.port_3.p),
|
||||
),
|
||||
)
|
||||
|
||||
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
|
||||
self.port_2.h_outflow = connected_h["port_3"]
|
||||
self.port_3.h_outflow = connected_h["port_2"]
|
||||
@@ -0,0 +1,310 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from math import isclose, log10, pi, 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,
|
||||
ResultVariableDefinition,
|
||||
)
|
||||
from app.simulation.core.medium import IdealGasMedium
|
||||
from app.simulation.core.ports import PortDefinition
|
||||
|
||||
|
||||
class AmesimPnl00r(AlgebraicComponent):
|
||||
"""AMESim PNL00R pneumatic pipe friction resistance.
|
||||
|
||||
The public model exposes the AMESim PNL00R catalog/XML contract and uses
|
||||
an auditable Darcy-Weisbach resistance with Reynolds/roughness-dependent
|
||||
friction. Exact `pn2pipefr_` parity is left for the later model tuning pass.
|
||||
"""
|
||||
|
||||
MODEL_TYPE = "amesim_pnl00r"
|
||||
MODEL_VERSION = "0.1.0"
|
||||
PORTS = (
|
||||
PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),
|
||||
PortDefinition.pneumatic("port_2", nominal_role="bidirectional"),
|
||||
)
|
||||
PARAMETERS = (
|
||||
ParameterDefinition(
|
||||
"diam",
|
||||
0.01,
|
||||
label="管径",
|
||||
quantity="length",
|
||||
unit="m",
|
||||
minimum=0.0,
|
||||
minimum_exclusive=True,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"le",
|
||||
1.0,
|
||||
label="管长",
|
||||
quantity="length",
|
||||
unit="m",
|
||||
minimum=0.0,
|
||||
minimum_exclusive=True,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"rr",
|
||||
1.0e-5,
|
||||
label="相对粗糙度",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
minimum=0.0,
|
||||
maximum=0.1,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"gi",
|
||||
1.0,
|
||||
label="气体类型索引",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
minimum=1.0,
|
||||
maximum=99.0,
|
||||
),
|
||||
)
|
||||
RESULT_VARIABLES = (
|
||||
ResultVariableDefinition(
|
||||
"re",
|
||||
label="Reynolds 数",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
category="derived",
|
||||
order=10,
|
||||
),
|
||||
ResultVariableDefinition(
|
||||
"cm",
|
||||
label="质量流量参数",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
category="derived",
|
||||
order=20,
|
||||
),
|
||||
ResultVariableDefinition(
|
||||
"v",
|
||||
label="平均气体速度",
|
||||
quantity="velocity",
|
||||
unit="m/s",
|
||||
category="derived",
|
||||
order=30,
|
||||
),
|
||||
ResultVariableDefinition(
|
||||
"ff",
|
||||
label="摩擦因子",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
category="derived",
|
||||
order=40,
|
||||
),
|
||||
)
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="PNL00R 气动管路阻力",
|
||||
library_id="amesim",
|
||||
category_id="flow",
|
||||
symbol="pipe",
|
||||
ports=(
|
||||
PortDisplaySpec("port_1", "left", order=10),
|
||||
PortDisplaySpec("port_2", "right", order=20),
|
||||
),
|
||||
order=20,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
*,
|
||||
diam: float = 0.01,
|
||||
le: float = 1.0,
|
||||
rr: float = 1.0e-5,
|
||||
gi: float = 1.0,
|
||||
) -> None:
|
||||
super().__init__(name=name)
|
||||
self.set_parameter_values({"diam": diam, "le": le, "rr": rr, "gi": gi})
|
||||
self.medium = medium
|
||||
self.diam = float(diam)
|
||||
self.le = float(le)
|
||||
self.rr = float(rr)
|
||||
self.gi = self._integer_parameter("gi", gi)
|
||||
self.area = pi * self.diam * self.diam / 4.0
|
||||
|
||||
initial_h = medium.specific_enthalpy(medium.T_ref)
|
||||
self.port_1 = self.register_declared_port("port_1")
|
||||
self.port_1.h_outflow = initial_h
|
||||
self.port_2 = self.register_declared_port("port_2")
|
||||
self.port_2.h_outflow = initial_h
|
||||
|
||||
@staticmethod
|
||||
def _integer_parameter(name: str, value: float) -> int:
|
||||
rounded = round(value)
|
||||
if not isclose(value, rounded, rel_tol=0.0, abs_tol=1.0e-12):
|
||||
raise ValueError(f"PNL00R parameter {name} must be an integer value.")
|
||||
return int(rounded)
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> AmesimPnl00r:
|
||||
return cls(
|
||||
name=name,
|
||||
medium=medium,
|
||||
diam=parameters["diam"],
|
||||
le=parameters["le"],
|
||||
rr=parameters["rr"],
|
||||
gi=parameters["gi"],
|
||||
)
|
||||
|
||||
def _port_temperature(self, port_name: str) -> float:
|
||||
port = self.get_port(port_name)
|
||||
if port.h_outflow > 0.0:
|
||||
return max(port.h_outflow / self.medium.cp_ref, 1.0)
|
||||
return self.medium.T_ref
|
||||
|
||||
@staticmethod
|
||||
def _dynamic_viscosity(temperature_k: float) -> float:
|
||||
if temperature_k <= 0.0:
|
||||
raise ValueError("temperature_k must be positive")
|
||||
reference_temperature = 293.15
|
||||
reference_viscosity = 1.82e-5
|
||||
sutherland_constant = 110.4
|
||||
return (
|
||||
reference_viscosity
|
||||
* (temperature_k / reference_temperature) ** 1.5
|
||||
* (reference_temperature + sutherland_constant)
|
||||
/ (temperature_k + sutherland_constant)
|
||||
)
|
||||
|
||||
def reynolds_number(self, mass_flow: float, temperature: float) -> float:
|
||||
viscosity = self._dynamic_viscosity(temperature)
|
||||
return 4.0 * abs(mass_flow) / (pi * self.diam * viscosity)
|
||||
|
||||
def friction_factor(self, reynolds_number: float) -> float:
|
||||
if reynolds_number <= 0.0:
|
||||
return 64_000_000.0
|
||||
laminar = 64.0 / reynolds_number
|
||||
if reynolds_number <= 2300.0:
|
||||
return laminar
|
||||
turbulent = 1.0 / (
|
||||
-1.8 * log10((self.rr / 3.7) ** 1.11 + 6.9 / reynolds_number)
|
||||
) ** 2
|
||||
if reynolds_number >= 4000.0:
|
||||
return turbulent
|
||||
fraction = (reynolds_number - 2300.0) / 1700.0
|
||||
return laminar + fraction * (turbulent - laminar)
|
||||
|
||||
def darcy_pressure_drop(
|
||||
self,
|
||||
mass_flow: float,
|
||||
*,
|
||||
density: float,
|
||||
temperature: float,
|
||||
) -> float:
|
||||
if mass_flow == 0.0:
|
||||
return 0.0
|
||||
reynolds = self.reynolds_number(mass_flow, temperature)
|
||||
friction = self.friction_factor(reynolds)
|
||||
velocity = mass_flow / (density * self.area)
|
||||
magnitude = (
|
||||
friction
|
||||
* (self.le / self.diam)
|
||||
* density
|
||||
* velocity
|
||||
* velocity
|
||||
/ 2.0
|
||||
)
|
||||
return magnitude if mass_flow > 0.0 else -magnitude
|
||||
|
||||
def _mass_flow_for_pressure_drop(
|
||||
self,
|
||||
pressure_drop: float,
|
||||
*,
|
||||
density: float,
|
||||
temperature: float,
|
||||
) -> float:
|
||||
if pressure_drop <= 0.0:
|
||||
return 0.0
|
||||
upper = 1.0e-9
|
||||
while self.darcy_pressure_drop(upper, density=density, temperature=temperature) < pressure_drop:
|
||||
upper *= 10.0
|
||||
if upper > 1.0e3:
|
||||
raise ValueError("unable to bracket PNL00R resistance flow")
|
||||
lower = 0.0
|
||||
for _ in range(48):
|
||||
middle = 0.5 * (lower + upper)
|
||||
if self.darcy_pressure_drop(middle, density=density, temperature=temperature) < pressure_drop:
|
||||
lower = middle
|
||||
else:
|
||||
upper = middle
|
||||
return 0.5 * (lower + upper)
|
||||
|
||||
def mass_flow(self, p_1: float, p_2: float) -> float:
|
||||
if p_1 == p_2:
|
||||
return 0.0
|
||||
pressure_difference = p_1 - p_2
|
||||
upstream_pressure = max(p_1, p_2, 1.0)
|
||||
upstream_temperature = self._port_temperature("port_1" if pressure_difference > 0.0 else "port_2")
|
||||
density = max(self.medium.density(upstream_pressure, upstream_temperature), 1.0e-12)
|
||||
magnitude = self._mass_flow_for_pressure_drop(
|
||||
abs(pressure_difference),
|
||||
density=density,
|
||||
temperature=upstream_temperature,
|
||||
)
|
||||
return magnitude if pressure_difference > 0.0 else -magnitude
|
||||
|
||||
def component_result_values(self) -> Mapping[str, float]:
|
||||
m_flow = self.mass_flow(self.port_1.p, self.port_2.p)
|
||||
upstream_pressure = max(self.port_1.p, self.port_2.p, 1.0)
|
||||
upstream_temperature = self._port_temperature(
|
||||
"port_1" if self.port_1.p >= self.port_2.p else "port_2"
|
||||
)
|
||||
density = max(self.medium.density(upstream_pressure, upstream_temperature), 1.0e-12)
|
||||
reynolds = self.reynolds_number(m_flow, upstream_temperature)
|
||||
velocity = m_flow / (density * self.area)
|
||||
cm = abs(m_flow) / max(self.area * upstream_pressure, 1.0e-18)
|
||||
return {
|
||||
"re": reynolds,
|
||||
"cm": cm,
|
||||
"v": velocity,
|
||||
"ff": self.friction_factor(reynolds),
|
||||
}
|
||||
|
||||
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_1.m_flow",
|
||||
f"{self.name}.port_2.m_flow",
|
||||
),
|
||||
role="flow",
|
||||
value=self.port_1.m_flow + self.port_2.m_flow,
|
||||
),
|
||||
EquationResidual(
|
||||
id=f"{self.name}:pressure_flow_relation",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="constitutive",
|
||||
variables=(
|
||||
f"{self.name}.port_1.p",
|
||||
f"{self.name}.port_2.p",
|
||||
f"{self.name}.port_1.m_flow",
|
||||
),
|
||||
role="flow",
|
||||
value=self.port_1.m_flow
|
||||
- self.mass_flow(self.port_1.p, self.port_2.p),
|
||||
),
|
||||
)
|
||||
|
||||
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
|
||||
self.port_1.h_outflow = connected_h["port_2"]
|
||||
self.port_2.h_outflow = connected_h["port_1"]
|
||||
@@ -0,0 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from app.simulation.components.amesim.junctions.nodes import AmesimP4Node2, AmesimPn3Node2
|
||||
|
||||
__all__ = ["AmesimP4Node2", "AmesimPn3Node2"]
|
||||
@@ -0,0 +1,149 @@
|
||||
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
|
||||
|
||||
|
||||
class _AmesimPneumaticNode(AlgebraicComponent):
|
||||
"""Shared implementation for AMESim pneumatic junction submodels."""
|
||||
|
||||
REFERENCE_PORT = "port_2"
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
super().__init__(name=name)
|
||||
self.set_parameter_values({})
|
||||
for definition in self.PORTS:
|
||||
setattr(self, definition.name, self.register_declared_port(definition.name))
|
||||
|
||||
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:
|
||||
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 = connected_h.get(
|
||||
self.REFERENCE_PORT,
|
||||
sum(connected_h.values()) / len(connected_h) if connected_h else 0.0,
|
||||
)
|
||||
for port in self.ports.values():
|
||||
port.h_outflow = mixed_h
|
||||
|
||||
|
||||
class AmesimPn3Node2(_AmesimPneumaticNode):
|
||||
"""AMESim PN3NODE2 pneumatic three-port junction."""
|
||||
|
||||
MODEL_TYPE = "amesim_pn3node2"
|
||||
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"),
|
||||
)
|
||||
PARAMETERS = ()
|
||||
RESULT_VARIABLES = ()
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="PN3NODE2 三端气动节点",
|
||||
library_id="amesim",
|
||||
category_id="junctions",
|
||||
symbol="tee",
|
||||
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:
|
||||
return cls(name=name)
|
||||
|
||||
|
||||
class AmesimP4Node2(_AmesimPneumaticNode):
|
||||
"""AMESim P4NODE2 pneumatic four-port junction."""
|
||||
|
||||
MODEL_TYPE = "amesim_p4node2"
|
||||
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 = ()
|
||||
RESULT_VARIABLES = ()
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="P4NODE2 四端气动节点",
|
||||
library_id="amesim",
|
||||
category_id="junctions",
|
||||
symbol="generic",
|
||||
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:
|
||||
return cls(name=name)
|
||||
@@ -0,0 +1,31 @@
|
||||
"""AMESim-compatible public component library."""
|
||||
|
||||
from app.simulation.core.catalog import (
|
||||
ComponentCategorySpec,
|
||||
ComponentLibrarySpec,
|
||||
)
|
||||
|
||||
|
||||
LIBRARY = ComponentLibrarySpec(
|
||||
id="amesim",
|
||||
label="AMESim 组件库",
|
||||
version="0.1.0",
|
||||
source_package="app.simulation.components.amesim",
|
||||
temporary=True,
|
||||
order=200,
|
||||
categories=(
|
||||
ComponentCategorySpec(id="storage", label="储能元件", order=10),
|
||||
ComponentCategorySpec(id="flow", label="流动元件", order=20),
|
||||
ComponentCategorySpec(id="junctions", label="连接元件", order=30),
|
||||
ComponentCategorySpec(id="boundary", label="边界元件", order=40),
|
||||
),
|
||||
models=(
|
||||
"app.simulation.components.amesim.boundary.sources:AmesimPnpl01",
|
||||
"app.simulation.components.amesim.storage.chambers:AmesimPnch023",
|
||||
"app.simulation.components.amesim.flow.orifices:AmesimPnor001",
|
||||
"app.simulation.components.amesim.flow.orifices:AmesimPnvo001FixedOpening",
|
||||
"app.simulation.components.amesim.flow.pipes:AmesimPnl00r",
|
||||
"app.simulation.components.amesim.junctions.nodes:AmesimPn3Node2",
|
||||
"app.simulation.components.amesim.junctions.nodes:AmesimP4Node2",
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
"""AMESim pneumatic storage components."""
|
||||
@@ -0,0 +1,250 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from math import isclose
|
||||
|
||||
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 AmesimPnch023(ThermodynamicVolumeComponent):
|
||||
"""AMESim PNCH023 simple pneumatic chamber with heat exchange.
|
||||
|
||||
The AMESim submodel owns pressure and temperature states and exposes two
|
||||
pneumatic flow ports. This public component maps those states onto the
|
||||
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 = (
|
||||
ParameterDefinition(
|
||||
"cvol",
|
||||
0.057,
|
||||
label="气室容积",
|
||||
quantity="volume",
|
||||
unit="m3",
|
||||
minimum=0.0,
|
||||
minimum_exclusive=True,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"kth",
|
||||
0.0,
|
||||
label="换热系数",
|
||||
quantity="heat_transfer_coefficient",
|
||||
unit="W/(m2*K)",
|
||||
minimum=0.0,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"sth",
|
||||
0.1,
|
||||
label="换热面积",
|
||||
quantity="area",
|
||||
unit="m2",
|
||||
minimum=0.0,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"extemp",
|
||||
293.15,
|
||||
label="外部温度",
|
||||
quantity="temperature",
|
||||
unit="K",
|
||||
minimum=0.0,
|
||||
minimum_exclusive=True,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"gi",
|
||||
1.0,
|
||||
label="气体类型索引",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
minimum=1.0,
|
||||
maximum=99.0,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"p0",
|
||||
100000.0,
|
||||
label="初始压力",
|
||||
quantity="pressure",
|
||||
unit="Pa",
|
||||
minimum=0.0,
|
||||
minimum_exclusive=True,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"T0",
|
||||
293.15,
|
||||
label="初始温度",
|
||||
quantity="temperature",
|
||||
unit="K",
|
||||
minimum=0.0,
|
||||
minimum_exclusive=True,
|
||||
),
|
||||
)
|
||||
RESULT_VARIABLES = THERMODYNAMIC_VOLUME_RESULT_VARIABLES
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="PNCH023 固定容积气室",
|
||||
library_id="amesim",
|
||||
category_id="storage",
|
||||
symbol="tank",
|
||||
ports=(
|
||||
PortDisplaySpec("port_1", "left", order=10),
|
||||
PortDisplaySpec("port_2", "right", order=20),
|
||||
),
|
||||
order=10,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
*,
|
||||
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.medium = medium
|
||||
self.cvol = float(cvol)
|
||||
self.kth = float(kth)
|
||||
self.sth = float(sth)
|
||||
self.extemp = float(extemp)
|
||||
self.gi = self._integer_parameter("gi", gi)
|
||||
self.p0 = float(p0)
|
||||
self.T0 = float(T0)
|
||||
m0 = self.p0 * self.cvol / (medium.R_gas * self.T0)
|
||||
U0 = m0 * medium.specific_internal_energy(self.T0)
|
||||
self.state = VolumeState(m=m0, U=U0)
|
||||
initial_h = medium.specific_enthalpy(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
|
||||
|
||||
@staticmethod
|
||||
def _integer_parameter(name: str, value: float) -> int:
|
||||
rounded = round(value)
|
||||
if not isclose(value, rounded, rel_tol=0.0, abs_tol=1.0e-12):
|
||||
raise ValueError(f"PNCH023 parameter {name} must be an integer value.")
|
||||
return int(rounded)
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
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_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,
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,282 @@
|
||||
# 元件建模规范与示例
|
||||
|
||||
规范的权威版本位于
|
||||
[`docs/component-model-authoring-spec-v1.md`](../../../docs/component-model-authoring-spec-v1.md)。
|
||||
本文档保留在组件目录中,作为离模型源码最近的完整示例;若两者不一致,应在同一次
|
||||
修改中同步,不能让示例形成另一套规则。
|
||||
|
||||
本文档是 `app/simulation/components` 下新增元件的最小开发规范。当前
|
||||
`experimental` 是用于验证规范的临时组件库;后续正式模型应建立独立组件库,
|
||||
不要继续堆放在 `experimental` 中。
|
||||
|
||||
目标是让元件的端口、输入参数和可展示结果都由元件类显式声明,避免 XML
|
||||
校验、求解器和前端分别维护同一份含义。
|
||||
|
||||
## 一、元件类必须声明的内容
|
||||
|
||||
每个对外注册的元件类至少需要声明以下六个类属性:
|
||||
|
||||
```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/component-library-spec-v1.md)。
|
||||
@@ -0,0 +1,12 @@
|
||||
"""Temporary component library used to validate the model authoring contract."""
|
||||
|
||||
from app.simulation.components.experimental.library import LIBRARY
|
||||
|
||||
|
||||
# Compatibility aliases for code written before the v1 library manifest.
|
||||
LIBRARY_ID = LIBRARY.id
|
||||
LIBRARY_LABEL = LIBRARY.label
|
||||
LIBRARY_VERSION = LIBRARY.version
|
||||
LIBRARY_ORDER = LIBRARY.order
|
||||
LIBRARY_SOURCE_PACKAGE = LIBRARY.source_package
|
||||
LIBRARY_TEMPORARY = LIBRARY.temporary
|
||||
@@ -0,0 +1 @@
|
||||
"""Flow-path and resistance components."""
|
||||
@@ -0,0 +1,118 @@
|
||||
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"
|
||||
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,
|
||||
),
|
||||
)
|
||||
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,
|
||||
)
|
||||
|
||||
def __init__(self, name: str, opening: float = 1.0, K: float = 1e-5) -> None:
|
||||
super().__init__(name=name)
|
||||
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")
|
||||
|
||||
@classmethod
|
||||
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"]
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
"""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",)
|
||||
@@ -0,0 +1,185 @@
|
||||
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"
|
||||
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,
|
||||
),
|
||||
)
|
||||
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,
|
||||
)
|
||||
|
||||
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:
|
||||
super().__init__(name=name)
|
||||
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
|
||||
self.lambda_darcy = lambda_darcy
|
||||
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
|
||||
|
||||
@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"]
|
||||
@@ -0,0 +1 @@
|
||||
"""Flow junction components."""
|
||||
@@ -0,0 +1,266 @@
|
||||
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"
|
||||
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,
|
||||
)
|
||||
|
||||
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")
|
||||
|
||||
@classmethod
|
||||
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)
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Manifest for the temporary library used to validate component authoring."""
|
||||
|
||||
from app.simulation.core.catalog import (
|
||||
ComponentCategorySpec,
|
||||
ComponentLibrarySpec,
|
||||
)
|
||||
|
||||
|
||||
LIBRARY = ComponentLibrarySpec(
|
||||
id="experimental",
|
||||
label="临时测试组件库",
|
||||
version="0.1.0",
|
||||
source_package="app.simulation.components.experimental",
|
||||
temporary=True,
|
||||
order=100,
|
||||
categories=(
|
||||
ComponentCategorySpec(id="storage", label="储能元件", order=10),
|
||||
ComponentCategorySpec(id="flow", label="流动元件", order=20),
|
||||
ComponentCategorySpec(id="junctions", label="连接元件", order=30),
|
||||
),
|
||||
models=(
|
||||
"app.simulation.components.experimental.storage.cylinder:Cylinder",
|
||||
"app.simulation.components.experimental.storage.tank:Tank",
|
||||
"app.simulation.components.experimental.flow.resistive_pipe:ResistivePipe",
|
||||
"app.simulation.components.experimental.flow.orifice:Orifice",
|
||||
"app.simulation.components.experimental.junctions.tee:Tee",
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
"""Storage and thermodynamic volume components."""
|
||||
@@ -0,0 +1,155 @@
|
||||
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 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,
|
||||
),
|
||||
)
|
||||
RESULT_VARIABLES = THERMODYNAMIC_VOLUME_RESULT_VARIABLES
|
||||
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:
|
||||
super().__init__(name=name)
|
||||
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")
|
||||
|
||||
@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)
|
||||
@@ -0,0 +1,155 @@
|
||||
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 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,
|
||||
),
|
||||
)
|
||||
RESULT_VARIABLES = THERMODYNAMIC_VOLUME_RESULT_VARIABLES
|
||||
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:
|
||||
super().__init__(name=name)
|
||||
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")
|
||||
|
||||
@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)
|
||||
Reference in new issue
Block a user