1472 lines
50 KiB
Python
1472 lines
50 KiB
Python
from __future__ import annotations
|
|
|
|
from collections.abc import Mapping
|
|
from functools import lru_cache
|
|
from math import isclose, log, log10, pi, sqrt, tanh
|
|
|
|
from app.simulation.components.amesim.gases import (
|
|
AMESIM_GAS_INDEX_PARAMETER,
|
|
normalize_amesim_gas_index,
|
|
)
|
|
from app.simulation.core.base import AlgebraicComponent, DynamicComponent, ThermodynamicVolumeComponent
|
|
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,
|
|
THERMODYNAMIC_VOLUME_RESULT_VARIABLES,
|
|
)
|
|
from app.simulation.core.medium import GasMedium, ThermodynamicProperties
|
|
from app.simulation.core.ports import PortDefinition
|
|
from app.simulation.core.state import VolumeState
|
|
|
|
|
|
_DYNAMIC_PIPE_POLYTROPIC_MODE = ParameterCondition("mode", (1.0,))
|
|
_DYNAMIC_PIPE_HEAT_EXCHANGE_MODE = ParameterCondition("mode", (2.0,))
|
|
_DYNAMIC_PIPE_PARAMETER_GROUPS = (
|
|
ParameterGroupDisplaySpec(
|
|
id="thermodynamics",
|
|
label="热力学",
|
|
parameters=("k", "kth", "extemp"),
|
|
order=10,
|
|
),
|
|
)
|
|
|
|
|
|
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.3.0"
|
|
PORTS = (
|
|
PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),
|
|
PortDefinition.pneumatic("port_2", nominal_role="bidirectional"),
|
|
)
|
|
PARAMETERS = (
|
|
AMESIM_GAS_INDEX_PARAMETER,
|
|
ParameterDefinition(
|
|
"diam",
|
|
0.01,
|
|
label="管径",
|
|
quantity="length",
|
|
unit="m",
|
|
minimum=0.0,
|
|
minimum_exclusive=True,
|
|
description="管路的有效内径,用于计算流通面积和摩擦压降。",
|
|
),
|
|
ParameterDefinition(
|
|
"le",
|
|
1.0,
|
|
label="管长",
|
|
quantity="length",
|
|
unit="m",
|
|
minimum=0.0,
|
|
minimum_exclusive=True,
|
|
description="参与摩擦压降计算的管路有效长度。",
|
|
),
|
|
ParameterDefinition(
|
|
"rr",
|
|
1.0e-5,
|
|
label="相对粗糙度",
|
|
quantity="dimensionless",
|
|
unit="",
|
|
minimum=0.0,
|
|
maximum=0.1,
|
|
description="管壁绝对粗糙度与管径之比,用于计算 Darcy 摩擦因子。",
|
|
),
|
|
)
|
|
RESULT_VARIABLES = (
|
|
ResultVariableDefinition(
|
|
"re",
|
|
label="Reynolds 数",
|
|
quantity="dimensionless",
|
|
unit="",
|
|
category="derived",
|
|
order=10,
|
|
),
|
|
ResultVariableDefinition(
|
|
"cm",
|
|
label="质量流量参数",
|
|
quantity="dimensionless",
|
|
unit="",
|
|
category="derived",
|
|
order=20,
|
|
),
|
|
ResultVariableDefinition(
|
|
"v",
|
|
label="平均气体速度",
|
|
quantity="velocity",
|
|
unit="m/s",
|
|
category="derived",
|
|
order=30,
|
|
),
|
|
ResultVariableDefinition(
|
|
"ff",
|
|
label="摩擦因子",
|
|
quantity="dimensionless",
|
|
unit="",
|
|
category="derived",
|
|
order=40,
|
|
),
|
|
)
|
|
DISPLAY = ComponentDisplaySpec(
|
|
label="PNL00R 气动管路阻力",
|
|
library_id="amesim",
|
|
category_id="flow",
|
|
symbol="amesim_pnl00r",
|
|
ports=(
|
|
PortDisplaySpec("port_1", "left", order=10),
|
|
PortDisplaySpec("port_2", "right", order=20),
|
|
),
|
|
order=20,
|
|
)
|
|
|
|
def __init__(
|
|
self,
|
|
name: str,
|
|
medium: GasMedium,
|
|
*,
|
|
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 = normalize_amesim_gas_index(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: GasMedium,
|
|
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)
|
|
return max(
|
|
self.medium.temperature_from_pressure_enthalpy(
|
|
max(port.p, 1.0),
|
|
port.h_outflow,
|
|
),
|
|
1.0,
|
|
)
|
|
|
|
def _dynamic_viscosity(self, temperature_k: float) -> float:
|
|
return self.medium.dynamic_viscosity(temperature_k)
|
|
|
|
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
|
|
|
|
# pn2pipefr does not apply the fully rough correction at every
|
|
# turbulent Reynolds number. Its saved ff curves first follow the
|
|
# hydraulically smooth law and approach the rough asymptote as Re*rr
|
|
# grows. Keeping those two limits separate reproduces the AMESim
|
|
# curves for both 14 mm and 20 mm test_mql pipes; putting both terms
|
|
# directly inside one Haaland logarithm over-predicts PNL0002 friction
|
|
# by about 23 percent near Re=57,000.
|
|
smooth_turbulent = 1.0 / (
|
|
-1.8 * log10(6.9 / reynolds_number)
|
|
) ** 2
|
|
if self.rr <= 0.0:
|
|
turbulent = smooth_turbulent
|
|
else:
|
|
# Nikuradse's fully rough asymptote is the Re-independent limit
|
|
# of Colebrook. Haaland's rounded all-regime approximation is
|
|
# about 0.2003% high at the test_mql roughness values, enough to
|
|
# bias its long high-Re PNL0001 filling transient.
|
|
fully_rough = 1.0 / (
|
|
-2.0 * log10(self.rr / 3.7)
|
|
) ** 2
|
|
roughness_reynolds = reynolds_number * self.rr
|
|
roughness_weight = roughness_reynolds * roughness_reynolds / (
|
|
roughness_reynolds * roughness_reynolds + 180.0 * 180.0
|
|
)
|
|
turbulent = smooth_turbulent + roughness_weight * (
|
|
fully_rough - smooth_turbulent
|
|
)
|
|
if reynolds_number >= 4000.0:
|
|
return turbulent
|
|
fraction = (reynolds_number - 2300.0) / 1700.0
|
|
return laminar + fraction**0.58 * (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:
|
|
return 1.0e3
|
|
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 isclose(p_1, p_2, rel_tol=1.0e-7, abs_tol=1.0e-9):
|
|
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)
|
|
* sqrt(upstream_temperature)
|
|
/ 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"]
|
|
|
|
|
|
class AmesimPnl0001(ThermodynamicVolumeComponent):
|
|
"""AMESim PNL0001 C-R pneumatic pipe with compressibility and friction."""
|
|
|
|
MODEL_TYPE = "amesim_pnl0001"
|
|
MODEL_VERSION = "0.4.0"
|
|
PORTS = (
|
|
PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),
|
|
PortDefinition.pneumatic("port_2", nominal_role="bidirectional"),
|
|
)
|
|
PARAMETERS = (
|
|
AMESIM_GAS_INDEX_PARAMETER,
|
|
ParameterDefinition(
|
|
"diam",
|
|
0.01,
|
|
label="管径",
|
|
quantity="length",
|
|
unit="m",
|
|
minimum=0.0,
|
|
minimum_exclusive=True,
|
|
description="管路的有效内径,用于计算流通面积、储气容积和摩擦压降。",
|
|
),
|
|
ParameterDefinition(
|
|
"le",
|
|
1.0,
|
|
label="管长",
|
|
quantity="length",
|
|
unit="m",
|
|
minimum=0.0,
|
|
minimum_exclusive=True,
|
|
description="管路的有效长度,用于计算储气容积、换热面积和摩擦压降。",
|
|
),
|
|
ParameterDefinition(
|
|
"rr",
|
|
1.0e-5,
|
|
label="相对粗糙度",
|
|
quantity="dimensionless",
|
|
unit="",
|
|
minimum=0.0,
|
|
maximum=0.1,
|
|
description="管壁绝对粗糙度与管径之比,用于计算 Darcy 摩擦因子。",
|
|
),
|
|
ParameterDefinition(
|
|
"k",
|
|
1.35,
|
|
label="多方指数",
|
|
quantity="dimensionless",
|
|
unit="",
|
|
minimum=0.0,
|
|
minimum_exclusive=True,
|
|
maximum=2.0,
|
|
description=(
|
|
"mode=1 多方过程使用的指数;当前公开求解器保留该 AMESim "
|
|
"配置,尚未实现多方指数对状态方程的修正。"
|
|
),
|
|
visible_when=(_DYNAMIC_PIPE_POLYTROPIC_MODE,),
|
|
),
|
|
ParameterDefinition(
|
|
"kth",
|
|
0.0,
|
|
label="换热系数",
|
|
quantity="heat_transfer_coefficient",
|
|
unit="W/(m2*K)",
|
|
minimum=0.0,
|
|
description="mode=2 带换热过程使用的气体与外部环境对流换热系数。",
|
|
visible_when=(_DYNAMIC_PIPE_HEAT_EXCHANGE_MODE,),
|
|
),
|
|
ParameterDefinition(
|
|
"extemp",
|
|
293.15,
|
|
label="外部温度",
|
|
quantity="temperature",
|
|
unit="K",
|
|
minimum=0.0,
|
|
minimum_exclusive=True,
|
|
description="mode=2 带换热过程使用的外部环境绝对温度。",
|
|
visible_when=(_DYNAMIC_PIPE_HEAT_EXCHANGE_MODE,),
|
|
),
|
|
ParameterDefinition(
|
|
"mode",
|
|
2.0,
|
|
label="热模型",
|
|
quantity="dimensionless",
|
|
unit="",
|
|
minimum=1.0,
|
|
maximum=2.0,
|
|
editor="choice",
|
|
options=(
|
|
ParameterOption(1.0, "多方过程"),
|
|
ParameterOption(2.0, "带换热"),
|
|
),
|
|
description=(
|
|
"AMESim 原始编码:1 为多方过程,2 为带换热。当前公开求解器在"
|
|
"多方模式下关闭环境换热,在带换热模式下按换热系数和外部温度"
|
|
"计算环境换热。"
|
|
),
|
|
),
|
|
ParameterDefinition(
|
|
"p0",
|
|
100000.0,
|
|
label="初始压力",
|
|
quantity="pressure",
|
|
unit="Pa",
|
|
minimum=0.0,
|
|
minimum_exclusive=True,
|
|
description="仿真开始时管内气体的绝对压力。",
|
|
),
|
|
ParameterDefinition(
|
|
"T0",
|
|
293.15,
|
|
label="初始温度",
|
|
quantity="temperature",
|
|
unit="K",
|
|
minimum=0.0,
|
|
minimum_exclusive=True,
|
|
description="仿真开始时管内气体的绝对温度。",
|
|
),
|
|
)
|
|
RESULT_VARIABLES = THERMODYNAMIC_VOLUME_RESULT_VARIABLES + (
|
|
ResultVariableDefinition(
|
|
"re",
|
|
label="Reynolds 数",
|
|
quantity="dimensionless",
|
|
unit="",
|
|
category="derived",
|
|
order=100,
|
|
),
|
|
ResultVariableDefinition(
|
|
"cm",
|
|
label="质量流量参数",
|
|
quantity="dimensionless",
|
|
unit="",
|
|
category="derived",
|
|
order=110,
|
|
),
|
|
ResultVariableDefinition(
|
|
"v",
|
|
label="平均气体速度",
|
|
quantity="velocity",
|
|
unit="m/s",
|
|
category="derived",
|
|
order=120,
|
|
),
|
|
ResultVariableDefinition(
|
|
"ff",
|
|
label="摩擦因子",
|
|
quantity="dimensionless",
|
|
unit="",
|
|
category="derived",
|
|
order=130,
|
|
),
|
|
)
|
|
DISPLAY = ComponentDisplaySpec(
|
|
label="PNL0001 C-R 动态管路",
|
|
library_id="amesim",
|
|
category_id="flow",
|
|
symbol="amesim_pnl0001",
|
|
ports=(
|
|
PortDisplaySpec("port_1", "left", order=10),
|
|
PortDisplaySpec("port_2", "right", order=20),
|
|
),
|
|
order=30,
|
|
parameter_groups=_DYNAMIC_PIPE_PARAMETER_GROUPS,
|
|
)
|
|
|
|
def __init__(
|
|
self,
|
|
name: str,
|
|
medium: GasMedium,
|
|
*,
|
|
diam: float = 0.01,
|
|
le: float = 1.0,
|
|
rr: float = 1.0e-5,
|
|
k: float = 1.35,
|
|
kth: float = 0.0,
|
|
extemp: float = 293.15,
|
|
gi: float = 1.0,
|
|
mode: float = 2.0,
|
|
p0: float = 100000.0,
|
|
T0: float = 293.15,
|
|
) -> None:
|
|
super().__init__(name=name)
|
|
self.set_parameter_values(
|
|
{
|
|
"diam": diam,
|
|
"le": le,
|
|
"rr": rr,
|
|
"k": k,
|
|
"kth": kth,
|
|
"extemp": extemp,
|
|
"gi": gi,
|
|
"mode": mode,
|
|
"p0": p0,
|
|
"T0": T0,
|
|
}
|
|
)
|
|
self.medium = medium
|
|
self.diam = float(diam)
|
|
self.le = float(le)
|
|
self.rr = float(rr)
|
|
self.k = float(k)
|
|
self.kth = float(kth)
|
|
self.extemp = float(extemp)
|
|
self.gi = normalize_amesim_gas_index(gi)
|
|
self.mode = self._integer_parameter("mode", mode)
|
|
self.p0 = float(p0)
|
|
self.T0 = float(T0)
|
|
self.area = pi * self.diam * self.diam / 4.0
|
|
self.volume = self.area * self.le
|
|
self.exchange_area = pi * self.diam * self.le
|
|
m0 = medium.density(self.p0, self.T0) * self.volume
|
|
U0 = m0 * medium.specific_internal_energy_at_pressure(self.p0, self.T0)
|
|
self.state = VolumeState(m=m0, U=U0)
|
|
initial_h = medium.specific_enthalpy_at_pressure(self.p0, self.T0)
|
|
self.port_1 = self.register_declared_port("port_1")
|
|
self.port_1.p = self.p0
|
|
self.port_1.h_outflow = initial_h
|
|
self.port_2 = self.register_declared_port("port_2")
|
|
self.port_2.p = self.p0
|
|
self.port_2.h_outflow = initial_h
|
|
self._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"PNL0001 parameter {name} must be an integer value.")
|
|
integer = int(rounded)
|
|
if name == "mode" and integer not in {1, 2}:
|
|
raise ValueError("PNL0001 parameter mode must be one of 1, 2.")
|
|
return integer
|
|
|
|
@classmethod
|
|
def create(
|
|
cls,
|
|
*,
|
|
name: str,
|
|
medium: GasMedium,
|
|
parameters: Mapping[str, float],
|
|
) -> "AmesimPnl0001":
|
|
return cls(
|
|
name=name,
|
|
medium=medium,
|
|
diam=parameters["diam"],
|
|
le=parameters["le"],
|
|
rr=parameters["rr"],
|
|
k=parameters["k"],
|
|
kth=parameters["kth"],
|
|
extemp=parameters["extemp"],
|
|
gi=parameters["gi"],
|
|
mode=parameters["mode"],
|
|
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.volume)
|
|
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:
|
|
if self.mode == 1:
|
|
return 0.0
|
|
return self.kth * self.exchange_area * (self.extemp - temperature)
|
|
|
|
def _dynamic_viscosity(self, temperature_k: float) -> float:
|
|
return self.medium.dynamic_viscosity(temperature_k)
|
|
|
|
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:
|
|
return AmesimPnl00r.friction_factor(self, reynolds_number)
|
|
|
|
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:
|
|
return 1.0e3
|
|
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)
|
|
|
|
@lru_cache(maxsize=32768)
|
|
def _one_way_pn2pipefr_mass_flow(
|
|
self,
|
|
*,
|
|
upstream_pressure: float,
|
|
downstream_pressure: float,
|
|
upstream_temperature: float,
|
|
resistance_length: float,
|
|
) -> float:
|
|
"""AMESim pn2pipefr-style compressible friction flow."""
|
|
|
|
p_up = max(float(upstream_pressure), 1.0)
|
|
p_down = max(min(float(downstream_pressure), p_up), 0.0)
|
|
T_up = max(float(upstream_temperature), 1.0)
|
|
if resistance_length <= 0.0:
|
|
raise ValueError("Pipe resistance length must be positive.")
|
|
|
|
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)
|
|
)
|
|
|
|
def mass_flow_parameter(ratio: float) -> float:
|
|
if ratio <= critical_ratio:
|
|
value = (
|
|
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))
|
|
)
|
|
effective_ratio = critical_ratio
|
|
else:
|
|
expansion = ratio ** (2.0 * gamma_s) - ratio ** (1.0 + gamma_s)
|
|
value = sqrt(
|
|
max(
|
|
2.0
|
|
/ (1.0 - gamma_s)
|
|
* density
|
|
* T_up
|
|
/ p_up
|
|
* expansion,
|
|
0.0,
|
|
)
|
|
)
|
|
effective_ratio = ratio
|
|
|
|
accuracy = 0.9999
|
|
reference_expansion = (
|
|
accuracy ** (2.0 * gamma_s)
|
|
- accuracy ** (1.0 + gamma_s)
|
|
)
|
|
reference = sqrt(
|
|
max(
|
|
2.0
|
|
/ (1.0 - gamma_s)
|
|
* density
|
|
* T_up
|
|
/ p_up
|
|
* reference_expansion,
|
|
0.0,
|
|
)
|
|
)
|
|
if value > 0.0 and reference > 0.0:
|
|
smoothing_argument = (
|
|
12.0
|
|
* abs(value / reference)
|
|
* log(effective_ratio)
|
|
/ log(accuracy)
|
|
)
|
|
value *= tanh(max(smoothing_argument, 0.0))
|
|
return value
|
|
|
|
def target_flow(mass_flow: float) -> float:
|
|
reynolds = self.reynolds_number(mass_flow, T_up)
|
|
friction = self.friction_factor(reynolds)
|
|
flow_coefficient = sqrt(
|
|
self.diam / (resistance_length * friction)
|
|
)
|
|
return (
|
|
flow_coefficient
|
|
* self.area
|
|
* p_up
|
|
* mass_flow_parameter(pressure_ratio)
|
|
/ sqrt(T_up)
|
|
)
|
|
|
|
flow_coefficient = sqrt(self.diam / (resistance_length * 0.02))
|
|
magnitude = (
|
|
flow_coefficient
|
|
* self.area
|
|
* p_up
|
|
* mass_flow_parameter(pressure_ratio)
|
|
/ sqrt(T_up)
|
|
)
|
|
for _iteration in range(16):
|
|
next_magnitude = target_flow(magnitude)
|
|
if abs(next_magnitude - magnitude) <= max(
|
|
1.0e-12,
|
|
abs(magnitude) * 1.0e-9,
|
|
):
|
|
return next_magnitude
|
|
magnitude = 0.5 * (magnitude + next_magnitude)
|
|
return magnitude
|
|
|
|
def mass_flow(self, p_1: float, p_2: float, temperature: float) -> float:
|
|
if isclose(p_1, p_2, rel_tol=0.0, abs_tol=1.0e-8):
|
|
return 0.0
|
|
pressure_difference = p_1 - p_2
|
|
upstream_temperature = max(float(temperature), 1.0)
|
|
resistance_length = getattr(self, "resistance_length", self.le)
|
|
magnitude = self._one_way_pn2pipefr_mass_flow(
|
|
upstream_pressure=max(p_1, p_2),
|
|
downstream_pressure=min(p_1, p_2),
|
|
upstream_temperature=upstream_temperature,
|
|
resistance_length=resistance_length,
|
|
)
|
|
return magnitude if pressure_difference > 0.0 else -magnitude
|
|
|
|
def component_result_values(self) -> Mapping[str, float]:
|
|
props = self.properties()
|
|
flow = self.mass_flow(self.port_1.p, props.p, props.T)
|
|
upstream_pressure = max(self.port_1.p, props.p, 1.0)
|
|
density = max(self.medium.density(upstream_pressure, props.T), 1.0e-12)
|
|
reynolds = self.reynolds_number(flow, props.T)
|
|
return {
|
|
"m": self.state.m,
|
|
"U": self.state.U,
|
|
"p": props.p,
|
|
"T": props.T,
|
|
"rho": props.rho,
|
|
"u": props.u,
|
|
"h": props.h,
|
|
"re": reynolds,
|
|
"cm": (
|
|
abs(flow)
|
|
* sqrt(props.T)
|
|
/ max(self.area * upstream_pressure, 1.0e-18)
|
|
),
|
|
"v": flow / (density * self.area),
|
|
"ff": self.friction_factor(reynolds),
|
|
}
|
|
|
|
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
|
props = self.medium.properties_from_mU(self.state.m, self.state.U, self.volume)
|
|
return (
|
|
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 - props.p,
|
|
),
|
|
EquationResidual(
|
|
id=f"{self.name}:port_1_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, props.p, props.T),
|
|
),
|
|
)
|
|
|
|
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()
|
|
|
|
|
|
class AmesimPnl0002(AmesimPnl0001):
|
|
"""AMESim PNL0002 R-C-R pneumatic pipe with one center compliance."""
|
|
|
|
MODEL_TYPE = "amesim_pnl0002"
|
|
MODEL_VERSION = "0.6.0"
|
|
PORTS = (
|
|
PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),
|
|
PortDefinition.pneumatic("port_2", nominal_role="bidirectional"),
|
|
)
|
|
PARAMETERS = AmesimPnl0001.PARAMETERS
|
|
RESULT_VARIABLES = AmesimPnl0001.RESULT_VARIABLES
|
|
DISPLAY = ComponentDisplaySpec(
|
|
label="PNL0002 R-C-R 动态管路",
|
|
library_id="amesim",
|
|
category_id="flow",
|
|
symbol="amesim_pnl0002",
|
|
ports=(
|
|
PortDisplaySpec("port_1", "left", order=10),
|
|
PortDisplaySpec("port_2", "right", order=20),
|
|
),
|
|
order=40,
|
|
parameter_groups=_DYNAMIC_PIPE_PARAMETER_GROUPS,
|
|
)
|
|
|
|
@classmethod
|
|
def create(
|
|
cls,
|
|
*,
|
|
name: str,
|
|
medium: GasMedium,
|
|
parameters: Mapping[str, float],
|
|
) -> "AmesimPnl0002":
|
|
return cls(
|
|
name=name,
|
|
medium=medium,
|
|
diam=parameters["diam"],
|
|
le=parameters["le"],
|
|
rr=parameters["rr"],
|
|
k=parameters["k"],
|
|
kth=parameters["kth"],
|
|
extemp=parameters["extemp"],
|
|
gi=parameters["gi"],
|
|
mode=parameters["mode"],
|
|
p0=parameters["p0"],
|
|
T0=parameters["T0"],
|
|
)
|
|
|
|
@property
|
|
def resistance_length(self) -> float:
|
|
return self.le / 2.0
|
|
|
|
def properties(self) -> ThermodynamicProperties:
|
|
props = self.medium.properties_from_mU(self.state.m, self.state.U, self.volume)
|
|
self.port_1.h_outflow = props.h
|
|
self.port_2.h_outflow = props.h
|
|
return props
|
|
|
|
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.resistance_length / self.diam)
|
|
* density
|
|
* velocity
|
|
* velocity
|
|
/ 2.0
|
|
)
|
|
return magnitude if mass_flow > 0.0 else -magnitude
|
|
|
|
def port_mass_flow(
|
|
self,
|
|
port_pressure: float,
|
|
center_pressure: float,
|
|
center_temperature: float,
|
|
*,
|
|
port_name: str | None = None,
|
|
) -> float:
|
|
upstream_temperature = center_temperature
|
|
if (
|
|
port_name is not None
|
|
and port_name in self._connected_h
|
|
and port_pressure > center_pressure
|
|
):
|
|
inlet_h = self._connected_h[port_name]
|
|
upstream_temperature = self.medium.temperature_from_pressure_enthalpy(
|
|
max(port_pressure, 1.0),
|
|
inlet_h,
|
|
)
|
|
return self.mass_flow(
|
|
port_pressure,
|
|
center_pressure,
|
|
max(upstream_temperature, 1.0),
|
|
)
|
|
|
|
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
|
|
self._connected_h = dict(connected_h)
|
|
|
|
def update_flow_temperature_references(
|
|
self,
|
|
connected_h: Mapping[str, float],
|
|
) -> None:
|
|
self._connected_h = dict(connected_h)
|
|
|
|
def state_derivative_from_ports(
|
|
self,
|
|
connected_h: Mapping[str, float],
|
|
) -> list[float]:
|
|
# Junctions allocate their energy-balanced outlet enthalpy per port.
|
|
# The separate cache is only the temperature input to pn2pipefr.
|
|
return super().state_derivative_from_ports(connected_h)
|
|
|
|
def component_result_values(self) -> Mapping[str, float]:
|
|
props = self.properties()
|
|
flow_1 = self.port_mass_flow(
|
|
self.port_1.p,
|
|
props.p,
|
|
props.T,
|
|
port_name="port_1",
|
|
)
|
|
flow_2 = self.port_mass_flow(
|
|
self.port_2.p,
|
|
props.p,
|
|
props.T,
|
|
port_name="port_2",
|
|
)
|
|
resistance_diagnostics: list[tuple[float, float, float, float]] = []
|
|
for port_name, port, flow in (
|
|
("port_1", self.port_1, flow_1),
|
|
("port_2", self.port_2, flow_2),
|
|
):
|
|
if flow >= 0.0:
|
|
upstream_pressure = max(port.p, 1.0)
|
|
upstream_h = self._connected_h.get(port_name, props.h)
|
|
upstream_temperature = max(
|
|
self.medium.temperature_from_pressure_enthalpy(
|
|
upstream_pressure,
|
|
upstream_h,
|
|
),
|
|
1.0,
|
|
)
|
|
else:
|
|
upstream_pressure = max(props.p, 1.0)
|
|
upstream_temperature = props.T
|
|
density = max(
|
|
self.medium.density(upstream_pressure, upstream_temperature),
|
|
1.0e-12,
|
|
)
|
|
reynolds = self.reynolds_number(flow, upstream_temperature)
|
|
resistance_diagnostics.append(
|
|
(
|
|
reynolds,
|
|
(
|
|
abs(flow)
|
|
* sqrt(upstream_temperature)
|
|
/ max(self.area * upstream_pressure, 1.0e-18)
|
|
),
|
|
abs(flow) / (density * self.area),
|
|
self.friction_factor(reynolds),
|
|
)
|
|
)
|
|
reynolds, cm, velocity, friction = (
|
|
sum(values) / len(resistance_diagnostics)
|
|
for values in zip(*resistance_diagnostics)
|
|
)
|
|
return {
|
|
"m": self.state.m,
|
|
"U": self.state.U,
|
|
"p": props.p,
|
|
"T": props.T,
|
|
"rho": props.rho,
|
|
"u": props.u,
|
|
"h": props.h,
|
|
"re": reynolds,
|
|
"cm": cm,
|
|
"v": velocity,
|
|
"ff": friction,
|
|
}
|
|
|
|
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
|
props = self.medium.properties_from_mU(self.state.m, self.state.U, self.volume)
|
|
return (
|
|
EquationResidual(
|
|
id=f"{self.name}:port_1_pressure_flow_relation",
|
|
owner="component",
|
|
owner_id=self.name,
|
|
relation="constitutive",
|
|
variables=(
|
|
f"{self.name}.port_1.p",
|
|
f"{self.name}.state",
|
|
f"{self.name}.port_1.m_flow",
|
|
),
|
|
role="flow",
|
|
value=self.port_1.m_flow
|
|
- self.port_mass_flow(
|
|
self.port_1.p,
|
|
props.p,
|
|
props.T,
|
|
port_name="port_1",
|
|
),
|
|
),
|
|
EquationResidual(
|
|
id=f"{self.name}:port_2_pressure_flow_relation",
|
|
owner="component",
|
|
owner_id=self.name,
|
|
relation="constitutive",
|
|
variables=(
|
|
f"{self.name}.port_2.p",
|
|
f"{self.name}.state",
|
|
f"{self.name}.port_2.m_flow",
|
|
),
|
|
role="flow",
|
|
value=self.port_2.m_flow
|
|
- self.port_mass_flow(
|
|
self.port_2.p,
|
|
props.p,
|
|
props.T,
|
|
port_name="port_2",
|
|
),
|
|
),
|
|
)
|
|
|
|
|
|
class AmesimPnl0003(DynamicComponent):
|
|
"""AMESim PNL0003 C-R-C pneumatic pipe with two end compliances."""
|
|
|
|
state_size = 4
|
|
MODEL_TYPE = "amesim_pnl0003"
|
|
MODEL_VERSION = "0.4.0"
|
|
PORTS = (
|
|
PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),
|
|
PortDefinition.pneumatic("port_2", nominal_role="bidirectional"),
|
|
)
|
|
PARAMETERS = AmesimPnl0001.PARAMETERS[:-2] + (
|
|
ParameterDefinition(
|
|
"p1_0",
|
|
100000.0,
|
|
label="端口 1 初始压力",
|
|
quantity="pressure",
|
|
unit="Pa",
|
|
minimum=0.0,
|
|
minimum_exclusive=True,
|
|
),
|
|
ParameterDefinition(
|
|
"T1_0",
|
|
293.15,
|
|
label="端口 1 初始温度",
|
|
quantity="temperature",
|
|
unit="K",
|
|
minimum=0.0,
|
|
minimum_exclusive=True,
|
|
),
|
|
ParameterDefinition(
|
|
"p2_0",
|
|
100000.0,
|
|
label="端口 2 初始压力",
|
|
quantity="pressure",
|
|
unit="Pa",
|
|
minimum=0.0,
|
|
minimum_exclusive=True,
|
|
),
|
|
ParameterDefinition(
|
|
"T2_0",
|
|
293.15,
|
|
label="端口 2 初始温度",
|
|
quantity="temperature",
|
|
unit="K",
|
|
minimum=0.0,
|
|
minimum_exclusive=True,
|
|
),
|
|
)
|
|
RESULT_VARIABLES = (
|
|
ResultVariableDefinition("m1", "端口 1 侧质量", "mass", "kg", "state", 10),
|
|
ResultVariableDefinition("U1", "端口 1 侧内能", "internal_energy", "J", "state", 20),
|
|
ResultVariableDefinition("p1", "端口 1 侧压力", "pressure", "Pa", "thermodynamic", 30),
|
|
ResultVariableDefinition("T1", "端口 1 侧温度", "temperature", "K", "thermodynamic", 40),
|
|
ResultVariableDefinition("rho1", "端口 1 侧密度", "density", "kg/m³", "thermodynamic", 50),
|
|
ResultVariableDefinition("u1", "端口 1 侧比内能", "specific_internal_energy", "J/kg", "thermodynamic", 60),
|
|
ResultVariableDefinition("h1", "端口 1 侧比焓", "specific_enthalpy", "J/kg", "thermodynamic", 70),
|
|
ResultVariableDefinition("m2", "端口 2 侧质量", "mass", "kg", "state", 80),
|
|
ResultVariableDefinition("U2", "端口 2 侧内能", "internal_energy", "J", "state", 90),
|
|
ResultVariableDefinition("p2", "端口 2 侧压力", "pressure", "Pa", "thermodynamic", 100),
|
|
ResultVariableDefinition("T2", "端口 2 侧温度", "temperature", "K", "thermodynamic", 110),
|
|
ResultVariableDefinition("rho2", "端口 2 侧密度", "density", "kg/m³", "thermodynamic", 120),
|
|
ResultVariableDefinition("u2", "端口 2 侧比内能", "specific_internal_energy", "J/kg", "thermodynamic", 130),
|
|
ResultVariableDefinition("h2", "端口 2 侧比焓", "specific_enthalpy", "J/kg", "thermodynamic", 140),
|
|
ResultVariableDefinition("dmctr", "中心质量流量", "mass_flow", "kg/s", "derived", 150),
|
|
ResultVariableDefinition("re", "Reynolds 数", "dimensionless", "", "derived", 160),
|
|
ResultVariableDefinition("cm", "质量流量参数", "dimensionless", "", "derived", 170),
|
|
ResultVariableDefinition("v", "平均气体速度", "velocity", "m/s", "derived", 180),
|
|
ResultVariableDefinition("ff", "摩擦因子", "dimensionless", "", "derived", 190),
|
|
)
|
|
DISPLAY = ComponentDisplaySpec(
|
|
label="PNL0003 C-R-C 动态管路",
|
|
library_id="amesim",
|
|
category_id="flow",
|
|
symbol="amesim_pnl0003",
|
|
ports=(
|
|
PortDisplaySpec("port_1", "left", order=10),
|
|
PortDisplaySpec("port_2", "right", order=20),
|
|
),
|
|
order=50,
|
|
parameter_groups=_DYNAMIC_PIPE_PARAMETER_GROUPS,
|
|
)
|
|
|
|
def __init__(
|
|
self,
|
|
name: str,
|
|
medium: GasMedium,
|
|
*,
|
|
diam: float = 0.01,
|
|
le: float = 1.0,
|
|
rr: float = 1.0e-5,
|
|
k: float = 1.35,
|
|
kth: float = 0.0,
|
|
extemp: float = 293.15,
|
|
gi: float = 1.0,
|
|
mode: float = 2.0,
|
|
p1_0: float = 100000.0,
|
|
T1_0: float = 293.15,
|
|
p2_0: float = 100000.0,
|
|
T2_0: float = 293.15,
|
|
) -> None:
|
|
super().__init__(name=name)
|
|
self.set_parameter_values(
|
|
{
|
|
"diam": diam,
|
|
"le": le,
|
|
"rr": rr,
|
|
"k": k,
|
|
"kth": kth,
|
|
"extemp": extemp,
|
|
"gi": gi,
|
|
"mode": mode,
|
|
"p1_0": p1_0,
|
|
"T1_0": T1_0,
|
|
"p2_0": p2_0,
|
|
"T2_0": T2_0,
|
|
}
|
|
)
|
|
self.medium = medium
|
|
self.diam = float(diam)
|
|
self.le = float(le)
|
|
self.rr = float(rr)
|
|
self.k = float(k)
|
|
self.kth = float(kth)
|
|
self.extemp = float(extemp)
|
|
self.gi = normalize_amesim_gas_index(gi)
|
|
self.mode = AmesimPnl0001._integer_parameter("mode", mode)
|
|
self.area = pi * self.diam * self.diam / 4.0
|
|
self.volume = self.area * self.le
|
|
self.compliance_volume = self.volume / 2.0
|
|
self.exchange_area = pi * self.diam * self.le
|
|
self.state_1 = self._initial_state(float(p1_0), float(T1_0))
|
|
self.state_2 = self._initial_state(float(p2_0), float(T2_0))
|
|
h1 = medium.specific_enthalpy_at_pressure(float(p1_0), float(T1_0))
|
|
h2 = medium.specific_enthalpy_at_pressure(float(p2_0), float(T2_0))
|
|
self.port_1 = self.register_declared_port("port_1")
|
|
self.port_1.p = float(p1_0)
|
|
self.port_1.h_outflow = h1
|
|
self.port_2 = self.register_declared_port("port_2")
|
|
self.port_2.p = float(p2_0)
|
|
self.port_2.h_outflow = h2
|
|
|
|
@classmethod
|
|
def create(
|
|
cls,
|
|
*,
|
|
name: str,
|
|
medium: GasMedium,
|
|
parameters: Mapping[str, float],
|
|
) -> "AmesimPnl0003":
|
|
return cls(name=name, medium=medium, **dict(parameters))
|
|
|
|
def _initial_state(self, pressure: float, temperature: float) -> VolumeState:
|
|
mass = self.medium.density(pressure, temperature) * self.compliance_volume
|
|
return VolumeState(
|
|
m=mass,
|
|
U=mass * self.medium.specific_internal_energy_at_pressure(
|
|
pressure,
|
|
temperature,
|
|
),
|
|
)
|
|
|
|
def get_state_vector(self) -> list[float]:
|
|
return [*self.state_1.as_vector(), *self.state_2.as_vector()]
|
|
|
|
def set_state_vector(self, values: list[float]) -> None:
|
|
if len(values) != 4:
|
|
raise ValueError("PNL0003 state vector requires four values")
|
|
self.state_1 = VolumeState.from_vector(values[:2])
|
|
self.state_2 = VolumeState.from_vector(values[2:])
|
|
|
|
def _properties(self, state: VolumeState) -> ThermodynamicProperties:
|
|
return self.medium.properties_from_mU(state.m, state.U, self.compliance_volume)
|
|
|
|
def properties_1(self) -> ThermodynamicProperties:
|
|
props = self._properties(self.state_1)
|
|
self.port_1.p = props.p
|
|
self.port_1.h_outflow = props.h
|
|
return props
|
|
|
|
def properties_2(self) -> ThermodynamicProperties:
|
|
props = self._properties(self.state_2)
|
|
self.port_2.p = props.p
|
|
self.port_2.h_outflow = props.h
|
|
return props
|
|
|
|
def refresh_thermodynamic_ports(self) -> tuple[ThermodynamicProperties, ThermodynamicProperties]:
|
|
return self.properties_1(), self.properties_2()
|
|
|
|
def _dynamic_viscosity(self, temperature_k: float) -> float:
|
|
return self.medium.dynamic_viscosity(temperature_k)
|
|
|
|
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:
|
|
return AmesimPnl00r.friction_factor(self, reynolds_number)
|
|
|
|
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:
|
|
return 1.0e3
|
|
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 resistance_mass_flow(self) -> float:
|
|
port_1 = self._properties(self.state_1)
|
|
port_2 = self._properties(self.state_2)
|
|
pressure_difference = port_1.p - port_2.p
|
|
if isclose(port_1.p, port_2.p, rel_tol=1.0e-7, abs_tol=1.0e-9):
|
|
return 0.0
|
|
upstream = port_1 if pressure_difference > 0.0 else port_2
|
|
magnitude = self._mass_flow_for_pressure_drop(
|
|
abs(pressure_difference),
|
|
density=upstream.rho,
|
|
temperature=upstream.T,
|
|
)
|
|
return magnitude if pressure_difference > 0.0 else -magnitude
|
|
|
|
def _heat_flow_each(self, temperature_1: float, temperature_2: float) -> float:
|
|
if self.mode == 1:
|
|
return 0.0
|
|
return self.kth * self.exchange_area * (self.extemp - 0.5 * (temperature_1 + temperature_2)) / 2.0
|
|
|
|
def component_result_values(self) -> Mapping[str, float]:
|
|
port_1 = self.properties_1()
|
|
port_2 = self.properties_2()
|
|
center_flow = self.resistance_mass_flow()
|
|
upstream = port_1 if center_flow >= 0.0 else port_2
|
|
reynolds = self.reynolds_number(center_flow, upstream.T)
|
|
return {
|
|
"m1": self.state_1.m,
|
|
"U1": self.state_1.U,
|
|
"p1": port_1.p,
|
|
"T1": port_1.T,
|
|
"rho1": port_1.rho,
|
|
"u1": port_1.u,
|
|
"h1": port_1.h,
|
|
"m2": self.state_2.m,
|
|
"U2": self.state_2.U,
|
|
"p2": port_2.p,
|
|
"T2": port_2.T,
|
|
"rho2": port_2.rho,
|
|
"u2": port_2.u,
|
|
"h2": port_2.h,
|
|
"dmctr": center_flow,
|
|
"re": reynolds,
|
|
"cm": (
|
|
abs(center_flow)
|
|
* sqrt(upstream.T)
|
|
/ max(self.area * max(port_1.p, port_2.p, 1.0), 1.0e-18)
|
|
),
|
|
"v": center_flow / (max(upstream.rho, 1.0e-12) * self.area),
|
|
"ff": self.friction_factor(reynolds),
|
|
}
|
|
|
|
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
|
port_1 = self._properties(self.state_1)
|
|
port_2 = self._properties(self.state_2)
|
|
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 - port_1.p,
|
|
),
|
|
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 - port_2.p,
|
|
),
|
|
)
|
|
|
|
def state_derivative_from_ports(self, connected_h: Mapping[str, float]) -> list[float]:
|
|
port_1 = self.properties_1()
|
|
port_2 = self.properties_2()
|
|
center_flow = self.resistance_mass_flow()
|
|
heat_flow_each = self._heat_flow_each(port_1.T, port_2.T)
|
|
port_1_external_h = self.connection_inlet_enthalpy(
|
|
port_m_flow=self.port_1.m_flow,
|
|
connected_h=connected_h["port_1"],
|
|
internal_h=port_1.h,
|
|
)
|
|
port_2_external_h = self.connection_inlet_enthalpy(
|
|
port_m_flow=self.port_2.m_flow,
|
|
connected_h=connected_h["port_2"],
|
|
internal_h=port_2.h,
|
|
)
|
|
port_1_center_h = self.connection_inlet_enthalpy(
|
|
port_m_flow=-center_flow,
|
|
connected_h=port_2.h,
|
|
internal_h=port_1.h,
|
|
)
|
|
port_2_center_h = self.connection_inlet_enthalpy(
|
|
port_m_flow=center_flow,
|
|
connected_h=port_1.h,
|
|
internal_h=port_2.h,
|
|
)
|
|
d1 = VolumeState(
|
|
m=self.port_1.m_flow - center_flow,
|
|
U=self.port_1.m_flow * port_1_external_h - center_flow * port_1_center_h + heat_flow_each,
|
|
)
|
|
d2 = VolumeState(
|
|
m=self.port_2.m_flow + center_flow,
|
|
U=self.port_2.m_flow * port_2_external_h + center_flow * port_2_center_h + heat_flow_each,
|
|
)
|
|
return [*d1.as_vector(), *d2.as_vector()]
|