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, 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, ), )