173 lines
15 KiB
Python
173 lines
15 KiB
Python
"""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
|
||
from app.simulation.core.port_computation import FLOW_SUPPLY
|
||
_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', computation=FLOW_SUPPLY), PortDefinition.pneumatic('port_2', computation=FLOW_SUPPLY))
|
||
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. This optional variant exposes the
|
||
pneumatic ports and replaces that signal with a normalized `opening`
|
||
parameter; use AmesimPnvo001SignalOpening for a time-varying control signal.
|
||
"""
|
||
MODEL_TYPE = 'amesim_pnvo001_fixed'
|
||
MODEL_VERSION = '0.2.0'
|
||
PORTS = (PortDefinition.pneumatic('port_2', computation=FLOW_SUPPLY), PortDefinition.pneumatic('port_3', computation=FLOW_SUPPLY))
|
||
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', computation=FLOW_SUPPLY), PortDefinition.pneumatic('port_3', computation=FLOW_SUPPLY))
|
||
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'})
|