691 lines
24 KiB
Python
691 lines
24 KiB
Python
from __future__ import annotations
|
|
|
|
from collections.abc import Mapping, Sequence
|
|
from dataclasses import dataclass
|
|
from math import isfinite
|
|
|
|
from app.simulation.components.amesim.gases import (
|
|
AMESIM_GAS_INDEX_PARAMETER,
|
|
normalize_amesim_gas_index,
|
|
)
|
|
from app.simulation.core.base import ThermodynamicVolumeComponent
|
|
from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
|
|
from app.simulation.core.equations import EquationResidual
|
|
from app.simulation.core.metadata import (
|
|
ParameterDefinition,
|
|
ResultVariableDefinition,
|
|
THERMODYNAMIC_VOLUME_RESULT_VARIABLES,
|
|
)
|
|
from app.simulation.core.medium import (
|
|
GasMedium,
|
|
ThermodynamicProperties,
|
|
ThermodynamicPropertiesLinearization,
|
|
)
|
|
from app.simulation.core.ports import PortDefinition
|
|
from app.simulation.core.state import VolumeState
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Pnch012DerivativeLinearization:
|
|
derivative: tuple[float, float]
|
|
tangents: tuple[tuple[float, ...], tuple[float, ...]]
|
|
properties: ThermodynamicPropertiesLinearization
|
|
valid: bool = True
|
|
reason: str | None = None
|
|
|
|
|
|
class AmesimPnch023(ThermodynamicVolumeComponent):
|
|
"""AMESim PNCH023 simple pneumatic chamber with heat exchange.
|
|
|
|
The AMESim submodel owns pressure and temperature states and exposes two
|
|
pneumatic flow ports. This public component maps those states onto the
|
|
framework's mass/internal-energy volume state and keeps the AMESim
|
|
heat-transfer contract `kth * sth * (extemp - T)`.
|
|
"""
|
|
|
|
MODEL_TYPE = "amesim_pnch023"
|
|
MODEL_VERSION = "0.1.0"
|
|
PORTS = (
|
|
PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),
|
|
PortDefinition.pneumatic("port_2", nominal_role="bidirectional"),
|
|
)
|
|
PARAMETERS = (
|
|
AMESIM_GAS_INDEX_PARAMETER,
|
|
ParameterDefinition(
|
|
"cvol",
|
|
0.057,
|
|
label="气室容积",
|
|
quantity="volume",
|
|
unit="m3",
|
|
minimum=0.0,
|
|
minimum_exclusive=True,
|
|
description="气室内部用于储存气体的固定有效容积。",
|
|
),
|
|
ParameterDefinition(
|
|
"kth",
|
|
0.0,
|
|
label="换热系数",
|
|
quantity="heat_transfer_coefficient",
|
|
unit="W/(m2*K)",
|
|
minimum=0.0,
|
|
description="气室与环境之间的对流换热系数,与换热面积共同决定换热功率。",
|
|
),
|
|
ParameterDefinition(
|
|
"sth",
|
|
0.1,
|
|
label="换热面积",
|
|
quantity="area",
|
|
unit="m2",
|
|
minimum=0.0,
|
|
description="气室与环境进行热交换的有效表面积。",
|
|
),
|
|
ParameterDefinition(
|
|
"extemp",
|
|
293.15,
|
|
label="外部温度",
|
|
quantity="temperature",
|
|
unit="K",
|
|
minimum=0.0,
|
|
minimum_exclusive=True,
|
|
description="气室外部环境的绝对温度,用于计算气体与环境之间的换热。",
|
|
),
|
|
ParameterDefinition(
|
|
"p0",
|
|
100000.0,
|
|
label="初始压力",
|
|
quantity="pressure",
|
|
unit="Pa",
|
|
minimum=0.0,
|
|
minimum_exclusive=True,
|
|
description="仿真开始时气室内气体的绝对压力。",
|
|
),
|
|
ParameterDefinition(
|
|
"T0",
|
|
293.15,
|
|
label="初始温度",
|
|
quantity="temperature",
|
|
unit="K",
|
|
minimum=0.0,
|
|
minimum_exclusive=True,
|
|
description="仿真开始时气室内气体的绝对温度。",
|
|
),
|
|
)
|
|
RESULT_VARIABLES = THERMODYNAMIC_VOLUME_RESULT_VARIABLES
|
|
DISPLAY = ComponentDisplaySpec(
|
|
label="PNCH023 固定容积气室",
|
|
library_id="amesim",
|
|
category_id="storage",
|
|
symbol="amesim_pnch023",
|
|
ports=(
|
|
PortDisplaySpec("port_1", "left", order=10),
|
|
PortDisplaySpec("port_2", "right", order=20),
|
|
),
|
|
order=10,
|
|
)
|
|
|
|
def __init__(
|
|
self,
|
|
name: str,
|
|
medium: GasMedium,
|
|
*,
|
|
cvol: float = 0.057,
|
|
kth: float = 0.0,
|
|
sth: float = 0.1,
|
|
extemp: float = 293.15,
|
|
gi: float = 1.0,
|
|
p0: float = 100000.0,
|
|
T0: float = 293.15,
|
|
) -> None:
|
|
super().__init__(name=name)
|
|
self.set_parameter_values(
|
|
{
|
|
"cvol": cvol,
|
|
"kth": kth,
|
|
"sth": sth,
|
|
"extemp": extemp,
|
|
"gi": gi,
|
|
"p0": p0,
|
|
"T0": T0,
|
|
}
|
|
)
|
|
self.medium = medium
|
|
self.cvol = float(cvol)
|
|
self.kth = float(kth)
|
|
self.sth = float(sth)
|
|
self.extemp = float(extemp)
|
|
self.gi = normalize_amesim_gas_index(gi)
|
|
self.p0 = float(p0)
|
|
self.T0 = float(T0)
|
|
m0 = medium.density(self.p0, self.T0) * self.cvol
|
|
U0 = m0 * medium.specific_internal_energy_at_pressure(self.p0, self.T0)
|
|
self.state = VolumeState(m=m0, U=U0)
|
|
initial_h = medium.specific_enthalpy_at_pressure(self.p0, self.T0)
|
|
self.port_1 = self.register_declared_port("port_1")
|
|
self.port_1.p = self.p0
|
|
self.port_1.h_outflow = initial_h
|
|
self.port_2 = self.register_declared_port("port_2")
|
|
self.port_2.p = self.p0
|
|
self.port_2.h_outflow = initial_h
|
|
|
|
@classmethod
|
|
def create(
|
|
cls,
|
|
*,
|
|
name: str,
|
|
medium: GasMedium,
|
|
parameters: Mapping[str, float],
|
|
) -> AmesimPnch023:
|
|
return cls(
|
|
name=name,
|
|
medium=medium,
|
|
cvol=parameters["cvol"],
|
|
kth=parameters["kth"],
|
|
sth=parameters["sth"],
|
|
extemp=parameters["extemp"],
|
|
gi=parameters["gi"],
|
|
p0=parameters["p0"],
|
|
T0=parameters["T0"],
|
|
)
|
|
|
|
def get_state_vector(self) -> list[float]:
|
|
return self.state.as_vector()
|
|
|
|
def set_state_vector(self, values: list[float]) -> None:
|
|
self.state = VolumeState.from_vector(values)
|
|
|
|
def properties(self) -> ThermodynamicProperties:
|
|
props = self.medium.properties_from_mU(self.state.m, self.state.U, self.cvol)
|
|
self.port_1.p = props.p
|
|
self.port_1.h_outflow = props.h
|
|
self.port_2.p = props.p
|
|
self.port_2.h_outflow = props.h
|
|
return props
|
|
|
|
def refresh_thermodynamic_ports(self) -> ThermodynamicProperties:
|
|
return self.properties()
|
|
|
|
def thermal_energy_flow_w(self, temperature: float) -> float:
|
|
return self.kth * self.sth * (self.extemp - temperature)
|
|
|
|
def state_derivative_from_ports(
|
|
self,
|
|
connected_h: Mapping[str, float],
|
|
) -> list[float]:
|
|
props = self.properties()
|
|
inlet_h_1 = self.connection_inlet_enthalpy(
|
|
port_m_flow=self.port_1.m_flow,
|
|
connected_h=connected_h["port_1"],
|
|
internal_h=props.h,
|
|
)
|
|
inlet_h_2 = self.connection_inlet_enthalpy(
|
|
port_m_flow=self.port_2.m_flow,
|
|
connected_h=connected_h["port_2"],
|
|
internal_h=props.h,
|
|
)
|
|
derivative = VolumeState(
|
|
m=self.port_1.m_flow + self.port_2.m_flow,
|
|
U=(
|
|
self.port_1.m_flow * inlet_h_1
|
|
+ self.port_2.m_flow * inlet_h_2
|
|
+ self.thermal_energy_flow_w(props.T)
|
|
),
|
|
)
|
|
return derivative.as_vector()
|
|
|
|
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
|
pressure = self.medium.properties_from_mU(
|
|
self.state.m,
|
|
self.state.U,
|
|
self.cvol,
|
|
).p
|
|
return (
|
|
self.port_1.p - pressure,
|
|
self.port_2.p - pressure,
|
|
)
|
|
|
|
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
|
pressure = self.medium.properties_from_mU(
|
|
self.state.m,
|
|
self.state.U,
|
|
self.cvol,
|
|
).p
|
|
return (
|
|
EquationResidual(
|
|
id=f"{self.name}:port_1_pressure_state",
|
|
owner="component",
|
|
owner_id=self.name,
|
|
relation="state",
|
|
variables=(f"{self.name}.port_1.p", f"{self.name}.state"),
|
|
role="effort",
|
|
value=self.port_1.p - pressure,
|
|
),
|
|
EquationResidual(
|
|
id=f"{self.name}:port_2_pressure_state",
|
|
owner="component",
|
|
owner_id=self.name,
|
|
relation="state",
|
|
variables=(f"{self.name}.port_2.p", f"{self.name}.state"),
|
|
role="effort",
|
|
value=self.port_2.p - pressure,
|
|
),
|
|
)
|
|
|
|
|
|
class AmesimPnch012(ThermodynamicVolumeComponent):
|
|
"""AMESim PNCH012 variable-volume pneumatic chamber.
|
|
|
|
AMESim supplies four external volume and volume-rate inputs through the
|
|
chamber ports. Fixed/prescribed contributions remain available as SI
|
|
parameters, while connected moving-boundary components can now add live
|
|
volume and volume-rate values through the pneumatic connector contract.
|
|
"""
|
|
|
|
MODEL_TYPE = "amesim_pnch012"
|
|
MODEL_VERSION = "0.1.0"
|
|
PORTS = (
|
|
PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),
|
|
PortDefinition.pneumatic("port_2", nominal_role="bidirectional"),
|
|
PortDefinition.pneumatic("port_3", nominal_role="bidirectional"),
|
|
PortDefinition.pneumatic("port_4", nominal_role="bidirectional"),
|
|
)
|
|
PARAMETERS = (
|
|
AMESIM_GAS_INDEX_PARAMETER,
|
|
ParameterDefinition(
|
|
"cvol0",
|
|
0.015,
|
|
label="死容积",
|
|
quantity="volume",
|
|
unit="m3",
|
|
minimum=0.0,
|
|
minimum_exclusive=True,
|
|
description="变容气室在所有外部容积为零时仍保留的基础容积。",
|
|
),
|
|
ParameterDefinition(
|
|
"kth",
|
|
0.0,
|
|
label="换热系数",
|
|
quantity="heat_transfer_coefficient",
|
|
unit="W/(m2*K)",
|
|
minimum=0.0,
|
|
description="气室与环境之间的对流换热系数,与换热面积共同决定换热功率。",
|
|
),
|
|
ParameterDefinition(
|
|
"sth",
|
|
0.1,
|
|
label="换热面积",
|
|
quantity="area",
|
|
unit="m2",
|
|
minimum=0.0,
|
|
description="气室与环境进行热交换的有效表面积。",
|
|
),
|
|
ParameterDefinition(
|
|
"extemp",
|
|
293.15,
|
|
label="外部温度",
|
|
quantity="temperature",
|
|
unit="K",
|
|
minimum=0.0,
|
|
minimum_exclusive=True,
|
|
description="气室外部环境的绝对温度,用于计算气体与环境之间的换热。",
|
|
),
|
|
ParameterDefinition(
|
|
"p0",
|
|
100000.0,
|
|
label="初始压力",
|
|
quantity="pressure",
|
|
unit="Pa",
|
|
minimum=0.0,
|
|
minimum_exclusive=True,
|
|
description="仿真开始时气室内气体的绝对压力。",
|
|
),
|
|
ParameterDefinition(
|
|
"T0",
|
|
293.15,
|
|
label="初始温度",
|
|
quantity="temperature",
|
|
unit="K",
|
|
minimum=0.0,
|
|
minimum_exclusive=True,
|
|
description="仿真开始时气室内气体的绝对温度。",
|
|
),
|
|
ParameterDefinition("vol1", 0.0, label="端口 1 外部容积", quantity="volume", unit="m3"),
|
|
ParameterDefinition("vol2", 0.0, label="端口 2 外部容积", quantity="volume", unit="m3"),
|
|
ParameterDefinition("vol3", 0.0, label="端口 3 外部容积", quantity="volume", unit="m3"),
|
|
ParameterDefinition("vol4", 0.0, label="端口 4 外部容积", quantity="volume", unit="m3"),
|
|
ParameterDefinition("dvol1", 0.0, label="端口 1 容积变化率", quantity="volume_flow", unit="m3/s"),
|
|
ParameterDefinition("dvol2", 0.0, label="端口 2 容积变化率", quantity="volume_flow", unit="m3/s"),
|
|
ParameterDefinition("dvol3", 0.0, label="端口 3 容积变化率", quantity="volume_flow", unit="m3/s"),
|
|
ParameterDefinition("dvol4", 0.0, label="端口 4 容积变化率", quantity="volume_flow", unit="m3/s"),
|
|
)
|
|
RESULT_VARIABLES = THERMODYNAMIC_VOLUME_RESULT_VARIABLES + (
|
|
ResultVariableDefinition("vol", "气室总容积", "volume", "m3", "derived", 100),
|
|
ResultVariableDefinition("dvol", "总容积变化率", "volume_flow", "m3/s", "derived", 110),
|
|
)
|
|
DISPLAY = ComponentDisplaySpec(
|
|
label="PNCH012 变容气室",
|
|
library_id="amesim",
|
|
category_id="storage",
|
|
symbol="amesim_pnch012",
|
|
ports=(
|
|
PortDisplaySpec("port_1", "left", order=10),
|
|
PortDisplaySpec("port_2", "right", order=20),
|
|
PortDisplaySpec("port_3", "left", order=30),
|
|
PortDisplaySpec("port_4", "right", order=40),
|
|
),
|
|
order=20,
|
|
)
|
|
|
|
def __init__(
|
|
self,
|
|
name: str,
|
|
medium: GasMedium,
|
|
*,
|
|
cvol0: float = 0.015,
|
|
kth: float = 0.0,
|
|
sth: float = 0.1,
|
|
extemp: float = 293.15,
|
|
gi: float = 1.0,
|
|
p0: float = 100000.0,
|
|
T0: float = 293.15,
|
|
vol1: float = 0.0,
|
|
vol2: float = 0.0,
|
|
vol3: float = 0.0,
|
|
vol4: float = 0.0,
|
|
dvol1: float = 0.0,
|
|
dvol2: float = 0.0,
|
|
dvol3: float = 0.0,
|
|
dvol4: float = 0.0,
|
|
) -> None:
|
|
super().__init__(name=name)
|
|
self.set_parameter_values(
|
|
{
|
|
"cvol0": cvol0,
|
|
"kth": kth,
|
|
"sth": sth,
|
|
"extemp": extemp,
|
|
"gi": gi,
|
|
"p0": p0,
|
|
"T0": T0,
|
|
"vol1": vol1,
|
|
"vol2": vol2,
|
|
"vol3": vol3,
|
|
"vol4": vol4,
|
|
"dvol1": dvol1,
|
|
"dvol2": dvol2,
|
|
"dvol3": dvol3,
|
|
"dvol4": dvol4,
|
|
}
|
|
)
|
|
self.medium = medium
|
|
self.cvol0 = float(cvol0)
|
|
self.kth = float(kth)
|
|
self.sth = float(sth)
|
|
self.extemp = float(extemp)
|
|
self.gi = normalize_amesim_gas_index(gi)
|
|
self.p0 = float(p0)
|
|
self.T0 = float(T0)
|
|
self.external_volumes = {
|
|
"port_1": float(vol1),
|
|
"port_2": float(vol2),
|
|
"port_3": float(vol3),
|
|
"port_4": float(vol4),
|
|
}
|
|
self.external_volume_rates = {
|
|
"port_1": float(dvol1),
|
|
"port_2": float(dvol2),
|
|
"port_3": float(dvol3),
|
|
"port_4": float(dvol4),
|
|
}
|
|
if self.total_volume() <= 0.0:
|
|
raise ValueError("PNCH012 total volume must be positive.")
|
|
m0 = medium.density(self.p0, self.T0) * self.total_volume()
|
|
U0 = m0 * medium.specific_internal_energy_at_pressure(self.p0, self.T0)
|
|
self.state = VolumeState(m=m0, U=U0)
|
|
initial_h = medium.specific_enthalpy_at_pressure(self.p0, self.T0)
|
|
for port_name in ("port_1", "port_2", "port_3", "port_4"):
|
|
port = self.register_declared_port(port_name)
|
|
port.p = self.p0
|
|
port.h_outflow = initial_h
|
|
setattr(self, port_name, port)
|
|
|
|
@classmethod
|
|
def create(
|
|
cls,
|
|
*,
|
|
name: str,
|
|
medium: GasMedium,
|
|
parameters: Mapping[str, float],
|
|
) -> "AmesimPnch012":
|
|
return cls(name=name, medium=medium, **dict(parameters))
|
|
|
|
def connected_external_volume(self) -> float:
|
|
return sum(
|
|
getattr(getattr(self, port_name, None), "volume", 0.0)
|
|
for port_name in self.external_volumes
|
|
)
|
|
|
|
def connected_external_volume_rate(self) -> float:
|
|
return sum(
|
|
getattr(getattr(self, port_name, None), "volume_flow", 0.0)
|
|
for port_name in self.external_volume_rates
|
|
)
|
|
|
|
def total_volume(self) -> float:
|
|
minimum_volume = self.cvol0 / 100.0
|
|
return max(
|
|
self.cvol0 + sum(self.external_volumes.values()) + self.connected_external_volume(),
|
|
minimum_volume,
|
|
)
|
|
|
|
def total_volume_rate(self) -> float:
|
|
if self.total_volume() <= self.cvol0 / 100.0:
|
|
return 0.0
|
|
return sum(self.external_volume_rates.values()) + self.connected_external_volume_rate()
|
|
|
|
def get_state_vector(self) -> list[float]:
|
|
return self.state.as_vector()
|
|
|
|
def set_state_vector(self, values: list[float]) -> None:
|
|
self.state = VolumeState.from_vector(values)
|
|
|
|
def properties(self) -> ThermodynamicProperties:
|
|
props = self.medium.properties_from_mU(self.state.m, self.state.U, self.total_volume())
|
|
for port_name in ("port_1", "port_2", "port_3", "port_4"):
|
|
port = self.get_port(port_name)
|
|
port.p = props.p
|
|
port.h_outflow = props.h
|
|
return props
|
|
|
|
def refresh_thermodynamic_ports(self) -> ThermodynamicProperties:
|
|
return self.properties()
|
|
|
|
def thermal_energy_flow_w(self, temperature: float) -> float:
|
|
return self.kth * self.sth * (self.extemp - temperature)
|
|
|
|
def component_result_values(self) -> Mapping[str, float]:
|
|
props = self.properties()
|
|
return {
|
|
"m": self.state.m,
|
|
"U": self.state.U,
|
|
"p": props.p,
|
|
"T": props.T,
|
|
"rho": props.rho,
|
|
"u": props.u,
|
|
"h": props.h,
|
|
"vol": self.total_volume(),
|
|
"dvol": self.total_volume_rate(),
|
|
}
|
|
|
|
def state_derivative_from_ports(self, connected_h: Mapping[str, float]) -> list[float]:
|
|
props = self.properties()
|
|
mass_derivative = 0.0
|
|
energy_derivative = 0.0
|
|
for port_name in ("port_1", "port_2", "port_3", "port_4"):
|
|
port = self.get_port(port_name)
|
|
inlet_h = self.connection_inlet_enthalpy(
|
|
port_m_flow=port.m_flow,
|
|
connected_h=connected_h[port_name],
|
|
internal_h=props.h,
|
|
)
|
|
mass_derivative += port.m_flow
|
|
energy_derivative += port.m_flow * inlet_h
|
|
energy_derivative += self.thermal_energy_flow_w(props.T)
|
|
energy_derivative -= props.p * self.total_volume_rate()
|
|
return VolumeState(m=mass_derivative, U=energy_derivative).as_vector()
|
|
|
|
def linearize_state_derivative(
|
|
self,
|
|
connected_h: Mapping[str, float],
|
|
*,
|
|
state_mass_tangent: Sequence[float],
|
|
state_energy_tangent: Sequence[float],
|
|
external_volume_tangent: Sequence[float],
|
|
external_volume_rate_tangent: Sequence[float],
|
|
port_mass_flow_tangents: Mapping[str, Sequence[float]],
|
|
connected_h_tangents: Mapping[str, Sequence[float]],
|
|
property_linearization: ThermodynamicPropertiesLinearization | None = None,
|
|
flow_boundary_tolerance: float = 1.0e-12,
|
|
) -> Pnch012DerivativeLinearization:
|
|
"""Linearize the chamber balance while keeping stream modes fixed."""
|
|
|
|
port_names = ("port_1", "port_2", "port_3", "port_4")
|
|
vectors = {
|
|
"state_mass": tuple(float(value) for value in state_mass_tangent),
|
|
"state_energy": tuple(float(value) for value in state_energy_tangent),
|
|
"volume": tuple(float(value) for value in external_volume_tangent),
|
|
"volume_rate": tuple(
|
|
float(value) for value in external_volume_rate_tangent
|
|
),
|
|
}
|
|
for port_name in port_names:
|
|
vectors[f"flow:{port_name}"] = tuple(
|
|
float(value) for value in port_mass_flow_tangents[port_name]
|
|
)
|
|
vectors[f"enthalpy:{port_name}"] = tuple(
|
|
float(value) for value in connected_h_tangents[port_name]
|
|
)
|
|
widths = {len(values) for values in vectors.values()}
|
|
if len(widths) != 1:
|
|
raise ValueError("PNCH012 tangent vectors must have equal lengths.")
|
|
width = len(vectors["state_mass"])
|
|
invalid_reason: str | None = None
|
|
if not all(isfinite(value) for values in vectors.values() for value in values):
|
|
invalid_reason = "non_finite_tangent_input"
|
|
|
|
raw_volume = (
|
|
self.cvol0
|
|
+ sum(self.external_volumes.values())
|
|
+ self.connected_external_volume()
|
|
)
|
|
minimum_volume = self.cvol0 / 100.0
|
|
volume_scale = max(abs(raw_volume), abs(minimum_volume), 1.0e-18)
|
|
on_volume_boundary = (
|
|
abs(raw_volume - minimum_volume) <= 1.0e-12 * volume_scale
|
|
)
|
|
supplied_volume_tangent = vectors["volume"]
|
|
if raw_volume < minimum_volume or on_volume_boundary:
|
|
used_volume_tangent = (0.0,) * width
|
|
used_volume_rate_tangent = (0.0,) * width
|
|
if on_volume_boundary and any(
|
|
value != 0.0
|
|
for value in (
|
|
*supplied_volume_tangent,
|
|
*vectors["volume_rate"],
|
|
)
|
|
):
|
|
invalid_reason = invalid_reason or "volume_floor_boundary"
|
|
else:
|
|
used_volume_tangent = supplied_volume_tangent
|
|
used_volume_rate_tangent = vectors["volume_rate"]
|
|
|
|
properties = property_linearization or self.medium.linearize_properties_from_mU(
|
|
self.state.m,
|
|
self.state.U,
|
|
self.total_volume(),
|
|
vectors["state_mass"],
|
|
vectors["state_energy"],
|
|
used_volume_tangent,
|
|
)
|
|
if properties.tangents.width != width:
|
|
raise ValueError(
|
|
"PNCH012 property tangent width must match balance tangents."
|
|
)
|
|
props = properties.properties
|
|
if not properties.valid:
|
|
invalid_reason = invalid_reason or properties.reason
|
|
|
|
mass_derivative = sum(
|
|
self.get_port(port_name).m_flow for port_name in port_names
|
|
)
|
|
volume_rate = self.total_volume_rate()
|
|
energy_derivative = self.thermal_energy_flow_w(props.T) - props.p * volume_rate
|
|
mass_tangent = [0.0] * width
|
|
energy_tangent = [
|
|
-self.kth * self.sth * properties.tangents.T[index]
|
|
- volume_rate * properties.tangents.p[index]
|
|
- props.p * used_volume_rate_tangent[index]
|
|
for index in range(width)
|
|
]
|
|
|
|
for port_name in port_names:
|
|
port = self.get_port(port_name)
|
|
flow_tangent = vectors[f"flow:{port_name}"]
|
|
if (
|
|
abs(port.m_flow) <= flow_boundary_tolerance
|
|
and any(value != 0.0 for value in flow_tangent)
|
|
):
|
|
invalid_reason = invalid_reason or (
|
|
f"flow_direction_boundary:{port_name}"
|
|
)
|
|
if port.m_flow > 0.0:
|
|
inlet_h = connected_h[port_name]
|
|
inlet_h_tangent = vectors[f"enthalpy:{port_name}"]
|
|
else:
|
|
inlet_h = props.h
|
|
inlet_h_tangent = properties.tangents.h
|
|
energy_derivative += port.m_flow * inlet_h
|
|
for index in range(width):
|
|
mass_tangent[index] += flow_tangent[index]
|
|
energy_tangent[index] += (
|
|
inlet_h * flow_tangent[index]
|
|
+ port.m_flow * inlet_h_tangent[index]
|
|
)
|
|
|
|
return Pnch012DerivativeLinearization(
|
|
derivative=(mass_derivative, energy_derivative),
|
|
tangents=(tuple(mass_tangent), tuple(energy_tangent)),
|
|
properties=properties,
|
|
valid=invalid_reason is None,
|
|
reason=invalid_reason,
|
|
)
|
|
|
|
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
|
pressure = self.medium.properties_from_mU(
|
|
self.state.m,
|
|
self.state.U,
|
|
self.total_volume(),
|
|
).p
|
|
return tuple(
|
|
self.get_port(port_name).p - pressure
|
|
for port_name in ("port_1", "port_2", "port_3", "port_4")
|
|
)
|
|
|
|
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
|
pressure = self.medium.properties_from_mU(
|
|
self.state.m,
|
|
self.state.U,
|
|
self.total_volume(),
|
|
).p
|
|
return tuple(
|
|
EquationResidual(
|
|
id=f"{self.name}:{port_name}_pressure_state",
|
|
owner="component",
|
|
owner_id=self.name,
|
|
relation="state",
|
|
variables=(f"{self.name}.{port_name}.p", f"{self.name}.state"),
|
|
role="effort",
|
|
value=self.get_port(port_name).p - pressure,
|
|
)
|
|
for port_name in ("port_1", "port_2", "port_3", "port_4")
|
|
)
|