from __future__ import annotations from functools import lru_cache from collections.abc import Mapping from math import isclose, log, sqrt, tanh 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, ParameterGroupDisplaySpec, PortDisplaySpec, ) from app.simulation.core.equations import EquationResidual from app.simulation.core.metadata import ( ParameterCondition, ParameterDefinition, ParameterOption, ResultVariableDefinition, ) from app.simulation.core.medium import GasMedium from app.simulation.core.ports import PortDefinition _FLOW_COEFFICIENT_OPTIONS = ( ParameterOption(1.0, "Cq"), ParameterOption(2.0, "Cv"), ParameterOption(3.0, "Kv"), ) _FLOWSET_USES_CQ = (ParameterCondition("flowset", (1.0,)),) _FLOWSET_USES_CV = (ParameterCondition("flowset", (2.0,)),) _FLOWSET_USES_KV = (ParameterCondition("flowset", (3.0,)),) _PN_PRESSURE_RATIO_ACCURACY = 0.9999 _PN_LAMINAR_SMOOTHING_GAIN = 12.0 _PNVO001_CLOSED_OPENING_ABS_TOL = 1.0e-12 _PNOR001_FLOW_COEFFICIENT_GROUP = ParameterGroupDisplaySpec( id="flow_coefficient", label="流量系数", parameters=("cq", "area", "Cv", "Kv"), order=10, ) _PNVO001_FLOW_COEFFICIENT_GROUP = ParameterGroupDisplaySpec( id="flow_coefficient", label="流量系数", parameters=("cq", "area0", "Cv", "Kv"), order=10, ) class AmesimPnor001(AlgebraicComponent): """AMESim PNOR001 constant-flow-coefficient pneumatic orifice. This public component preserves the PNOR001 catalog/XML contract and uses real-gas pressure-ratio flow with AMESim-style near-equal-pressure smoothing. """ MODEL_TYPE = "amesim_pnor001" MODEL_VERSION = "0.3.0" PRESSURE_FLOW_DEPENDS_ON_STREAM = True PRESSURE_FLOW_EXACT_SUM_TO_ZERO_EQUATION_SUFFIXES = frozenset( ("mass_flow_balance",) ) PORTS = ( PortDefinition.pneumatic("port_1", nominal_role="bidirectional"), PortDefinition.pneumatic("port_2", nominal_role="bidirectional"), ) PARAMETERS = ( AMESIM_GAS_INDEX_PARAMETER, ParameterDefinition( "cq", 0.72, label="流量系数 Cq", quantity="dimensionless", unit="", minimum=1.0e-10, maximum=1.0, description="孔口实际质量流量相对于理想可压缩流量的无量纲修正系数。", visible_when=_FLOWSET_USES_CQ, ), ParameterDefinition( "area", 5.0e-6, label="孔口面积", quantity="area", unit="m2", minimum=0.0, maximum=1.0, description="选择 Cq/面积方式时用于流量计算的有效孔口面积。", visible_when=_FLOWSET_USES_CQ, ), ParameterDefinition( "Cv", 0.5, label="流量系数 Cv", quantity="dimensionless", unit="", minimum=0.0, description="选择 Cv 方式时使用的英制流量系数。", visible_when=_FLOWSET_USES_CV, ), ParameterDefinition( "Kv", 0.4, label="流量系数 Kv", quantity="dimensionless", unit="", minimum=0.0, description="选择 Kv 方式时使用的公制流量系数。", visible_when=_FLOWSET_USES_KV, ), ParameterDefinition( "flowset", 1.0, label="流量系数设置", quantity="dimensionless", unit="", minimum=1.0, maximum=3.0, editor="choice", options=_FLOW_COEFFICIENT_OPTIONS, description="流量参数方式:1 使用 Cq 和面积,2 使用 Cv,3 使用 Kv。", ), ) RESULT_VARIABLES = ( ResultVariableDefinition( "cm", label="质量流量参数", quantity="dimensionless", unit="", category="derived", order=10, ), ResultVariableDefinition( "gasvel", label="缩流截面气体速度", quantity="velocity", unit="m/s", category="derived", order=20, ), ) DISPLAY = ComponentDisplaySpec( label="PNOR001 常系数气动孔口", library_id="amesim", category_id="flow", symbol="amesim_pnor001", ports=( PortDisplaySpec("port_1", "left", order=10), PortDisplaySpec("port_2", "right", order=20), ), order=10, parameter_groups=(_PNOR001_FLOW_COEFFICIENT_GROUP,), ) def __init__( self, name: str, medium: GasMedium, *, cq: float = 0.72, area: float = 5.0e-6, Cv: float = 0.5, Kv: float = 0.4, gi: float = 1.0, flowset: float = 1.0, ) -> None: super().__init__(name=name) self.set_parameter_values( { "cq": cq, "area": area, "Cv": Cv, "Kv": Kv, "gi": gi, "flowset": flowset, } ) self.medium = medium self.cq = float(cq) self.area = float(area) self.Cv = float(Cv) self.Kv = float(Kv) self.gi = normalize_amesim_gas_index(gi) self.flowset = self._integer_parameter("flowset", flowset) if self.flowset not in {1, 2, 3}: raise ValueError("PNOR001 flowset must be 1, 2, or 3.") 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 self._connected_h: dict[str, float] = {} @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"PNOR001 parameter {name} must be an integer value.") return int(rounded) @classmethod def create( cls, *, name: str, medium: GasMedium, parameters: Mapping[str, float], ) -> AmesimPnor001: return cls( name=name, medium=medium, cq=parameters["cq"], area=parameters["area"], Cv=parameters["Cv"], Kv=parameters["Kv"], gi=parameters["gi"], flowset=parameters["flowset"], ) @property def effective_cq(self) -> float: return self.cq if self.flowset == 1 else 0.72 @property def effective_area(self) -> float: if self.flowset == 1: return self.area if self.flowset == 2: return self._area_from_cv(self.Cv, self.effective_cq) return self._area_from_kv(self.Kv, self.effective_cq) @staticmethod def _area_from_cv(Cv: float, cq: float) -> float: water_density = 999.0 reference_flow_m3_s = Cv * 6.30901964e-5 reference_dp_pa = 6894.75729 return reference_flow_m3_s / (cq * sqrt(2.0 * reference_dp_pa / water_density)) @staticmethod def _area_from_kv(Kv: float, cq: float) -> float: water_density = 999.0 reference_flow_m3_s = Kv / 3600.0 reference_dp_pa = 100000.0 return reference_flow_m3_s / (cq * sqrt(2.0 * reference_dp_pa / water_density)) def _upstream_temperature(self, port_name: str) -> float: port = self.get_port(port_name) inlet_h = self._connected_h.get(port_name, port.h_outflow) return max( self.medium.temperature_from_pressure_enthalpy( max(port.p, 1.0), inlet_h, ), 1.0, ) @staticmethod def _subsonic_mass_flow_parameter( *, pressure_ratio: float, gamma_s: float, density: float, upstream_temperature: float, upstream_pressure: float, ) -> float: expansion = ( pressure_ratio ** (2.0 * gamma_s) - pressure_ratio ** (1.0 + gamma_s) ) return sqrt( max( 2.0 / (1.0 - gamma_s) * density * upstream_temperature / upstream_pressure * expansion, 0.0, ) ) @lru_cache(maxsize=32768) def _one_way_flow_characteristics( self, *, upstream_pressure: float, downstream_pressure: float, upstream_temperature: float, ) -> tuple[float, float]: p_up = max(upstream_pressure, 1.0) p_down = max(min(downstream_pressure, p_up), 0.0) T_up = max(upstream_temperature, 1.0) gamma_s = self.medium.isentropic_density_pressure_factor( p_up, T_up, p_down, ) gamma_s = min(max(gamma_s, 1.0e-9), 1.0 - 1.0e-9) density = max(self.medium.density(p_up, T_up), 1.0e-12) pressure_ratio = max(p_down / p_up, 0.0) critical_ratio = (2.0 * gamma_s / (gamma_s + 1.0)) ** ( 1.0 / (1.0 - gamma_s) ) if pressure_ratio <= critical_ratio: effective_pressure_ratio = critical_ratio mass_flow_parameter = ( sqrt(2.0 / (1.0 + gamma_s) * density * T_up / p_up) * (2.0 * gamma_s / (gamma_s + 1.0)) ** (gamma_s / (1.0 - gamma_s)) ) gas_velocity = sqrt(2.0 / (1.0 + gamma_s) * p_up / density) else: effective_pressure_ratio = pressure_ratio mass_flow_parameter = self._subsonic_mass_flow_parameter( pressure_ratio=pressure_ratio, gamma_s=gamma_s, density=density, upstream_temperature=T_up, upstream_pressure=p_up, ) gas_velocity = sqrt( max( 2.0 / (1.0 - gamma_s) * p_up / density * (1.0 - pressure_ratio ** (1.0 - gamma_s)), 0.0, ) ) reference = self._subsonic_mass_flow_parameter( pressure_ratio=_PN_PRESSURE_RATIO_ACCURACY, gamma_s=gamma_s, density=density, upstream_temperature=T_up, upstream_pressure=p_up, ) if mass_flow_parameter > 0.0 and reference > 0.0: argument = ( _PN_LAMINAR_SMOOTHING_GAIN * abs(mass_flow_parameter / reference) * log(effective_pressure_ratio) / log(_PN_PRESSURE_RATIO_ACCURACY) ) smoothing_factor = tanh(max(argument, 0.0)) mass_flow_parameter *= smoothing_factor gas_velocity *= smoothing_factor return mass_flow_parameter, gas_velocity def mass_flow(self, p_1: float, p_2: float) -> float: if ( isclose(p_1, p_2, rel_tol=0.0, abs_tol=1.0e-8) or self.effective_area == 0.0 ): return 0.0 if p_1 > p_2: return self._one_way_mass_flow( upstream_pressure=p_1, downstream_pressure=p_2, upstream_temperature=self._upstream_temperature("port_1"), ) return -self._one_way_mass_flow( upstream_pressure=p_2, downstream_pressure=p_1, upstream_temperature=self._upstream_temperature("port_2"), ) def _one_way_mass_flow( self, *, upstream_pressure: float, downstream_pressure: float, upstream_temperature: float, ) -> float: p_up = max(upstream_pressure, 1.0) T_up = max(upstream_temperature, 1.0) mass_flow_parameter, _ = self._one_way_flow_characteristics( upstream_pressure=p_up, downstream_pressure=downstream_pressure, upstream_temperature=T_up, ) return ( self.effective_cq * self.effective_area * p_up * mass_flow_parameter / sqrt(T_up) ) def component_result_values(self) -> Mapping[str, float]: p_1 = max(self.port_1.p, 1.0) p_2 = max(self.port_2.p, 1.0) if p_1 >= p_2: upstream_port_name = "port_1" upstream_pressure = p_1 downstream_pressure = p_2 flow_direction = 1.0 else: upstream_port_name = "port_2" upstream_pressure = p_2 downstream_pressure = p_1 flow_direction = -1.0 mass_flow_parameter, gas_velocity = self._one_way_flow_characteristics( upstream_pressure=upstream_pressure, downstream_pressure=downstream_pressure, upstream_temperature=self._upstream_temperature(upstream_port_name), ) return { "cm": mass_flow_parameter, "gasvel": flow_direction * gas_velocity, } def pressure_flow_equation_values(self) -> tuple[float, ...]: return ( self.port_1.m_flow + self.port_2.m_flow, self.port_1.m_flow - self.mass_flow(self.port_1.p, self.port_2.p), ) 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._connected_h = dict(connected_h) self.port_1.h_outflow = connected_h["port_2"] self.port_2.h_outflow = connected_h["port_1"] class AmesimPnvo001FixedOpening(AlgebraicComponent): """Fixed-opening public variant of AMESim PNVO001. Full PNVO001 has a signal input port. The current public component library does not support signal simulation, so this model exposes the pneumatic ports and replaces the signal with a normalized `opening` parameter. """ MODEL_TYPE = "amesim_pnvo001_fixed" MODEL_VERSION = "0.2.0" PRESSURE_FLOW_DEPENDS_ON_STREAM = True PRESSURE_FLOW_EXACT_SUM_TO_ZERO_EQUATION_SUFFIXES = frozenset( ("mass_flow_balance",) ) PORTS = ( PortDefinition.pneumatic("port_2", nominal_role="bidirectional"), PortDefinition.pneumatic("port_3", nominal_role="bidirectional"), ) PARAMETERS = ( AMESIM_GAS_INDEX_PARAMETER, ParameterDefinition( "cq", 0.72, label="流量系数 Cq", quantity="dimensionless", unit="", minimum=1.0e-10, maximum=1.0, description="孔口实际质量流量相对于理想可压缩流量的无量纲修正系数。", visible_when=_FLOWSET_USES_CQ, ), ParameterDefinition( "area0", 5.0e-6, label="最大孔口面积", quantity="area", unit="m2", minimum=0.0, maximum=1.0, description="阀门完全开启时的最大有效孔口面积。", visible_when=_FLOWSET_USES_CQ, ), ParameterDefinition( "Cv", 0.5, label="最大流量系数 Cv", quantity="dimensionless", unit="", minimum=0.0, description="选择 Cv 方式时使用的最大英制流量系数。", visible_when=_FLOWSET_USES_CV, ), ParameterDefinition( "Kv", 0.4, label="最大流量系数 Kv", quantity="dimensionless", unit="", minimum=0.0, description="选择 Kv 方式时使用的最大公制流量系数。", visible_when=_FLOWSET_USES_KV, ), ParameterDefinition( "flowset", 1.0, label="流量系数设置", quantity="dimensionless", unit="", minimum=1.0, maximum=3.0, editor="choice", options=_FLOW_COEFFICIENT_OPTIONS, description="流量参数方式:1 使用 Cq 和面积,2 使用 Cv,3 使用 Kv。", ), ParameterDefinition( "opening", 1.0, label="固定开度", quantity="dimensionless", unit="", minimum=0.0, maximum=1.0, description="固定的归一化阀门开度;0 表示关闭,1 表示完全开启。", ), ) RESULT_VARIABLES = ( ResultVariableDefinition( "xv", label="有效开度", quantity="dimensionless", unit="", category="derived", order=10, ), ResultVariableDefinition( "cm", label="质量流量参数", quantity="dimensionless", unit="", category="derived", order=20, ), ResultVariableDefinition( "gasvel", label="缩流截面气体速度", quantity="velocity", unit="m/s", category="derived", order=30, ), ) DISPLAY = ComponentDisplaySpec( label="PNVO001 固定开度气动孔口", library_id="amesim", category_id="flow", symbol="amesim_pnvo001_fixed", ports=( PortDisplaySpec("port_2", "left", order=10), PortDisplaySpec("port_3", "right", order=20), ), order=30, parameter_groups=(_PNVO001_FLOW_COEFFICIENT_GROUP,), ) def __init__( self, name: str, medium: GasMedium, *, cq: float = 0.72, area0: float = 5.0e-6, Cv: float = 0.5, Kv: float = 0.4, gi: float = 1.0, flowset: float = 1.0, opening: float = 1.0, ) -> None: super().__init__(name=name) self.set_parameter_values( { "cq": cq, "area0": area0, "Cv": Cv, "Kv": Kv, "gi": gi, "flowset": flowset, "opening": opening, } ) self.medium = medium self.cq = float(cq) self.area0 = float(area0) self.Cv = float(Cv) self.Kv = float(Kv) self.gi = normalize_amesim_gas_index(gi) self.flowset = self._integer_parameter("flowset", flowset) if self.flowset not in {1, 2, 3}: raise ValueError("PNVO001 fixed-opening flowset must be 1, 2, or 3.") self.opening = min(1.0, max(0.0, float(opening))) initial_h = medium.specific_enthalpy(medium.T_ref) self.port_2 = self.register_declared_port("port_2") self.port_2.h_outflow = initial_h self.port_3 = self.register_declared_port("port_3") self.port_3.h_outflow = initial_h self._connected_h: dict[str, float] = {} @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"PNVO001 fixed-opening parameter {name} must be an integer value.") return int(rounded) @classmethod def create( cls, *, name: str, medium: GasMedium, parameters: Mapping[str, float], ) -> AmesimPnvo001FixedOpening: return cls( name=name, medium=medium, cq=parameters["cq"], area0=parameters["area0"], Cv=parameters["Cv"], Kv=parameters["Kv"], gi=parameters["gi"], flowset=parameters["flowset"], opening=parameters["opening"], ) @property def effective_cq(self) -> float: return self.cq if self.flowset == 1 else 0.72 @property def maximum_area(self) -> float: if self.flowset == 1: return self.area0 if self.flowset == 2: return AmesimPnor001._area_from_cv(self.Cv, self.effective_cq) return AmesimPnor001._area_from_kv(self.Kv, self.effective_cq) @property def effective_area(self) -> float: return self.opening * self.maximum_area def _upstream_temperature(self, port_name: str) -> float: port = self.get_port(port_name) # A component port's h_outflow describes fluid leaving the valve; the # upstream state comes from the connection on that same physical side. inlet_h = self._connected_h.get(port_name, port.h_outflow) return max( self.medium.temperature_from_pressure_enthalpy( max(port.p, 1.0), inlet_h, ), 1.0, ) @staticmethod def _subsonic_mass_flow_parameter( *, pressure_ratio: float, gamma_s: float, density: float, upstream_temperature: float, upstream_pressure: float, ) -> float: expansion = ( pressure_ratio ** (2.0 * gamma_s) - pressure_ratio ** (1.0 + gamma_s) ) return sqrt( max( 2.0 / (1.0 - gamma_s) * density * upstream_temperature / upstream_pressure * expansion, 0.0, ) ) def mass_flow(self, p_2: float, p_3: float) -> float: if ( isclose(p_2, p_3, rel_tol=0.0, abs_tol=1.0e-8) or self.effective_area == 0.0 ): return 0.0 if p_2 > p_3: return self._one_way_mass_flow( upstream_pressure=p_2, downstream_pressure=p_3, upstream_temperature=self._upstream_temperature("port_2"), ) return -self._one_way_mass_flow( upstream_pressure=p_3, downstream_pressure=p_2, upstream_temperature=self._upstream_temperature("port_3"), ) @lru_cache(maxsize=32768) def _one_way_flow_characteristics( self, *, upstream_pressure: float, downstream_pressure: float, upstream_temperature: float, ) -> tuple[float, float]: p_up = max(upstream_pressure, 1.0) p_down = max(min(downstream_pressure, p_up), 0.0) T_up = max(upstream_temperature, 1.0) gamma_s = self.medium.isentropic_density_pressure_factor( p_up, T_up, p_down, ) gamma_s = min(max(gamma_s, 1.0e-9), 1.0 - 1.0e-9) density = max(self.medium.density(p_up, T_up), 1.0e-12) pressure_ratio = max(p_down / p_up, 0.0) critical_ratio = (2.0 * gamma_s / (gamma_s + 1.0)) ** ( 1.0 / (1.0 - gamma_s) ) if pressure_ratio <= critical_ratio: effective_pressure_ratio = critical_ratio mass_flow_parameter = ( sqrt(2.0 / (1.0 + gamma_s) * density * T_up / p_up) * (2.0 * gamma_s / (gamma_s + 1.0)) ** (gamma_s / (1.0 - gamma_s)) ) gas_velocity = sqrt( 2.0 / (1.0 + gamma_s) * p_up / density ) else: effective_pressure_ratio = pressure_ratio mass_flow_parameter = self._subsonic_mass_flow_parameter( pressure_ratio=pressure_ratio, gamma_s=gamma_s, density=density, upstream_temperature=T_up, upstream_pressure=p_up, ) gas_velocity = sqrt( max( 2.0 / (1.0 - gamma_s) * p_up / density * (1.0 - pressure_ratio ** (1.0 - gamma_s)), 0.0, ) ) # AMESim's gas_cm_prc_ applies this factor continuously over the # complete pressure-ratio range. It is effectively one outside the # near-equal-pressure region and makes Cm (and vena-contracta # velocity) approach zero quadratically as the pressure ratio tends # to one. The reference Cm intentionally reuses the current gamma_s. reference_mass_flow_parameter = self._subsonic_mass_flow_parameter( pressure_ratio=_PN_PRESSURE_RATIO_ACCURACY, gamma_s=gamma_s, density=density, upstream_temperature=T_up, upstream_pressure=p_up, ) if mass_flow_parameter > 0.0 and reference_mass_flow_parameter > 0.0: smoothing_argument = ( _PN_LAMINAR_SMOOTHING_GAIN * abs(mass_flow_parameter / reference_mass_flow_parameter) * log(effective_pressure_ratio) / log(_PN_PRESSURE_RATIO_ACCURACY) ) smoothing_factor = tanh(max(smoothing_argument, 0.0)) mass_flow_parameter *= smoothing_factor gas_velocity *= smoothing_factor return mass_flow_parameter, gas_velocity def _one_way_mass_flow( self, *, upstream_pressure: float, downstream_pressure: float, upstream_temperature: float, ) -> float: p_up = max(upstream_pressure, 1.0) T_up = max(upstream_temperature, 1.0) mass_flow_parameter, _gas_velocity = self._one_way_flow_characteristics( upstream_pressure=p_up, downstream_pressure=downstream_pressure, upstream_temperature=T_up, ) return ( self.effective_cq * self.effective_area * p_up * mass_flow_parameter / sqrt(T_up) ) def component_result_values(self) -> Mapping[str, float]: p_2 = max(self.port_2.p, 1.0) p_3 = max(self.port_3.p, 1.0) if p_2 >= p_3: upstream_port_name = "port_2" upstream_pressure = p_2 downstream_pressure = p_3 flow_direction = 1.0 else: upstream_port_name = "port_3" upstream_pressure = p_3 downstream_pressure = p_2 flow_direction = -1.0 upstream_temperature = self._upstream_temperature( upstream_port_name ) mass_flow_parameter, gas_velocity = self._one_way_flow_characteristics( upstream_pressure=upstream_pressure, downstream_pressure=downstream_pressure, upstream_temperature=upstream_temperature, ) # AMESim reports no vena-contracta velocity while the valve is closed. # Signal propagation around a step can leave a round-off-sized opening, # so apply the same numerical-zero convention to this diagnostic only. if isclose( self.opening, 0.0, rel_tol=0.0, abs_tol=_PNVO001_CLOSED_OPENING_ABS_TOL, ): gas_velocity = 0.0 return { "xv": self.opening, "cm": mass_flow_parameter, "gasvel": flow_direction * gas_velocity, } def pressure_flow_equation_values(self) -> tuple[float, ...]: return ( self.port_2.m_flow + self.port_3.m_flow, self.port_2.m_flow - self.mass_flow(self.port_2.p, self.port_3.p), ) 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_2.m_flow", f"{self.name}.port_3.m_flow", ), role="flow", value=self.port_2.m_flow + self.port_3.m_flow, ), EquationResidual( id=f"{self.name}:pressure_flow_relation", owner="component", owner_id=self.name, relation="constitutive", variables=( f"{self.name}.port_2.p", f"{self.name}.port_3.p", f"{self.name}.port_2.m_flow", ), role="flow", value=self.port_2.m_flow - self.mass_flow(self.port_2.p, self.port_3.p), ), ) def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None: self._connected_h = dict(connected_h) self.port_2.h_outflow = connected_h["port_3"] self.port_3.h_outflow = connected_h["port_2"] class AmesimPnvo001SignalOpening(AmesimPnvo001FixedOpening): """AMESim PNVO001 signal-controlled pneumatic orifice.""" MODEL_TYPE = "amesim_pnvo001" MODEL_VERSION = "0.2.0" PRESSURE_FLOW_DEPENDS_ON_STREAM = True # Repeat the exact-sum promise on this concrete subclass deliberately. PRESSURE_FLOW_EXACT_SUM_TO_ZERO_EQUATION_SUFFIXES = frozenset( ("mass_flow_balance",) ) PORTS = ( PortDefinition.signal("res", nominal_role="input"), PortDefinition.pneumatic("port_2", nominal_role="bidirectional"), PortDefinition.pneumatic("port_3", nominal_role="bidirectional"), ) PARAMETERS = ( AMESIM_GAS_INDEX_PARAMETER, ParameterDefinition( "cq", 0.72, label="流量系数 Cq", quantity="dimensionless", unit="", minimum=1.0e-10, maximum=1.0, description="孔口实际质量流量相对于理想可压缩流量的无量纲修正系数。", visible_when=_FLOWSET_USES_CQ, ), ParameterDefinition( "area0", 5.0e-6, label="最大孔口面积", quantity="area", unit="m2", minimum=0.0, maximum=1.0, description="阀门完全开启时的最大有效孔口面积。", visible_when=_FLOWSET_USES_CQ, ), ParameterDefinition( "Cv", 0.5, label="最大流量系数 Cv", quantity="dimensionless", unit="", minimum=0.0, description="选择 Cv 方式时使用的最大英制流量系数。", visible_when=_FLOWSET_USES_CV, ), ParameterDefinition( "Kv", 0.4, label="最大流量系数 Kv", quantity="dimensionless", unit="", minimum=0.0, description="选择 Kv 方式时使用的最大公制流量系数。", visible_when=_FLOWSET_USES_KV, ), ParameterDefinition( "flowset", 1.0, label="流量系数设置", quantity="dimensionless", unit="", minimum=1.0, maximum=3.0, editor="choice", options=_FLOW_COEFFICIENT_OPTIONS, description="流量参数方式:1 使用 Cq 和面积,2 使用 Cv,3 使用 Kv。", ), ParameterDefinition( "opening0", 1.0, label="初始开度", quantity="dimensionless", unit="", minimum=0.0, maximum=1.0, description="信号尚未传播时使用的归一化初始开度;0 表示关闭,1 表示完全开启。", ), ) RESULT_VARIABLES = AmesimPnvo001FixedOpening.RESULT_VARIABLES DISPLAY = ComponentDisplaySpec( label="PNVO001 信号开度气动孔口", library_id="amesim", category_id="flow", symbol="amesim_pnvo001", ports=( PortDisplaySpec("res", "left", order=5), PortDisplaySpec("port_2", "left", order=10), PortDisplaySpec("port_3", "right", order=20), ), order=35, parameter_groups=(_PNVO001_FLOW_COEFFICIENT_GROUP,), ) def __init__( self, name: str, medium: GasMedium, *, cq: float = 0.72, area0: float = 5.0e-6, Cv: float = 0.5, Kv: float = 0.4, gi: float = 1.0, flowset: float = 1.0, opening0: float = 1.0, ) -> None: AlgebraicComponent.__init__(self, name=name) self.set_parameter_values( { "cq": cq, "area0": area0, "Cv": Cv, "Kv": Kv, "gi": gi, "flowset": flowset, "opening0": opening0, } ) self.medium = medium self.cq = float(cq) self.area0 = float(area0) self.Cv = float(Cv) self.Kv = float(Kv) self.gi = normalize_amesim_gas_index(gi) self.flowset = self._integer_parameter("flowset", flowset) if self.flowset not in {1, 2, 3}: raise ValueError("PNVO001 signal-opening flowset must be 1, 2, or 3.") self.opening0 = min(1.0, max(0.0, float(opening0))) self.res = self.register_declared_port("res") self.res.signal = self.opening0 initial_h = medium.specific_enthalpy(medium.T_ref) self.port_2 = self.register_declared_port("port_2") self.port_2.h_outflow = initial_h self.port_3 = self.register_declared_port("port_3") self.port_3.h_outflow = initial_h self._connected_h: dict[str, float] = {} @classmethod def create( cls, *, name: str, medium: GasMedium, parameters: Mapping[str, float], ) -> "AmesimPnvo001SignalOpening": return cls(name=name, medium=medium, **dict(parameters)) @property def opening(self) -> float: return min(1.0, max(0.0, self.res.signal))