1059 lines
35 KiB
Python
1059 lines
35 KiB
Python
from __future__ import annotations
|
||
|
||
from functools import lru_cache
|
||
from collections.abc import Mapping
|
||
from math import isclose, log, sqrt, tanh
|
||
|
||
from app.simulation.components.amesim.gases import (
|
||
AMESIM_GAS_INDEX_PARAMETER,
|
||
normalize_amesim_gas_index,
|
||
)
|
||
from app.simulation.core.base import AlgebraicComponent
|
||
from app.simulation.core.catalog import (
|
||
ComponentDisplaySpec,
|
||
ParameterGroupDisplaySpec,
|
||
PortDisplaySpec,
|
||
)
|
||
from app.simulation.core.equations import EquationResidual
|
||
from app.simulation.core.metadata import (
|
||
ParameterCondition,
|
||
ParameterDefinition,
|
||
ParameterOption,
|
||
ResultVariableDefinition,
|
||
)
|
||
from app.simulation.core.medium import GasMedium
|
||
from app.simulation.core.ports import PortDefinition
|
||
|
||
|
||
_FLOW_COEFFICIENT_OPTIONS = (
|
||
ParameterOption(1.0, "Cq"),
|
||
ParameterOption(2.0, "Cv"),
|
||
ParameterOption(3.0, "Kv"),
|
||
)
|
||
_FLOWSET_USES_CQ = (ParameterCondition("flowset", (1.0,)),)
|
||
_FLOWSET_USES_CV = (ParameterCondition("flowset", (2.0,)),)
|
||
_FLOWSET_USES_KV = (ParameterCondition("flowset", (3.0,)),)
|
||
_PN_PRESSURE_RATIO_ACCURACY = 0.9999
|
||
_PN_LAMINAR_SMOOTHING_GAIN = 12.0
|
||
_PNVO001_CLOSED_OPENING_ABS_TOL = 1.0e-12
|
||
_PNOR001_FLOW_COEFFICIENT_GROUP = ParameterGroupDisplaySpec(
|
||
id="flow_coefficient",
|
||
label="流量系数",
|
||
parameters=("cq", "area", "Cv", "Kv"),
|
||
order=10,
|
||
)
|
||
_PNVO001_FLOW_COEFFICIENT_GROUP = ParameterGroupDisplaySpec(
|
||
id="flow_coefficient",
|
||
label="流量系数",
|
||
parameters=("cq", "area0", "Cv", "Kv"),
|
||
order=10,
|
||
)
|
||
|
||
|
||
class AmesimPnor001(AlgebraicComponent):
|
||
"""AMESim PNOR001 constant-flow-coefficient pneumatic orifice.
|
||
|
||
This public component preserves the PNOR001 catalog/XML contract and uses
|
||
real-gas pressure-ratio flow with AMESim-style near-equal-pressure smoothing.
|
||
"""
|
||
|
||
MODEL_TYPE = "amesim_pnor001"
|
||
MODEL_VERSION = "0.3.0"
|
||
PRESSURE_FLOW_DEPENDS_ON_STREAM = True
|
||
PRESSURE_FLOW_EXACT_SUM_TO_ZERO_EQUATION_SUFFIXES = frozenset(
|
||
("mass_flow_balance",)
|
||
)
|
||
PORTS = (
|
||
PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),
|
||
PortDefinition.pneumatic("port_2", nominal_role="bidirectional"),
|
||
)
|
||
PARAMETERS = (
|
||
AMESIM_GAS_INDEX_PARAMETER,
|
||
ParameterDefinition(
|
||
"cq",
|
||
0.72,
|
||
label="流量系数 Cq",
|
||
quantity="dimensionless",
|
||
unit="",
|
||
minimum=1.0e-10,
|
||
maximum=1.0,
|
||
description="孔口实际质量流量相对于理想可压缩流量的无量纲修正系数。",
|
||
visible_when=_FLOWSET_USES_CQ,
|
||
),
|
||
ParameterDefinition(
|
||
"area",
|
||
5.0e-6,
|
||
label="孔口面积",
|
||
quantity="area",
|
||
unit="m2",
|
||
minimum=0.0,
|
||
maximum=1.0,
|
||
description="选择 Cq/面积方式时用于流量计算的有效孔口面积。",
|
||
visible_when=_FLOWSET_USES_CQ,
|
||
),
|
||
ParameterDefinition(
|
||
"Cv",
|
||
0.5,
|
||
label="流量系数 Cv",
|
||
quantity="dimensionless",
|
||
unit="",
|
||
minimum=0.0,
|
||
description="选择 Cv 方式时使用的英制流量系数。",
|
||
visible_when=_FLOWSET_USES_CV,
|
||
),
|
||
ParameterDefinition(
|
||
"Kv",
|
||
0.4,
|
||
label="流量系数 Kv",
|
||
quantity="dimensionless",
|
||
unit="",
|
||
minimum=0.0,
|
||
description="选择 Kv 方式时使用的公制流量系数。",
|
||
visible_when=_FLOWSET_USES_KV,
|
||
),
|
||
ParameterDefinition(
|
||
"flowset",
|
||
1.0,
|
||
label="流量系数设置",
|
||
quantity="dimensionless",
|
||
unit="",
|
||
minimum=1.0,
|
||
maximum=3.0,
|
||
editor="choice",
|
||
options=_FLOW_COEFFICIENT_OPTIONS,
|
||
description="流量参数方式:1 使用 Cq 和面积,2 使用 Cv,3 使用 Kv。",
|
||
),
|
||
)
|
||
RESULT_VARIABLES = (
|
||
ResultVariableDefinition(
|
||
"cm",
|
||
label="质量流量参数",
|
||
quantity="dimensionless",
|
||
unit="",
|
||
category="derived",
|
||
order=10,
|
||
),
|
||
ResultVariableDefinition(
|
||
"gasvel",
|
||
label="缩流截面气体速度",
|
||
quantity="velocity",
|
||
unit="m/s",
|
||
category="derived",
|
||
order=20,
|
||
),
|
||
)
|
||
DISPLAY = ComponentDisplaySpec(
|
||
label="PNOR001 常系数气动孔口",
|
||
library_id="amesim",
|
||
category_id="flow",
|
||
symbol="amesim_pnor001",
|
||
ports=(
|
||
PortDisplaySpec("port_1", "left", order=10),
|
||
PortDisplaySpec("port_2", "right", order=20),
|
||
),
|
||
order=10,
|
||
parameter_groups=(_PNOR001_FLOW_COEFFICIENT_GROUP,),
|
||
)
|
||
|
||
def __init__(
|
||
self,
|
||
name: str,
|
||
medium: GasMedium,
|
||
*,
|
||
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 = normalize_amesim_gas_index(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
|
||
self._connected_h: dict[str, float] = {}
|
||
|
||
@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: GasMedium,
|
||
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)
|
||
inlet_h = self._connected_h.get(port_name, port.h_outflow)
|
||
return max(
|
||
self.medium.temperature_from_pressure_enthalpy(
|
||
max(port.p, 1.0),
|
||
inlet_h,
|
||
),
|
||
1.0,
|
||
)
|
||
|
||
@staticmethod
|
||
def _subsonic_mass_flow_parameter(
|
||
*,
|
||
pressure_ratio: float,
|
||
gamma_s: float,
|
||
density: float,
|
||
upstream_temperature: float,
|
||
upstream_pressure: float,
|
||
) -> float:
|
||
expansion = (
|
||
pressure_ratio ** (2.0 * gamma_s)
|
||
- pressure_ratio ** (1.0 + gamma_s)
|
||
)
|
||
return sqrt(
|
||
max(
|
||
2.0
|
||
/ (1.0 - gamma_s)
|
||
* density
|
||
* upstream_temperature
|
||
/ upstream_pressure
|
||
* expansion,
|
||
0.0,
|
||
)
|
||
)
|
||
|
||
@lru_cache(maxsize=32768)
|
||
def _one_way_flow_characteristics(
|
||
self,
|
||
*,
|
||
upstream_pressure: float,
|
||
downstream_pressure: float,
|
||
upstream_temperature: float,
|
||
) -> tuple[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_s = self.medium.isentropic_density_pressure_factor(
|
||
p_up,
|
||
T_up,
|
||
p_down,
|
||
)
|
||
gamma_s = min(max(gamma_s, 1.0e-9), 1.0 - 1.0e-9)
|
||
density = max(self.medium.density(p_up, T_up), 1.0e-12)
|
||
pressure_ratio = max(p_down / p_up, 0.0)
|
||
critical_ratio = (2.0 * gamma_s / (gamma_s + 1.0)) ** (
|
||
1.0 / (1.0 - gamma_s)
|
||
)
|
||
if pressure_ratio <= critical_ratio:
|
||
effective_pressure_ratio = critical_ratio
|
||
mass_flow_parameter = (
|
||
sqrt(2.0 / (1.0 + gamma_s) * density * T_up / p_up)
|
||
* (2.0 * gamma_s / (gamma_s + 1.0))
|
||
** (gamma_s / (1.0 - gamma_s))
|
||
)
|
||
gas_velocity = sqrt(2.0 / (1.0 + gamma_s) * p_up / density)
|
||
else:
|
||
effective_pressure_ratio = pressure_ratio
|
||
mass_flow_parameter = self._subsonic_mass_flow_parameter(
|
||
pressure_ratio=pressure_ratio,
|
||
gamma_s=gamma_s,
|
||
density=density,
|
||
upstream_temperature=T_up,
|
||
upstream_pressure=p_up,
|
||
)
|
||
gas_velocity = sqrt(
|
||
max(
|
||
2.0
|
||
/ (1.0 - gamma_s)
|
||
* p_up
|
||
/ density
|
||
* (1.0 - pressure_ratio ** (1.0 - gamma_s)),
|
||
0.0,
|
||
)
|
||
)
|
||
|
||
reference = self._subsonic_mass_flow_parameter(
|
||
pressure_ratio=_PN_PRESSURE_RATIO_ACCURACY,
|
||
gamma_s=gamma_s,
|
||
density=density,
|
||
upstream_temperature=T_up,
|
||
upstream_pressure=p_up,
|
||
)
|
||
if mass_flow_parameter > 0.0 and reference > 0.0:
|
||
argument = (
|
||
_PN_LAMINAR_SMOOTHING_GAIN
|
||
* abs(mass_flow_parameter / reference)
|
||
* log(effective_pressure_ratio)
|
||
/ log(_PN_PRESSURE_RATIO_ACCURACY)
|
||
)
|
||
smoothing_factor = tanh(max(argument, 0.0))
|
||
mass_flow_parameter *= smoothing_factor
|
||
gas_velocity *= smoothing_factor
|
||
return mass_flow_parameter, gas_velocity
|
||
|
||
def mass_flow(self, p_1: float, p_2: float) -> float:
|
||
if (
|
||
isclose(p_1, p_2, rel_tol=0.0, abs_tol=1.0e-8)
|
||
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)
|
||
T_up = max(upstream_temperature, 1.0)
|
||
mass_flow_parameter, _ = self._one_way_flow_characteristics(
|
||
upstream_pressure=p_up,
|
||
downstream_pressure=downstream_pressure,
|
||
upstream_temperature=T_up,
|
||
)
|
||
return (
|
||
self.effective_cq
|
||
* self.effective_area
|
||
* p_up
|
||
* mass_flow_parameter
|
||
/ sqrt(T_up)
|
||
)
|
||
|
||
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)
|
||
if p_1 >= p_2:
|
||
upstream_port_name = "port_1"
|
||
upstream_pressure = p_1
|
||
downstream_pressure = p_2
|
||
flow_direction = 1.0
|
||
else:
|
||
upstream_port_name = "port_2"
|
||
upstream_pressure = p_2
|
||
downstream_pressure = p_1
|
||
flow_direction = -1.0
|
||
mass_flow_parameter, gas_velocity = self._one_way_flow_characteristics(
|
||
upstream_pressure=upstream_pressure,
|
||
downstream_pressure=downstream_pressure,
|
||
upstream_temperature=self._upstream_temperature(upstream_port_name),
|
||
)
|
||
return {
|
||
"cm": mass_flow_parameter,
|
||
"gasvel": flow_direction * gas_velocity,
|
||
}
|
||
|
||
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
||
return (
|
||
self.port_1.m_flow + self.port_2.m_flow,
|
||
self.port_1.m_flow - self.mass_flow(self.port_1.p, self.port_2.p),
|
||
)
|
||
|
||
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._connected_h = dict(connected_h)
|
||
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.2.0"
|
||
PRESSURE_FLOW_DEPENDS_ON_STREAM = True
|
||
PRESSURE_FLOW_EXACT_SUM_TO_ZERO_EQUATION_SUFFIXES = frozenset(
|
||
("mass_flow_balance",)
|
||
)
|
||
PORTS = (
|
||
PortDefinition.pneumatic("port_2", nominal_role="bidirectional"),
|
||
PortDefinition.pneumatic("port_3", nominal_role="bidirectional"),
|
||
)
|
||
PARAMETERS = (
|
||
AMESIM_GAS_INDEX_PARAMETER,
|
||
ParameterDefinition(
|
||
"cq",
|
||
0.72,
|
||
label="流量系数 Cq",
|
||
quantity="dimensionless",
|
||
unit="",
|
||
minimum=1.0e-10,
|
||
maximum=1.0,
|
||
description="孔口实际质量流量相对于理想可压缩流量的无量纲修正系数。",
|
||
visible_when=_FLOWSET_USES_CQ,
|
||
),
|
||
ParameterDefinition(
|
||
"area0",
|
||
5.0e-6,
|
||
label="最大孔口面积",
|
||
quantity="area",
|
||
unit="m2",
|
||
minimum=0.0,
|
||
maximum=1.0,
|
||
description="阀门完全开启时的最大有效孔口面积。",
|
||
visible_when=_FLOWSET_USES_CQ,
|
||
),
|
||
ParameterDefinition(
|
||
"Cv",
|
||
0.5,
|
||
label="最大流量系数 Cv",
|
||
quantity="dimensionless",
|
||
unit="",
|
||
minimum=0.0,
|
||
description="选择 Cv 方式时使用的最大英制流量系数。",
|
||
visible_when=_FLOWSET_USES_CV,
|
||
),
|
||
ParameterDefinition(
|
||
"Kv",
|
||
0.4,
|
||
label="最大流量系数 Kv",
|
||
quantity="dimensionless",
|
||
unit="",
|
||
minimum=0.0,
|
||
description="选择 Kv 方式时使用的最大公制流量系数。",
|
||
visible_when=_FLOWSET_USES_KV,
|
||
),
|
||
ParameterDefinition(
|
||
"flowset",
|
||
1.0,
|
||
label="流量系数设置",
|
||
quantity="dimensionless",
|
||
unit="",
|
||
minimum=1.0,
|
||
maximum=3.0,
|
||
editor="choice",
|
||
options=_FLOW_COEFFICIENT_OPTIONS,
|
||
description="流量参数方式:1 使用 Cq 和面积,2 使用 Cv,3 使用 Kv。",
|
||
),
|
||
ParameterDefinition(
|
||
"opening",
|
||
1.0,
|
||
label="固定开度",
|
||
quantity="dimensionless",
|
||
unit="",
|
||
minimum=0.0,
|
||
maximum=1.0,
|
||
description="固定的归一化阀门开度;0 表示关闭,1 表示完全开启。",
|
||
),
|
||
)
|
||
RESULT_VARIABLES = (
|
||
ResultVariableDefinition(
|
||
"xv",
|
||
label="有效开度",
|
||
quantity="dimensionless",
|
||
unit="",
|
||
category="derived",
|
||
order=10,
|
||
),
|
||
ResultVariableDefinition(
|
||
"cm",
|
||
label="质量流量参数",
|
||
quantity="dimensionless",
|
||
unit="",
|
||
category="derived",
|
||
order=20,
|
||
),
|
||
ResultVariableDefinition(
|
||
"gasvel",
|
||
label="缩流截面气体速度",
|
||
quantity="velocity",
|
||
unit="m/s",
|
||
category="derived",
|
||
order=30,
|
||
),
|
||
)
|
||
DISPLAY = ComponentDisplaySpec(
|
||
label="PNVO001 固定开度气动孔口",
|
||
library_id="amesim",
|
||
category_id="flow",
|
||
symbol="amesim_pnvo001_fixed",
|
||
ports=(
|
||
# AMESim default geometry places port 2 right and port 3 left.
|
||
PortDisplaySpec("port_2", "right", order=10),
|
||
PortDisplaySpec("port_3", "left", order=20),
|
||
),
|
||
order=30,
|
||
parameter_groups=(_PNVO001_FLOW_COEFFICIENT_GROUP,),
|
||
)
|
||
|
||
def __init__(
|
||
self,
|
||
name: str,
|
||
medium: GasMedium,
|
||
*,
|
||
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 = normalize_amesim_gas_index(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
|
||
self._connected_h: dict[str, float] = {}
|
||
|
||
@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: GasMedium,
|
||
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)
|
||
# A component port's h_outflow describes fluid leaving the valve; the
|
||
# upstream state comes from the connection on that same physical side.
|
||
inlet_h = self._connected_h.get(port_name, port.h_outflow)
|
||
return max(
|
||
self.medium.temperature_from_pressure_enthalpy(
|
||
max(port.p, 1.0),
|
||
inlet_h,
|
||
),
|
||
1.0,
|
||
)
|
||
|
||
@staticmethod
|
||
def _subsonic_mass_flow_parameter(
|
||
*,
|
||
pressure_ratio: float,
|
||
gamma_s: float,
|
||
density: float,
|
||
upstream_temperature: float,
|
||
upstream_pressure: float,
|
||
) -> float:
|
||
expansion = (
|
||
pressure_ratio ** (2.0 * gamma_s)
|
||
- pressure_ratio ** (1.0 + gamma_s)
|
||
)
|
||
return sqrt(
|
||
max(
|
||
2.0
|
||
/ (1.0 - gamma_s)
|
||
* density
|
||
* upstream_temperature
|
||
/ upstream_pressure
|
||
* expansion,
|
||
0.0,
|
||
)
|
||
)
|
||
|
||
def mass_flow(self, p_2: float, p_3: float) -> float:
|
||
if (
|
||
isclose(p_2, p_3, rel_tol=0.0, abs_tol=1.0e-8)
|
||
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"),
|
||
)
|
||
|
||
@lru_cache(maxsize=32768)
|
||
def _one_way_flow_characteristics(
|
||
self,
|
||
*,
|
||
upstream_pressure: float,
|
||
downstream_pressure: float,
|
||
upstream_temperature: float,
|
||
) -> tuple[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_s = self.medium.isentropic_density_pressure_factor(
|
||
p_up,
|
||
T_up,
|
||
p_down,
|
||
)
|
||
gamma_s = min(max(gamma_s, 1.0e-9), 1.0 - 1.0e-9)
|
||
density = max(self.medium.density(p_up, T_up), 1.0e-12)
|
||
pressure_ratio = max(p_down / p_up, 0.0)
|
||
critical_ratio = (2.0 * gamma_s / (gamma_s + 1.0)) ** (
|
||
1.0 / (1.0 - gamma_s)
|
||
)
|
||
if pressure_ratio <= critical_ratio:
|
||
effective_pressure_ratio = critical_ratio
|
||
mass_flow_parameter = (
|
||
sqrt(2.0 / (1.0 + gamma_s) * density * T_up / p_up)
|
||
* (2.0 * gamma_s / (gamma_s + 1.0))
|
||
** (gamma_s / (1.0 - gamma_s))
|
||
)
|
||
gas_velocity = sqrt(
|
||
2.0 / (1.0 + gamma_s) * p_up / density
|
||
)
|
||
else:
|
||
effective_pressure_ratio = pressure_ratio
|
||
mass_flow_parameter = self._subsonic_mass_flow_parameter(
|
||
pressure_ratio=pressure_ratio,
|
||
gamma_s=gamma_s,
|
||
density=density,
|
||
upstream_temperature=T_up,
|
||
upstream_pressure=p_up,
|
||
)
|
||
gas_velocity = sqrt(
|
||
max(
|
||
2.0
|
||
/ (1.0 - gamma_s)
|
||
* p_up
|
||
/ density
|
||
* (1.0 - pressure_ratio ** (1.0 - gamma_s)),
|
||
0.0,
|
||
)
|
||
)
|
||
|
||
# AMESim's gas_cm_prc_ applies this factor continuously over the
|
||
# complete pressure-ratio range. It is effectively one outside the
|
||
# near-equal-pressure region and makes Cm (and vena-contracta
|
||
# velocity) approach zero quadratically as the pressure ratio tends
|
||
# to one. The reference Cm intentionally reuses the current gamma_s.
|
||
reference_mass_flow_parameter = self._subsonic_mass_flow_parameter(
|
||
pressure_ratio=_PN_PRESSURE_RATIO_ACCURACY,
|
||
gamma_s=gamma_s,
|
||
density=density,
|
||
upstream_temperature=T_up,
|
||
upstream_pressure=p_up,
|
||
)
|
||
if mass_flow_parameter > 0.0 and reference_mass_flow_parameter > 0.0:
|
||
smoothing_argument = (
|
||
_PN_LAMINAR_SMOOTHING_GAIN
|
||
* abs(mass_flow_parameter / reference_mass_flow_parameter)
|
||
* log(effective_pressure_ratio)
|
||
/ log(_PN_PRESSURE_RATIO_ACCURACY)
|
||
)
|
||
smoothing_factor = tanh(max(smoothing_argument, 0.0))
|
||
mass_flow_parameter *= smoothing_factor
|
||
gas_velocity *= smoothing_factor
|
||
return mass_flow_parameter, gas_velocity
|
||
|
||
def _one_way_mass_flow(
|
||
self,
|
||
*,
|
||
upstream_pressure: float,
|
||
downstream_pressure: float,
|
||
upstream_temperature: float,
|
||
) -> float:
|
||
p_up = max(upstream_pressure, 1.0)
|
||
T_up = max(upstream_temperature, 1.0)
|
||
mass_flow_parameter, _gas_velocity = self._one_way_flow_characteristics(
|
||
upstream_pressure=p_up,
|
||
downstream_pressure=downstream_pressure,
|
||
upstream_temperature=T_up,
|
||
)
|
||
return (
|
||
self.effective_cq
|
||
* self.effective_area
|
||
* p_up
|
||
* mass_flow_parameter
|
||
/ sqrt(T_up)
|
||
)
|
||
|
||
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)
|
||
if p_2 >= p_3:
|
||
upstream_port_name = "port_2"
|
||
upstream_pressure = p_2
|
||
downstream_pressure = p_3
|
||
flow_direction = 1.0
|
||
else:
|
||
upstream_port_name = "port_3"
|
||
upstream_pressure = p_3
|
||
downstream_pressure = p_2
|
||
flow_direction = -1.0
|
||
upstream_temperature = self._upstream_temperature(
|
||
upstream_port_name
|
||
)
|
||
mass_flow_parameter, gas_velocity = self._one_way_flow_characteristics(
|
||
upstream_pressure=upstream_pressure,
|
||
downstream_pressure=downstream_pressure,
|
||
upstream_temperature=upstream_temperature,
|
||
)
|
||
# AMESim reports no vena-contracta velocity while the valve is closed.
|
||
# Signal propagation around a step can leave a round-off-sized opening,
|
||
# so apply the same numerical-zero convention to this diagnostic only.
|
||
if isclose(
|
||
self.opening,
|
||
0.0,
|
||
rel_tol=0.0,
|
||
abs_tol=_PNVO001_CLOSED_OPENING_ABS_TOL,
|
||
):
|
||
gas_velocity = 0.0
|
||
return {
|
||
"xv": self.opening,
|
||
"cm": mass_flow_parameter,
|
||
"gasvel": flow_direction * gas_velocity,
|
||
}
|
||
|
||
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
||
return (
|
||
self.port_2.m_flow + self.port_3.m_flow,
|
||
self.port_2.m_flow - self.mass_flow(self.port_2.p, self.port_3.p),
|
||
)
|
||
|
||
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._connected_h = dict(connected_h)
|
||
self.port_2.h_outflow = connected_h["port_3"]
|
||
self.port_3.h_outflow = connected_h["port_2"]
|
||
|
||
|
||
class AmesimPnvo001SignalOpening(AmesimPnvo001FixedOpening):
|
||
"""AMESim PNVO001 signal-controlled pneumatic orifice."""
|
||
|
||
MODEL_TYPE = "amesim_pnvo001"
|
||
MODEL_VERSION = "0.2.0"
|
||
PRESSURE_FLOW_DEPENDS_ON_STREAM = True
|
||
# Repeat the exact-sum promise on this concrete subclass deliberately.
|
||
PRESSURE_FLOW_EXACT_SUM_TO_ZERO_EQUATION_SUFFIXES = frozenset(
|
||
("mass_flow_balance",)
|
||
)
|
||
PORTS = (
|
||
PortDefinition.signal("res", nominal_role="input"),
|
||
PortDefinition.pneumatic("port_2", nominal_role="bidirectional"),
|
||
PortDefinition.pneumatic("port_3", nominal_role="bidirectional"),
|
||
)
|
||
PARAMETERS = (
|
||
AMESIM_GAS_INDEX_PARAMETER,
|
||
ParameterDefinition(
|
||
"cq",
|
||
0.72,
|
||
label="流量系数 Cq",
|
||
quantity="dimensionless",
|
||
unit="",
|
||
minimum=1.0e-10,
|
||
maximum=1.0,
|
||
description="孔口实际质量流量相对于理想可压缩流量的无量纲修正系数。",
|
||
visible_when=_FLOWSET_USES_CQ,
|
||
),
|
||
ParameterDefinition(
|
||
"area0",
|
||
5.0e-6,
|
||
label="最大孔口面积",
|
||
quantity="area",
|
||
unit="m2",
|
||
minimum=0.0,
|
||
maximum=1.0,
|
||
description="阀门完全开启时的最大有效孔口面积。",
|
||
visible_when=_FLOWSET_USES_CQ,
|
||
),
|
||
ParameterDefinition(
|
||
"Cv",
|
||
0.5,
|
||
label="最大流量系数 Cv",
|
||
quantity="dimensionless",
|
||
unit="",
|
||
minimum=0.0,
|
||
description="选择 Cv 方式时使用的最大英制流量系数。",
|
||
visible_when=_FLOWSET_USES_CV,
|
||
),
|
||
ParameterDefinition(
|
||
"Kv",
|
||
0.4,
|
||
label="最大流量系数 Kv",
|
||
quantity="dimensionless",
|
||
unit="",
|
||
minimum=0.0,
|
||
description="选择 Kv 方式时使用的最大公制流量系数。",
|
||
visible_when=_FLOWSET_USES_KV,
|
||
),
|
||
ParameterDefinition(
|
||
"flowset",
|
||
1.0,
|
||
label="流量系数设置",
|
||
quantity="dimensionless",
|
||
unit="",
|
||
minimum=1.0,
|
||
maximum=3.0,
|
||
editor="choice",
|
||
options=_FLOW_COEFFICIENT_OPTIONS,
|
||
description="流量参数方式:1 使用 Cq 和面积,2 使用 Cv,3 使用 Kv。",
|
||
),
|
||
ParameterDefinition(
|
||
"opening0",
|
||
1.0,
|
||
label="初始开度",
|
||
quantity="dimensionless",
|
||
unit="",
|
||
minimum=0.0,
|
||
maximum=1.0,
|
||
description="信号尚未传播时使用的归一化初始开度;0 表示关闭,1 表示完全开启。",
|
||
),
|
||
)
|
||
RESULT_VARIABLES = AmesimPnvo001FixedOpening.RESULT_VARIABLES
|
||
DISPLAY = ComponentDisplaySpec(
|
||
label="PNVO001 信号开度气动孔口",
|
||
library_id="amesim",
|
||
category_id="flow",
|
||
symbol="amesim_pnvo001",
|
||
ports=(
|
||
PortDisplaySpec("res", "left", order=5),
|
||
# AMESim default geometry places port 2 right and port 3 left.
|
||
PortDisplaySpec("port_2", "right", order=10),
|
||
PortDisplaySpec("port_3", "left", order=20),
|
||
),
|
||
order=35,
|
||
parameter_groups=(_PNVO001_FLOW_COEFFICIENT_GROUP,),
|
||
)
|
||
|
||
def __init__(
|
||
self,
|
||
name: str,
|
||
medium: GasMedium,
|
||
*,
|
||
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,
|
||
opening0: float = 1.0,
|
||
) -> None:
|
||
AlgebraicComponent.__init__(self, name=name)
|
||
self.set_parameter_values(
|
||
{
|
||
"cq": cq,
|
||
"area0": area0,
|
||
"Cv": Cv,
|
||
"Kv": Kv,
|
||
"gi": gi,
|
||
"flowset": flowset,
|
||
"opening0": opening0,
|
||
}
|
||
)
|
||
self.medium = medium
|
||
self.cq = float(cq)
|
||
self.area0 = float(area0)
|
||
self.Cv = float(Cv)
|
||
self.Kv = float(Kv)
|
||
self.gi = normalize_amesim_gas_index(gi)
|
||
self.flowset = self._integer_parameter("flowset", flowset)
|
||
if self.flowset not in {1, 2, 3}:
|
||
raise ValueError("PNVO001 signal-opening flowset must be 1, 2, or 3.")
|
||
self.opening0 = min(1.0, max(0.0, float(opening0)))
|
||
self.res = self.register_declared_port("res")
|
||
self.res.signal = self.opening0
|
||
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
|
||
self._connected_h: dict[str, float] = {}
|
||
|
||
@classmethod
|
||
def create(
|
||
cls,
|
||
*,
|
||
name: str,
|
||
medium: GasMedium,
|
||
parameters: Mapping[str, float],
|
||
) -> "AmesimPnvo001SignalOpening":
|
||
return cls(name=name, medium=medium, **dict(parameters))
|
||
|
||
@property
|
||
def opening(self) -> float:
|
||
return min(1.0, max(0.0, self.res.signal))
|