Files
SystemSimulationApp/app/simulation/components/amesim/storage/chambers.py
T

522 lines
17 KiB
Python

from __future__ import annotations
from collections.abc import Mapping
from math import isclose
from app.simulation.core.base import ThermodynamicVolumeComponent
from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
from app.simulation.core.equations import EquationResidual
from app.simulation.core.metadata import (
ParameterDefinition,
ResultVariableDefinition,
THERMODYNAMIC_VOLUME_RESULT_VARIABLES,
)
from app.simulation.core.medium import IdealGasMedium, ThermodynamicProperties
from app.simulation.core.ports import PortDefinition
from app.simulation.core.state import VolumeState
class AmesimPnch023(ThermodynamicVolumeComponent):
"""AMESim PNCH023 simple pneumatic chamber with heat exchange.
The AMESim submodel owns pressure and temperature states and exposes two
pneumatic flow ports. This public component maps those states onto the
framework's mass/internal-energy volume state and keeps the AMESim
heat-transfer contract `kth * sth * (extemp - T)`.
"""
MODEL_TYPE = "amesim_pnch023"
MODEL_VERSION = "0.1.0"
PORTS = (
PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),
PortDefinition.pneumatic("port_2", nominal_role="bidirectional"),
)
PARAMETERS = (
ParameterDefinition(
"cvol",
0.057,
label="气室容积",
quantity="volume",
unit="m3",
minimum=0.0,
minimum_exclusive=True,
),
ParameterDefinition(
"kth",
0.0,
label="换热系数",
quantity="heat_transfer_coefficient",
unit="W/(m2*K)",
minimum=0.0,
),
ParameterDefinition(
"sth",
0.1,
label="换热面积",
quantity="area",
unit="m2",
minimum=0.0,
),
ParameterDefinition(
"extemp",
293.15,
label="外部温度",
quantity="temperature",
unit="K",
minimum=0.0,
minimum_exclusive=True,
),
ParameterDefinition(
"gi",
1.0,
label="气体类型索引",
quantity="dimensionless",
unit="",
minimum=1.0,
maximum=99.0,
),
ParameterDefinition(
"p0",
100000.0,
label="初始压力",
quantity="pressure",
unit="Pa",
minimum=0.0,
minimum_exclusive=True,
),
ParameterDefinition(
"T0",
293.15,
label="初始温度",
quantity="temperature",
unit="K",
minimum=0.0,
minimum_exclusive=True,
),
)
RESULT_VARIABLES = THERMODYNAMIC_VOLUME_RESULT_VARIABLES
DISPLAY = ComponentDisplaySpec(
label="PNCH023 固定容积气室",
library_id="amesim",
category_id="storage",
symbol="tank",
ports=(
PortDisplaySpec("port_1", "left", order=10),
PortDisplaySpec("port_2", "right", order=20),
),
order=10,
)
def __init__(
self,
name: str,
medium: IdealGasMedium,
*,
cvol: float = 0.057,
kth: float = 0.0,
sth: float = 0.1,
extemp: float = 293.15,
gi: float = 1.0,
p0: float = 100000.0,
T0: float = 293.15,
) -> None:
super().__init__(name=name)
self.set_parameter_values(
{
"cvol": cvol,
"kth": kth,
"sth": sth,
"extemp": extemp,
"gi": gi,
"p0": p0,
"T0": T0,
}
)
self.medium = medium
self.cvol = float(cvol)
self.kth = float(kth)
self.sth = float(sth)
self.extemp = float(extemp)
self.gi = self._integer_parameter("gi", gi)
self.p0 = float(p0)
self.T0 = float(T0)
m0 = self.p0 * self.cvol / (medium.R_gas * self.T0)
U0 = m0 * medium.specific_internal_energy(self.T0)
self.state = VolumeState(m=m0, U=U0)
initial_h = medium.specific_enthalpy(self.T0)
self.port_1 = self.register_declared_port("port_1")
self.port_1.p = self.p0
self.port_1.h_outflow = initial_h
self.port_2 = self.register_declared_port("port_2")
self.port_2.p = self.p0
self.port_2.h_outflow = initial_h
@staticmethod
def _integer_parameter(name: str, value: float) -> int:
rounded = round(value)
if not isclose(value, rounded, rel_tol=0.0, abs_tol=1.0e-12):
raise ValueError(f"PNCH023 parameter {name} must be an integer value.")
return int(rounded)
@classmethod
def create(
cls,
*,
name: str,
medium: IdealGasMedium,
parameters: Mapping[str, float],
) -> AmesimPnch023:
return cls(
name=name,
medium=medium,
cvol=parameters["cvol"],
kth=parameters["kth"],
sth=parameters["sth"],
extemp=parameters["extemp"],
gi=parameters["gi"],
p0=parameters["p0"],
T0=parameters["T0"],
)
def get_state_vector(self) -> list[float]:
return self.state.as_vector()
def set_state_vector(self, values: list[float]) -> None:
self.state = VolumeState.from_vector(values)
def properties(self) -> ThermodynamicProperties:
props = self.medium.properties_from_mU(self.state.m, self.state.U, self.cvol)
self.port_1.p = props.p
self.port_1.h_outflow = props.h
self.port_2.p = props.p
self.port_2.h_outflow = props.h
return props
def refresh_thermodynamic_ports(self) -> ThermodynamicProperties:
return self.properties()
def thermal_energy_flow_w(self, temperature: float) -> float:
return self.kth * self.sth * (self.extemp - temperature)
def state_derivative_from_ports(
self,
connected_h: Mapping[str, float],
) -> list[float]:
props = self.properties()
inlet_h_1 = self.connection_inlet_enthalpy(
port_m_flow=self.port_1.m_flow,
connected_h=connected_h["port_1"],
internal_h=props.h,
)
inlet_h_2 = self.connection_inlet_enthalpy(
port_m_flow=self.port_2.m_flow,
connected_h=connected_h["port_2"],
internal_h=props.h,
)
derivative = VolumeState(
m=self.port_1.m_flow + self.port_2.m_flow,
U=(
self.port_1.m_flow * inlet_h_1
+ self.port_2.m_flow * inlet_h_2
+ self.thermal_energy_flow_w(props.T)
),
)
return derivative.as_vector()
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
pressure = self.medium.properties_from_mU(
self.state.m,
self.state.U,
self.cvol,
).p
return (
EquationResidual(
id=f"{self.name}:port_1_pressure_state",
owner="component",
owner_id=self.name,
relation="state",
variables=(f"{self.name}.port_1.p", f"{self.name}.state"),
role="effort",
value=self.port_1.p - pressure,
),
EquationResidual(
id=f"{self.name}:port_2_pressure_state",
owner="component",
owner_id=self.name,
relation="state",
variables=(f"{self.name}.port_2.p", f"{self.name}.state"),
role="effort",
value=self.port_2.p - pressure,
),
)
class AmesimPnch012(ThermodynamicVolumeComponent):
"""AMESim PNCH012 variable-volume pneumatic chamber.
AMESim supplies four external volume and volume-rate inputs through the
chamber ports. The current public System XML contract has pneumatic ports
only, so this first public model exposes those external volume inputs as SI
parameters. This represents fixed or prescribed-volume PNCH012 cases and is
not yet the full mechanical-coupled submodel.
"""
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 = (
ParameterDefinition(
"cvol0",
0.015,
label="死容积",
quantity="volume",
unit="m3",
minimum=0.0,
minimum_exclusive=True,
),
ParameterDefinition(
"kth",
0.0,
label="换热系数",
quantity="heat_transfer_coefficient",
unit="W/(m2*K)",
minimum=0.0,
),
ParameterDefinition(
"sth",
0.1,
label="换热面积",
quantity="area",
unit="m2",
minimum=0.0,
),
ParameterDefinition(
"extemp",
293.15,
label="外部温度",
quantity="temperature",
unit="K",
minimum=0.0,
minimum_exclusive=True,
),
ParameterDefinition(
"gi",
1.0,
label="气体类型索引",
quantity="dimensionless",
unit="",
minimum=1.0,
maximum=99.0,
),
ParameterDefinition(
"p0",
100000.0,
label="初始压力",
quantity="pressure",
unit="Pa",
minimum=0.0,
minimum_exclusive=True,
),
ParameterDefinition(
"T0",
293.15,
label="初始温度",
quantity="temperature",
unit="K",
minimum=0.0,
minimum_exclusive=True,
),
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="tank",
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: IdealGasMedium,
*,
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 = AmesimPnch023._integer_parameter("gi", 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 = self.p0 * self.total_volume() / (medium.R_gas * self.T0)
U0 = m0 * medium.specific_internal_energy(self.T0)
self.state = VolumeState(m=m0, U=U0)
initial_h = medium.specific_enthalpy(self.T0)
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: IdealGasMedium,
parameters: Mapping[str, float],
) -> "AmesimPnch012":
return cls(name=name, medium=medium, **dict(parameters))
def total_volume(self) -> float:
minimum_volume = self.cvol0 / 100.0
return max(self.cvol0 + sum(self.external_volumes.values()), 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())
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 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")
)