同步仿真框架并接入AMESim气动组件
This commit is contained in:
1 parent
420bafeb4e
commit
db4bdb4b70
109 files changed
+26920
-420
No files matched your search
@@ -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"]
|
||||
Reference in new issue
Block a user