from __future__ import annotations from collections.abc import Mapping from math import pi from app.simulation.components.amesim.gases import ( AMESIM_GAS_INDEX_PARAMETER, normalize_amesim_gas_index, ) from app.simulation.core.base import AlgebraicComponent from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec from app.simulation.core.equations import EquationResidual from app.simulation.core.metadata import ParameterDefinition, ResultVariableDefinition from app.simulation.core.medium import GasMedium from app.simulation.core.ports import PortDefinition AMESIM_REFERENCE_PRESSURE_PA = 101300.0 class AmesimPnrp17(AlgebraicComponent): """AMESim PNRP17 pneumatic piston with two mechanical faces. Mechanical ports 2/5 share the piston-side motion and ports 3/4 share the cylinder-side motion. The pneumatic port contributes its swept volume and volume rate to the connected variable-volume chamber. """ MODEL_TYPE = "amesim_pnrp17" MODEL_VERSION = "0.1.0" PORTS = ( PortDefinition.pneumatic("port_1", nominal_role="bidirectional"), PortDefinition.mechanical_translational("port_2"), PortDefinition.mechanical_translational("port_3"), PortDefinition.mechanical_translational("port_4"), PortDefinition.mechanical_translational("port_5"), ) PARAMETERS = ( AMESIM_GAS_INDEX_PARAMETER, ParameterDefinition( "dp", 0.2, label="活塞直径", quantity="length", unit="m", minimum=0.0, minimum_exclusive=True, description="活塞外径;与活塞杆直径共同确定有效受压面积。", ), ParameterDefinition( "dr", 0.001, label="活塞杆直径", quantity="length", unit="m", minimum=0.0, description="穿过气室一侧的活塞杆直径,必须不大于活塞直径。", ), ParameterDefinition( "x0", 0.0, label="初始腔长", quantity="length", unit="m", description="机械端位移均为零时的气动腔长度。", ), ) RESULT_VARIABLES = ( ResultVariableDefinition("volume", "扫掠容积", "volume", "m3", "derived", 10), ResultVariableDefinition( "volume_flow", "扫掠容积变化率", "volume_flow", "m3/s", "derived", 20, ), ResultVariableDefinition("length", "气动腔长度", "length", "m", "derived", 30), ResultVariableDefinition( "pressure_force", "气压力", "force", "N", "derived", 40, ), ) DISPLAY = ComponentDisplaySpec( label="PNRP17 气动活塞", library_id="amesim", category_id="mechanical", symbol="amesim_pnrp17", ports=( PortDisplaySpec("port_1", "left", order=10), PortDisplaySpec("port_3", "left", order=20), PortDisplaySpec("port_2", "left", order=30), PortDisplaySpec("port_4", "right", order=40), PortDisplaySpec("port_5", "right", order=50), ), order=60, ) def __init__( self, name: str, medium: GasMedium, *, gi: float = 0.0, dp: float = 0.2, dr: float = 0.001, x0: float = 0.0, ) -> None: super().__init__(name=name) self.set_parameter_values({"gi": gi, "dp": dp, "dr": dr, "x0": x0}) self.medium = medium self.gi = normalize_amesim_gas_index(gi) self.dp = float(dp) self.dr = float(dr) self.x0 = float(x0) if self.dr > self.dp: raise ValueError("PNRP17 rod diameter dr must not exceed piston diameter dp.") for definition in self.PORTS: port = self.register_declared_port(definition.name) setattr(self, definition.name, port) self.port_1.h_outflow = medium.specific_enthalpy(medium.T_ref) @classmethod def create( cls, *, name: str, medium: GasMedium, parameters: Mapping[str, float], ) -> "AmesimPnrp17": return cls(name=name, medium=medium, **dict(parameters)) @property def effective_area(self) -> float: return pi * (self.dp * self.dp - self.dr * self.dr) / 4.0 @property def chamber_length(self) -> float: return self.x0 + self.port_5.x - self.port_4.x @property def chamber_volume(self) -> float: return self.effective_area * self.chamber_length @property def chamber_volume_flow(self) -> float: return self.effective_area * (self.port_5.v - self.port_4.v) @property def pressure_force(self) -> float: return (self.port_1.p - AMESIM_REFERENCE_PRESSURE_PA) * self.effective_area def pressure_flow_equation_values(self) -> tuple[float, ...]: values = [self.port_1.m_flow] effort_pairs = (("port_2", "port_5"), ("port_3", "port_4")) for first_name, second_name in effort_pairs: first = self.get_port(first_name) second = self.get_port(second_name) values.extend((first.x - second.x, first.v - second.v)) force = self.pressure_force values.extend( ( self.port_2.f + self.port_5.f + force, self.port_3.f + self.port_4.f - force, ) ) return tuple(values) def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]: effort_pairs = (("port_2", "port_5"), ("port_3", "port_4")) residuals: list[EquationResidual] = [ EquationResidual( id=f"{self.name}:pneumatic_zero_mass_flow", owner="component", owner_id=self.name, relation="constitutive", variables=(f"{self.name}.port_1.m_flow",), role="flow", value=self.port_1.m_flow, ) ] for first_name, second_name in effort_pairs: first = self.get_port(first_name) second = self.get_port(second_name) for variable in ("x", "v"): residuals.append( EquationResidual( id=f"{self.name}:{first_name}_{second_name}_{variable}_equal", owner="component", owner_id=self.name, relation="equal", variables=( f"{self.name}.{first_name}.{variable}", f"{self.name}.{second_name}.{variable}", ), role="effort", value=getattr(first, variable) - getattr(second, variable), ) ) force = self.pressure_force residuals.extend( ( EquationResidual( id=f"{self.name}:piston_side_force_balance", owner="component", owner_id=self.name, relation="constitutive", variables=(f"{self.name}.port_2.f", f"{self.name}.port_5.f", f"{self.name}.port_1.p"), role="flow", value=self.port_2.f + self.port_5.f + force, ), EquationResidual( id=f"{self.name}:cylinder_side_force_balance", owner="component", owner_id=self.name, relation="constitutive", variables=(f"{self.name}.port_3.f", f"{self.name}.port_4.f", f"{self.name}.port_1.p"), role="flow", value=self.port_3.f + self.port_4.f - force, ), ) ) return tuple(residuals) def pneumatic_volume_outputs(self) -> Mapping[str, tuple[float, float]]: return {"port_1": (self.chamber_volume, self.chamber_volume_flow)} def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None: self.port_1.h_outflow = connected_h.get( "port_1", self.medium.specific_enthalpy(self.medium.T_ref), ) def component_result_values(self) -> Mapping[str, float]: return { "volume": self.chamber_volume, "volume_flow": self.chamber_volume_flow, "length": self.chamber_length, "pressure_force": self.pressure_force, }