from __future__ import annotations from collections.abc import Mapping from math import isclose, log10, pi, sqrt 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 IdealGasMedium from app.simulation.core.ports import PortDefinition class AmesimPnl00r(AlgebraicComponent): """AMESim PNL00R pneumatic pipe friction resistance. The public model exposes the AMESim PNL00R catalog/XML contract and uses an auditable Darcy-Weisbach resistance with Reynolds/roughness-dependent friction. Exact `pn2pipefr_` parity is left for the later model tuning pass. """ MODEL_TYPE = "amesim_pnl00r" MODEL_VERSION = "0.1.0" PORTS = ( PortDefinition.pneumatic("port_1", nominal_role="bidirectional"), PortDefinition.pneumatic("port_2", nominal_role="bidirectional"), ) PARAMETERS = ( ParameterDefinition( "diam", 0.01, label="管径", quantity="length", unit="m", minimum=0.0, minimum_exclusive=True, ), ParameterDefinition( "le", 1.0, label="管长", quantity="length", unit="m", minimum=0.0, minimum_exclusive=True, ), ParameterDefinition( "rr", 1.0e-5, label="相对粗糙度", quantity="dimensionless", unit="", minimum=0.0, maximum=0.1, ), ParameterDefinition( "gi", 1.0, label="气体类型索引", quantity="dimensionless", unit="", minimum=1.0, maximum=99.0, ), ) RESULT_VARIABLES = ( ResultVariableDefinition( "re", label="Reynolds 数", quantity="dimensionless", unit="", category="derived", order=10, ), ResultVariableDefinition( "cm", label="质量流量参数", quantity="dimensionless", unit="", category="derived", order=20, ), ResultVariableDefinition( "v", label="平均气体速度", quantity="velocity", unit="m/s", category="derived", order=30, ), ResultVariableDefinition( "ff", label="摩擦因子", quantity="dimensionless", unit="", category="derived", order=40, ), ) DISPLAY = ComponentDisplaySpec( label="PNL00R 气动管路阻力", library_id="amesim", category_id="flow", symbol="pipe", ports=( PortDisplaySpec("port_1", "left", order=10), PortDisplaySpec("port_2", "right", order=20), ), order=20, ) def __init__( self, name: str, medium: IdealGasMedium, *, diam: float = 0.01, le: float = 1.0, rr: float = 1.0e-5, gi: float = 1.0, ) -> None: super().__init__(name=name) self.set_parameter_values({"diam": diam, "le": le, "rr": rr, "gi": gi}) self.medium = medium self.diam = float(diam) self.le = float(le) self.rr = float(rr) self.gi = self._integer_parameter("gi", gi) self.area = pi * self.diam * self.diam / 4.0 initial_h = medium.specific_enthalpy(medium.T_ref) self.port_1 = self.register_declared_port("port_1") self.port_1.h_outflow = initial_h self.port_2 = self.register_declared_port("port_2") 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"PNL00R parameter {name} must be an integer value.") return int(rounded) @classmethod def create( cls, *, name: str, medium: IdealGasMedium, parameters: Mapping[str, float], ) -> AmesimPnl00r: return cls( name=name, medium=medium, diam=parameters["diam"], le=parameters["le"], rr=parameters["rr"], gi=parameters["gi"], ) def _port_temperature(self, port_name: str) -> float: port = self.get_port(port_name) if port.h_outflow > 0.0: return max(port.h_outflow / self.medium.cp_ref, 1.0) return self.medium.T_ref @staticmethod def _dynamic_viscosity(temperature_k: float) -> float: if temperature_k <= 0.0: raise ValueError("temperature_k must be positive") reference_temperature = 293.15 reference_viscosity = 1.82e-5 sutherland_constant = 110.4 return ( reference_viscosity * (temperature_k / reference_temperature) ** 1.5 * (reference_temperature + sutherland_constant) / (temperature_k + sutherland_constant) ) def reynolds_number(self, mass_flow: float, temperature: float) -> float: viscosity = self._dynamic_viscosity(temperature) return 4.0 * abs(mass_flow) / (pi * self.diam * viscosity) def friction_factor(self, reynolds_number: float) -> float: if reynolds_number <= 0.0: return 64_000_000.0 laminar = 64.0 / reynolds_number if reynolds_number <= 2300.0: return laminar turbulent = 1.0 / ( -1.8 * log10((self.rr / 3.7) ** 1.11 + 6.9 / reynolds_number) ) ** 2 if reynolds_number >= 4000.0: return turbulent fraction = (reynolds_number - 2300.0) / 1700.0 return laminar + fraction * (turbulent - laminar) def darcy_pressure_drop( self, mass_flow: float, *, density: float, temperature: float, ) -> float: if mass_flow == 0.0: return 0.0 reynolds = self.reynolds_number(mass_flow, temperature) friction = self.friction_factor(reynolds) velocity = mass_flow / (density * self.area) magnitude = ( friction * (self.le / self.diam) * density * velocity * velocity / 2.0 ) return magnitude if mass_flow > 0.0 else -magnitude def _mass_flow_for_pressure_drop( self, pressure_drop: float, *, density: float, temperature: float, ) -> float: if pressure_drop <= 0.0: return 0.0 upper = 1.0e-9 while self.darcy_pressure_drop(upper, density=density, temperature=temperature) < pressure_drop: upper *= 10.0 if upper > 1.0e3: raise ValueError("unable to bracket PNL00R resistance flow") lower = 0.0 for _ in range(48): middle = 0.5 * (lower + upper) if self.darcy_pressure_drop(middle, density=density, temperature=temperature) < pressure_drop: lower = middle else: upper = middle return 0.5 * (lower + upper) def mass_flow(self, p_1: float, p_2: float) -> float: if p_1 == p_2: return 0.0 pressure_difference = p_1 - p_2 upstream_pressure = max(p_1, p_2, 1.0) upstream_temperature = self._port_temperature("port_1" if pressure_difference > 0.0 else "port_2") density = max(self.medium.density(upstream_pressure, upstream_temperature), 1.0e-12) magnitude = self._mass_flow_for_pressure_drop( abs(pressure_difference), density=density, temperature=upstream_temperature, ) return magnitude if pressure_difference > 0.0 else -magnitude def component_result_values(self) -> Mapping[str, float]: m_flow = self.mass_flow(self.port_1.p, self.port_2.p) upstream_pressure = max(self.port_1.p, self.port_2.p, 1.0) upstream_temperature = self._port_temperature( "port_1" if self.port_1.p >= self.port_2.p else "port_2" ) density = max(self.medium.density(upstream_pressure, upstream_temperature), 1.0e-12) reynolds = self.reynolds_number(m_flow, upstream_temperature) velocity = m_flow / (density * self.area) cm = abs(m_flow) / max(self.area * upstream_pressure, 1.0e-18) return { "re": reynolds, "cm": cm, "v": velocity, "ff": self.friction_factor(reynolds), } def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]: return ( EquationResidual( id=f"{self.name}:mass_flow_balance", owner="component", owner_id=self.name, relation="sumToZero", variables=( f"{self.name}.port_1.m_flow", f"{self.name}.port_2.m_flow", ), role="flow", value=self.port_1.m_flow + self.port_2.m_flow, ), EquationResidual( id=f"{self.name}:pressure_flow_relation", owner="component", owner_id=self.name, relation="constitutive", variables=( f"{self.name}.port_1.p", f"{self.name}.port_2.p", f"{self.name}.port_1.m_flow", ), role="flow", value=self.port_1.m_flow - self.mass_flow(self.port_1.p, self.port_2.p), ), ) def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None: self.port_1.h_outflow = connected_h["port_2"] self.port_2.h_outflow = connected_h["port_1"]