diff --git a/app/simulation/components/amesim/flow/orifices.py b/app/simulation/components/amesim/flow/orifices.py index bdfe46b..c1c0f60 100644 --- a/app/simulation/components/amesim/flow/orifices.py +++ b/app/simulation/components/amesim/flow/orifices.py @@ -8,16 +8,44 @@ from app.simulation.components.amesim.gases import ( normalize_amesim_gas_index, ) from app.simulation.core.base import AlgebraicComponent -from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec +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,)),) +_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. @@ -27,7 +55,7 @@ class AmesimPnor001(AlgebraicComponent): """ MODEL_TYPE = "amesim_pnor001" - MODEL_VERSION = "0.1.0" + MODEL_VERSION = "0.2.0" PORTS = ( PortDefinition.pneumatic("port_1", nominal_role="bidirectional"), PortDefinition.pneumatic("port_2", nominal_role="bidirectional"), @@ -43,6 +71,7 @@ class AmesimPnor001(AlgebraicComponent): minimum=1.0e-10, maximum=1.0, description="孔口实际质量流量相对于理想可压缩流量的无量纲修正系数。", + visible_when=_FLOWSET_USES_CQ, ), ParameterDefinition( "area", @@ -53,6 +82,7 @@ class AmesimPnor001(AlgebraicComponent): minimum=0.0, maximum=1.0, description="选择 Cq/面积方式时用于流量计算的有效孔口面积。", + visible_when=_FLOWSET_USES_CQ, ), ParameterDefinition( "Cv", @@ -62,6 +92,7 @@ class AmesimPnor001(AlgebraicComponent): unit="", minimum=0.0, description="选择 Cv 方式时使用的英制流量系数。", + visible_when=_FLOWSET_USES_CV, ), ParameterDefinition( "Kv", @@ -71,6 +102,7 @@ class AmesimPnor001(AlgebraicComponent): unit="", minimum=0.0, description="选择 Kv 方式时使用的公制流量系数。", + visible_when=_FLOWSET_USES_KV, ), ParameterDefinition( "flowset", @@ -80,6 +112,8 @@ class AmesimPnor001(AlgebraicComponent): unit="", minimum=1.0, maximum=3.0, + editor="choice", + options=_FLOW_COEFFICIENT_OPTIONS, description="流量参数方式:1 使用 Cq 和面积,2 使用 Cv,3 使用 Kv。", ), ) @@ -111,6 +145,7 @@ class AmesimPnor001(AlgebraicComponent): PortDisplaySpec("port_2", "right", order=20), ), order=10, + parameter_groups=(_PNOR001_FLOW_COEFFICIENT_GROUP,), ) def __init__( @@ -320,7 +355,7 @@ class AmesimPnvo001FixedOpening(AlgebraicComponent): """ MODEL_TYPE = "amesim_pnvo001_fixed" - MODEL_VERSION = "0.1.0" + MODEL_VERSION = "0.2.0" PORTS = ( PortDefinition.pneumatic("port_2", nominal_role="bidirectional"), PortDefinition.pneumatic("port_3", nominal_role="bidirectional"), @@ -336,6 +371,7 @@ class AmesimPnvo001FixedOpening(AlgebraicComponent): minimum=1.0e-10, maximum=1.0, description="孔口实际质量流量相对于理想可压缩流量的无量纲修正系数。", + visible_when=_FLOWSET_USES_CQ, ), ParameterDefinition( "area0", @@ -346,6 +382,7 @@ class AmesimPnvo001FixedOpening(AlgebraicComponent): minimum=0.0, maximum=1.0, description="阀门完全开启时的最大有效孔口面积。", + visible_when=_FLOWSET_USES_CQ, ), ParameterDefinition( "Cv", @@ -355,6 +392,7 @@ class AmesimPnvo001FixedOpening(AlgebraicComponent): unit="", minimum=0.0, description="选择 Cv 方式时使用的最大英制流量系数。", + visible_when=_FLOWSET_USES_CV, ), ParameterDefinition( "Kv", @@ -364,6 +402,7 @@ class AmesimPnvo001FixedOpening(AlgebraicComponent): unit="", minimum=0.0, description="选择 Kv 方式时使用的最大公制流量系数。", + visible_when=_FLOWSET_USES_KV, ), ParameterDefinition( "flowset", @@ -373,6 +412,8 @@ class AmesimPnvo001FixedOpening(AlgebraicComponent): unit="", minimum=1.0, maximum=3.0, + editor="choice", + options=_FLOW_COEFFICIENT_OPTIONS, description="流量参数方式:1 使用 Cq 和面积,2 使用 Cv,3 使用 Kv。", ), ParameterDefinition( @@ -422,6 +463,7 @@ class AmesimPnvo001FixedOpening(AlgebraicComponent): PortDisplaySpec("port_3", "right", order=20), ), order=30, + parameter_groups=(_PNVO001_FLOW_COEFFICIENT_GROUP,), ) def __init__( @@ -685,7 +727,7 @@ class AmesimPnvo001SignalOpening(AmesimPnvo001FixedOpening): """AMESim PNVO001 signal-controlled pneumatic orifice.""" MODEL_TYPE = "amesim_pnvo001" - MODEL_VERSION = "0.1.0" + MODEL_VERSION = "0.2.0" PORTS = ( PortDefinition.signal("res", nominal_role="input"), PortDefinition.pneumatic("port_2", nominal_role="bidirectional"), @@ -693,12 +735,70 @@ class AmesimPnvo001SignalOpening(AmesimPnvo001FixedOpening): ) PARAMETERS = ( AMESIM_GAS_INDEX_PARAMETER, - ParameterDefinition("cq", 0.72, label="流量系数 Cq", quantity="dimensionless", unit="", minimum=1.0e-10, maximum=1.0), - ParameterDefinition("area0", 5.0e-6, label="最大孔口面积", quantity="area", unit="m2", minimum=0.0, maximum=1.0), - ParameterDefinition("Cv", 0.5, label="最大流量系数 Cv", quantity="dimensionless", unit="", minimum=0.0), - ParameterDefinition("Kv", 0.4, label="最大流量系数 Kv", quantity="dimensionless", unit="", minimum=0.0), - ParameterDefinition("flowset", 1.0, label="流量系数设置", quantity="dimensionless", unit="", minimum=1.0, maximum=3.0), - ParameterDefinition("opening0", 1.0, label="初始开度", quantity="dimensionless", unit="", minimum=0.0, maximum=1.0), + 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( @@ -712,6 +812,7 @@ class AmesimPnvo001SignalOpening(AmesimPnvo001FixedOpening): PortDisplaySpec("port_3", "right", order=20), ), order=35, + parameter_groups=(_PNVO001_FLOW_COEFFICIENT_GROUP,), ) def __init__( diff --git a/app/simulation/components/amesim/flow/pipes.py b/app/simulation/components/amesim/flow/pipes.py index 625b320..d635614 100644 --- a/app/simulation/components/amesim/flow/pipes.py +++ b/app/simulation/components/amesim/flow/pipes.py @@ -8,10 +8,16 @@ from app.simulation.components.amesim.gases import ( normalize_amesim_gas_index, ) from app.simulation.core.base import AlgebraicComponent, DynamicComponent, ThermodynamicVolumeComponent -from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec +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, THERMODYNAMIC_VOLUME_RESULT_VARIABLES, ) @@ -20,6 +26,18 @@ from app.simulation.core.ports import PortDefinition from app.simulation.core.state import VolumeState +_DYNAMIC_PIPE_POLYTROPIC_MODE = ParameterCondition("mode", (1.0,)) +_DYNAMIC_PIPE_HEAT_EXCHANGE_MODE = ParameterCondition("mode", (2.0,)) +_DYNAMIC_PIPE_PARAMETER_GROUPS = ( + ParameterGroupDisplaySpec( + id="thermodynamics", + label="热力学", + parameters=("k", "kth", "extemp"), + order=10, + ), +) + + class AmesimPnl00r(AlgebraicComponent): """AMESim PNL00R pneumatic pipe friction resistance. @@ -308,7 +326,7 @@ class AmesimPnl0001(ThermodynamicVolumeComponent): """AMESim PNL0001 C-R pneumatic pipe with compressibility and friction.""" MODEL_TYPE = "amesim_pnl0001" - MODEL_VERSION = "0.1.0" + MODEL_VERSION = "0.2.0" PORTS = ( PortDefinition.pneumatic("port_1", nominal_role="bidirectional"), PortDefinition.pneumatic("port_2", nominal_role="bidirectional"), @@ -354,7 +372,11 @@ class AmesimPnl0001(ThermodynamicVolumeComponent): minimum=0.0, minimum_exclusive=True, maximum=2.0, - description="AMESim 管路热力学配置使用的多方指数。", + description=( + "mode=1 多方过程使用的指数;当前公开求解器保留该 AMESim " + "配置,尚未实现多方指数对状态方程的修正。" + ), + visible_when=(_DYNAMIC_PIPE_POLYTROPIC_MODE,), ), ParameterDefinition( "kth", @@ -363,7 +385,8 @@ class AmesimPnl0001(ThermodynamicVolumeComponent): quantity="heat_transfer_coefficient", unit="W/(m2*K)", minimum=0.0, - description="管内气体与外部环境之间的对流换热系数。", + description="mode=2 带换热过程使用的气体与外部环境对流换热系数。", + visible_when=(_DYNAMIC_PIPE_HEAT_EXCHANGE_MODE,), ), ParameterDefinition( "extemp", @@ -373,7 +396,8 @@ class AmesimPnl0001(ThermodynamicVolumeComponent): unit="K", minimum=0.0, minimum_exclusive=True, - description="管路外部环境的绝对温度,用于计算换热功率。", + description="mode=2 带换热过程使用的外部环境绝对温度。", + visible_when=(_DYNAMIC_PIPE_HEAT_EXCHANGE_MODE,), ), ParameterDefinition( "mode", @@ -383,7 +407,16 @@ class AmesimPnl0001(ThermodynamicVolumeComponent): unit="", minimum=1.0, maximum=2.0, - description="热模型选择:1 为绝热,2 为按换热系数计算环境换热。", + editor="choice", + options=( + ParameterOption(1.0, "多方过程"), + ParameterOption(2.0, "带换热"), + ), + description=( + "AMESim 原始编码:1 为多方过程,2 为带换热。当前公开求解器在" + "多方模式下关闭环境换热,在带换热模式下按换热系数和外部温度" + "计算环境换热。" + ), ), ParameterDefinition( "p0", @@ -450,6 +483,7 @@ class AmesimPnl0001(ThermodynamicVolumeComponent): PortDisplaySpec("port_2", "right", order=20), ), order=30, + parameter_groups=_DYNAMIC_PIPE_PARAMETER_GROUPS, ) def __init__( @@ -513,7 +547,10 @@ class AmesimPnl0001(ThermodynamicVolumeComponent): rounded = round(value) if not isclose(value, rounded, rel_tol=0.0, abs_tol=1.0e-12): raise ValueError(f"PNL0001 parameter {name} must be an integer value.") - return int(rounded) + integer = int(rounded) + if name == "mode" and integer not in {1, 2}: + raise ValueError("PNL0001 parameter mode must be one of 1, 2.") + return integer @classmethod def create( @@ -713,7 +750,7 @@ class AmesimPnl0002(AmesimPnl0001): """AMESim PNL0002 R-C-R pneumatic pipe with one center compliance.""" MODEL_TYPE = "amesim_pnl0002" - MODEL_VERSION = "0.1.0" + MODEL_VERSION = "0.2.0" PORTS = ( PortDefinition.pneumatic("port_1", nominal_role="bidirectional"), PortDefinition.pneumatic("port_2", nominal_role="bidirectional"), @@ -730,6 +767,7 @@ class AmesimPnl0002(AmesimPnl0001): PortDisplaySpec("port_2", "right", order=20), ), order=40, + parameter_groups=_DYNAMIC_PIPE_PARAMETER_GROUPS, ) @classmethod @@ -856,7 +894,7 @@ class AmesimPnl0003(DynamicComponent): state_size = 4 MODEL_TYPE = "amesim_pnl0003" - MODEL_VERSION = "0.1.0" + MODEL_VERSION = "0.2.0" PORTS = ( PortDefinition.pneumatic("port_1", nominal_role="bidirectional"), PortDefinition.pneumatic("port_2", nominal_role="bidirectional"), @@ -930,6 +968,7 @@ class AmesimPnl0003(DynamicComponent): PortDisplaySpec("port_2", "right", order=20), ), order=50, + parameter_groups=_DYNAMIC_PIPE_PARAMETER_GROUPS, ) def __init__( diff --git a/app/simulation/components/amesim/mechanical/translational.py b/app/simulation/components/amesim/mechanical/translational.py index 6093cc1..5a52583 100644 --- a/app/simulation/components/amesim/mechanical/translational.py +++ b/app/simulation/components/amesim/mechanical/translational.py @@ -27,6 +27,8 @@ _MECMAS21_ELASTIC_STOP = ParameterCondition("stoptype", (2.0,)) _MECMAS21_RESTITUTION_STOP = ParameterCondition("stoptype", (3.0,)) _MECMAS21_ADVANCED_FRICTION = ParameterCondition("frictionType", (2.0,)) _MECMAS21_STRIBECK_ENABLED = ParameterCondition("strib", (2.0,)) +_LSTP00A_NUMERICAL_STIFFNESS = ParameterCondition("stiffmode", (1.0,)) +_LSTP00A_GEOMETRICAL_STIFFNESS = ParameterCondition("stiffmode", (2.0,)) class AmesimF000(AlgebraicComponent): @@ -713,22 +715,126 @@ class AmesimLstp00a(AlgebraicComponent): """AMESim LSTP00A first public elastic contact component.""" MODEL_TYPE = "amesim_lstp00a" - MODEL_VERSION = "0.1.0" + MODEL_VERSION = "0.2.0" PORTS = ( PortDefinition.mechanical_translational("port_1"), PortDefinition.mechanical_translational("port_2"), ) PARAMETERS = ( - ParameterDefinition("na", 10.0, label="有效圈数", quantity="dimensionless", unit="", minimum=0.0, minimum_exclusive=True), - ParameterDefinition("gap0", 0.0, label="初始间隙", quantity="length", unit="m"), - ParameterDefinition("kcont", 1.0e6, label="接触刚度", quantity="translational_stiffness", unit="N/m", minimum=0.0), - ParameterDefinition("G", 8.57e10, label="剪切模量", quantity="pressure", unit="Pa", minimum=0.0), - ParameterDefinition("sdiam", 0.02, label="弹簧直径", quantity="length", unit="m", minimum=0.0), - ParameterDefinition("wdiam", 0.002, label="线径", quantity="length", unit="m", minimum=0.0), - ParameterDefinition("rcont", 0.0, label="接触阻尼", quantity="translational_damping", unit="N/(m/s)", minimum=0.0), - ParameterDefinition("Pdis", 1.0e-7, label="满阻尼穿透", quantity="length", unit="m", minimum=0.0), - ParameterDefinition("stiffmode", 1.0, label="刚度模式", quantity="dimensionless", unit="", minimum=0.0), - ParameterDefinition("discContactOption", 1.0, label="接触选项", quantity="dimensionless", unit="", minimum=0.0), + ParameterDefinition( + "na", + 10.0, + label="有效圈数", + quantity="dimensionless", + unit="", + minimum=0.0, + minimum_exclusive=True, + description="几何刚度模式下使用的弹簧有效圈数。", + visible_when=(_LSTP00A_GEOMETRICAL_STIFFNESS,), + ), + ParameterDefinition( + "gap0", + 0.0, + label="初始间隙", + quantity="length", + unit="m", + description="两个机械端口开始产生接触力前的初始间隙。", + ), + ParameterDefinition( + "kcont", + 1.0e6, + label="接触刚度", + quantity="translational_stiffness", + unit="N/m", + minimum=0.0, + description="数值刚度模式下直接指定的接触刚度。", + visible_when=(_LSTP00A_NUMERICAL_STIFFNESS,), + ), + ParameterDefinition( + "G", + 8.57e10, + label="剪切模量", + quantity="pressure", + unit="Pa", + minimum=0.0, + description="几何刚度模式下的材料剪切模量。", + visible_when=(_LSTP00A_GEOMETRICAL_STIFFNESS,), + ), + ParameterDefinition( + "sdiam", + 0.02, + label="弹簧直径", + quantity="length", + unit="m", + minimum=0.0, + description="几何刚度模式下的弹簧平均直径。", + visible_when=(_LSTP00A_GEOMETRICAL_STIFFNESS,), + ), + ParameterDefinition( + "wdiam", + 0.002, + label="线径", + quantity="length", + unit="m", + minimum=0.0, + description="几何刚度模式下的弹簧线径。", + visible_when=(_LSTP00A_GEOMETRICAL_STIFFNESS,), + ), + ParameterDefinition( + "rcont", + 0.0, + label="接触阻尼", + quantity="translational_damping", + unit="N/(m/s)", + minimum=0.0, + description="接触穿透过程中使用的最大阻尼系数。", + ), + ParameterDefinition( + "Pdis", + 1.0e-7, + label="满阻尼穿透", + quantity="length", + unit="m", + minimum=0.0, + description="接触阻尼由零增至全值所需的穿透量。", + ), + ParameterDefinition( + "stiffmode", + 1.0, + label="刚度模式", + quantity="dimensionless", + unit="", + minimum=1.0, + maximum=2.0, + editor="choice", + options=( + ParameterOption(1.0, "数值刚度"), + ParameterOption(2.0, "几何参数"), + ), + description=( + "AMESim 原始编码:1 直接使用接触刚度,2 使用弹簧几何参数。" + "当前公开求解器尚未实现由几何参数换算刚度,几何模式仍沿用" + "已保存的接触刚度值。" + ), + ), + ParameterDefinition( + "discContactOption", + 1.0, + label="允许负接触力", + quantity="dimensionless", + unit="", + minimum=1.0, + maximum=2.0, + editor="choice", + options=( + ParameterOption(1.0, "允许负接触力"), + ParameterOption(2.0, "不允许负接触力"), + ), + description=( + "AMESim 原始编码:1 保留阻尼项可能产生的负接触力," + "2 将接触力限制为非负。" + ), + ), ) RESULT_VARIABLES = ( ResultVariableDefinition("gap", "间隙", "length", "m", "derived", 10), @@ -745,6 +851,20 @@ class AmesimLstp00a(AlgebraicComponent): PortDisplaySpec("port_2", "right", order=20), ), order=40, + parameter_groups=( + ParameterGroupDisplaySpec( + id="stiffness", + label="刚度", + parameters=("kcont", "G", "sdiam", "wdiam", "na"), + order=10, + ), + ParameterGroupDisplaySpec( + id="contact", + label="接触", + parameters=("gap0", "rcont", "Pdis"), + order=20, + ), + ), ) def __init__(self, name: str, medium: IdealGasMedium, **parameters: float) -> None: @@ -773,9 +893,14 @@ class AmesimLstp00a(AlgebraicComponent): medium: IdealGasMedium, parameters: Mapping[str, float], ) -> "AmesimLstp00a": + definitions = {definition.name: definition for definition in cls.PARAMETERS} for integer_name in ("stiffmode", "discContactOption"): - if not float(parameters[integer_name]).is_integer(): + value = float(parameters[integer_name]) + if not value.is_integer(): raise ValueError(f"LSTP00A {integer_name} must be an integer.") + message = definitions[integer_name].validation_message(value) + if message is not None: + raise ValueError(f"LSTP00A {integer_name} {message}.") return cls(name=name, medium=medium, **dict(parameters)) @property diff --git a/app/simulation/components/amesim/signals/sources.py b/app/simulation/components/amesim/signals/sources.py index 90cc837..cb00eb5 100644 --- a/app/simulation/components/amesim/signals/sources.py +++ b/app/simulation/components/amesim/signals/sources.py @@ -4,12 +4,71 @@ from collections.abc import Mapping from math import floor from app.simulation.core.base import AlgebraicComponent -from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec -from app.simulation.core.metadata import ParameterDefinition, ResultVariableDefinition +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 IdealGasMedium from app.simulation.core.ports import PortDefinition +def _ud00_stage_parameters(index: int) -> tuple[ParameterDefinition, ...]: + visible_when = ( + () + if index == 1 + else ( + ParameterCondition( + "nstages", + tuple(float(stage_count) for stage_count in range(index, 9)), + ), + ) + ) + return ( + ParameterDefinition( + f"start{index}", + 0.0 if index == 1 else 1.0, + label=f"第 {index} 段起点", + quantity="dimensionless", + unit="", + description=f"第 {index} 段开始时的输出值。", + visible_when=visible_when, + ), + ParameterDefinition( + f"end{index}", + 1.0, + label=f"第 {index} 段终点", + quantity="dimensionless", + unit="", + description=f"第 {index} 段结束时的输出值。", + visible_when=visible_when, + ), + ParameterDefinition( + f"t{index}", + 1.0 if index == 1 else 0.0, + label=f"第 {index} 段时长", + quantity="time", + unit="s", + minimum=0.0, + description=f"第 {index} 段的持续时间。", + visible_when=visible_when, + ), + ) + + +_UD00_STAGE_PARAMETERS = tuple( + parameter + for stage_index in range(1, 9) + for parameter in _ud00_stage_parameters(stage_index) +) + + class AmesimStep0(AlgebraicComponent): """AMESim STEP0 scalar step signal source.""" @@ -89,36 +148,48 @@ class AmesimUd00(AlgebraicComponent): """AMESim UD00 piecewise-linear scalar signal source.""" MODEL_TYPE = "amesim_ud00" - MODEL_VERSION = "0.1.0" + MODEL_VERSION = "0.2.0" PORTS = (PortDefinition.signal("out", nominal_role="output"),) PARAMETERS = ( - ParameterDefinition("tstart", 0.0, label="启动时间", quantity="time", unit="s"), - ParameterDefinition("start1", 0.0, label="第 1 段起点", quantity="dimensionless", unit=""), - ParameterDefinition("end1", 1.0, label="第 1 段终点", quantity="dimensionless", unit=""), - ParameterDefinition("t1", 1.0, label="第 1 段时长", quantity="time", unit="s", minimum=0.0), - ParameterDefinition("start2", 1.0, label="第 2 段起点", quantity="dimensionless", unit=""), - ParameterDefinition("end2", 1.0, label="第 2 段终点", quantity="dimensionless", unit=""), - ParameterDefinition("t2", 0.0, label="第 2 段时长", quantity="time", unit="s", minimum=0.0), - ParameterDefinition("start3", 1.0, label="第 3 段起点", quantity="dimensionless", unit=""), - ParameterDefinition("end3", 1.0, label="第 3 段终点", quantity="dimensionless", unit=""), - ParameterDefinition("t3", 0.0, label="第 3 段时长", quantity="time", unit="s", minimum=0.0), - ParameterDefinition("start4", 1.0, label="第 4 段起点", quantity="dimensionless", unit=""), - ParameterDefinition("end4", 1.0, label="第 4 段终点", quantity="dimensionless", unit=""), - ParameterDefinition("t4", 0.0, label="第 4 段时长", quantity="time", unit="s", minimum=0.0), - ParameterDefinition("start5", 1.0, label="第 5 段起点", quantity="dimensionless", unit=""), - ParameterDefinition("end5", 1.0, label="第 5 段终点", quantity="dimensionless", unit=""), - ParameterDefinition("t5", 0.0, label="第 5 段时长", quantity="time", unit="s", minimum=0.0), - ParameterDefinition("start6", 1.0, label="第 6 段起点", quantity="dimensionless", unit=""), - ParameterDefinition("end6", 1.0, label="第 6 段终点", quantity="dimensionless", unit=""), - ParameterDefinition("t6", 0.0, label="第 6 段时长", quantity="time", unit="s", minimum=0.0), - ParameterDefinition("start7", 1.0, label="第 7 段起点", quantity="dimensionless", unit=""), - ParameterDefinition("end7", 1.0, label="第 7 段终点", quantity="dimensionless", unit=""), - ParameterDefinition("t7", 0.0, label="第 7 段时长", quantity="time", unit="s", minimum=0.0), - ParameterDefinition("start8", 1.0, label="第 8 段起点", quantity="dimensionless", unit=""), - ParameterDefinition("end8", 1.0, label="第 8 段终点", quantity="dimensionless", unit=""), - ParameterDefinition("t8", 0.0, label="第 8 段时长", quantity="time", unit="s", minimum=0.0), - ParameterDefinition("nstages", 1.0, label="段数", quantity="dimensionless", unit="", minimum=1.0, maximum=8.0), - ParameterDefinition("iscyclic", 0.0, label="循环", quantity="dimensionless", unit="", minimum=0.0, maximum=1.0), + ParameterDefinition( + "tstart", + 0.0, + label="启动时间", + quantity="time", + unit="s", + description="分段信号开始输出第一段之前的等待时间。", + ), + *_UD00_STAGE_PARAMETERS, + ParameterDefinition( + "nstages", + 1.0, + label="段数", + quantity="dimensionless", + unit="", + minimum=1.0, + maximum=8.0, + editor="choice", + options=tuple( + ParameterOption(float(stage_count), str(stage_count)) + for stage_count in range(1, 9) + ), + description="参与输出计算的有效线性分段数量。", + ), + ParameterDefinition( + "iscyclic", + 0.0, + label="循环", + quantity="dimensionless", + unit="", + minimum=0.0, + maximum=1.0, + editor="choice", + options=( + ParameterOption(0.0, "否"), + ParameterOption(1.0, "是"), + ), + description="当前公共协议编码:0 表示单次输出,1 表示循环输出。", + ), ) RESULT_VARIABLES = ( ResultVariableDefinition("y", "输出", "dimensionless", "", "signal", 10), @@ -130,6 +201,16 @@ class AmesimUd00(AlgebraicComponent): symbol="amesim_ud00", ports=(PortDisplaySpec("out", "right", order=10),), order=20, + parameter_groups=( + ParameterGroupDisplaySpec( + id="stages", + label="分段参数", + parameters=tuple( + parameter.name for parameter in _UD00_STAGE_PARAMETERS + ), + order=10, + ), + ), ) def __init__( @@ -174,10 +255,17 @@ class AmesimUd00(AlgebraicComponent): ) -> "AmesimUd00": nstages = parameters["nstages"] iscyclic = parameters["iscyclic"] - if not float(nstages).is_integer(): - raise ValueError("UD00 nstages must be an integer.") - if not float(iscyclic).is_integer(): - raise ValueError("UD00 iscyclic must be 0 or 1.") + definitions = {definition.name: definition for definition in cls.PARAMETERS} + for parameter_name, value in ( + ("nstages", nstages), + ("iscyclic", iscyclic), + ): + numeric_value = float(value) + if not numeric_value.is_integer(): + raise ValueError(f"UD00 {parameter_name} must be an integer.") + message = definitions[parameter_name].validation_message(numeric_value) + if message is not None: + raise ValueError(f"UD00 {parameter_name} {message}.") return cls( name=name, medium=medium, diff --git a/frontend/tests/e2e/fixtures.ts b/frontend/tests/e2e/fixtures.ts index 8047502..b6e2f81 100644 --- a/frontend/tests/e2e/fixtures.ts +++ b/frontend/tests/e2e/fixtures.ts @@ -14,6 +14,62 @@ const physicalPort = ( order: side === "left" ? 0 : 1, }); +const ud00StageParameters = Array.from({ length: 8 }, (_, offset) => offset + 1) + .flatMap((stage) => { + const visibleWhen = + stage === 1 + ? {} + : { + visibleWhen: [ + { + parameter: "nstages", + values: Array.from( + { length: 9 - stage }, + (_, offset) => stage + offset, + ), + }, + ], + }; + return [ + { + name: `start${stage}`, + label: `第 ${stage} 段起点`, + quantity: "dimensionless", + unit: "", + default: stage === 1 ? 0 : 1, + minimumExclusive: false, + description: `第 ${stage} 段开始时的输出值。`, + ...visibleWhen, + }, + { + name: `end${stage}`, + label: `第 ${stage} 段终点`, + quantity: "dimensionless", + unit: "", + default: 1, + minimumExclusive: false, + description: `第 ${stage} 段结束时的输出值。`, + ...visibleWhen, + }, + { + name: `t${stage}`, + label: `第 ${stage} 段时长`, + quantity: "time", + unit: "s", + default: stage === 1 ? 1 : 0, + minimum: 0, + minimumExclusive: false, + description: `第 ${stage} 段的持续时间。`, + ...visibleWhen, + }, + ]; + }); + +const ud00StageParameterNames = Array.from( + { length: 8 }, + (_, offset) => offset + 1, +).flatMap((stage) => [`start${stage}`, `end${stage}`, `t${stage}`]); + export const componentCatalog = { schemaVersion: 1, libraries: [ @@ -419,6 +475,380 @@ export const componentCatalog = { }, ], }, + { + type: "amesim_pnor001", + label: "PNOR001 常系数气动孔口", + modelType: "amesim_pnor001", + modelVersion: "0.2.0", + symbol: "symbol-not-registered", + order: 7, + category: { + id: "generic", + label: "通用组件", + order: 1, + }, + ports: [], + parameterGroups: [ + { + id: "flow_coefficient", + label: "流量系数", + order: 10, + parameters: ["cq", "area", "Cv", "Kv"], + defaultExpanded: false, + }, + ], + parameters: [ + { + name: "cq", + label: "流量系数 Cq", + quantity: "dimensionless", + unit: "", + default: 0.72, + minimum: 1e-10, + maximum: 1, + minimumExclusive: false, + description: "孔口实际质量流量相对于理想可压缩流量的无量纲修正系数。", + visibleWhen: [{ parameter: "flowset", values: [1] }], + }, + { + name: "area", + label: "孔口面积", + quantity: "area", + unit: "m2", + default: 5e-6, + minimum: 0, + maximum: 1, + minimumExclusive: false, + description: "选择 Cq/面积方式时用于流量计算的有效孔口面积。", + visibleWhen: [{ parameter: "flowset", values: [1] }], + }, + { + name: "Cv", + label: "流量系数 Cv", + quantity: "dimensionless", + unit: "", + default: 0.5, + minimum: 0, + minimumExclusive: false, + description: "选择 Cv 方式时使用的英制流量系数。", + visibleWhen: [{ parameter: "flowset", values: [2] }], + }, + { + name: "Kv", + label: "流量系数 Kv", + quantity: "dimensionless", + unit: "", + default: 0.4, + minimum: 0, + minimumExclusive: false, + description: "选择 Kv 方式时使用的公制流量系数。", + visibleWhen: [{ parameter: "flowset", values: [3] }], + }, + { + name: "flowset", + label: "流量系数设置", + quantity: "dimensionless", + unit: "", + default: 1, + minimum: 1, + maximum: 3, + minimumExclusive: false, + editor: "choice", + options: [ + { value: 1, label: "Cq" }, + { value: 2, label: "Cv" }, + { value: 3, label: "Kv" }, + ], + description: "流量参数方式:1 使用 Cq 和面积,2 使用 Cv,3 使用 Kv。", + }, + ], + }, + { + type: "amesim_pnl0001", + label: "PNL0001 C-R 动态管路", + modelType: "amesim_pnl0001", + modelVersion: "0.2.0", + symbol: "symbol-not-registered", + order: 8, + category: { + id: "generic", + label: "通用组件", + order: 1, + }, + ports: [], + parameterGroups: [ + { + id: "thermodynamics", + label: "热力学", + order: 10, + parameters: ["k", "kth", "extemp"], + defaultExpanded: false, + }, + ], + parameters: [ + { + name: "diam", + label: "管径", + quantity: "length", + unit: "m", + default: 0.01, + minimum: 0, + minimumExclusive: true, + description: "管路的有效内径,用于计算流通面积、储气容积和摩擦压降。", + }, + { + name: "k", + label: "多方指数", + quantity: "dimensionless", + unit: "", + default: 1.35, + minimum: 0, + maximum: 2, + minimumExclusive: true, + visibleWhen: [{ parameter: "mode", values: [1] }], + }, + { + name: "kth", + label: "换热系数", + quantity: "heat_transfer_coefficient", + unit: "W/(m2*K)", + default: 0, + minimum: 0, + minimumExclusive: false, + visibleWhen: [{ parameter: "mode", values: [2] }], + }, + { + name: "extemp", + label: "外部温度", + quantity: "temperature", + unit: "K", + default: 293.15, + minimum: 0, + minimumExclusive: true, + visibleWhen: [{ parameter: "mode", values: [2] }], + }, + { + name: "mode", + label: "热模型", + quantity: "dimensionless", + unit: "", + default: 2, + minimum: 1, + maximum: 2, + minimumExclusive: false, + editor: "choice", + options: [ + { value: 1, label: "多方过程" }, + { value: 2, label: "带换热" }, + ], + }, + ], + }, + { + type: "amesim_ud00", + label: "UD00 分段线性信号", + modelType: "amesim_ud00", + modelVersion: "0.2.0", + symbol: "symbol-not-registered", + order: 9, + category: { + id: "generic", + label: "通用组件", + order: 1, + }, + ports: [], + parameterGroups: [ + { + id: "stages", + label: "分段参数", + order: 10, + parameters: ud00StageParameterNames, + defaultExpanded: false, + }, + ], + parameters: [ + { + name: "tstart", + label: "启动时间", + quantity: "time", + unit: "s", + default: 0, + minimumExclusive: false, + description: "分段信号开始输出第一段之前的等待时间。", + }, + ...ud00StageParameters, + { + name: "nstages", + label: "段数", + quantity: "dimensionless", + unit: "", + default: 1, + minimum: 1, + maximum: 8, + minimumExclusive: false, + editor: "choice", + options: Array.from({ length: 8 }, (_, offset) => ({ + value: offset + 1, + label: String(offset + 1), + })), + description: "参与输出计算的有效线性分段数量。", + }, + { + name: "iscyclic", + label: "循环", + quantity: "dimensionless", + unit: "", + default: 0, + minimum: 0, + maximum: 1, + minimumExclusive: false, + editor: "choice", + options: [ + { value: 0, label: "否" }, + { value: 1, label: "是" }, + ], + description: "当前公共协议编码:0 表示单次输出,1 表示循环输出。", + }, + ], + }, + { + type: "amesim_lstp00a", + label: "LSTP00A 弹性接触", + modelType: "amesim_lstp00a", + modelVersion: "0.2.0", + symbol: "symbol-not-registered", + order: 10, + category: { + id: "generic", + label: "通用组件", + order: 1, + }, + ports: [], + parameterGroups: [ + { + id: "stiffness", + label: "刚度", + order: 10, + parameters: ["kcont", "G", "sdiam", "wdiam", "na"], + defaultExpanded: false, + }, + { + id: "contact", + label: "接触", + order: 20, + parameters: ["gap0", "rcont", "Pdis"], + defaultExpanded: false, + }, + ], + parameters: [ + { + name: "na", + label: "有效圈数", + quantity: "dimensionless", + unit: "", + default: 10, + minimum: 0, + minimumExclusive: true, + visibleWhen: [{ parameter: "stiffmode", values: [2] }], + }, + { + name: "gap0", + label: "初始间隙", + quantity: "length", + unit: "m", + default: 0, + minimumExclusive: false, + }, + { + name: "kcont", + label: "接触刚度", + quantity: "translational_stiffness", + unit: "N/m", + default: 1e6, + minimum: 0, + minimumExclusive: false, + visibleWhen: [{ parameter: "stiffmode", values: [1] }], + }, + { + name: "G", + label: "剪切模量", + quantity: "pressure", + unit: "Pa", + default: 8.57e10, + minimum: 0, + minimumExclusive: false, + visibleWhen: [{ parameter: "stiffmode", values: [2] }], + }, + { + name: "sdiam", + label: "弹簧直径", + quantity: "length", + unit: "m", + default: 0.02, + minimum: 0, + minimumExclusive: false, + visibleWhen: [{ parameter: "stiffmode", values: [2] }], + }, + { + name: "wdiam", + label: "线径", + quantity: "length", + unit: "m", + default: 0.002, + minimum: 0, + minimumExclusive: false, + visibleWhen: [{ parameter: "stiffmode", values: [2] }], + }, + { + name: "rcont", + label: "接触阻尼", + quantity: "translational_damping", + unit: "N/(m/s)", + default: 0, + minimum: 0, + minimumExclusive: false, + }, + { + name: "Pdis", + label: "满阻尼穿透", + quantity: "length", + unit: "m", + default: 1e-7, + minimum: 0, + minimumExclusive: false, + }, + { + name: "stiffmode", + label: "刚度模式", + quantity: "dimensionless", + unit: "", + default: 1, + minimum: 1, + maximum: 2, + minimumExclusive: false, + editor: "choice", + options: [ + { value: 1, label: "数值刚度" }, + { value: 2, label: "几何参数" }, + ], + }, + { + name: "discContactOption", + label: "允许负接触力", + quantity: "dimensionless", + unit: "", + default: 1, + minimum: 1, + maximum: 2, + minimumExclusive: false, + editor: "choice", + options: [ + { value: 1, label: "允许负接触力" }, + { value: 2, label: "不允许负接触力" }, + ], + }, + ], + }, ], }, ], diff --git a/frontend/tests/e2e/parameter-table.spec.ts b/frontend/tests/e2e/parameter-table.spec.ts index dddc7fc..7a7ee71 100644 --- a/frontend/tests/e2e/parameter-table.spec.ts +++ b/frontend/tests/e2e/parameter-table.spec.ts @@ -50,6 +50,22 @@ async function addMecmasEncodingTestComponent(page: Page) { await node.click(); } +async function addParameterMetadataTestComponent( + page: Page, + label: string, + type: string, +) { + const canvas = page.locator(".flow-canvas .react-flow__pane"); + await page + .getByRole("button", { name: label, exact: true }) + .dragTo(canvas, { targetPosition: { x: 620, y: 330 } }); + const node = page.locator( + `.flow-canvas .react-flow__node[data-id="${type}_1"]`, + ); + await expect(node).toBeVisible(); + await node.click(); +} + test("组件参数和仿真设置使用共享的三列表格", async ({ page }) => { await page.goto("/"); await addPneumaticTestComponent(page); @@ -310,6 +326,194 @@ test("通用离散选项按多条件控制参数行且保留隐藏值和未知 await expect(page.getByLabel("高级参数", { exact: true })).toHaveValue("42"); }); +test("PNOR001 和 PNL0001 控制项前置并切换折叠组中的条件参数", async ({ + page, +}) => { + await page.goto("/"); + await addParameterMetadataTestComponent( + page, + "PNOR001 常系数气动孔口", + "amesim_pnor001", + ); + + const componentTable = page.getByRole("table", { name: "组件参数" }); + const flowset = page.getByLabel("流量系数设置", { exact: true }); + const flowGroup = componentTable.getByRole("button", { name: /流量系数/ }); + await expect(flowset.locator("option")).toHaveText(["Cq", "Cv", "Kv"]); + await expect(flowset).toHaveValue("1"); + await expect(flowGroup).toHaveAttribute("aria-expanded", "false"); + await expect(page.getByLabel("流量系数 Cq", { exact: true })).toBeHidden(); + await expect(page.getByLabel("孔口面积", { exact: true })).toBeHidden(); + await expect(parameterRow(componentTable, "流量系数 Cv")).toHaveCount(0); + const flowsetBox = await parameterRow( + componentTable, + "流量系数设置", + ).boundingBox(); + const flowGroupBox = await flowGroup.boundingBox(); + expect(flowsetBox).not.toBeNull(); + expect(flowGroupBox).not.toBeNull(); + expect(flowsetBox!.y).toBeLessThan(flowGroupBox!.y); + + await flowGroup.click(); + const area = page.getByLabel("孔口面积", { exact: true }); + await expect(page.getByLabel("流量系数 Cq", { exact: true })).toBeVisible(); + await expect(area).toBeVisible(); + await area.fill("7.5e-6"); + await area.press("Enter"); + await expect(area).toHaveValue("7.5e-6"); + + await flowset.selectOption("2"); + await expect(parameterRow(componentTable, "流量系数 Cq")).toHaveCount(0); + await expect(parameterRow(componentTable, "孔口面积")).toHaveCount(0); + await expect(page.getByLabel("流量系数 Cv", { exact: true })).toBeVisible(); + await expect(parameterRow(componentTable, "流量系数 Kv")).toHaveCount(0); + await flowset.selectOption("1"); + await expect(area).toHaveValue("7.5e-6"); + + await addParameterMetadataTestComponent( + page, + "PNL0001 C-R 动态管路", + "amesim_pnl0001", + ); + const mode = page.getByLabel("热模型", { exact: true }); + const thermalGroup = componentTable.getByRole("button", { name: /热力学/ }); + await expect(mode.locator("option")).toHaveText(["多方过程", "带换热"]); + await expect(mode).toHaveValue("2"); + const pnlParameterOrder = ( + await componentTable.getByRole("rowheader").allTextContents() + ).map((text) => text.trim()); + expect(pnlParameterOrder.indexOf("热模型")).toBeLessThan( + pnlParameterOrder.indexOf("管径"), + ); + await expect(thermalGroup).toHaveAttribute("aria-expanded", "false"); + await thermalGroup.click(); + await expect(parameterRow(componentTable, "多方指数")).toHaveCount(0); + await expect(page.getByLabel("换热系数", { exact: true })).toBeVisible(); + await expect(page.getByLabel("外部温度", { exact: true })).toBeVisible(); + + await mode.selectOption("1"); + await expect(page.getByLabel("多方指数", { exact: true })).toBeVisible(); + await expect(parameterRow(componentTable, "换热系数")).toHaveCount(0); + await expect(parameterRow(componentTable, "外部温度")).toHaveCount(0); + await expect(thermalGroup).toHaveAttribute("aria-expanded", "true"); +}); + +test("UD00 段数前置并逐段显示第 2 至第 4 段参数", async ({ page }) => { + await page.goto("/"); + await addParameterMetadataTestComponent( + page, + "UD00 分段线性信号", + "amesim_ud00", + ); + + const componentTable = page.getByRole("table", { name: "组件参数" }); + const stageCount = page.getByLabel("段数", { exact: true }); + const cyclic = page.getByLabel("循环", { exact: true }); + const stageGroup = componentTable.getByRole("button", { name: /分段参数/ }); + await expect(stageCount.locator("option")).toHaveText([ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + ]); + await expect(cyclic.locator("option")).toHaveText(["否", "是"]); + const parameterOrder = ( + await componentTable.getByRole("rowheader").allTextContents() + ).map((text) => text.trim()); + expect(parameterOrder.indexOf("段数")).toBeLessThan( + parameterOrder.indexOf("启动时间"), + ); + expect(parameterOrder.indexOf("循环")).toBeLessThan( + parameterOrder.indexOf("启动时间"), + ); + await expect(stageGroup).toHaveAttribute("aria-expanded", "false"); + await expect(page.getByLabel("第 1 段起点", { exact: true })).toBeHidden(); + await stageGroup.click(); + await expect(page.getByLabel("第 1 段起点", { exact: true })).toBeVisible(); + + const expectStageVisibility = async (stage: number, visible: boolean) => { + for (const suffix of ["起点", "终点", "时长"]) { + const row = parameterRow(componentTable, `第 ${stage} 段${suffix}`); + if (visible) { + await expect(row).toBeVisible(); + } else { + await expect(row).toHaveCount(0); + } + } + }; + + await expectStageVisibility(2, false); + await expectStageVisibility(3, false); + await expectStageVisibility(4, false); + await stageCount.selectOption("2"); + await expectStageVisibility(2, true); + await expectStageVisibility(3, false); + await expectStageVisibility(4, false); + await stageCount.selectOption("3"); + await expectStageVisibility(2, true); + await expectStageVisibility(3, true); + await expectStageVisibility(4, false); + await stageCount.selectOption("4"); + await expectStageVisibility(2, true); + await expectStageVisibility(3, true); + await expectStageVisibility(4, true); + await expect(stageGroup).toHaveAttribute("aria-expanded", "true"); +}); + +test("LSTP00A 刚度模式前置并切换数值与几何刚度参数", async ({ page }) => { + await page.goto("/"); + await addParameterMetadataTestComponent( + page, + "LSTP00A 弹性接触", + "amesim_lstp00a", + ); + + const componentTable = page.getByRole("table", { name: "组件参数" }); + const stiffnessMode = page.getByLabel("刚度模式", { exact: true }); + const contactOption = page.getByLabel("允许负接触力", { exact: true }); + const stiffnessGroup = componentTable.getByRole("button", { name: /刚度/ }); + const contactGroup = componentTable.getByRole("button", { name: /接触/ }); + await expect(stiffnessMode.locator("option")).toHaveText([ + "数值刚度", + "几何参数", + ]); + await expect(contactOption.locator("option")).toHaveText([ + "允许负接触力", + "不允许负接触力", + ]); + await expect(stiffnessGroup).toHaveAttribute("aria-expanded", "false"); + await expect(contactGroup).toHaveAttribute("aria-expanded", "false"); + const stiffnessModeBox = await parameterRow( + componentTable, + "刚度模式", + ).boundingBox(); + const stiffnessGroupBox = await stiffnessGroup.boundingBox(); + expect(stiffnessModeBox).not.toBeNull(); + expect(stiffnessGroupBox).not.toBeNull(); + expect(stiffnessModeBox!.y).toBeLessThan(stiffnessGroupBox!.y); + + await stiffnessGroup.click(); + await expect(page.getByLabel("接触刚度", { exact: true })).toBeVisible(); + for (const label of ["有效圈数", "剪切模量", "弹簧直径", "线径"]) { + await expect(parameterRow(componentTable, label)).toHaveCount(0); + } + + await stiffnessMode.selectOption("2"); + await expect(parameterRow(componentTable, "接触刚度")).toHaveCount(0); + for (const label of ["有效圈数", "剪切模量", "弹簧直径", "线径"]) { + await expect(page.getByLabel(label, { exact: true })).toBeVisible(); + } + await expect(stiffnessGroup).toHaveAttribute("aria-expanded", "true"); + + await stiffnessMode.selectOption("1"); + await expect(page.getByLabel("接触刚度", { exact: true })).toBeVisible(); + await expect(parameterRow(componentTable, "剪切模量")).toHaveCount(0); +}); + test("MECMAS21 可配置参数前置并按条件显示可折叠参数组", async ({ page, }) => { diff --git a/tests/test_amesim_mechanical_public_components.py b/tests/test_amesim_mechanical_public_components.py index 22d1df4..fc4253a 100644 --- a/tests/test_amesim_mechanical_public_components.py +++ b/tests/test_amesim_mechanical_public_components.py @@ -280,6 +280,25 @@ class AmesimMechanicalPublicComponentTests(unittest.TestCase): {name: value}, ) + def test_lstp00a_registry_rejects_invalid_choice_values(self) -> None: + invalid_options = { + "stiffmode": (0.0, 1.5, 3.0), + "discContactOption": (0.0, 1.5, 3.0), + } + + for name, values in invalid_options.items(): + for value in values: + with self.subTest(name=name, value=value): + with self.assertRaisesRegex( + ValueError, + "must be (?:one of|at least|at most)", + ): + COMPONENT_MODEL_REGISTRY["amesim_lstp00a"].create( + "contact_1", + self.medium, + {name: value}, + ) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_amesim_pnl0001_component.py b/tests/test_amesim_pnl0001_component.py index e08eb75..11d8f22 100644 --- a/tests/test_amesim_pnl0001_component.py +++ b/tests/test_amesim_pnl0001_component.py @@ -45,7 +45,7 @@ class AmesimPnl0001ComponentTests(unittest.TestCase): self.medium, {"gi": 1.5}, ) - with self.assertRaisesRegex(ValueError, "mode must be an integer"): + with self.assertRaisesRegex(ValueError, "must be one of"): COMPONENT_MODEL_REGISTRY["amesim_pnl0001"].create( "pnl_1", self.medium, diff --git a/tests/test_amesim_pnor001_component.py b/tests/test_amesim_pnor001_component.py index 275058f..3bd48be 100644 --- a/tests/test_amesim_pnor001_component.py +++ b/tests/test_amesim_pnor001_component.py @@ -28,7 +28,7 @@ class AmesimPnor001ComponentTests(unittest.TestCase): self.assertEqual(orifice.flowset, 1) def test_rejects_fractional_integer_parameters(self) -> None: - with self.assertRaisesRegex(ValueError, "flowset must be an integer"): + with self.assertRaisesRegex(ValueError, "must be one of"): COMPONENT_MODEL_REGISTRY["amesim_pnor001"].create( "pnor_1", self.medium, diff --git a/tests/test_amesim_pnvo001_fixed_component.py b/tests/test_amesim_pnvo001_fixed_component.py index 3b51c34..b69a003 100644 --- a/tests/test_amesim_pnvo001_fixed_component.py +++ b/tests/test_amesim_pnvo001_fixed_component.py @@ -30,7 +30,7 @@ class AmesimPnvo001FixedOpeningComponentTests(unittest.TestCase): self.assertEqual(valve.opening, 1.0) def test_rejects_fractional_flowset(self) -> None: - with self.assertRaisesRegex(ValueError, "flowset must be an integer"): + with self.assertRaisesRegex(ValueError, "must be one of"): COMPONENT_MODEL_REGISTRY["amesim_pnvo001_fixed"].create( "valve_1", self.medium, diff --git a/tests/test_amesim_signal_components.py b/tests/test_amesim_signal_components.py index bf293b8..3bb59f1 100644 --- a/tests/test_amesim_signal_components.py +++ b/tests/test_amesim_signal_components.py @@ -59,12 +59,31 @@ class AmesimSignalComponentTests(unittest.TestCase): self.assertEqual(signal.signal_event_times(0.0, 5.0), (1.0, 2.0, 3.0, 4.0)) def test_ud00_registry_rejects_fractional_stage_controls(self) -> None: - with self.assertRaisesRegex(ValueError, "nstages must be an integer"): + with self.assertRaisesRegex(ValueError, "must be one of"): COMPONENT_MODEL_REGISTRY["amesim_ud00"].create( "piecewise_1", self.medium, {"nstages": 1.5}, ) + for name, value in (("nstages", 9.0), ("iscyclic", 2.0)): + with self.subTest(name=name): + with self.assertRaisesRegex( + ValueError, + "must be (?:one of|at most)", + ): + COMPONENT_MODEL_REGISTRY["amesim_ud00"].create( + "piecewise_1", + self.medium, + {name: value}, + ) + + def test_pnvo001_registry_rejects_invalid_flowset_choice(self) -> None: + with self.assertRaisesRegex(ValueError, "must be one of"): + COMPONENT_MODEL_REGISTRY["amesim_pnvo001"].create( + "valve_1", + self.medium, + {"flowset": 1.5}, + ) def test_pnvo001_signal_opening_reads_res_port(self) -> None: valve = COMPONENT_MODEL_REGISTRY["amesim_pnvo001"].create( diff --git a/tests/test_component_catalog.py b/tests/test_component_catalog.py index 3163a8d..10afd1d 100644 --- a/tests/test_component_catalog.py +++ b/tests/test_component_catalog.py @@ -401,6 +401,174 @@ class ComponentCatalogTests(unittest.TestCase): self.assertEqual(pnl0003_parameters["p1_0"]["unit"], "Pa") self.assertEqual(pnl0003_parameters["T2_0"]["quantity"], "temperature") + def test_amesim_configurable_parameters_expose_choices_and_groups(self) -> None: + def parameters(model_type: str) -> dict[str, dict[str, object]]: + return { + parameter["name"]: parameter + for parameter in self.components[model_type]["parameters"] + } + + flow_options = [ + {"value": 1.0, "label": "Cq"}, + {"value": 2.0, "label": "Cv"}, + {"value": 3.0, "label": "Kv"}, + ] + for model_type, area_name in ( + ("amesim_pnor001", "area"), + ("amesim_pnvo001_fixed", "area0"), + ("amesim_pnvo001", "area0"), + ): + with self.subTest(model_type=model_type): + component = self.components[model_type] + model_parameters = parameters(model_type) + self.assertEqual(component["modelVersion"], "0.2.0") + self.assertEqual(model_parameters["flowset"]["editor"], "choice") + self.assertEqual(model_parameters["flowset"]["options"], flow_options) + self.assertEqual( + model_parameters["cq"]["visibleWhen"], + [{"parameter": "flowset", "values": [1.0]}], + ) + self.assertEqual( + model_parameters[area_name]["visibleWhen"], + [{"parameter": "flowset", "values": [1.0]}], + ) + self.assertEqual( + model_parameters["Cv"]["visibleWhen"], + [{"parameter": "flowset", "values": [2.0]}], + ) + self.assertEqual( + model_parameters["Kv"]["visibleWhen"], + [{"parameter": "flowset", "values": [3.0]}], + ) + self.assertEqual( + component["parameterGroups"], + [ + { + "id": "flow_coefficient", + "label": "流量系数", + "parameters": ["cq", area_name, "Cv", "Kv"], + "order": 10, + "defaultExpanded": False, + } + ], + ) + + thermal_options = [ + {"value": 1.0, "label": "多方过程"}, + {"value": 2.0, "label": "带换热"}, + ] + for model_type in ("amesim_pnl0001", "amesim_pnl0002", "amesim_pnl0003"): + with self.subTest(model_type=model_type): + component = self.components[model_type] + model_parameters = parameters(model_type) + self.assertEqual(component["modelVersion"], "0.2.0") + self.assertEqual(model_parameters["mode"]["editor"], "choice") + self.assertEqual(model_parameters["mode"]["options"], thermal_options) + self.assertEqual( + model_parameters["k"]["visibleWhen"], + [{"parameter": "mode", "values": [1.0]}], + ) + for name in ("kth", "extemp"): + self.assertEqual( + model_parameters[name]["visibleWhen"], + [{"parameter": "mode", "values": [2.0]}], + ) + self.assertEqual( + component["parameterGroups"], + [ + { + "id": "thermodynamics", + "label": "热力学", + "parameters": ["k", "kth", "extemp"], + "order": 10, + "defaultExpanded": False, + } + ], + ) + + ud00 = self.components["amesim_ud00"] + ud00_parameters = parameters("amesim_ud00") + self.assertEqual(ud00["modelVersion"], "0.2.0") + self.assertEqual(ud00_parameters["nstages"]["editor"], "choice") + self.assertEqual( + ud00_parameters["nstages"]["options"], + [ + {"value": float(stage_count), "label": str(stage_count)} + for stage_count in range(1, 9) + ], + ) + self.assertEqual( + ud00_parameters["iscyclic"]["options"], + [{"value": 0.0, "label": "否"}, {"value": 1.0, "label": "是"}], + ) + self.assertNotIn("visibleWhen", ud00_parameters["start1"]) + self.assertEqual( + ud00_parameters["start2"]["visibleWhen"], + [{"parameter": "nstages", "values": [2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]}], + ) + self.assertEqual( + ud00_parameters["t8"]["visibleWhen"], + [{"parameter": "nstages", "values": [8.0]}], + ) + self.assertEqual( + ud00["parameterGroups"][0]["parameters"], + [ + name + for stage_index in range(1, 9) + for name in ( + f"start{stage_index}", + f"end{stage_index}", + f"t{stage_index}", + ) + ], + ) + + lstp = self.components["amesim_lstp00a"] + lstp_parameters = parameters("amesim_lstp00a") + self.assertEqual(lstp["modelVersion"], "0.2.0") + self.assertEqual( + lstp_parameters["stiffmode"]["options"], + [ + {"value": 1.0, "label": "数值刚度"}, + {"value": 2.0, "label": "几何参数"}, + ], + ) + self.assertEqual( + lstp_parameters["discContactOption"]["options"], + [ + {"value": 1.0, "label": "允许负接触力"}, + {"value": 2.0, "label": "不允许负接触力"}, + ], + ) + self.assertEqual( + lstp_parameters["kcont"]["visibleWhen"], + [{"parameter": "stiffmode", "values": [1.0]}], + ) + for name in ("na", "G", "sdiam", "wdiam"): + self.assertEqual( + lstp_parameters[name]["visibleWhen"], + [{"parameter": "stiffmode", "values": [2.0]}], + ) + self.assertEqual( + lstp["parameterGroups"], + [ + { + "id": "stiffness", + "label": "刚度", + "parameters": ["kcont", "G", "sdiam", "wdiam", "na"], + "order": 10, + "defaultExpanded": False, + }, + { + "id": "contact", + "label": "接触", + "parameters": ["gap0", "rcont", "Pdis"], + "order": 20, + "defaultExpanded": False, + }, + ], + ) + def test_every_registered_component_is_in_the_catalog(self) -> None: self.assertEqual( set(self.components),