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