diff --git a/AmesimModels/test_mql/README.md b/AmesimModels/test_mql/README.md index 85208bc..1687048 100644 --- a/AmesimModels/test_mql/README.md +++ b/AmesimModels/test_mql/README.md @@ -60,28 +60,46 @@ Python 输出只有通过 AMESim baseline 对比后才能作为数值一致性 ## 当前对比结果 -默认 comparison 使用 `0 -> 1e-5 s` 的短时域,目的是在数值误差扩散前检查初值、RHS 和局部耦合。 +默认 comparison 仍使用 `0 -> 1e-5 s` 的短时域,并已新增 +`dm1@pneumatic_69` 流量诊断。 -当前已确认: +本轮确认原最大偏差的主因不是 PNL0001 Darcy 阻力,而是 8 个 +`PNVO001` 在 Python 中被固定为 `opening=1`,忽略了各自的 +`STEP0` 控制。AMESim baseline 显示 `step_8..15` 均在 +`t=0.04 s` 从 0 跳到 1,对应 PNVO 在此前保持关闭。 -- 最大绝对误差路径是 `press@pn_c1_8`。 -- 初值已对齐:Python 约 `-1300 Pa`,AMESim 约 `-1300 Pa`。 -- `t=1e-5 s` 时 Python 约 `-1290.75976 Pa`,AMESim 约 `-1299.98825 Pa`,末值绝对误差约 `9.22849 Pa`。 -- `pn_c1_8` 的边界体积功只有约 `0.026 W`,端口焓流约 `32722 W`;当前偏差主要不像是活塞体积功导致。 +接入 STEP0 控制后的默认短时对比: -这表明 132 状态仿真链路已经能够暴露真实偏差,但 PNL0001、节点流量、孔口或 PNCH012 方程仍需数值标定,尚不能声称已经达到 AMESim parity。 +- `press@pn_c1_8` 末值误差从约 `9.22849 Pa` 降为 + `0.0117249 Pa`。 +- Python `dm1@pneumatic_69` 末值约 + `-4.97737e-7 g/s`,AMESim 线性插值约 + `-2.05425e-6 g/s`。 +- canonical chamber-to-line flow 定义为 + `-dm1 * 1e-3`,因此两者分别约为 + `4.97737e-10 kg/s` 和 `2.05425e-9 kg/s`,绝对误差约 + `1.55652e-9 kg/s`。 +- AMESim 首个 `0.01 s` 区间的 `mgas1@pn_c1_8` 有限差分约 + `-9.86349e-7 kg/s`;用该区间首末 `dm1` 做梯形平均得到的 + chamber 质量变化率约 `-1.02713e-6 kg/s`,符号和量级一致。 + +当前剩余 `1e-5 s` 误差不能直接解释为物理方程误差:AMESim +baseline 只按 `0.01 s` 保存,而 comparison 在首个保存区间内做 +线性插值;位移、体积和压力的初始响应并不一定是线性的。 ## 下一步 -下一步只围绕当前最大偏差推进: +1. 增加 AMESim 保存时刻对齐的诊断,优先比较事件前的 + `t=0.01 s`,避免把 `1e-5 s` 线性插值误差当成物理偏差。 +2. 导出 `xv@pn_morifice_1` 和 `dm2@pn_morifice_1`,检查 + `t=0.04 -> 0.05 s` 开启窗口的 PNVO 流量及事件传播。 +3. 在控制时序对齐后,再判断是否需要校准 PNVO 流量系数、 + `pneumatic_69` 的 PNL0001 阻力或 PNCH012 能量方程。 +4. 继续保留 `press@pn_c1_8`、`dm1@pneumatic_69` 和 chamber + RHS breakdown 作为同一条诊断链。 -1. 导出 Python 的 `p4_port3_remote_chamber_to_line_flow`。 -2. 与 AMESim 的 `dm1@pneumatic_69` 比较,先做 `g/s -> kg/s` 单位转换并确认符号方向。 -3. 比较 `t=0` 和 `t=1e-5 s`,并用 `mgas1@pn_c1_8` 的有限差分交叉验证质量导数。 -4. 若流量偏差明显,检查 `pneumatic_69` 的 PNL0001 `resistance_mass_flow`、参数和 `pnnode4_16` 节点平衡。 -5. 若流量吻合,再校准 PNCH012 焓流、氦气物性以及质量/能量到压力导数的换算。 - -`pn_c1_8` 的直接主线是 `pneumatic_69`。`pneumatic_96` 属于 `pn_node3_8 -> pn_general_chamber` 固定气室支路,不是本轮流量诊断对象。 +`pn_c1_8` 的直接主线是 `pneumatic_69`;`pneumatic_96` +属于另一条固定气室支路,不是该诊断对象。 ## 运行与验证 diff --git a/PythonModels/scripts/run_test_mql_full_state_comparison.py b/PythonModels/scripts/run_test_mql_full_state_comparison.py index dd68ee9..2b65f5d 100644 --- a/PythonModels/scripts/run_test_mql_full_state_comparison.py +++ b/PythonModels/scripts/run_test_mql_full_state_comparison.py @@ -34,6 +34,7 @@ DEFAULT_FULL_STATE_COMPARISON_DATA_PATHS = ( "x1@mass_friction_endstops_18", "v1@mass_friction_endstops_18", "acc1@mass_friction_endstops_18", + "dm1@pneumatic_69", ) @@ -50,6 +51,23 @@ class TestMqlFullStateSignalDiagnostic: final_abs_error: float +@dataclass(frozen=True) +class TestMqlFullStateFlowDiagnostic: + data_path: str + initial_time_s: float + final_time_s: float + initial_python_dm1_g_s: float + initial_amesim_dm1_g_s: float + initial_python_canonical_kg_s: float + initial_amesim_canonical_kg_s: float + initial_canonical_abs_error_kg_s: float + final_python_dm1_g_s: float + final_amesim_dm1_g_s: float + final_python_canonical_kg_s: float + final_amesim_canonical_kg_s: float + final_canonical_abs_error_kg_s: float + + @dataclass(frozen=True) class TestMqlFullStateComparisonRun: system: TestMqlSystem @@ -112,6 +130,35 @@ class TestMqlFullStateComparisonRun: final_abs_error=abs(final_python - final_amesim), ) + def pnl0001_mass_flow_diagnostic( + self, + data_path: str = "dm1@pneumatic_69", + ) -> TestMqlFullStateFlowDiagnostic: + diagnostic = self.signal_diagnostic(data_path) + initial_python_canonical = -diagnostic.initial_python_value * 1.0e-3 + initial_amesim_canonical = -diagnostic.initial_amesim_value * 1.0e-3 + final_python_canonical = -diagnostic.final_python_value * 1.0e-3 + final_amesim_canonical = -diagnostic.final_amesim_value * 1.0e-3 + return TestMqlFullStateFlowDiagnostic( + data_path=data_path, + initial_time_s=diagnostic.initial_time_s, + final_time_s=diagnostic.final_time_s, + initial_python_dm1_g_s=diagnostic.initial_python_value, + initial_amesim_dm1_g_s=diagnostic.initial_amesim_value, + initial_python_canonical_kg_s=initial_python_canonical, + initial_amesim_canonical_kg_s=initial_amesim_canonical, + initial_canonical_abs_error_kg_s=abs( + initial_python_canonical - initial_amesim_canonical + ), + final_python_dm1_g_s=diagnostic.final_python_value, + final_amesim_dm1_g_s=diagnostic.final_amesim_value, + final_python_canonical_kg_s=final_python_canonical, + final_amesim_canonical_kg_s=final_amesim_canonical, + final_canonical_abs_error_kg_s=abs( + final_python_canonical - final_amesim_canonical + ), + ) + def diagnostics_by_final_abs_error(self): return tuple( sorted( @@ -134,6 +181,7 @@ class TestMqlFullStateComparisonRun: return self.closure.variable_chamber_rhs_diagnostic( chamber_alias=chamber_alias, state_vector=state_vector, + time_s=float(self.result.t[sample_index]), ) @@ -298,6 +346,28 @@ def format_test_mql_full_state_comparison_summary( f"final_amesim={diagnostic.final_amesim_value}, " f"final_abs_error={diagnostic.final_abs_error}" ) + flow_diagnostic = _pneumatic_69_flow_diagnostic(run) + if flow_diagnostic is not None: + lines.append( + "PNL0001 canonical mass-flow diagnostic: " + f"{flow_diagnostic.data_path}" + ) + lines.extend( + [ + " - convention=Python chamber-to-line flow " + "equals -AMESim dm1 * 1e-3", + f" - initial_python_kg_s=" + f"{flow_diagnostic.initial_python_canonical_kg_s}", + f" - initial_amesim_kg_s=" + f"{flow_diagnostic.initial_amesim_canonical_kg_s}", + f" - final_python_kg_s=" + f"{flow_diagnostic.final_python_canonical_kg_s}", + f" - final_amesim_kg_s=" + f"{flow_diagnostic.final_amesim_canonical_kg_s}", + f" - final_abs_error_kg_s=" + f"{flow_diagnostic.final_canonical_abs_error_kg_s}", + ] + ) chamber_diagnostic = _largest_chamber_rhs_diagnostic(run) if chamber_diagnostic is not None: lines.append( @@ -319,6 +389,13 @@ def format_test_mql_full_state_comparison_summary( return "\n".join(lines) + "\n" +def _pneumatic_69_flow_diagnostic(run: TestMqlFullStateComparisonRun): + try: + return run.pnl0001_mass_flow_diagnostic() + except KeyError: + return None + + def _largest_chamber_rhs_diagnostic(run: TestMqlFullStateComparisonRun): diagnostic = run.largest_final_abs_error_diagnostic if diagnostic is None or "@" not in diagnostic.data_path: diff --git a/PythonModels/systems/test_mql.py b/PythonModels/systems/test_mql.py index f88639b..069c42a 100644 --- a/PythonModels/systems/test_mql.py +++ b/PythonModels/systems/test_mql.py @@ -3908,7 +3908,9 @@ def _default_full_state_data_paths() -> tuple[str, ...]: for index in range(10, 20) for signal in ("x1", "v1", "acc1") ] - return tuple([*chamber_paths, *piston_paths, *mass_paths]) + return tuple( + [*chamber_paths, *piston_paths, *mass_paths, "dm1@pneumatic_69"] + ) _PNCH012_ALIAS_BY_SNAPSHOT_FIELD = { @@ -3967,10 +3969,22 @@ _PRIMARY_CHAMBER_DIAGNOSTIC_FIELDS = { } +_PNL0001_MASS_FLOW_DIAGNOSTIC_FIELDS = { + "pneumatic_69": "p4_port3_remote_chamber_to_line_flow", +} + + class TestMqlFullStateClosure: - def __init__(self, *, pneumatic_closure: object, mechanical_closure: object) -> None: + def __init__( + self, + *, + pneumatic_closure: object, + mechanical_closure: object, + pneumatic_assembly: object, + ) -> None: self.pneumatic_closure = pneumatic_closure self.mechanical_closure = mechanical_closure + self.pneumatic_assembly = pneumatic_assembly self.pneumatic_state_count = len(pneumatic_closure.initial_state_vector()) self.mechanical_state_count = len(mechanical_closure.initial_state_vector()) @@ -3993,10 +4007,18 @@ class TestMqlFullStateClosure: ) def snapshot(self, state_vector: list[float] | None = None) -> TestMqlFullStateSnapshot: + return self.snapshot_at(0.0, state_vector) + + def snapshot_at( + self, + time_s: float, + state_vector: list[float] | None = None, + ) -> TestMqlFullStateSnapshot: values = self.initial_state_vector() if state_vector is None else list(state_vector) pneumatic_state, mechanical_state = self._split_state(values) mechanical_snapshot = self.mechanical_closure.snapshot(mechanical_state) self._sync_variable_chamber_kinematics(mechanical_snapshot) + self.pneumatic_assembly.set_variable_orifice_openings(time_s) pneumatic_snapshot = self.pneumatic_closure.snapshot(pneumatic_state) return TestMqlFullStateSnapshot( pneumatic=pneumatic_snapshot, @@ -4016,6 +4038,7 @@ class TestMqlFullStateClosure: pneumatic_state, mechanical_state = self._split_state(list(state_vector)) mechanical_snapshot = self.mechanical_closure.snapshot(mechanical_state) self._sync_variable_chamber_kinematics(mechanical_snapshot) + self.pneumatic_assembly.set_variable_orifice_openings(time_s) pneumatic_snapshot = self.pneumatic_closure.snapshot(pneumatic_state) return [ *self.pneumatic_closure.rhs(pneumatic_state), @@ -4054,6 +4077,7 @@ class TestMqlFullStateClosure: *, chamber_alias: str, state_vector: list[float], + time_s: float = 0.0, ) -> TestMqlVariableChamberRhsDiagnostic: if chamber_alias not in _PRIMARY_CHAMBER_DIAGNOSTIC_FIELDS: raise KeyError(chamber_alias) @@ -4065,7 +4089,7 @@ class TestMqlFullStateClosure: for _mass_alias, piston_alias, bound_chamber_field in _PISTON_FORCE_BINDINGS if bound_chamber_field == chamber_field ) - snapshot = self.snapshot(state_vector) + snapshot = self.snapshot_at(time_s, state_vector) chamber = getattr(self.pneumatic_closure.components, chamber_field) chamber_properties = getattr(snapshot.pneumatic, chamber_field) line_properties = getattr(snapshot.pneumatic, line_field) @@ -4113,7 +4137,7 @@ class TestMqlFullStateClosure: from PythonModels.systems.test_mql_pneumatic import pressure_to_amesim_gauge_pa selected_paths = tuple(data_paths) if data_paths is not None else _default_full_state_data_paths() - snapshot = self.snapshot(state_vector) + snapshot = self.snapshot_at(time_s, state_vector) rhs = self.rhs_at(time_s, state_vector) chamber_properties_by_alias = {} chamber_by_alias = {} @@ -4130,7 +4154,15 @@ class TestMqlFullStateClosure: values_by_data_path = {} for data_path in selected_paths: signal, alias = self._split_data_path(data_path) - if alias in chamber_properties_by_alias: + if alias in _PNL0001_MASS_FLOW_DIAGNOSTIC_FIELDS: + if signal != "dm1": + raise KeyError(data_path) + canonical_flow_kg_s = getattr( + snapshot.pneumatic, + _PNL0001_MASS_FLOW_DIAGNOSTIC_FIELDS[alias], + ) + values_by_data_path[data_path] = -canonical_flow_kg_s * 1.0e3 + elif alias in chamber_properties_by_alias: chamber_properties = chamber_properties_by_alias[alias] chamber = chamber_by_alias[alias] if signal == "press": @@ -4631,6 +4663,7 @@ class TestMqlSystem: resistance_boundary_temperature_k=resistance_boundary_temperature_k, ), mechanical_closure=self.mechanical_mass_closure(), + pneumatic_assembly=self.pneumatic_assembly, ) def simulate_full_state_from_spec( diff --git a/PythonModels/systems/test_mql_pneumatic.py b/PythonModels/systems/test_mql_pneumatic.py index 36b05e1..a203989 100644 --- a/PythonModels/systems/test_mql_pneumatic.py +++ b/PythonModels/systems/test_mql_pneumatic.py @@ -18,12 +18,39 @@ DEFAULT_TEST_MQL_TEMPERATURE_K = 293.15 DEFAULT_VARIABLE_CHAMBER_PRESSURE_BAR = 1.0 +@dataclass(frozen=True) +class TestMqlStepSignalSpec: + alias: str + initial_output: float + final_output: float + step_time_s: float + transition_duration_s: float + transition_type: int + + def output_at(self, time_s: float) -> float: + if self.transition_type != 1: + raise ValueError( + f"unsupported STEP0 transition type {self.transition_type} on {self.alias}" + ) + return self.initial_output if time_s < self.step_time_s else self.final_output + + +@dataclass(frozen=True) +class TestMqlVariableOrificeControl: + orifice_alias: str + step: TestMqlStepSignalSpec + + def opening_at(self, time_s: float) -> float: + return self.step.output_at(time_s) + + @dataclass(frozen=True) class TestMqlPneumaticAssembly: fixed_chambers: dict[str, AmesimPneumaticVolume] variable_chambers: dict[str, AmesimVariablePneumaticVolume] fixed_orifices: dict[str, AmesimPneumaticOrifice] variable_orifices: dict[str, AmesimPneumaticOrifice] + variable_orifice_controls: dict[str, TestMqlVariableOrificeControl] fixed_initial_absolute_pressure_pa: float variable_initial_absolute_pressure_pa: float @@ -51,6 +78,14 @@ class TestMqlPneumaticAssembly: def component_count(self) -> int: return self.chamber_count + self.orifice_count + @property + def variable_orifice_control_count(self) -> int: + return len(self.variable_orifice_controls) + + def set_variable_orifice_openings(self, time_s: float) -> None: + for alias, control in self.variable_orifice_controls.items(): + self.variable_orifices[alias].opening = control.opening_at(time_s) + @property def aliases(self) -> tuple[str, ...]: return tuple( @@ -101,12 +136,13 @@ def build_test_mql_pneumatic_assembly( ) for component in config.components_by_submodel("PNOR001") } + variable_orifice_controls = _build_variable_orifice_controls(config) variable_orifices = { component.alias: _build_orifice( component, area_parameter="area0", gas=gas, - opening=1.0, + opening=variable_orifice_controls[component.alias].opening_at(0.0), ) for component in config.components_by_submodel("PNVO001") } @@ -115,11 +151,58 @@ def build_test_mql_pneumatic_assembly( variable_chambers=variable_chambers, fixed_orifices=fixed_orifices, variable_orifices=variable_orifices, + variable_orifice_controls=variable_orifice_controls, fixed_initial_absolute_pressure_pa=fixed_initial_absolute_pressure_pa, variable_initial_absolute_pressure_pa=variable_initial_absolute_pressure_pa, ) +def _build_variable_orifice_controls( + config: TestMqlConfig, +) -> dict[str, TestMqlVariableOrificeControl]: + from PythonModels.systems.test_mql import CONNECTION_SPECS + + components_by_alias = {component.alias: component for component in config.components} + variable_orifice_aliases = { + component.alias for component in config.components_by_submodel("PNVO001") + } + controls: dict[str, TestMqlVariableOrificeControl] = {} + for connection in CONNECTION_SPECS: + if connection["submodel"] != "DIRECT": + continue + source_alias = str(connection["source_component"]) + target_alias = str(connection["target_component"]) + if target_alias in variable_orifice_aliases: + orifice_alias = target_alias + step_alias = source_alias + elif source_alias in variable_orifice_aliases: + orifice_alias = source_alias + step_alias = target_alias + else: + continue + step_component = components_by_alias.get(step_alias) + if step_component is None or step_component.submodel != "STEP0": + continue + controls[orifice_alias] = TestMqlVariableOrificeControl( + orifice_alias=orifice_alias, + step=TestMqlStepSignalSpec( + alias=step_alias, + initial_output=step_component.parameter_value("out0"), + final_output=step_component.parameter_value("out1"), + step_time_s=step_component.parameter_value("t0"), + transition_duration_s=step_component.parameter_value("td"), + transition_type=int(step_component.parameter_value("transitionType")), + ), + ) + missing = variable_orifice_aliases - controls.keys() + if missing: + raise ValueError( + "missing STEP0 controls for PNVO001 components: " + + ", ".join(sorted(missing)) + ) + return controls + + def absolute_pressure_from_amesim_bar_parameter(pressure_bar: float) -> float: return pressure_bar * BAR_TO_PA diff --git a/tests/test_run_test_mql_full_state_comparison.py b/tests/test_run_test_mql_full_state_comparison.py index 1d975ff..eb4fb4a 100644 --- a/tests/test_run_test_mql_full_state_comparison.py +++ b/tests/test_run_test_mql_full_state_comparison.py @@ -29,7 +29,11 @@ class RunTestMqlFullStateComparisonScriptTests(unittest.TestCase): output_dir=output_dir, ), execution=TestMqlFullStateComparisonExecutionConfig( - data_paths=("press@pn_c1_8", "vol1@pn_brp2_8"), + data_paths=( + "press@pn_c1_8", + "vol1@pn_brp2_8", + "dm1@pneumatic_69", + ), solver=SolveIVPConfig(t_start=0.0, t_stop=0.0), t_eval=(0.0,), ), @@ -44,7 +48,7 @@ class RunTestMqlFullStateComparisonScriptTests(unittest.TestCase): self.assertEqual(returned_output_dir, output_dir) self.assertEqual(run.sample_count, 1) - self.assertEqual(run.signal_count, 2) + self.assertEqual(run.signal_count, 3) self.assertAlmostEqual(run.comparison.max_abs_error, 0.0, delta=1.0e-8) self.assertEqual( run.metrics_by_max_abs_error()[0].data_path, @@ -59,6 +63,14 @@ class RunTestMqlFullStateComparisonScriptTests(unittest.TestCase): run.diagnostics_by_final_abs_error()[0].data_path, "press@pn_c1_8", ) + flow_diagnostic = run.pnl0001_mass_flow_diagnostic() + self.assertEqual(flow_diagnostic.data_path, "dm1@pneumatic_69") + self.assertAlmostEqual(flow_diagnostic.initial_python_dm1_g_s, 0.0) + self.assertAlmostEqual(flow_diagnostic.initial_amesim_dm1_g_s, 0.0) + self.assertAlmostEqual( + flow_diagnostic.initial_python_canonical_kg_s, + -flow_diagnostic.initial_python_dm1_g_s * 1.0e-3, + ) chamber_diagnostic = run.chamber_rhs_diagnostic("pn_c1_8") self.assertEqual(chamber_diagnostic.chamber_alias, "pn_c1_8") self.assertEqual(chamber_diagnostic.piston_alias, "pn_brp2_8") @@ -71,11 +83,16 @@ class RunTestMqlFullStateComparisonScriptTests(unittest.TestCase): ) self.assertTrue(csv_summary_exists) self.assertIn("Mode: Python 132 full-state closure comparison", summary) - self.assertIn("Compared signals: 2", summary) + self.assertIn("Compared signals: 3", summary) self.assertIn("Largest absolute error: press@pn_c1_8=", summary) self.assertIn("Largest final endpoint error: press@pn_c1_8=", summary) self.assertIn("Metrics by max absolute error:", summary) self.assertIn("Endpoint diagnostics by final absolute error:", summary) + self.assertIn( + "PNL0001 canonical mass-flow diagnostic: dm1@pneumatic_69", + summary, + ) + self.assertIn("equals -AMESim dm1 * 1e-3", summary) self.assertIn("Largest endpoint chamber RHS breakdown: pn_c1_8", summary) self.assertIn("piston_alias=pn_brp2_8", summary) self.assertIn("mass_derivative_kg_s=", summary) @@ -84,6 +101,7 @@ class RunTestMqlFullStateComparisonScriptTests(unittest.TestCase): self.assertIn("final_amesim=", summary) self.assertIn("python.press@pn_c1_8", csv_header) self.assertIn("amesim.vol1@pn_brp2_8", csv_header) + self.assertIn("python.dm1@pneumatic_69", csv_header) def test_full_state_script_can_skip_artifact_files(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: diff --git a/tests/test_test_mql_pneumatic.py b/tests/test_test_mql_pneumatic.py index b779891..226ff1d 100644 --- a/tests/test_test_mql_pneumatic.py +++ b/tests/test_test_mql_pneumatic.py @@ -69,6 +69,14 @@ class TestMqlPneumaticAssemblyTests(unittest.TestCase): self.assertAlmostEqual(fixed_orifice.flow_coefficient, 0.9) self.assertAlmostEqual(variable_orifice.area, 78.5e-6) self.assertAlmostEqual(variable_orifice.flow_coefficient, 0.45) + self.assertAlmostEqual(variable_orifice.opening, 0.0) + self.assertEqual(assembly.variable_orifice_control_count, 8) + control = assembly.variable_orifice_controls["pn_morifice_11"] + self.assertEqual(control.step.alias, "step_8") + self.assertAlmostEqual(control.opening_at(0.0), 0.0) + self.assertAlmostEqual(control.opening_at(0.039), 0.0) + self.assertAlmostEqual(control.opening_at(0.04), 1.0) + assembly.set_variable_orifice_openings(0.04) self.assertAlmostEqual(variable_orifice.opening, 1.0) def test_chamber_initial_observables_match_amesim_results(self) -> None: diff --git a/tests/test_test_mql_pnl0001_segment.py b/tests/test_test_mql_pnl0001_segment.py index af34639..ca80e0d 100644 --- a/tests/test_test_mql_pnl0001_segment.py +++ b/tests/test_test_mql_pnl0001_segment.py @@ -199,6 +199,8 @@ class TestMqlPn3NodeChamberSegmentTests(unittest.TestCase): state = closure.initial_state_vector() state[4] *= 0.99 state[5] *= 0.99 + self.assertAlmostEqual(closure.components.node_orifice.opening, 0.0) + closure.components.node_orifice.opening = 1.0 snapshot = closure.snapshot(state) rhs = closure.rhs(state) @@ -710,6 +712,39 @@ class TestMqlPn3P4NodeChamberSegmentTests(unittest.TestCase): + diagnostic.boundary_work_w, ) + def test_full_state_closure_applies_step_controls_to_variable_orifices(self) -> None: + closure = self.system.full_state_closure_from_spec( + self.spec, + inlet_node_pressure_pa=15.31e6, + resistance_boundary_pressure_pa=15.29e6, + ) + state = closure.initial_state_vector() + + closed_snapshot = closure.snapshot_at(0.0, state).pneumatic + open_snapshot = closure.snapshot_at(0.04, state).pneumatic + + self.assertAlmostEqual( + closed_snapshot.p4_port3_remote_node_to_primary_line_flow, + 0.0, + ) + self.assertGreater( + open_snapshot.p4_port3_remote_node_to_primary_line_flow, + 0.0, + ) + self.assertAlmostEqual( + self.system.pneumatic_assembly.variable_orifices[ + "pn_morifice_1" + ].opening, + 1.0, + ) + closure.snapshot_at(0.0, state) + self.assertAlmostEqual( + self.system.pneumatic_assembly.variable_orifices[ + "pn_morifice_1" + ].opening, + 0.0, + ) + def test_simulates_full_state_key_data_paths_for_amesim_comparison(self) -> None: data_paths = ( "press@pn_c1_8", @@ -1105,6 +1140,8 @@ class TestMqlPn3P4NodeChamberSegmentTests(unittest.TestCase): resistance_boundary_pressure_pa=15.29e6, ) + self.assertAlmostEqual(closure.components.node_orifice.opening, 0.0) + closure.components.node_orifice.opening = 1.0 snapshot = closure.snapshot() rhs = closure.rhs(closure.initial_state_vector())