"""Component parameters, ports and output definitions; numerical equations execute in C.""" from __future__ import annotations from collections.abc import Mapping from math import isclose, sqrt 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.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,)),) _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' 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=1e-10, maximum=1.0, description='孔口实际质量流量相对于理想可压缩流量的无量纲修正系数。', visible_when=_FLOWSET_USES_CQ), ParameterDefinition('area', 5e-06, 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=5e-06, 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.') self.port_1 = self.register_declared_port('port_1') self.port_2 = self.register_declared_port('port_2') @staticmethod def _integer_parameter(name: str, value: float) -> int: rounded = round(value) if not isclose(value, rounded, rel_tol=0.0, abs_tol=1e-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-05 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)) EQUATIONS = ({'id': '__MODEL__:mass_flow_balance', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'sumToZero', 'variables': ['__MODEL__.port_1.m_flow', '__MODEL__.port_2.m_flow'], 'role': 'flow'}, {'id': '__MODEL__:pressure_flow_relation', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'constitutive', 'variables': ['__MODEL__.port_1.p', '__MODEL__.port_2.p', '__MODEL__.port_1.m_flow'], 'role': 'flow'}) 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' 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=1e-10, maximum=1.0, description='孔口实际质量流量相对于理想可压缩流量的无量纲修正系数。', visible_when=_FLOWSET_USES_CQ), ParameterDefinition('area0', 5e-06, 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', 'right', order=10), PortDisplaySpec('port_3', 'left', order=20)), order=30, parameter_groups=(_PNVO001_FLOW_COEFFICIENT_GROUP,)) def __init__(self, name: str, medium: GasMedium, *, cq: float=0.72, area0: float=5e-06, 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))) self.port_2 = self.register_declared_port('port_2') self.port_3 = self.register_declared_port('port_3') @staticmethod def _integer_parameter(name: str, value: float) -> int: rounded = round(value) if not isclose(value, rounded, rel_tol=0.0, abs_tol=1e-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 EQUATIONS = ({'id': '__MODEL__:mass_flow_balance', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'sumToZero', 'variables': ['__MODEL__.port_2.m_flow', '__MODEL__.port_3.m_flow'], 'role': 'flow'}, {'id': '__MODEL__:pressure_flow_relation', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'constitutive', 'variables': ['__MODEL__.port_2.p', '__MODEL__.port_3.p', '__MODEL__.port_2.m_flow'], 'role': 'flow'}) class AmesimPnvo001SignalOpening(AmesimPnvo001FixedOpening): """AMESim PNVO001 signal-controlled pneumatic orifice.""" MODEL_TYPE = 'amesim_pnvo001' MODEL_VERSION = '0.2.0' 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=1e-10, maximum=1.0, description='孔口实际质量流量相对于理想可压缩流量的无量纲修正系数。', visible_when=_FLOWSET_USES_CQ), ParameterDefinition('area0', 5e-06, 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', 'right', order=10), PortDisplaySpec('port_3', 'left', order=20)), order=35, parameter_groups=(_PNVO001_FLOW_COEFFICIENT_GROUP,)) def __init__(self, name: str, medium: GasMedium, *, cq: float=0.72, area0: float=5e-06, 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.port_2 = self.register_declared_port('port_2') self.port_3 = self.register_declared_port('port_3') @classmethod def create(cls, *, name: str, medium: GasMedium, parameters: Mapping[str, float]) -> 'AmesimPnvo001SignalOpening': return cls(name=name, medium=medium, **dict(parameters)) EQUATIONS = ({'id': '__MODEL__:mass_flow_balance', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'sumToZero', 'variables': ['__MODEL__.port_2.m_flow', '__MODEL__.port_3.m_flow'], 'role': 'flow'}, {'id': '__MODEL__:pressure_flow_relation', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'constitutive', 'variables': ['__MODEL__.port_2.p', '__MODEL__.port_3.p', '__MODEL__.port_2.m_flow'], 'role': 'flow'})