From e7177ab03e6c416df55ad39ea51abf4c58bf93d8 Mon Sep 17 00:00:00 2001 From: ljz <425868052@qq.com> Date: Fri, 31 Jul 2026 23:36:58 +0800 Subject: [PATCH] feat: integrate AMESim media models and editor UI --- .../component-model-authoring-spec-v1(1).md | 680 -- AmesimModels/test_mql/README.md | 32 +- PythonModels/.gitignore | 2 - PythonModels/README.md | 371 - PythonModels/__init__.py | 2 - .../testmodel_modelica_comparison_summary.txt | 4 - .../testmodel/testmodel_primary_series.csv | 202 - PythonModels/components/__init__.py | 2 - PythonModels/components/cylinder.py | 55 - PythonModels/components/orifice.py | 28 - PythonModels/components/pipe.py | 133 - PythonModels/components/tank.py | 55 - PythonModels/components/tee.py | 172 - PythonModels/core/__init__.py | 2 - PythonModels/core/base.py | 48 - PythonModels/core/medium.py | 96 - PythonModels/core/peng_robinson.py | 237 - PythonModels/core/ports.py | 13 - PythonModels/core/solver.py | 318 - PythonModels/core/state.py | 21 - PythonModels/reporting/__init__.py | 23 - PythonModels/reporting/amesim_results.py | 263 - .../test_mql_chamber_observations.py | 195 - PythonModels/reporting/test_mql_comparison.py | 237 - .../reporting/test_mql_line_observations.py | 211 - .../test_mql_mechanical_observations.py | 395 - .../reporting/test_mql_observations.py | 210 - .../test_mql_orifice_observations.py | 194 - .../reporting/test_mql_output_schema.py | 100 - .../reporting/test_mql_output_validation.py | 161 - PythonModels/reporting/test_mql_variables.py | 111 - PythonModels/reporting/testmodel_outputs.py | 393 - .../test_mql_model_summary.txt | 31 - .../test_mql_model_summary.txt | 31 - .../test_mql_model_summary.txt | 31 - .../test_mql_model_summary.txt | 31 - .../test_mql_model_summary.txt | 31 - .../test_mql_baseline_summary.txt | 8 - .../test_mql_baseline_summary.txt | 8 - PythonModels/scripts/run_test_mql.py | 74 - PythonModels/scripts/run_testmodel.py | 219 - PythonModels/systems/__init__.py | 2 - PythonModels/systems/test_mql.py | 6334 ----------------- PythonModels/systems/test_mql_closure.py | 4213 ----------- PythonModels/systems/test_mql_config.py | 151 - .../systems/test_mql_line_parameters.py | 451 -- PythonModels/systems/test_mql_mechanical.py | 544 -- PythonModels/systems/test_mql_nodes.py | 215 - PythonModels/systems/test_mql_pneumatic.py | 273 - .../systems/test_mql_pneumatic_lines.py | 194 - PythonModels/systems/testmodel.py | 303 - PythonModels/systems/testmodel_closure.py | 668 -- README.md | 6 +- app/main.py | 118 +- app/simulation/README.md | 57 +- .../components/amesim/flow/orifices.py | 61 +- .../components/amesim/flow/pipes.py | 89 +- app/simulation/components/amesim/gases.py | 264 + app/simulation/components/amesim/library.py | 5 +- .../components/amesim/media/__init__.py | 33 + .../components/amesim/media/mediums.py | 109 + .../components/amesim/media/properties.py | 196 + .../components/amesim/storage/chambers.py | 62 +- app/simulation/core/base.py | 4 +- app/simulation/core/catalog.py | 2 + app/simulation/core/medium.py | 79 + app/simulation/core/metadata.py | 34 + app/simulation/examples/test_mql/__init__.py | 8 + .../simulation/examples/test_mql/baseline.py | 10 +- .../closure.py} | 12 +- .../simulation/examples/test_mql/computed.py | 18 +- .../test_mql_config.py => test_mql/config.py} | 4 +- .../line_parameters.py} | 4 +- .../simulation/examples/test_mql/lines.py | 6 +- .../mechanical.py} | 8 +- .../test_mql_nodes.py => test_mql/nodes.py} | 2 +- .../pneumatic.py} | 6 +- .../pneumatic_lines.py} | 6 +- .../examples/test_mql/pneumatic_topology.py | 4 +- .../examples/test_mql/primitives/__init__.py | 1 + .../test_mql/primitives/mechanical.py | 0 .../examples/test_mql/primitives/pneumatic.py | 10 +- .../test_mql/primitives/pneumatic_lines.py | 12 +- app/simulation/examples/test_mql/run.py | 22 +- .../examples/test_mql/run_baseline.py | 11 +- .../test_mql/run_full_state_comparison.py | 27 +- app/simulation/examples/test_mql/solver.py | 18 + .../examples/test_mql/structural_network.py | 2 +- .../test_mql.py => test_mql/system.py} | 72 +- .../simulation/examples/test_mql/topology.py | 0 .../examples/testmodel/run_test_mql.py | 74 - .../testmodel/run_test_mql_baseline.py | 77 - .../run_test_mql_full_state_comparison.py | 4620 ------------ .../examples/testmodel/test_mql_baseline.py | 77 - .../examples/testmodel/test_mql_computed.py | 468 -- .../examples/testmodel/test_mql_lines.py | 102 - .../testmodel/test_mql_pneumatic_topology.py | 128 - .../examples/testmodel/test_mql_topology.py | 118 - app/simulation/paths.py | 1 + app/simulation/registry.py | 91 +- .../test_mql_chamber_observations.py | 6 +- .../reporting/test_mql_comparison.py | 2 +- .../reporting/test_mql_line_observations.py | 6 +- .../test_mql_mechanical_observations.py | 6 +- .../reporting/test_mql_observations.py | 12 +- .../test_mql_orifice_observations.py | 6 +- .../reporting/test_mql_output_schema.py | 6 +- .../reporting/test_mql_output_validation.py | 6 +- .../reporting/test_mql_variables.py | 4 +- app/simulation/solvers/solver.py | 36 +- app/system_xml.py | 410 +- docs/README.md | 4 + docs/amesim-component-migration-matrix.md | 56 +- docs/amesim-helium-peng-robinson.md | 76 + docs/component-library-spec-v1.md | 30 + docs/system-xml-v2.md | 69 +- frontend/src/App.tsx | 1658 ++++- frontend/src/ComponentSymbol.tsx | 235 + frontend/src/ParameterTable.tsx | 484 ++ frontend/src/styles.css | 777 +- frontend/tests/e2e/amesim-medium.spec.ts | 202 + frontend/tests/e2e/fixtures.ts | 134 +- frontend/tests/e2e/palette-display.spec.ts | 273 + frontend/tests/e2e/parameter-table.spec.ts | 203 + schemas/component-catalog-v1.schema.json | 77 +- schemas/system-simulation-v2.xsd | 1 + .../test_amesim_component_migration_matrix.py | 2 +- tests/test_amesim_gas_registry.py | 472 ++ tests/test_amesim_helium_medium.py | 154 + tests/test_amesim_mechanical_components.py | 4 +- tests/test_amesim_pnch012_xml.py | 2 +- tests/test_amesim_pnch023_component.py | 2 +- tests/test_amesim_pnch023_xml.py | 8 +- tests/test_amesim_pneumatic_components.py | 2 +- .../test_amesim_pneumatic_line_components.py | 2 +- tests/test_amesim_pnl0001_pipe.py | 2 +- tests/test_amesim_pnl0001_xml.py | 4 +- tests/test_amesim_pnl0002_pnl0003_xml.py | 4 +- tests/test_amesim_pnl00r_component.py | 2 +- tests/test_amesim_pnl00r_xml.py | 2 +- tests/test_amesim_pnor001_xml.py | 2 +- tests/test_amesim_pnpl01_xml.py | 2 +- tests/test_amesim_pnvo001_fixed_xml.py | 4 +- tests/test_amesim_pnvo001_signal_xml.py | 2 +- tests/test_amesim_results.py | 2 +- tests/test_amesim_ud00_xml.py | 2 +- tests/test_component_catalog.py | 118 +- tests/test_component_registry.py | 99 + tests/test_core_solver.py | 5 +- tests/test_medium_reference_contract.py | 505 ++ tests/test_peng_robinson.py | 2 +- ...test_reactflow_project_medium_reference.py | 65 + tests/test_run_test_mql_baseline.py | 2 +- ...test_run_test_mql_full_state_comparison.py | 8 +- tests/test_system_xml_protocol.py | 1 + tests/test_test_mql_baseline.py | 2 +- tests/test_test_mql_branch_simulation.py | 4 +- tests/test_test_mql_chamber_observations.py | 4 +- tests/test_test_mql_chamber_segment.py | 4 +- tests/test_test_mql_closure.py | 4 +- tests/test_test_mql_comparison.py | 4 +- tests/test_test_mql_computed.py | 2 +- tests/test_test_mql_config.py | 2 +- tests/test_test_mql_example_runner.py | 6 +- tests/test_test_mql_line_observations.py | 4 +- tests/test_test_mql_lines.py | 6 +- tests/test_test_mql_mechanical.py | 10 +- .../test_test_mql_mechanical_observations.py | 4 +- tests/test_test_mql_nodes.py | 6 +- tests/test_test_mql_observations.py | 6 +- tests/test_test_mql_orifice_observations.py | 6 +- tests/test_test_mql_output_schema.py | 6 +- tests/test_test_mql_output_validation.py | 8 +- tests/test_test_mql_pneumatic.py | 6 +- tests/test_test_mql_pnl0001.py | 6 +- tests/test_test_mql_pnl0001_segment.py | 10 +- tests/test_test_mql_pnl0002.py | 8 +- tests/test_test_mql_pnl0003_pnl00r.py | 8 +- tests/test_test_mql_structural.py | 4 +- tests/test_test_mql_topology.py | 2 +- tests/test_test_mql_variables.py | 4 +- 181 files changed, 7042 insertions(+), 25270 deletions(-) delete mode 100644 AmesimModels/component-model-authoring-spec-v1(1).md delete mode 100644 PythonModels/.gitignore delete mode 100644 PythonModels/README.md delete mode 100644 PythonModels/__init__.py delete mode 100644 PythonModels/baselines/testmodel/testmodel_modelica_comparison_summary.txt delete mode 100644 PythonModels/baselines/testmodel/testmodel_primary_series.csv delete mode 100644 PythonModels/components/__init__.py delete mode 100644 PythonModels/components/cylinder.py delete mode 100644 PythonModels/components/orifice.py delete mode 100644 PythonModels/components/pipe.py delete mode 100644 PythonModels/components/tank.py delete mode 100644 PythonModels/components/tee.py delete mode 100644 PythonModels/core/__init__.py delete mode 100644 PythonModels/core/base.py delete mode 100644 PythonModels/core/medium.py delete mode 100644 PythonModels/core/peng_robinson.py delete mode 100644 PythonModels/core/ports.py delete mode 100644 PythonModels/core/solver.py delete mode 100644 PythonModels/core/state.py delete mode 100644 PythonModels/reporting/__init__.py delete mode 100644 PythonModels/reporting/amesim_results.py delete mode 100644 PythonModels/reporting/test_mql_chamber_observations.py delete mode 100644 PythonModels/reporting/test_mql_comparison.py delete mode 100644 PythonModels/reporting/test_mql_line_observations.py delete mode 100644 PythonModels/reporting/test_mql_mechanical_observations.py delete mode 100644 PythonModels/reporting/test_mql_observations.py delete mode 100644 PythonModels/reporting/test_mql_orifice_observations.py delete mode 100644 PythonModels/reporting/test_mql_output_schema.py delete mode 100644 PythonModels/reporting/test_mql_output_validation.py delete mode 100644 PythonModels/reporting/test_mql_variables.py delete mode 100644 PythonModels/reporting/testmodel_outputs.py delete mode 100644 PythonModels/runs/test_mql_20260715_085526_745726/test_mql_model_summary.txt delete mode 100644 PythonModels/runs/test_mql_20260715_085712_158379/test_mql_model_summary.txt delete mode 100644 PythonModels/runs/test_mql_20260715_091524_614886/test_mql_model_summary.txt delete mode 100644 PythonModels/runs/test_mql_20260720_084308_403642/test_mql_model_summary.txt delete mode 100644 PythonModels/runs/test_mql_20260720_085120_858664/test_mql_model_summary.txt delete mode 100644 PythonModels/runs/test_mql_baseline_20260716_024434_287016/test_mql_baseline_summary.txt delete mode 100644 PythonModels/runs/test_mql_baseline_20260720_084318_747036/test_mql_baseline_summary.txt delete mode 100644 PythonModels/scripts/run_test_mql.py delete mode 100644 PythonModels/scripts/run_testmodel.py delete mode 100644 PythonModels/systems/__init__.py delete mode 100644 PythonModels/systems/test_mql.py delete mode 100644 PythonModels/systems/test_mql_closure.py delete mode 100644 PythonModels/systems/test_mql_config.py delete mode 100644 PythonModels/systems/test_mql_line_parameters.py delete mode 100644 PythonModels/systems/test_mql_mechanical.py delete mode 100644 PythonModels/systems/test_mql_nodes.py delete mode 100644 PythonModels/systems/test_mql_pneumatic.py delete mode 100644 PythonModels/systems/test_mql_pneumatic_lines.py delete mode 100644 PythonModels/systems/testmodel.py delete mode 100644 PythonModels/systems/testmodel_closure.py create mode 100644 app/simulation/components/amesim/gases.py create mode 100644 app/simulation/components/amesim/media/__init__.py create mode 100644 app/simulation/components/amesim/media/mediums.py create mode 100644 app/simulation/components/amesim/media/properties.py rename PythonModels/systems/test_mql_baseline.py => app/simulation/examples/test_mql/baseline.py (85%) rename app/simulation/examples/{testmodel/test_mql_closure.py => test_mql/closure.py} (99%) rename PythonModels/systems/test_mql_computed.py => app/simulation/examples/test_mql/computed.py (96%) rename app/simulation/examples/{testmodel/test_mql_config.py => test_mql/config.py} (96%) rename app/simulation/examples/{testmodel/test_mql_line_parameters.py => test_mql/line_parameters.py} (99%) rename PythonModels/systems/test_mql_lines.py => app/simulation/examples/test_mql/lines.py (93%) rename app/simulation/examples/{testmodel/test_mql_mechanical.py => test_mql/mechanical.py} (98%) rename app/simulation/examples/{testmodel/test_mql_nodes.py => test_mql/nodes.py} (99%) rename app/simulation/examples/{testmodel/test_mql_pneumatic.py => test_mql/pneumatic.py} (97%) rename app/simulation/examples/{testmodel/test_mql_pneumatic_lines.py => test_mql/pneumatic_lines.py} (96%) rename PythonModels/systems/test_mql_pneumatic_topology.py => app/simulation/examples/test_mql/pneumatic_topology.py (96%) create mode 100644 app/simulation/examples/test_mql/primitives/__init__.py rename PythonModels/components/amesim_mechanical.py => app/simulation/examples/test_mql/primitives/mechanical.py (100%) rename PythonModels/components/amesim_pneumatic.py => app/simulation/examples/test_mql/primitives/pneumatic.py (97%) rename PythonModels/components/amesim_pneumatic_line.py => app/simulation/examples/test_mql/primitives/pneumatic_lines.py (98%) rename PythonModels/scripts/run_test_mql_baseline.py => app/simulation/examples/test_mql/run_baseline.py (89%) rename PythonModels/scripts/run_test_mql_full_state_comparison.py => app/simulation/examples/test_mql/run_full_state_comparison.py (99%) create mode 100644 app/simulation/examples/test_mql/solver.py rename PythonModels/core/network.py => app/simulation/examples/test_mql/structural_network.py (97%) rename app/simulation/examples/{testmodel/test_mql.py => test_mql/system.py} (98%) rename PythonModels/systems/test_mql_topology.py => app/simulation/examples/test_mql/topology.py (100%) delete mode 100644 app/simulation/examples/testmodel/run_test_mql.py delete mode 100644 app/simulation/examples/testmodel/run_test_mql_baseline.py delete mode 100644 app/simulation/examples/testmodel/run_test_mql_full_state_comparison.py delete mode 100644 app/simulation/examples/testmodel/test_mql_baseline.py delete mode 100644 app/simulation/examples/testmodel/test_mql_computed.py delete mode 100644 app/simulation/examples/testmodel/test_mql_lines.py delete mode 100644 app/simulation/examples/testmodel/test_mql_pneumatic_topology.py delete mode 100644 app/simulation/examples/testmodel/test_mql_topology.py create mode 100644 docs/amesim-helium-peng-robinson.md create mode 100644 frontend/src/ComponentSymbol.tsx create mode 100644 frontend/src/ParameterTable.tsx create mode 100644 frontend/tests/e2e/amesim-medium.spec.ts create mode 100644 frontend/tests/e2e/palette-display.spec.ts create mode 100644 frontend/tests/e2e/parameter-table.spec.ts create mode 100644 tests/test_amesim_gas_registry.py create mode 100644 tests/test_amesim_helium_medium.py create mode 100644 tests/test_medium_reference_contract.py create mode 100644 tests/test_reactflow_project_medium_reference.py diff --git a/AmesimModels/component-model-authoring-spec-v1(1).md b/AmesimModels/component-model-authoring-spec-v1(1).md deleted file mode 100644 index d81dd72..0000000 --- a/AmesimModels/component-model-authoring-spec-v1(1).md +++ /dev/null @@ -1,680 +0,0 @@ -# 组件模型建模规范 v1 - -状态:已在 `experimental` 临时组件库实施 -适用对象:人工开发者、代码生成工具和 AI 编程助手 -配套读取规范:[组件库分类、发现与读取规范 v1](component-library-spec-v1.md) - -## 1. 文档目标 - -本文档规定一个 Python 仿真元件应如何创建、修改、测试和注册。完成后的模型必须 -同时满足四个使用方: - -1. 求解器能够实例化模型并调用方程。 -2. System XML 能够根据稳定类型找到模型。 -3. React Flow 能够自动显示图标、端口和参数。 -4. 结果页面能够根据结构化元数据展示变量。 - -本文档是模型代码的开发合同。若本文档与当前代码行为不一致,应把它视为缺陷: -先核对实际实现,再在同一次修改中同步代码、测试和文档,禁止让两套规则长期并存。 - -## 2. 开始前先判断任务类型 - -### 2.1 新增公开模型 - -公开模型会出现在前端组件库中,也能被 System XML 创建。必须: - -- 放入某个组件库的分类目录。 -- 实现完整模型契约。 -- 加入该库 `library.py` 的 `models` 清单。 -- 添加目录、契约、方程和最小仿真测试。 - -### 2.2 修改已有公开模型 - -必须先判断改动是否破坏已有工程: - -| 改动 | 版本建议 | 兼容性要求 | -| --- | --- | --- | -| 修复数值实现但不改变契约 | 修订版本 | 旧 XML 和工程继续可用 | -| 新增有默认值的参数或结果 | 次版本 | 旧工程缺少该字段时必须有迁移或默认值 | -| 修改界面名称或图标 | 库修订版本 | 不修改机器标识 | -| 修改方程的物理语义 | 根据影响提高次版本或主版本 | 补充基准和变更说明 | -| 删除、改名端口或参数 | 主版本 | 必须设计工程和 XML 迁移 | -| 修改 `MODEL_TYPE` | 视为新模型 | 旧类型必须保留迁移映射 | - -### 2.3 新增内部模型 - -仅供固定算例或研究代码使用、不进入前端目录的模型,不加入 `library.py`。这类模型 -应放在对应 `examples/` 或专用系统目录,不能与公开模型混放后依赖扫描规则排除。 - -当前示例是 -[`app/simulation/examples/testmodel/dynamic_pipe.py`](../app/simulation/examples/testmodel/dynamic_pipe.py)。 - -### 2.4 新增物理域 - -仅新增模型类不足以支持新物理域。除了模型,还必须设计: - -- `PortDefinition` 和端口变量。 -- 变量角色与连接规则。 -- 网络兼容性检查。 -- 代数方程和 stream/signal 传播。 -- XML 端口协议。 -- 前端连线兼容规则。 -- 最小闭合系统与求解测试。 - -没有完成这些基础能力时,不得仅通过修改 `domain` 字符串宣称支持新物理域。 - -## 3. 开发前必须读取的文件 - -人工或 AI 在修改模型前,应按顺序读取: - -1. 本文档。 -2. 目标库的 `library.py`。 -3. 同分类中物理行为最接近的现有模型。 -4. [`core/base.py`](../app/simulation/core/base.py)。 -5. [`core/ports.py`](../app/simulation/core/ports.py)。 -6. [`core/metadata.py`](../app/simulation/core/metadata.py)。 -7. [`core/catalog.py`](../app/simulation/core/catalog.py)。 -8. [`registry.py`](../app/simulation/registry.py) 中的启动校验。 -9. 与目标模型最接近的测试。 - -不要只根据文件名、前端图标或旧 XML 猜测模型语义。 - -## 4. 文件位置和命名 - -公开模型放在: - -```text -app/simulation/components///.py -``` - -例如: - -```text -app/simulation/components/experimental/storage/cylinder.py -app/simulation/components/experimental/flow/orifice.py -app/simulation/components/experimental/junctions/tee.py -``` - -规则: - -- 一个公开模型原则上对应一个文件和一个主要模型类。 -- 模块名、`MODEL_TYPE`、端口名和参数名使用稳定机器标识。 -- `MODEL_TYPE` 使用小写 `snake_case`。 -- 参数和结果变量允许保留已有热力学惯例,如 `T0`、`T`、`U`。 -- 中文名称只写入 `label`,不能代替机器标识。 -- 求解器、介质和网络通用逻辑不得复制到模型文件。 - -## 5. 公开模型完整契约 - -每个公开模型类必须在自身类体中显式声明: - -```python -MODEL_TYPE = "example_component" -MODEL_VERSION = "1.0.0" -PORTS = (...) -PARAMETERS = (...) -RESULT_VARIABLES = (...) -DISPLAY = ... -``` - -同时必须实现: - -```python -@classmethod -def create( - cls, - *, - name: str, - medium: IdealGasMedium, - parameters: Mapping[str, float], -) -> Component: - ... -``` - -注册器要求这些字段直接存在于公开模型类中。不要依赖父类隐式提供 -`MODEL_TYPE`、`MODEL_VERSION`、`PORTS`、`PARAMETERS`、`RESULT_VARIABLES`、 -`DISPLAY` 或 `create()`。 - -## 6. 基类选择 - -### 6.1 `AlgebraicComponent` - -适用于没有积分状态、由当前端口变量和参数直接决定残差的元件,例如: - -- 孔板 -- 阀门 -- 阻性管段 -- 理想三通 - -至少实现: - -- 构造函数和端口注册。 -- `create()`。 -- `pressure_flow_equation_residuals()`。 -- 需要传递 stream 变量时实现 `update_stream_outflows()`。 - -### 6.2 `ThermodynamicVolumeComponent` - -适用于包含质量和能量状态的气体容腔,例如: - -- 气瓶 -- 贮箱 -- 有容积的管段 - -至少实现: - -- `get_state_vector()`。 -- `set_state_vector()`。 -- `refresh_thermodynamic_ports()`。 -- `state_derivative_from_ports()`。 -- `pressure_flow_equation_residuals()`。 - -该基类已经提供标准热力学组件结果: - -```text -m, U, p, T, rho, u, h -``` - -除非物理含义不同,不要重新复制这组结果声明。 - -### 6.3 其他基类 - -如果现有基类不能表达模型,应先评估是否缺少一种通用组件能力。不要为了一个模型 -直接把专用判断塞入 `SimulationNetwork` 或求解器。 - -## 7. 端口建模规范 - -当前气动模型使用: - -```python -PortDefinition.pneumatic( - "port_a", - nominal_role="bidirectional", -) -``` - -气动端口包含: - -| 变量 | 角色 | 连接规则 | SI 单位 | -| --- | --- | --- | --- | -| `p` | `effort` | `equal` | `Pa` | -| `m_flow` | `flow` | `sumToZero` | `kg/s` | -| `h_outflow` | `stream` | `streamMix` | `J/kg` | - -必须遵守: - -- `m_flow > 0` 表示质量流入当前组件。 -- `nominal_role` 只用于界面和默认布局,不限制实际流向。 -- 物理连接是非因果的,连接线端点顺序不代表流向。 -- 所有声明端口必须使用 `register_declared_port()` 创建。 -- `DISPLAY.ports` 必须与 `PORTS` 名称集合完全一致。 -- 分支连接使用三通等连接元件,不能让一个物理端口直接连接多条边。 - -禁止: - -- 在模型内部根据画布左右方向判断流向。 -- 为了前端显示另造一套端口名。 -- 把 `port_a` 固定解释为真实入口、把 `port_b` 固定解释为真实出口。 -- 直接绕过端口状态读写其他组件对象。 - -## 8. 参数建模规范 - -所有用户可配置输入必须使用 `ParameterDefinition`: - -```python -ParameterDefinition( - name="volume", - label="容积", - quantity="volume", - unit="m3", - default=0.1, - minimum=0.0, - minimum_exclusive=True, -) -``` - -字段含义: - -| 字段 | 规则 | -| --- | --- | -| `name` | 稳定机器名,同时用于 XML、工程文件和 `create()` | -| `label` | 前端显示名称,不能为空 | -| `quantity` | 受控物理量标识 | -| `unit` | 后端 SI 基准单位 | -| `default` | 必须能够创建有效模型 | -| `minimum` / `maximum` | 必须反映方程有效范围 | -| `minimum_exclusive` | 用于直径、容积等严格大于零的量 | - -当前受控单位定义在 `SI_UNIT_BY_QUANTITY`: - -| quantity | SI 单位 | -| --- | --- | -| `dimensionless` | 空字符串 | -| `density` | `kg/m³` | -| `flow_coefficient` | `kg/(s*Pa^0.5)` | -| `internal_energy` | `J` | -| `length` | `m` | -| `mass` | `kg` | -| `mass_flow` | `kg/s` | -| `pressure` | `Pa` | -| `specific_enthalpy` | `J/kg` | -| `specific_internal_energy` | `J/kg` | -| `temperature` | `K` | -| `volume` | `m3` | - -新增物理量时必须先扩展后端受控单位表,再评估前端是否需要单位换算选项。禁止在 -单个模型中私自拼写新的同义 `quantity`。 - -构造函数必须调用: - -```python -self.set_parameter_values( - { - "volume": volume, - "p0": p0, - "T0": T0, - } -) -``` - -保存值、方程计算和结果输出都使用 SI。前端显示单位变化不能改变后端参数语义。 - -## 9. 结果变量规范 - -### 9.1 组件级结果 - -组件自身状态或派生量使用 `ResultVariableDefinition`: - -```python -ResultVariableDefinition( - name="pressure_drop", - label="压降", - quantity="pressure", - unit="Pa", - category="derived", - order=10, -) -``` - -声明后必须在 `component_result_values()` 返回同名值: - -```python -def component_result_values(self) -> Mapping[str, float]: - return { - "pressure_drop": self.port_a.p - self.port_b.p, - } -``` - -声明集合和返回键必须一致。 - -### 9.2 端口结果 - -端口结果由 `PORTS` 的端口变量自动产生,不要在 `RESULT_VARIABLES` 中重复声明 -`port_a.p`、`port_a.m_flow` 等字段。 - -### 9.3 禁止暴露的内容 - -以下内容默认不能作为用户结果: - -- 非线性求解器内部未知量索引。 -- 缩放残差和迭代缓存。 -- 仅用于调试的临时中间值。 -- 可以由已有结果稳定推导、但没有明确工程用途的重复字段。 - -## 10. 显示声明规范 - -公开模型必须声明 `DISPLAY`: - -```python -DISPLAY = ComponentDisplaySpec( - label="示例阻力元件", - library_id="experimental", - category_id="flow", - symbol="generic", - ports=( - PortDisplaySpec("port_a", "left", order=10), - PortDisplaySpec("port_b", "right", order=20), - ), - order=90, -) -``` - -规则: - -- `library_id` 必须等于所属库 ID。 -- `category_id` 必须存在于所属库的 `categories`。 -- `symbol` 是前端图形键,不是模型类型。 -- 未实现专用图标时使用新的稳定键,前端会回退到通用图形。 -- 只有确实需要专用工程图标时才修改前端图标渲染器。 -- `side` 只允许 `left` 或 `right`。 -- 旋转和镜像不能改变端口名或物理语义。 - -## 11. 标准创建入口 - -`create()` 是注册器创建模型的唯一入口: - -```python -@classmethod -def create( - cls, - *, - name: str, - medium: IdealGasMedium, - parameters: Mapping[str, float], -) -> ExampleComponent: - return cls( - name=name, - medium=medium, - coefficient=parameters["coefficient"], - ) -``` - -注册器会在调用前: - -1. 补齐默认参数。 -2. 拒绝未知参数。 -3. 检查有限值和边界。 - -调用后还会检查: - -1. 返回对象类型正确。 -2. 实例 `model_type` 与 `MODEL_TYPE` 一致。 -3. 实际端口与 `PORTS` 完全一致。 -4. 实例保存的参数与规范化参数完全一致。 - -`create()` 不应重复实现参数默认值和边界校验,也不能静默修改传入参数。 - -## 12. 方程实现要求 - -模型方程必须满足: - -- 残差形式统一为“期望等式左侧减右侧”。 -- 每条 `EquationResidual` 使用稳定、可定位的 `id`。 -- `variables` 列出该残差实际涉及的端口量或状态。 -- `role` 与方程主要约束的物理角色一致。 -- 对零压差、零流量和反向流动给出有限结果。 -- 必要正则化必须有物理解释,并通过边界测试保护。 -- 不得用画布坐标、连接线方向或组件名称决定方程。 - -动态模型还必须: - -- 状态向量长度稳定。 -- `get_state_vector()` 和 `set_state_vector()` 互为逆操作。 -- 状态导数满足质量和能量守恒约定。 -- 初始化默认值能够产生有限介质状态。 - -## 13. 可复制的代数模型模板 - -下面是一个符合当前规范的两端口代数阻力模板。复制后必须根据真实物理模型修改 -类型、参数、方程、名称和测试,不能只改类名就注册。 - -```python -from __future__ import annotations - -from collections.abc import Mapping -from math import sqrt - -from app.simulation.core.base import AlgebraicComponent -from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec -from app.simulation.core.equations import EquationResidual -from app.simulation.core.metadata import ParameterDefinition -from app.simulation.core.medium import IdealGasMedium -from app.simulation.core.ports import PortDefinition - - -class ExampleRestriction(AlgebraicComponent): - MODEL_TYPE = "example_restriction" - MODEL_VERSION = "1.0.0" - PORTS = ( - PortDefinition.pneumatic("port_a", nominal_role="bidirectional"), - PortDefinition.pneumatic("port_b", nominal_role="bidirectional"), - ) - PARAMETERS = ( - ParameterDefinition( - name="K", - label="流量系数", - quantity="flow_coefficient", - unit="kg/(s*Pa^0.5)", - default=1e-5, - minimum=0.0, - ), - ) - RESULT_VARIABLES = () - DISPLAY = ComponentDisplaySpec( - label="示例阻力元件", - library_id="experimental", - category_id="flow", - symbol="generic", - ports=( - PortDisplaySpec("port_a", "left", order=10), - PortDisplaySpec("port_b", "right", order=20), - ), - order=90, - ) - - def __init__(self, name: str, K: float = 1e-5) -> None: - super().__init__(name) - self.set_parameter_values({"K": K}) - self.K = K - self.port_a = self.register_declared_port("port_a") - self.port_b = self.register_declared_port("port_b") - - @classmethod - def create( - cls, - *, - name: str, - medium: IdealGasMedium, - parameters: Mapping[str, float], - ) -> ExampleRestriction: - return cls(name=name, K=parameters["K"]) - - def pressure_flow_equation_residuals( - self, - ) -> tuple[EquationResidual, ...]: - pressure_difference = self.port_a.p - self.port_b.p - expected_flow = ( - self.K - * sqrt(abs(pressure_difference)) - * (1.0 if pressure_difference > 0.0 else -1.0) - if pressure_difference != 0.0 - else 0.0 - ) - return ( - EquationResidual( - id=f"{self.name}:mass_flow_balance", - owner="component", - owner_id=self.name, - relation="sumToZero", - variables=( - f"{self.name}.port_a.m_flow", - f"{self.name}.port_b.m_flow", - ), - role="flow", - value=self.port_a.m_flow + self.port_b.m_flow, - ), - EquationResidual( - id=f"{self.name}:pressure_flow_relation", - owner="component", - owner_id=self.name, - relation="constitutive", - variables=( - f"{self.name}.port_a.p", - f"{self.name}.port_b.p", - f"{self.name}.port_a.m_flow", - ), - role="flow", - value=self.port_a.m_flow - expected_flow, - ), - ) - - def update_stream_outflows( - self, - connected_h: Mapping[str, float], - ) -> None: - self.port_a.h_outflow = connected_h["port_b"] - self.port_b.h_outflow = connected_h["port_a"] -``` - -真实现有模型可参考: - -- 储能元件: - [`cylinder.py`](../app/simulation/components/experimental/storage/cylinder.py) -- 阻性元件: - [`orifice.py`](../app/simulation/components/experimental/flow/orifice.py) -- 多端口连接元件: - [`tee.py`](../app/simulation/components/experimental/junctions/tee.py) - -## 14. 注册模型 - -模型文件完成后,只修改所属库的 `library.py`: - -```python -models=( - # 已有模型 - "app.simulation.components.experimental.flow.example_restriction:ExampleRestriction", -) -``` - -禁止: - -- 直接修改 `COMPONENT_MODEL_REGISTRY`。 -- 在前端复制参数和端口定义作为正式来源。 -- 递归扫描组件目录自动导入所有 `.py`。 -- 同时注册两个相同 `MODEL_TYPE`。 -- 把测试类、抽象基类或内部算例模型加入公开清单。 - -## 15. 测试要求 - -每个公开模型至少添加: - -1. 静态契约测试。 -2. 默认参数创建测试。 -3. 参数边界测试。 -4. 端口与显示布局一致性测试。 -5. 关键方程残差测试。 -6. 零流量或反向流动测试。 -7. 目录输出测试。 -8. 最小 XML 编译测试。 -9. 能进入通用求解器的模型,再添加短时仿真测试。 - -推荐先运行: - -```powershell -.\.venv-win\Scripts\python.exe -m unittest ` - tests.test_component_registry ` - tests.test_component_catalog ` - tests.test_component_metadata -``` - -然后运行完整回归: - -```powershell -.\.venv-win\Scripts\python.exe -m unittest discover -s tests -``` - -目录契约影响前端时还要运行: - -```powershell -cd frontend -$env:Path = 'F:\Master\SystemSimulationApp\.tools\node-v24.18.0-win-x64;' + $env:Path -npm.cmd run build -``` - -## 16. 修改已有模型的安全步骤 - -1. 找到 `MODEL_TYPE` 的所有 XML、工程和测试引用。 -2. 记录修改前的端口、参数、结果和默认行为。 -3. 判断版本级别和是否需要迁移。 -4. 先增加或修改测试,明确预期物理行为。 -5. 修改模型类,不在注册器和前端复制规则。 -6. 检查默认实例和旧参数是否仍能创建。 -7. 检查最小系统是否仍然闭合。 -8. 运行针对性测试和完整回归。 -9. 同步本文档或模型专属说明中的物理假设。 - -## 17. 人工或 AI 的任务输入卡 - -为了减少猜测,新增模型前建议先填写: - -```text -模型中文名称: -MODEL_TYPE: -所属 library_id: -所属 category_id: -物理域: -模型用途和边界: -端口列表及含义: -参数列表、SI 单位、默认值和范围: -状态变量: -代数方程或微分方程: -正流量约定: -需要显示的组件结果: -已知参考模型或工程公式: -最小测试系统: -允许的近似: -明确不实现的能力: -``` - -如果关键物理信息缺失,AI 应先通过现有模型、测试或用户提供的参考补齐;不能仅凭 -组件名称自行创造方程。 - -## 18. AI 修改协议 - -AI 创建或修改模型时必须遵守: - -### 修改前 - -1. 读取第 3 节列出的文件。 -2. 检查工作区已有改动,不能覆盖无关修改。 -3. 明确模型是公开模型还是内部模型。 -4. 明确端口物理域、状态、参数、方程和结果。 -5. 找到最接近的现有模型并沿用代码风格。 - -### 修改中 - -1. 将物理契约保存在模型类中。 -2. 只在库清单中登记公开模型。 -3. 不修改集中注册表来加入单个模型。 -4. 不为了让测试通过而放宽全局校验。 -5. 不改变现有模型标识,除非任务明确要求迁移。 -6. 不把前端拖拽方向当作物理流向。 -7. 不把求解器失败简单隐藏为默认结果。 - -### 修改后 - -1. 展示涉及的模型、清单和测试文件。 -2. 报告版本变化和兼容性影响。 -3. 运行针对性测试、完整后端测试和必要的前端构建。 -4. 检查 `GET /api/components/catalog` 中的模型、分类、端口和参数。 -5. 告知用户需要重启 FastAPI 才能加载新的 Python 模块。 -6. 未执行的校验必须明确说明原因。 - -## 19. 常见失败与处理 - -| 现象 | 常见原因 | 处理 | -| --- | --- | --- | -| FastAPI 启动时报模型缺少声明 | 字段继承自父类或漏写 | 在公开模型类中显式声明 | -| 模型未出现在前端 | 未加入 `library.py` 或后端未重启 | 检查清单并重启 FastAPI | -| 前端显示“内置兜底” | `/api/components/catalog` 不可用 | 检查 8000 端口和接口响应 | -| 显示端口校验失败 | `DISPLAY.ports` 与 `PORTS` 不一致 | 使用相同端口名和完整集合 | -| 单位校验失败 | `quantity` 与 SI 单位不匹配 | 使用受控单位表或先扩展规范 | -| 默认模型无法注册 | 默认参数越界或构造函数未保存参数 | 修复默认值和 `set_parameter_values()` | -| XML 报不支持模型 | XML `type` 与 `MODEL_TYPE` 不一致 | 修正类型或提供迁移 | -| 模型可显示但无法仿真 | 只完成目录元数据,方程或物理域求解未实现 | 补齐方程、网络和求解测试 | - -## 20. 完成定义 - -一个模型只有同时满足以下条件才算完成: - -- 模型契约完整且启动校验通过。 -- 默认参数和边界有效。 -- 端口、参数和结果具有稳定物理含义。 -- 方程覆盖零流量、正常流动和必要的反向流动。 -- 模型已加入正确库清单。 -- 目录接口能自动输出模型。 -- 前端无需复制参数和端口定义即可使用。 -- XML 能映射到正确模型。 -- 最小系统能够编译;声称可仿真的模型必须产生有限结果。 -- 针对性测试、完整回归和必要的前端构建通过。 -- 文档记录了模型假设、适用范围和已知限制。 diff --git a/AmesimModels/test_mql/README.md b/AmesimModels/test_mql/README.md index 0ba836e..fef95e1 100644 --- a/AmesimModels/test_mql/README.md +++ b/AmesimModels/test_mql/README.md @@ -1,6 +1,6 @@ # test_mql -本目录记录 AMESim 模型 `test_mql.ame` 向 `PythonModels` 迁移时使用的源模型信息、结果对齐约定和当前进度。 +本目录记录 AMESim 模型 `test_mql.ame` 向 `app.simulation` 迁移时使用的源模型信息、结果对齐约定和当前进度。 ## 目标 @@ -13,9 +13,9 @@ Python 输出只有通过 AMESim baseline 对比后才能作为数值一致性 ## 模型与源数据 - AMESim 源模型:`AmesimModels/test_mql.ame` -- Python 系统类:`PythonModels.systems.test_mql.TestMqlSystem` -- 结构运行入口:`PythonModels/scripts/run_test_mql.py` -- 132 状态比较入口:`PythonModels/scripts/run_test_mql_full_state_comparison.py` +- Python 系统类:`app.simulation.examples.test_mql.system.TestMqlSystem` +- 结构运行入口:`app.simulation.examples.test_mql.run` +- 132 状态比较入口:`app.simulation.examples.test_mql.run_full_state_comparison` - AMESim 组件数:117 - LINE 连接数:84 - 连续状态数:132 @@ -47,16 +47,16 @@ Python 输出只有通过 AMESim baseline 对比后才能作为数值一致性 主要实现位置: -- `PythonModels/systems/test_mql.py` -- `PythonModels/systems/test_mql_closure.py` -- `PythonModels/systems/test_mql_pneumatic.py` -- `PythonModels/systems/test_mql_mechanical.py` -- `PythonModels/systems/test_mql_lines.py` -- `PythonModels/components/amesim_pneumatic.py` -- `PythonModels/components/amesim_mechanical.py` -- `PythonModels/reporting/amesim_results.py` -- `PythonModels/reporting/test_mql_comparison.py` -- `PythonModels/scripts/run_test_mql_full_state_comparison.py` +- `app/simulation/examples/test_mql/system.py` +- `app/simulation/examples/test_mql/closure.py` +- `app/simulation/examples/test_mql/pneumatic.py` +- `app/simulation/examples/test_mql/mechanical.py` +- `app/simulation/examples/test_mql/lines.py` +- `app/simulation/examples/test_mql/primitives/` +- `app/simulation/examples/test_mql/structural_network.py` +- `app/simulation/reporting/amesim_results.py` +- `app/simulation/reporting/test_mql_comparison.py` +- `app/simulation/examples/test_mql/run_full_state_comparison.py` ## 当前对比结果 @@ -109,13 +109,13 @@ Python 输出只有通过 AMESim baseline 对比后才能作为数值一致性 运行默认短时域 comparison: ```bash -python3 -m PythonModels.scripts.run_test_mql_full_state_comparison +python3 -m app.simulation.examples.test_mql.run_full_state_comparison ``` 运行 PNVO 事件边界诊断: ```bash -python3 -m PythonModels.scripts.run_test_mql_full_state_comparison --pnvo-event-boundary +python3 -m app.simulation.examples.test_mql.run_full_state_comparison --pnvo-event-boundary ``` 运行相关测试: diff --git a/PythonModels/.gitignore b/PythonModels/.gitignore deleted file mode 100644 index 7a60b85..0000000 --- a/PythonModels/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -__pycache__/ -*.pyc diff --git a/PythonModels/README.md b/PythonModels/README.md deleted file mode 100644 index 6152293..0000000 --- a/PythonModels/README.md +++ /dev/null @@ -1,371 +0,0 @@ -# PythonModels - -`PythonModels` 用于承接 Modelica 和 AMESim 模型的 Python 平台移植。 - -目标不是逐行翻译源模型,而是建立可运行、可测试、可导出,并能与 OpenModelica 或 AMESim baseline 对比的 Python 仿真框架。 - -当前包含两条模型线:`Testmodel` 已有可运行的 ODE 近似和 OpenModelica 对比能力;`test_mql` 已形成 132 状态气动机械总闭包,正在按 AMESim baseline 做数值校准。 - -## 当前目录 - -- `core/`: 通用基础设施 - 包含组件基类、状态与端口数据结构、介质模型、网络装配、积分入口。 -- `components/`: 元件级 Python 实现 - 包含 `Cylinder`、`Tank`、`Pipe`、`Orifice`、`Tee`,以及 AMESim 气动和机械组件原语。 -- `systems/`: 系统级装配与闭合 - 包含旧 `TestModelSystem`,以及当前主线 `TestMqlSystem` 的配置、拓扑、closure、snapshot、端口写回和 RHS。 -- `reporting/`: 结果导出与对比 - 承接 OpenModelica 对比,以及 AMESim 结果读取、`Data_Path` schema validation、comparison 和诊断报告。 -- `scripts/`: 运行脚本 - 包含 `run_testmodel.py`、`run_test_mql.py` 和 `run_test_mql_full_state_comparison.py`。 -- `baselines/`: 提交进仓库的稳定基线 - 当前承接 Python 主变量基线和 Python 对 Modelica 的误差摘要基线。 -- `runs/`: 每次实际运行的默认输出目录 - 当前脚本默认会在这里创建带时间戳的子目录,用来放这次运行生成的产物。 - -当前关键文件: - -- `core/medium.py`: 温度相关的理想气体近似介质 `IdealGasMedium` -- `core/peng_robinson.py`: `test_mql` 使用的氦气 Peng-Robinson 物性 -- `core/network.py`: `SimulationNetwork`,负责组件注册、连接拓扑和状态向量拼装 -- `core/solver.py`: `integrate_ode()`,优先走 `SciPy solve_ivp`,缺依赖时回退到内置 RK4,并支持 `t_start == t_stop` 的零时长返回 -- `components/pipe.py`: 单阻容管道近似,入口压降 + 出口直连内容腔 -- `components/tee.py`: 三通的最小 stream 混合 helper -- `systems/testmodel.py`: `Testmodel` 的系统装配壳与外部运行入口 -- `systems/testmodel_closure.py`: `Testmodel` 当前专用的闭合、初始化投影、分支求解与端口回写 -- `systems/test_mql.py`: `test_mql` 系统装配、132 状态总闭包和关键输出映射 -- `systems/test_mql_closure.py`: `test_mql` 气动网络 closure、snapshot、流量计算和端口写回 -- `reporting/testmodel_outputs.py`: `Testmodel` 的 CSV/SVG/对比摘要导出 -- `scripts/run_test_mql_full_state_comparison.py`: `test_mql` 短时域 AMESim comparison 和诊断入口 -- `scripts/run_testmodel.py`: 基线运行与程序化执行入口 -- `tests/test_pythonmodels_regression.py`: 当前 Python 基线回归测试 - -## 当前阶段进度 - -这一阶段原先有 4 件重点工作,现在的状态如下: - -1. `mytee1` 的 stream/焓传播语义:已完成当前阶段收紧 - 现在如果只有一条支路发生倒流,下游来流焓统一按 `tank.h` 处理,不再临时借另一条支路的焓来凑。 -2. 下游初始化/约束处理:已完成当前阶段收口 - 之前是“直接改对象状态再开始积分”,现在已经收成显式的 `consistent_initial_state_vector()` 初始化入口。当前这一步会在不改下游总质量、总内能的前提下,把几段直接相连的体积拉回同一个连接压力。 -3. 自动校验:已完成当前阶段首版 - 已经补了标准库 `unittest` 回归测试,先把初始化投影是否守恒、是否污染原始状态,以及 4 个主变量的提交基线锁住。 -4. 更严格介质模型:已完成当前阶段首版 - 已经从固定 `cp/cv` 的理想气体近似,推进到随温度变化的空气近似,并接上了内能反解和初始化求根。 - -如果只看结果,可以把这一阶段理解成: - -- 连接器语义:首轮收紧已完成 -- 初始化入口:首轮收口已完成 -- 基线验证:首轮保护已完成 -- 介质精化:首轮近似已完成 - -## 当前阶段收口 - -上一轮 `N0-N3` 已全部完成首版,当前可以简单理解为: - -1. `N0`:系统层里最明显的流向/焓判断已经继续下沉到组件 helper。 -2. `N1`:模型参数和运行参数已经收口到配置对象。 -3. `N2`:运行接口已经分成“准备请求”和“执行请求”两层。 -4. `N3`:结果导出和命令行报告格式化已经统一收口到 `reporting/`。 - -这一轮结束后,项目已经不缺“能不能跑”的能力,下一步更重要的是把后续开发最容易卡住的地方先处理掉。 - -## 本次推送更新 - -本次推送已经把上一轮建议里的 `M2-M5` 推进到下面这个状态: - -1. `M2`:已完成当前阶段首版 - - 已把 `Testmodel` 的专用闭合、初始化投影、分支入口流量求解、下游支路出口流量闭合、端口状态回写,从 `systems/testmodel.py` 拆到新的 `systems/testmodel_closure.py` - - `TestModelSystem` 现在主要承担组件装配、网络注册和对闭合器的委托,不再继续堆积系统级手写细节 - -2. `M3`:已完成当前阶段首版 - - 已给两条支路入口流量固定点求解、下游公共压力投影补了显式诊断 - - 诊断内容至少包含 `converged / iterations / residual` - - 已支持严格模式;内部求解不收敛时可以直接抛错,而不是静默返回最后一个近似值 - - `run_testmodel()` 的结构化结果和 `testmodel_run_report.txt` 已能带出最后一次内部闭合求解诊断 - -3. `M4`:已完成当前阶段首版 - - 自动测试已不再只盯最终主变量结果 - - 现在已经覆盖: - - 改支路参数后,初始支路入口流量是否按预期变化 - - 更偏激配置下,初始化和内部闭合是否仍然收敛 - - 有无 Modelica 参考两种运行路径下,程序接口与产物行为是否一致 - -4. `M5`:已启动 - - 当前已经明确选择优先走“更容易扩展”的方向,而不是先追求更贴近 Modelica - - 已完成第一步:把闭合器内部原来大量写死的 `upper/lower` 双支路逻辑,收成可复用的 `BranchClosureComponents / BranchClosureState` 结构 - - 当前已继续推进到 `G1-G5` 的首轮兼容层改造:`snapshot` 已提供通用分支集合,系统层结果生成已拆成“通用键生成 + 旧键别名派生”两层,报告层已开始优先消费通用分支键,旧导出列名仍通过兼容映射保留,兼容测试已显式保护分支顺序和旧导出语义 - -## 下一阶段接手建议 - -如果继续往前推进,建议按下面顺序做,而不是再零散补功能: - -1. `G1`:已完成当前阶段首轮兼容接入 - - `TestModelSnapshot` 已新增 `branches` 集合 - - 每个分支当前至少带 `name / pipe / inlet_flow / outlet_flow / inlet_h / inlet_flow_diagnostics` - - `pipe_upper / pipe_lower / branch_inlet_flows / branch_outlet_flows` 目前仍保留为兼容属性,供旧调用方继续使用 - -2. `G2`:已完成当前阶段首轮内部迁移 - - `evaluate_solution()` 已改成从 `snapshot.branches` 读取数据,再通过显式分支名映射写回当前旧列名 - - `rhs()` 里的分支导数计算已改成通过通用 helper 按分支循环生成,再按当前状态向量顺序拼回 - - 当前外部导出列名仍保持兼容: - - `mypipe.p` - - `mypipe1.p` - - `branch_upper.in/out` - - `branch_lower.in/out` - -3. `G3`:已完成当前阶段首轮兼容测试 - - 当前测试已经显式保护: - - `branches` 顺序是否稳定 - - `snapshot` 新字段和兼容字段是否一致 - - 旧导出列名是否仍映射到正确分支语义 - - 参数变化后 `upper/lower` 的名字和顺序是否不会被打乱 - -4. `G4`:已完成当前阶段首轮兼容拆层 - - `evaluate_solution()` 现在会同时产出: - - 通用分支键:`branch..p/in/out` - - 旧兼容键:`mypipe.p`、`mypipe1.p`、`branch_upper.*`、`branch_lower.*` - - 报告层当前已开始优先读取通用分支键,旧键只作为兼容后备 - - 当前已经把“内部统一表达”和“旧接口兼容导出”拆成两层,但还没有把所有报告/导出逻辑都迁干净 - -5. `G5`:已完成当前阶段首轮兼容收口 - - `evaluate_solution()` 当前会先生成通用分支键,再统一派生旧兼容键 - - 报告层当前已支持“通用键优先、旧键兼容后备” - - 当前已经把系统层和 reporting 层的主要旧专名读取入口收口到少量 helper 上,后续继续迁移不会再到处散改 - -6. `P1`:下一阶段建议从这里接手 - 当前更合适的下一步,不是继续深挖内核通用化,而是切回结果导向主线: - - 定义一份稳定的外部输入参数 schema - - 明确这些结构化参数如何映射到 `TestModelConfig / TestModelRunConfig` - - 建立“结构化参数 -> 仿真执行 -> 结果产物/摘要”的稳定接口 - 这样可以直接服务后续文档解析、网页入口和报告生成,而不是继续在 `Testmodel` 内部做边际收益越来越低的抽象整理 - -7. `P2`:在 `P1` 完成后,再推进文档解析或报告生成链路 - 更现实的顺序应是: - - 先把结构化输入跑通 - - 再把结果摘要/产物组织成更接近最终产品的输出包 - - 最后再接 Word 解析或页面入口 - -如果后续继续推进,这个 README 也要一起更新,不要长期保留已经失效的路线描述。 - -## 当前实现了什么 - -当前代码已经实现: - -1. `m`、`U` 作为动态元件主状态,`p`、`T`、`rho`、`u`、`h` 作为派生量。 -2. `Cylinder`、`Tank`、`Pipe` 的刚性绝热容腔近似。 -3. `Orifice` 的压差开方流量关系。 -4. `Tee` 的简化混合焓处理。 -5. `Testmodel` 的系统级拓扑映射和一版可运行的 `rhs(t, x)`。 -6. 基于 `solve_ivp` 的积分入口,以及 SciPy 不可用时的 RK4 回退。 -7. 温度相关空气近似介质,包括 `cp(T)`、`h(T)`、`u(T)` 以及 `u -> T` 反解。 -8. 显式一致初值入口 `consistent_initial_state_vector()`,以及可迭代初始化器 `initialize_consistent_state()`。 -9. Python 主变量结果导出: - `mytank.p`、`mytank.T`、`mycylinder.p`、`mycylinder.T` -10. 贮箱温度曲线导出: - `testmodel_tank_temperature.csv` - `testmodel_tank_temperature.svg` -11. 基于 `ModelicaModels/Simulation/Testmodel_res.csv` 的逐时刻对比与误差摘要导出。 -12. 基于 `unittest` 的自动回归测试,当前已覆盖初始化守恒、主变量基线、运行接口、内部闭合诊断、通用分支兼容层、通用结果键与旧键别名一致性,以及部分中间闭合过程行为。 - -当前没有实现: - -- 通用 DAE 初始化器 -- `Modelica.Media.Air.SimpleAir` 的严格复刻 -- 面向任意拓扑的通用 connector/stream 求解器 - -## 当前怎么运行 - -最小运行方式: - -```bash -python3 -m PythonModels.scripts.run_testmodel -``` - -如果要改模型参数或运行参数,建议直接改配置对象,而不是改源码里的默认值。例如: - -```python -from PythonModels.core.solver import SolveIVPConfig -from PythonModels.scripts.run_testmodel import ( - TestModelRunConfig, - TestModelSamplingConfig, - run_testmodel, -) -from PythonModels.systems.testmodel import ( - BranchConfig, - CylinderConfig, - OrificeConfig, - PipeConfig, - TankConfig, - TestModelConfig, -) - -run_config = TestModelRunConfig( - model=TestModelConfig( - cylinder=CylinderConfig(p0=30e6), - upper_branch=BranchConfig( - orifice=OrificeConfig(K=8e-6), - pipe=PipeConfig(length=6.0, diameter=0.03), - ), - tank=TankConfig(volume=0.12), - ), - solver=SolveIVPConfig(t_start=0.0, t_stop=10.0, method="BDF"), - sampling=TestModelSamplingConfig(step=0.05), -) - -result = run_testmodel(run_config=run_config) -``` - -如果调用方想先确认“这次运行最后到底会用哪些路径、哪些采样点”,可以先准备请求,再执行: - -```python -from PythonModels.scripts.run_testmodel import ( - prepare_testmodel_run, - run_prepared_testmodel, - TestModelRunConfig, -) - -prepared = prepare_testmodel_run(run_config=TestModelRunConfig()) -print(prepared.output_dir) -print(prepared.t_eval) - -result = run_prepared_testmodel(prepared) -print(result.artifacts.primary_csv_path) -print(result.used_modelica_reference) -``` - -当前脚本会: - -1. 构建 `TestModelSystem` -2. 打印原始初值向量与约束一致后的初值向量 -3. 运行 `0 s -> 20 s` 的仿真,默认采样间隔 `0.1 s` -4. 将结果写入 `PythonModels/runs/` 下本次运行专属的时间戳目录 -5. 若存在 `ModelicaModels/Simulation/Testmodel_res.csv`,自动生成 Python 与 OpenModelica 对比结果 - -当前脚本默认不会再把运行结果直接写到提交基线目录,而是会在 `PythonModels/runs/` 下创建一个带时间戳的子目录,例如: - -- `PythonModels/runs/testmodel_20260512_103000_123456/` - -该目录里通常会包含: - -- `testmodel_primary_series.csv` -- `testmodel_tank_temperature.csv` -- `testmodel_tank_temperature.svg` -- `testmodel_run_report.txt` -- `testmodel_modelica_comparison.csv` -- `testmodel_modelica_comparison_summary.txt` - -## 基线结果 - -当前基线对比摘要来自: -[testmodel_modelica_comparison_summary.txt](baselines/testmodel/testmodel_modelica_comparison_summary.txt) - -当前四个主变量的最大误差为: - -- `mytank.p`: `max_abs_error = 134.960857 Pa`, `max_rel_error = 0.006798%` -- `mytank.T`: `max_abs_error = 0.035507 K`, `max_rel_error = 0.009016%` -- `mycylinder.p`: `max_abs_error = 1391.986349 Pa`, `max_rel_error = 0.009447%` -- `mycylinder.T`: `max_abs_error = 0.009069 K`, `max_rel_error = 0.003870%` - -这说明在当前基线工况下,Python 版主变量已经能较好贴近 OpenModelica 结果。 - -## AMESim test_mql 当前进度 - -`test_mql` 是从 `AmesimModels/test_mql.ame` 新增迁移的 AMESim 模型,当前只在独立路径下推进,不修改旧 `testmodel`。新增命名保持 AMESim 原始别名和 `Data_Path`,方便后续逐变量对齐。 - -当前已经完成: - -- 解析 117 个组件、84 条 LINE 连接、直接组件接触、全局参数、仿真设置以及 AMESim 变量目录。 -- 直接读取 `.ame` 包内 `test_mql_.var` 和 `test_mql_.results`;baseline 包含 1002 个时间点和 1116 个保存变量。 -- 使用氦气 Peng-Robinson 物性,内部统一使用绝对压力,对外按 AMESim 表压和原始单位输出。 -- 实现 `PNCH023 / PNCH012 / PNOR001 / PNVO001`,以及 `PNL0001 / PNL0002 / PNL0003 / PNL00R` 管路和 `PN3NODE2 / P4NODE2` 节点语义。 -- 完成气动真实拓扑装配、canonical flow、端口写回、snapshot 和 112 状态气动 RHS。 -- 实现 `PNRP17 / MECMAS21 / LSTP00A / LMECHN1 / UD00 / FORC` 当前工况可确认的机械行为,并形成 20 状态机械闭包。 -- 将气动和机械部分组合成 132 状态总闭包,接入活塞体积反馈、气动力、外力、端止动和质量约束,可通过现有 solver 短时积分。 -- 建立关键 `Data_Path` 序列导出、output schema、validation、AMESim 插值比较、误差排序、端点诊断和 PNCH012 RHS 项拆解。 - -当前确认的关键细节: - -- `PNRP17` 活塞腔体积使用环形有效面积 `piston_area - rod_area`。 -- `LSTP00A` 的 `gap` 观测单位是 mm,计算接触力前必须转换为 m。 -- `PNCH023` 固定气室初始压力来自 `P0=153 bar` 的绝对压力;AMESim `press` 输出为相对 `101300 Pa` 的表压。 -- `PNCH012` 变容腔初始压力对齐 AMESim 的 `1 bar` 绝对压力,`vol` 输出单位为 cm3,且末端体积等于基础死容积加对应活塞 `vol1`。 -- `MECMAS21` 的 `x1dup / v1dup / acc1dup` 是第二机械端口观测,相对 `x1 / v1 / acc1` 为反号,不是重复同值。 -- 本算例中 `MECMAS21` 的 `Fmin / Fmax / Fvisc / Ffric` 在 AMESim 结果里为零;当前只把这一工况能验证的部分写入测试,没有硬猜未激活碰撞/摩擦状态机。 - -当前默认 `0 -> 1e-5 s` comparison 已定位最大偏差为 `press@pn_c1_8`:初值对齐,但末值绝对误差约 `9.22849 Pa`。RHS 拆解显示边界体积功约 `0.026 W`,端口焓流约 `32722 W`,因此当前首要工作是比较 Python 的 `p4_port3_remote_chamber_to_line_flow` 与 AMESim 的 `dm1@pneumatic_69`,检查单位、符号、PNL0001 阻力和 `pnnode4_16` 节点平衡。 - -当前还不能宣称 `test_mql` 的 Python 时域仿真已经和 AMESim 全局一致。完整说明、运行命令和下一步校准路径见 `AmesimModels/test_mql/README.md`。 - -## Testmodel 当前架构判断 - -如果按“组件正确 -> 网络闭合 -> 积分可跑 -> 结果对齐 -> 去近似”来看,当前大致处于: - -- 组件级:已完成首版 -- 系统闭合:已完成首版 -- 积分入口:已完成首版 -- 基线结果对齐:已具备初步能力 -- 去近似:仍在进行中 - -所以当前最准确的说法不是“已完成移植”,而是: - -`Testmodel` 已有一版可运行、可导出、可对比的 Python 近似实现。 - -## Testmodel 已知限制 - -当前最主要的限制可以直接理解成下面几条: - -- 介质模型已从常 `cp/cv` 推进到温度相关空气近似,但仍不是 `Modelica.Media.Air.SimpleAir` 的严格复刻。 -- 系统整体仍是 ODE 化近似,不是原始 Modelica DAE 的直接复现。 -- `mytee1 -> mytank` 这一段虽然已经去掉早期的“虚拟出口导通系数”,改成了基于压力一致性的下游能量闭合,但本质上仍是工程近似。 -- 当前 `Tee` 的 stream 语义只覆盖了当前 `Testmodel` 需要的最小集合,还不是通用的 `inStream/actualStream` 框架。 -- 当前一致初值仍是 ODE 入口处的约束投影,不等同于真正的 DAE 初始化求解。 -- 当前自动校验主要锁的是 Python 提交基线,还不是稳定的 Modelica 阈值回归。 -- 当前闭合器、系统层和 reporting 层虽然已经开始做“双支路结构化”,但对外结果序列、报告字段和部分导出命名仍然保留 `Testmodel` 专名兼容层,还没有完全转成通用表达。 -- 当前内核已经足够支撑下一阶段“结构化参数 -> 仿真执行 -> 产物输出”的链路开发,但还没有现成的 Word 参数解析入口和正式报告生成链路。 - -所以,当前版本适合: - -- 架构验证 -- 组件接口验证 -- 基线工况对比 -- 结果导出与误差定位 - -但当前版本还不适合: - -- 直接宣称与 OpenModelica 严格等价 -- 作为最终工程结论的唯一依据 -- 直接扩展到更复杂拓扑而不补通用连接器语义 - -## Testmodel 文件级现状 - -按代码现状逐项看: - -- `core/base.py`: 正常 - 只提供最小抽象层,没有明显冗余。 -- `core/ports.py`: 正常 - `PortState` 目前只保留 `p`、`m_flow`、`h_outflow` 三个必要字段。 -- `core/state.py`: 正常 - `VolumeState` 只负责 `[m, U]` 状态打包。 -- `core/network.py`: 正常 - 负责状态向量拼装和连接摘要,不参与物理求解。 -- `core/solver.py`: 正常 - 已支持 SciPy、RK4 回退和零时长仿真。 -- `components/*.py`: 正常 - 都是当前一版近似模型,没有发现与 README 明显冲突的“未记录能力”。 -- `systems/testmodel.py`: 是当前最重要的技术债集中区 - 这里承载了下游流向切换、焓混合、压力投影等近似逻辑,后续演进应主要落在这里。 -- `scripts/run_testmodel.py`: 正常 - 已不是“最小打印脚本”,而是当前结果导出和对比入口。 -- `baselines/`: 是当前稳定基线,不应该随着日常运行频繁改动。 -- `runs/`: 是当前默认运行产物目录,不是手写源代码,也不应该当作提交基线使用。 - -## Testmodel 当前主技术债 - -目前最主要的技术债,可以直接理解成下面 4 件事: - -1. 当前初始化虽然已经引入迭代诊断,但本质上仍是 ODE 入口近似,不是真正的 DAE 初始化器。 -2. `systems/testmodel.py` 还是承载了太多系统级闭合和初始化逻辑,只是主要端口的手写 stream 方向判断已经搬到组件 helper 里了,装配参数本身已经基本收口到配置对象。 -3. 自动校验现在主要锁的是 Python 这一版自己的基线,还不是稳定的 Modelica 阈值回归。 -4. 当前空气物性已经完成首轮基线校准,但还不是 `SimpleAir` 的严格复刻。以后如果换工况,或者拿到更多 Modelica 原始结果,参数大概率还要继续调。 diff --git a/PythonModels/__init__.py b/PythonModels/__init__.py deleted file mode 100644 index a392a55..0000000 --- a/PythonModels/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -"""Python port scaffold for the Modelica-based pressurization system.""" - diff --git a/PythonModels/baselines/testmodel/testmodel_modelica_comparison_summary.txt b/PythonModels/baselines/testmodel/testmodel_modelica_comparison_summary.txt deleted file mode 100644 index b8835bd..0000000 --- a/PythonModels/baselines/testmodel/testmodel_modelica_comparison_summary.txt +++ /dev/null @@ -1,4 +0,0 @@ -mytank.p: max_abs_error=134.960858, max_rel_error=0.006798% -mytank.T: max_abs_error=0.035507, max_rel_error=0.009016% -mycylinder.p: max_abs_error=1391.986349, max_rel_error=0.009447% -mycylinder.T: max_abs_error=0.009069, max_rel_error=0.003870% diff --git a/PythonModels/baselines/testmodel/testmodel_primary_series.csv b/PythonModels/baselines/testmodel/testmodel_primary_series.csv deleted file mode 100644 index f908440..0000000 --- a/PythonModels/baselines/testmodel/testmodel_primary_series.csv +++ /dev/null @@ -1,202 +0,0 @@ -time_s,mytank.p,mytank.T,mycylinder.p,mycylinder.T -0.0,99999.9999998181,299.9999999994543,35000000.0,300.0 -0.1,113767.94796194455,310.9637987101763,34857995.191961475,299.65190130981887 -0.2,127490.75529043324,320.1741038867186,34716455.97160761,299.3039342009557 -0.30000000000000004,141168.22832317703,327.9540327439692,34575384.336400226,298.9561065982938 -0.4,154800.2272891848,334.6028385113769,34434781.727959625,298.60842486666263 -0.5,168386.6833862148,340.3440160419025,34294648.85592305,298.26089368263627 -0.6000000000000001,181927.56886228317,345.34597934661645,34154986.006528884,297.9135169662631 -0.7000000000000001,195422.8867284584,349.7379443903134,34015793.14872051,297.5662980942661 -0.8,208872.67226727083,353.62071952544704,33877069.91858829,297.2192381374397 -0.9,222277.06297793236,357.0742683513725,33738814.897943415,296.8723276411647 -1.0,235635.98150118813,360.16219760966084,33601028.88468161,296.5255848498291 -1.1,248949.42783703818,362.9362624751423,33463711.878802836,296.1790180498007 -1.2000000000000002,262217.75812283135,365.43943715392686,33326860.20704978,295.8325994898035 -1.3,275440.80324992316,367.7064864598734,33190475.613635924,295.48635453607636 -1.4000000000000001,288618.55460473493,369.766650344722,33054558.187403098,295.14029246566224 -1.5,301751.3171281971,371.6448887937757,32919104.783141974,294.79439468638526 -1.6,314839.14341954247,373.36206261131525,32784114.858335685,294.44866485903736 -1.7000000000000002,327882.06907680153,374.93594217338176,32649588.04582047,294.10310819940065 -1.8,340880.20077679906,376.3818124575356,32515523.2453146,293.7577259967503 -1.9000000000000001,353833.6270518691,377.7128659999238,32381919.54368144,293.41251959012544 -2.0,366742.42291137256,378.94054912194076,32248776.16726251,293.06749036423287 -2.1,379606.6730674497,380.0748368029417,32116092.24232331,292.72263968200787 -2.2,392426.50707225554,381.12448095768656,31983866.432642274,292.37796667994184 -2.3000000000000003,405202.0671563814,382.0971825271288,31852097.271230437,292.03346983037886 -2.4000000000000004,417933.4155500976,382.9997115435689,31720784.11623488,291.6891515780112 -2.5,430620.6510589185,383.838081019396,31589925.948559783,291.3450125479824 -2.6,443263.883710829,384.61764092425494,31459521.633358993,291.0010529534012 -2.7,455863.2542884696,385.3431706778388,31329569.71857793,290.6572718762472 -2.8000000000000003,468418.84583467664,386.0189391423481,31200069.34769955,290.31367046787994 -2.9000000000000004,480930.76401047717,386.64878296917834,31071019.430919208,289.9702490545257 -3.0,493399.1147767602,387.2361564731079,30942418.8753394,289.627007973335 -3.1,505824.0120818939,387.7841699997202,30814266.50567852,289.283948167411 -3.2,518205.5585633492,388.29565044444143,30686561.263317347,288.9410696919777 -3.3000000000000003,530543.860941083,388.7731587646163,30559302.04752924,288.5983729053581 -3.4000000000000004,542839.025935053,389.21902406251183,30432487.75758757,288.2558581528899 -3.5,555091.1532282452,389.63535331051384,30306117.365346134,287.9135275289302 -3.6,567300.3523937837,390.0240928732544,30180189.74065028,287.5713806480376 -3.7,579466.730902255,390.3870196149787,30054703.77503127,287.2294176336731 -3.8000000000000003,591590.3951155862,390.7257605811333,29929658.371455234,286.88763887333175 -3.9000000000000004,603671.4513957056,391.0418105423231,29805052.432888325,286.5460447413264 -4.0,615710.0061045411,391.3365451815722,29680884.862296656,286.20463559857706 -4.1000000000000005,627706.1656040212,391.61123273483645,29557154.562646367,285.8634117923989 -4.2,639660.0356829006,391.86704273384044,29433860.4428154,285.5223738511199 -4.3,651571.716232906,392.1050410519721,29311001.472504564,285.1815242995298 -4.4,663441.3179459961,392.32624321858174,29188576.510019373,284.840861980108 -4.5,675268.9468771729,392.53157932311984,29066584.46149164,284.5003872946073 -4.6000000000000005,687054.7090814385,392.72191331429804,28945024.2330532,284.16010063101663 -4.7,698798.7106137947,392.8980487868277,28823894.730835862,283.8200023633402 -4.800000000000001,710501.057529244,393.06073417111315,28703194.86097143,283.48009285137505 -4.9,722161.8558827877,393.21066739655134,28582923.529591747,283.1403724404863 -5.0,733781.2088290841,393.3484951327475,28463079.672743235,282.8008422184954 -5.1000000000000005,745359.2175579917,393.47481852250473,28343662.24673756,282.4615037792963 -5.2,756895.9922909128,393.5902150621596,28224670.114733644,282.1223563449153 -5.300000000000001,768391.6386869826,393.6952183545109,28106102.186946325,281.78340031124986 -5.4,779846.2624053361,393.7903288312906,27987957.373590477,281.44463605959197 -5.5,791259.96910511,393.8760162992802,27870234.58488091,281.10606395639473 -5.6000000000000005,802632.8644454395,393.9527222554955,27752932.731032487,280.76768435303745 -5.7,813965.0540854601,394.02086199551263,27636050.72226007,280.4294975855874 -5.800000000000001,825256.6344365194,394.08081904264304,27519587.564161647,280.0915054160418 -5.9,836507.7093909897,394.13296077650165,27403542.18517475,279.75370843660113 -6.0,847718.3891245491,394.17763867488134,27287913.44892988,279.4161062418491 -6.1000000000000005,858888.7786008355,394.2151805066602,27172700.272815377,279.0786992215644 -6.2,870018.9827834871,394.2458959595685,27057901.57421954,278.7414877501898 -6.300000000000001,881109.1066361419,394.27007787647636,26943516.2705307,278.40447218658807 -6.4,892159.2551224378,394.2880033916485,26829543.279137183,278.0676528737956 -6.5,903169.5332060129,394.2999349762812,26715981.5174273,277.73103013877227 -6.6000000000000005,914139.9829839376,394.30609016406663,26602830.551205155,277.3946116451124 -6.7,925070.7482002805,394.30672506364067,26490088.897871558,277.0583932025842 -6.800000000000001,935961.9434794089,394.30206871636136,26377755.375172496,276.7223739769823 -6.9,946813.6729747849,394.2923336718555,26265828.9088526,276.3865543393284 -7.0,957626.040839869,394.2777219546483,26154308.424656466,276.05093464451556 -7.1000000000000005,968399.1512281233,394.2584257130283,26043192.84832872,275.7155152310525 -7.2,979133.1082930088,394.23462782052076,25932481.10561397,275.3802964208052 -7.300000000000001,989828.0161879869,394.2065024339623,25822172.12225682,275.04527851873627 -7.4,1000483.9790665191,394.17421551178546,25712264.824001882,274.71046181264023 -7.5,1011101.1010820667,394.13792529578427,25602758.136593778,274.3758465728772 -7.6000000000000005,1021679.4863880915,394.09778275932865,25493650.985777102,274.04143305210266 -7.7,1032219.239138054,394.0539320247227,25384942.297296483,273.707221484995 -7.800000000000001,1042720.4634854163,394.0065107521557,25276630.996896524,273.37321208797937 -7.9,1053183.2635836392,393.95565050247825,25168716.01032184,273.0394050589494 -8.0,1063607.7435861847,393.9014770758345,25061196.263317037,272.705800576985 -8.1,1073994.0076465139,393.8441108280058,24954070.681626726,272.3723988020679 -8.200000000000001,1084342.0075784405,393.78360959211744,24847339.76225091,272.03921728278755 -8.3,1094651.950514763,393.7201237653114,24741001.368788917,271.7062444960846 -8.4,1104923.9566483083,393.65376548392067,24635054.261552785,271.3734787513138 -8.5,1115158.1282653515,393.5846357175065,24529497.385545585,271.0409203804046 -8.6,1125354.567652169,393.5128313057523,24424329.685770374,270.7085696976142 -8.700000000000001,1135513.3770950353,393.43844517133164,24319550.10723021,270.37642699925055 -8.8,1145634.6588802272,393.3615665197664,24215157.594928175,270.0444925633909 -8.9,1155718.5152940191,393.2822810271899,24111151.093867306,269.712766649598 -9.0,1165765.0486226876,393.2006710168648,24007529.54905069,269.3812494986329 -9.1,1175774.361152508,393.11681562522904,23904291.90548136,269.04994133216485 -9.200000000000001,1185746.555169756,393.03079095819226,23801437.108162407,268.7188423524781 -9.3,1195681.732960707,392.94267023834027,23698964.10209688,268.3879527421748 -9.4,1205579.9968116365,392.8525239436615,23596871.83228785,268.05727266387584 -9.5,1215441.4490088206,392.7604199383583,23495159.243738372,267.72680225991735 -9.600000000000001,1225266.1918385345,392.66642359626576,23393825.281451505,267.39654165204473 -9.700000000000001,1235054.3275870536,392.57059791736026,23292868.89043032,267.0664909411029 -9.8,1244805.7424247153,392.47293675670085,23192291.244732097,266.7366751290975 -9.9,1254520.6963966596,392.3735487251935,23092089.662209835,266.40707625875064 -10.0,1264199.3049931854,392.2724947546991,22992262.951678198,266.07769289844276 -10.100000000000001,1273841.6679760527,392.1698289153906,22892810.0841785,265.74852540655036 -10.200000000000001,1283447.8851070204,392.0656034102887,22793730.030752078,265.41957412301247 -10.3,1293018.0561478487,391.9598686580462,22695021.76244025,265.0908393690368 -10.4,1302552.2808602974,391.8526733713811,22596684.250284337,264.76232144680336 -10.5,1312050.6590061258,391.7440646314241,22498716.465325654,264.4340206391643 -10.600000000000001,1321513.290347094,391.63408795822716,22401117.37860553,264.10593720934105 -10.700000000000001,1330940.2746449616,391.5227873776613,22303885.961165283,263.7780714006174 -10.8,1340331.711661488,391.41020548491883,22207021.184046242,263.4504234360302 -10.9,1349687.7011584332,391.29638350481804,22110522.01828973,263.1229935180553 -11.0,1359008.342897557,391.18136134909884,22014387.434937052,262.79578182829164 -11.100000000000001,1368293.7366406189,391.0651776708804,21918616.405029543,262.4687885271409 -11.200000000000001,1377543.9821493784,390.9478699164463,21823207.89960853,262.1420137534841 -11.3,1386759.1791855951,390.82947437450605,21728160.88971532,261.81545762435434 -11.4,1395939.2416852904,390.70997702085253,21633476.26302751,261.4891421257888 -11.5,1405084.4058089943,390.5894488404711,21539151.583747786,261.1630514204039 -11.600000000000001,1414194.7727817593,390.46792355037144,21445185.807824824,260.83718544108797 -11.700000000000001,1423270.4378832965,390.3454323238288,21351577.952528503,260.5115448028764 -11.8,1432311.4962235806,390.22200536933536,21258327.036879413,260.1861301247683 -11.9,1441318.042742851,390.0976719679259,21165432.0816488,259.86094202976824 -12.0,1450290.1722116095,389.97246050877925,21072892.1093586,259.5359811449295 -12.100000000000001,1459227.9792306225,389.84639852319333,20980706.14428146,259.2112481013979 -12.200000000000001,1468131.5582309205,389.71951271701596,20888873.212440677,258.88674353445447 -12.3,1477001.0034737969,389.59182900161403,20797392.34161027,258.5624680835606 -12.4,1485836.4090508097,389.46337252345796,20706262.561314918,258.23842239240224 -12.5,1494637.8688837802,389.3341676923915,20615482.902829994,257.9146071089349 -12.600000000000001,1503405.4767247946,389.20423820865625,20525052.399181567,257.5910228854296 -12.700000000000001,1512139.3261562001,389.07360708873284,20434970.085146382,257.26767037851835 -12.8,1520839.5105906113,388.94229669006046,20345234.997251876,256.9445502492407 -12.9,1529506.1233409408,388.81032876227204,20255846.173053782,256.62166315000906 -13.0,1538139.2687433015,388.67772882293264,20166802.53641284,256.2990076411302 -13.100000000000001,1546739.030251722,388.5445136335602,20078103.22657088,255.97658615427704 -13.200000000000001,1555305.5007360894,388.4107031827765,19989747.285653118,255.65439933992917 -13.3,1563838.7729005008,388.27631690939495,19901733.757494725,255.332447852161 -13.4,1572338.9392832702,388.14137372147235,19814061.687640797,255.0107323486784 -13.5,1580806.0922569225,388.0058920145713,19726730.123346385,254.68925349085504 -13.600000000000001,1589240.3240281995,387.86988968927403,19639738.113576483,254.36801194376957 -13.700000000000001,1597641.7266380545,387.73338416798015,19553084.709006038,254.04700837624307 -13.8,1606010.3919616563,387.59639241102605,19466768.962019928,253.72624346087662 -13.9,1614346.4117083861,387.458930932153,19380789.926712975,253.40571787408948 -14.0,1622649.8774218408,387.32101581335974,19295146.658889957,253.0854322961577 -14.100000000000001,1630920.8804798292,387.18266271916264,19209838.21606559,252.76538741125262 -14.200000000000001,1639159.5120943757,387.0438869102949,19124863.657464534,252.4455839074805 -14.3,1647365.8633117182,386.90470325686863,19040222.0440214,252.12602247692183 -14.4,1655540.0250123062,386.76512625102305,18955912.438380726,251.8067038156715 -14.5,1663682.087910807,386.6251700190859,18871933.904897016,251.48762862387906 -14.600000000000001,1671792.1425560997,386.4848483332645,18788285.509634707,251.16879760578948 -14.700000000000001,1679870.2793312764,386.3441746228917,18704966.320368182,250.85021146978437 -14.8,1687916.588453644,386.2031619852454,18621975.406581767,250.5318709284234 -14.9,1695931.1599747243,386.06182319595837,18539311.83946974,250.21377669848638 -15.0,1703914.0837802505,385.920170719039,18456974.691936314,249.8959295010154 -15.100000000000001,1711865.4494154856,385.77821670970445,18374963.040397402,249.57833006564294 -15.200000000000001,1719785.3379250832,385.6359727064743,18293276.048945524,249.26097933285033 -15.3,1727673.8459201432,385.4934505781353,18211912.721118417,248.94387786218365 -15.4,1735531.0625176337,385.3506616315768,18130872.137749474,248.6270263958575 -15.5,1743357.0766656764,385.2076169067951,18050153.381413613,248.31042568075102 -15.600000000000001,1751151.9771435438,385.06432718490083,17969755.53642727,247.99407646845765 -15.700000000000001,1758915.8525616606,384.9208029958382,17889677.688848406,247.67797951533504 -15.8,1766648.7913616048,384.77705462583015,17809918.926476505,247.36213558255577 -15.9,1774350.881816107,384.63309212455715,17730478.33885257,247.04654543615806 -16.0,1782022.2120290494,384.4889253120841,17651355.017259125,246.73120984709743 -16.1,1789662.8699354655,384.3445637855432,17572548.05472022,246.41612959129878 -16.2,1797272.9433015438,384.2000169255848,17494056.546001427,246.10130544970855 -16.3,1804852.5197246224,384.05529390260284,17415879.58760983,245.78673820834797 -16.400000000000002,1812401.6866331936,383.9104036827472,17338016.277794052,245.47242865836654 -16.5,1819920.5312869006,383.76535503372884,17260465.716544226,245.15837759609565 -16.6,1827409.1407765402,383.6201565304272,17183227.005592,244.84458582310356 -16.7,1834867.6020240602,383.47481656030743,17106299.24841057,244.53105414625009 -16.8,1842296.0017825624,383.32934332865534,17029681.550214626,244.2177833777421 -16.900000000000002,1849694.4266362996,383.1837448636361,16953373.017960392,243.9047743351897 -17.0,1857062.9630006768,383.03802902118434,16877372.76034562,243.59202784166294 -17.1,1864401.697122252,382.8922034897328,16801679.887809563,243.27954472574828 -17.2,1871710.7150787353,382.7462757947841,16726293.512533028,242.96732582160686 -17.3,1878990.1030524147,382.60025337999576,16651212.745618157,242.65537190477806 -17.400000000000002,1886239.954455942,382.4541455990818,16576436.623591991,242.343682012199 -17.5,1893460.3482686526,382.3079576538116,16501964.331849081,242.03225853722532 -17.6,1900651.3699906773,382.16169648335216,16427794.988527464,241.72110231102408 -17.7,1907813.104956132,382.0153688850827,16353927.713477474,241.41021416968596 -17.8,1914945.6383331183,381.86898151834066,16280361.628261749,241.09959495427398 -17.900000000000002,1922049.0551237254,381.7225409080495,16207095.856155202,240.78924551087363 -18.0,1929123.4401640275,381.57605344823065,16134129.522145053,240.47916669064318 -18.1,1936168.8781240864,381.4295254054061,16061461.752930798,240.16935934986387 -18.2,1943185.4535079484,381.28296292189384,15989091.67692425,239.8598243499916 -18.3,1950173.2506536485,381.1363720190009,15917018.424249483,239.55056255770802 -18.400000000000002,1957132.3537332045,380.98975860011734,15845241.126742886,239.24157484497272 -18.5,1964062.8467526236,380.84312845371574,15773758.917953137,238.93286208907583 -18.6,1970964.8135518986,380.6964872562572,15702570.933141202,238.6244251726908 -18.7,1977838.3378050062,380.54984057500997,15631676.309280336,238.31626498392774 -18.8,1984683.5030199126,380.4031938707821,15561074.185056096,238.00838241638746 -18.900000000000002,1991500.3925385692,380.25655250057133,15490763.700866321,237.7007783692156 -19.0,1998289.0895369116,380.10992172013556,15420743.998821149,237.39345374715774 -19.1,2005049.677024864,379.96330668648653,15351014.222743012,237.08640946061425 -19.200000000000003,2011782.237846337,379.81671246030885,15281573.518166626,236.77964642569634 -19.3,2018486.8546792252,379.67014400830794,15212421.032339014,236.47316556428217 -19.400000000000002,2025163.610035411,379.5236062054879,15143555.914219463,236.16696780407335 -19.5,2031812.5863479478,379.3771038539995,15074977.31358037,235.861054060844 -19.6,2038433.8680519294,379.2306420805889,15006684.359544702,235.55542481191287 -19.700000000000003,2045027.536485746,379.084225365391,14938676.213175347,235.25008113730536 -19.8,2051593.6739729291,378.9378583245209,14870952.025374293,234.9450238874766 -19.900000000000002,2058132.3627231135,378.79154550141357,14803510.948218277,234.64025390703284 -20.0,2064643.6848320398,378.64529136859073,14736352.134958768,234.33577203462033 diff --git a/PythonModels/components/__init__.py b/PythonModels/components/__init__.py deleted file mode 100644 index d8e493c..0000000 --- a/PythonModels/components/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -"""Component implementations for the Python system model.""" - diff --git a/PythonModels/components/cylinder.py b/PythonModels/components/cylinder.py deleted file mode 100644 index 066fb27..0000000 --- a/PythonModels/components/cylinder.py +++ /dev/null @@ -1,55 +0,0 @@ -from __future__ import annotations - -from PythonModels.core.base import DynamicComponent -from PythonModels.core.medium import IdealGasMedium, ThermodynamicProperties -from PythonModels.core.ports import PortState -from PythonModels.core.state import VolumeState - - -class Cylinder(DynamicComponent): - """Python port of ModelicaModels.Mycylinder.""" - - def __init__( - self, - name: str, - medium: IdealGasMedium, - V: float = 0.01, - p0: float = 35e6, - T0: float = 300.0, - ) -> None: - super().__init__(name=name) - self.medium = medium - self.V = V - m0 = p0 * V / (medium.R_gas * T0) - U0 = m0 * medium.specific_internal_energy(T0) - self.state = VolumeState(m=m0, U=U0) - self.port_b = PortState() - - def get_state_vector(self) -> list[float]: - return self.state.as_vector() - - def set_state_vector(self, values: list[float]) -> None: - self.state = VolumeState.from_vector(values) - - def properties(self) -> ThermodynamicProperties: - props = self.medium.properties_from_mU(self.state.m, self.state.U, self.V) - self.port_b.p = props.p - self.port_b.h_outflow = props.h - return props - - def derivatives_from_connection( - self, - *, - connected_h: float, - port_m_flow: float, - internal_h: float, - ) -> VolumeState: - inlet_h = self.connection_inlet_enthalpy( - port_m_flow=port_m_flow, - connected_h=connected_h, - internal_h=internal_h, - ) - return self.derivatives(inlet_h, port_m_flow) - - def derivatives(self, inlet_h: float, m_flow: float) -> VolumeState: - return VolumeState(m=m_flow, U=m_flow * inlet_h) diff --git a/PythonModels/components/orifice.py b/PythonModels/components/orifice.py deleted file mode 100644 index 9da6282..0000000 --- a/PythonModels/components/orifice.py +++ /dev/null @@ -1,28 +0,0 @@ -from __future__ import annotations - -from math import sqrt - -from PythonModels.core.base import AlgebraicComponent -from PythonModels.core.ports import PortState - - -class Orifice(AlgebraicComponent): - """Python port of ModelicaModels.Myorifice.""" - - def __init__(self, name: str, opening: float = 1.0, K: float = 1e-7) -> None: - super().__init__(name=name) - self.opening = opening - self.K = K - self.port_a = PortState() - self.port_b = PortState() - - @property - def K_eff(self) -> float: - return self.K * max(self.opening, 0.001) - - def mass_flow(self, p_a: float, p_b: float) -> float: - dp = p_a - p_b - if dp == 0.0: - return 0.0 - return self.K_eff * sqrt(abs(dp)) * (1.0 if dp > 0.0 else -1.0) - diff --git a/PythonModels/components/pipe.py b/PythonModels/components/pipe.py deleted file mode 100644 index 6fb9fe5..0000000 --- a/PythonModels/components/pipe.py +++ /dev/null @@ -1,133 +0,0 @@ -from __future__ import annotations - -from PythonModels.core.base import DynamicComponent -from PythonModels.core.medium import IdealGasMedium, ThermodynamicProperties -from PythonModels.core.ports import PortState -from PythonModels.core.state import VolumeState - - -class Pipe(DynamicComponent): - """Python port of ModelicaModels.Mypipe.""" - - def __init__( - self, - name: str, - medium: IdealGasMedium, - L: float = 5.0, - D: float = 0.02, - lambda_darcy: float = 0.02, - p0: float = 1e5, - T0: float = 300.0, - ) -> None: - super().__init__(name=name) - self.medium = medium - self.L = L - self.D = D - self.lambda_darcy = lambda_darcy - self.area = 3.141592653589793 * D * D / 4.0 - self.V = self.area * L - m0 = p0 * self.V / (medium.R_gas * T0) - U0 = m0 * medium.specific_internal_energy(T0) - self.state = VolumeState(m=m0, U=U0) - self.port_a = PortState() - self.port_b = PortState() - - def get_state_vector(self) -> list[float]: - return self.state.as_vector() - - def set_state_vector(self, values: list[float]) -> None: - self.state = VolumeState.from_vector(values) - - def properties(self) -> ThermodynamicProperties: - props = self.medium.properties_from_mU(self.state.m, self.state.U, self.V) - self.port_b.p = props.p - self.port_a.h_outflow = props.h - self.port_b.h_outflow = props.h - return props - - def inlet_pressure(self, m_flow_a: float, rho: float, core_pressure: float) -> float: - resistance = self.lambda_darcy * (self.L / self.D) - dynamic_term = m_flow_a * abs(m_flow_a) / (2.0 * rho * self.area * self.area) - return core_pressure + resistance * dynamic_term - - def port_a_inlet_enthalpy( - self, - *, - port_a_m_flow: float, - connected_h: float, - internal_h: float, - ) -> float: - return self.connection_inlet_enthalpy( - port_m_flow=port_a_m_flow, - connected_h=connected_h, - internal_h=internal_h, - ) - - def port_b_inlet_enthalpy( - self, - *, - port_b_m_flow: float, - connected_h: float, - internal_h: float, - ) -> float: - return self.connection_inlet_enthalpy( - port_m_flow=port_b_m_flow, - connected_h=connected_h, - internal_h=internal_h, - ) - - def connection_inlet_enthalpies( - self, - *, - port_a_m_flow: float, - connected_h_a: float, - port_b_m_flow: float, - connected_h_b: float, - internal_h: float, - ) -> tuple[float, float]: - return ( - self.port_a_inlet_enthalpy( - port_a_m_flow=port_a_m_flow, - connected_h=connected_h_a, - internal_h=internal_h, - ), - self.port_b_inlet_enthalpy( - port_b_m_flow=port_b_m_flow, - connected_h=connected_h_b, - internal_h=internal_h, - ), - ) - - def derivatives_from_connections( - self, - *, - port_a_m_flow: float, - connected_h_a: float, - port_b_m_flow: float, - connected_h_b: float, - internal_h: float, - ) -> VolumeState: - inlet_h_a, inlet_h_b = self.connection_inlet_enthalpies( - port_a_m_flow=port_a_m_flow, - connected_h_a=connected_h_a, - port_b_m_flow=port_b_m_flow, - connected_h_b=connected_h_b, - internal_h=internal_h, - ) - return self.derivatives( - inlet_h_a=inlet_h_a, - inlet_h_b=inlet_h_b, - m_flow_a=port_a_m_flow, - m_flow_b=port_b_m_flow, - ) - - def derivatives( - self, - inlet_h_a: float, - inlet_h_b: float, - m_flow_a: float, - m_flow_b: float, - ) -> VolumeState: - dm_dt = m_flow_a + m_flow_b - dU_dt = m_flow_a * inlet_h_a + m_flow_b * inlet_h_b - return VolumeState(m=dm_dt, U=dU_dt) diff --git a/PythonModels/components/tank.py b/PythonModels/components/tank.py deleted file mode 100644 index 8f51d69..0000000 --- a/PythonModels/components/tank.py +++ /dev/null @@ -1,55 +0,0 @@ -from __future__ import annotations - -from PythonModels.core.base import DynamicComponent -from PythonModels.core.medium import IdealGasMedium, ThermodynamicProperties -from PythonModels.core.ports import PortState -from PythonModels.core.state import VolumeState - - -class Tank(DynamicComponent): - """Python port of ModelicaModels.Mytank.""" - - def __init__( - self, - name: str, - medium: IdealGasMedium, - V: float = 0.1, - p0: float = 1e5, - T0: float = 300.0, - ) -> None: - super().__init__(name=name) - self.medium = medium - self.V = V - m0 = p0 * V / (medium.R_gas * T0) - U0 = m0 * medium.specific_internal_energy(T0) - self.state = VolumeState(m=m0, U=U0) - self.port_a = PortState() - - def get_state_vector(self) -> list[float]: - return self.state.as_vector() - - def set_state_vector(self, values: list[float]) -> None: - self.state = VolumeState.from_vector(values) - - def properties(self) -> ThermodynamicProperties: - props = self.medium.properties_from_mU(self.state.m, self.state.U, self.V) - self.port_a.p = props.p - self.port_a.h_outflow = props.h - return props - - def derivatives_from_connection( - self, - *, - connected_h: float, - port_m_flow: float, - internal_h: float, - ) -> VolumeState: - inlet_h = self.connection_inlet_enthalpy( - port_m_flow=port_m_flow, - connected_h=connected_h, - internal_h=internal_h, - ) - return self.derivatives(inlet_h, port_m_flow) - - def derivatives(self, inlet_h: float, m_flow: float) -> VolumeState: - return VolumeState(m=m_flow, U=m_flow * inlet_h) diff --git a/PythonModels/components/tee.py b/PythonModels/components/tee.py deleted file mode 100644 index 6602435..0000000 --- a/PythonModels/components/tee.py +++ /dev/null @@ -1,172 +0,0 @@ -from __future__ import annotations - -from PythonModels.core.base import AlgebraicComponent -from PythonModels.core.ports import PortState - - -class Tee(AlgebraicComponent): - """Python port of ModelicaModels.Mytee.""" - - def __init__(self, name: str) -> None: - super().__init__(name=name) - self.port_in = PortState() - self.port_out1 = PortState() - self.port_out2 = PortState() - - def mixed_inlet_enthalpy( - self, - branch1_m_flow: float, - branch1_h: float, - branch2_m_flow: float, - branch2_h: float, - fallback_h: float = 0.0, - ) -> float: - positive_1 = max(branch1_m_flow, 0.0) - positive_2 = max(branch2_m_flow, 0.0) - total = positive_1 + positive_2 - if total <= 1e-9: - return fallback_h - return (positive_1 * branch1_h + positive_2 * branch2_h) / total - - def inlet_stream_enthalpy( - self, - branch1_m_flow: float, - branch1_h: float, - branch2_m_flow: float, - branch2_h: float, - fallback_h: float, - ) -> float: - """Approximate `inStream(port_in.h_outflow)` for the current tee topology.""" - - return self.mixed_inlet_enthalpy( - branch1_m_flow, - branch1_h, - branch2_m_flow, - branch2_h, - fallback_h=fallback_h, - ) - - def branch_actual_stream_enthalpy( - self, - branch_m_flow: float, - branch_h: float, - inlet_h: float, - ) -> float: - """Approximate `actualStream(branch.h_outflow)` for a tee branch port.""" - - return inlet_h if branch_m_flow > 0.0 else branch_h - - @staticmethod - def _solve_linear_2x2( - a11: float, - a12: float, - a21: float, - a22: float, - b1: float, - b2: float, - ) -> tuple[float, float] | None: - determinant = a11 * a22 - a12 * a21 - if abs(determinant) <= 1e-12: - return None - x1 = (b1 * a22 - b2 * a12) / determinant - x2 = (a11 * b2 - a21 * b1) / determinant - return x1, x2 - - def solve_branch_outlet_flows_from_energy_balance( - self, - *, - ratio_branch1: float, - ratio_branch2: float, - inlet_h_branch1: float, - inlet_h_branch2: float, - branch1_h: float, - branch2_h: float, - inlet_h: float, - q_in_branch1: float, - q_in_branch2: float, - tolerance: float = 1e-12, - ) -> tuple[float, float]: - """Solve branch outlet flows for the current three-port downstream tee use-case.""" - - rhs_branch1 = q_in_branch1 * inlet_h_branch1 - rhs_branch2 = q_in_branch2 * inlet_h_branch2 - - def solve_both_forward() -> tuple[float, float] | None: - return self._solve_linear_2x2( - (1.0 + ratio_branch1) * branch1_h, - ratio_branch1 * branch2_h, - ratio_branch2 * branch1_h, - (1.0 + ratio_branch2) * branch2_h, - rhs_branch1, - rhs_branch2, - ) - - def solve_one_reverse( - *, - branch1_reverse: bool, - ) -> tuple[float, float] | None: - if branch1_reverse: - return self._solve_linear_2x2( - inlet_h * (1.0 + ratio_branch1), - ratio_branch1 * inlet_h, - ratio_branch2 * inlet_h, - branch2_h + ratio_branch2 * inlet_h, - rhs_branch1, - rhs_branch2, - ) - - return self._solve_linear_2x2( - branch1_h + ratio_branch1 * inlet_h, - ratio_branch1 * inlet_h, - ratio_branch2 * inlet_h, - inlet_h * (1.0 + ratio_branch2), - rhs_branch1, - rhs_branch2, - ) - - def solve_both_reverse() -> tuple[float, float] | None: - return self._solve_linear_2x2( - inlet_h * (1.0 + ratio_branch1), - ratio_branch1 * inlet_h, - ratio_branch2 * inlet_h, - inlet_h * (1.0 + ratio_branch2), - rhs_branch1, - rhs_branch2, - ) - - candidate_solvers = ( - ( - solve_both_forward, - lambda q1, q2: q1 >= -tolerance and q2 >= -tolerance, - ), - ( - lambda: solve_one_reverse(branch1_reverse=True), - lambda q1, q2: q1 < -tolerance and q2 >= -tolerance and q1 + q2 > tolerance, - ), - ( - lambda: solve_one_reverse(branch1_reverse=True), - lambda q1, q2: q1 < -tolerance and q2 >= -tolerance and q1 + q2 <= tolerance, - ), - ( - lambda: solve_one_reverse(branch1_reverse=False), - lambda q1, q2: q2 < -tolerance and q1 >= -tolerance and q1 + q2 > tolerance, - ), - ( - lambda: solve_one_reverse(branch1_reverse=False), - lambda q1, q2: q2 < -tolerance and q1 >= -tolerance and q1 + q2 <= tolerance, - ), - ( - solve_both_reverse, - lambda q1, q2: q1 < -tolerance and q2 < -tolerance, - ), - ) - - for solver, predicate in candidate_solvers: - candidate = solver() - if candidate is None: - continue - q_out_branch1, q_out_branch2 = candidate - if predicate(q_out_branch1, q_out_branch2): - return q_out_branch1, q_out_branch2 - - return solve_both_forward() or (0.0, 0.0) diff --git a/PythonModels/core/__init__.py b/PythonModels/core/__init__.py deleted file mode 100644 index ae12d41..0000000 --- a/PythonModels/core/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -"""Core abstractions for the Python system model.""" - diff --git a/PythonModels/core/base.py b/PythonModels/core/base.py deleted file mode 100644 index bac666e..0000000 --- a/PythonModels/core/base.py +++ /dev/null @@ -1,48 +0,0 @@ -from __future__ import annotations - -from abc import ABC, abstractmethod - - -class Component(ABC): - def __init__(self, name: str) -> None: - self.name = name - - -class DynamicComponent(Component): - state_size = 2 - - @staticmethod - def actual_stream_enthalpy( - port_m_flow: float, - connected_h: float, - internal_h: float, - ) -> float: - """Approximate `actualStream(port.h_outflow)` for a mixed control volume port.""" - - return connected_h if port_m_flow > 0.0 else internal_h - - def connection_inlet_enthalpy( - self, - port_m_flow: float, - connected_h: float, - internal_h: float, - ) -> float: - """Resolve the enthalpy convected into this control volume through one port.""" - - return self.actual_stream_enthalpy( - port_m_flow=port_m_flow, - connected_h=connected_h, - internal_h=internal_h, - ) - - @abstractmethod - def get_state_vector(self) -> list[float]: - raise NotImplementedError - - @abstractmethod - def set_state_vector(self, values: list[float]) -> None: - raise NotImplementedError - - -class AlgebraicComponent(Component): - """Stateless element described by algebraic constraints only.""" diff --git a/PythonModels/core/medium.py b/PythonModels/core/medium.py deleted file mode 100644 index 3410590..0000000 --- a/PythonModels/core/medium.py +++ /dev/null @@ -1,96 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass - - -@dataclass(frozen=True) -class ThermodynamicProperties: - p: float - T: float - rho: float - u: float - h: float - - -@dataclass(frozen=True) -class IdealGasMedium: - """Temperature-dependent ideal-gas air approximation. - - This is still not a strict clone of `Modelica.Media.Air.SimpleAir`. - The small linear `cp(T)` term is kept configurable for calibration, but the - current default is calibrated against the committed Testmodel baseline and - therefore falls back to the constant-heat-capacity limit. - """ - - name: str = "SimpleAirApprox" - R_gas: float = 287.0 - cp_ref: float = 1005.0 - T_ref: float = 300.0 - cp_slope: float = 0.0 - - @property - def cv(self) -> float: - return self.cv_at_temperature(self.T_ref) - - @property - def gamma(self) -> float: - return self.cp_at_temperature(self.T_ref) / self.cv - - def cp_at_temperature(self, T: float) -> float: - return self.cp_ref + self.cp_slope * (T - self.T_ref) - - def cv_at_temperature(self, T: float) -> float: - return self.cp_at_temperature(T) - self.R_gas - - def density(self, p: float, T: float) -> float: - return p / (self.R_gas * T) - - def specific_internal_energy(self, T: float) -> float: - delta_T = T - self.T_ref - return ( - self.cv * self.T_ref - + self.cv * delta_T - + 0.5 * self.cp_slope * delta_T * delta_T - ) - - def specific_enthalpy(self, T: float) -> float: - delta_T = T - self.T_ref - return ( - self.cp_ref * self.T_ref - + self.cp_ref * delta_T - + 0.5 * self.cp_slope * delta_T * delta_T - ) - - def temperature_from_internal_energy(self, u: float) -> float: - reference_internal_energy = self.cv * self.T_ref - delta_u = u - reference_internal_energy - - if abs(self.cp_slope) <= 1e-15: - return self.T_ref + delta_u / self.cv - - a = 0.5 * self.cp_slope - b = self.cv - c = -delta_u - discriminant = max(b * b - 4.0 * a * c, 0.0) - positive_root = (-b + discriminant**0.5) / (2.0 * a) - negative_root = (-b - discriminant**0.5) / (2.0 * a) - delta_T = positive_root if abs(positive_root) <= abs(negative_root) else negative_root - return self.T_ref + delta_T - - def temperature_from_mass_internal_energy(self, m: float, U: float) -> float: - if m <= 0.0: - raise ValueError("Mass must stay positive when recovering temperature.") - return self.temperature_from_internal_energy(U / m) - - def pressure(self, m: float, T: float, V: float) -> float: - if V <= 0.0: - raise ValueError("Volume must stay positive.") - return m * self.R_gas * T / V - - def properties_from_mU(self, m: float, U: float, V: float) -> ThermodynamicProperties: - T = self.temperature_from_mass_internal_energy(m, U) - p = self.pressure(m, T, V) - rho = m / V - u = U / m - h = self.specific_enthalpy(T) - return ThermodynamicProperties(p=p, T=T, rho=rho, u=u, h=h) diff --git a/PythonModels/core/peng_robinson.py b/PythonModels/core/peng_robinson.py deleted file mode 100644 index f822189..0000000 --- a/PythonModels/core/peng_robinson.py +++ /dev/null @@ -1,237 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass -from math import acos, cos, isfinite, log, pi, sqrt - -UNIVERSAL_GAS_CONSTANT = 8.31446261815324 - - -@dataclass(frozen=True) -class PengRobinsonFluid: - """Pure-fluid Peng-Robinson equation-of-state helper. - - The class covers the equation-of-state layer plus the enthalpy departure - needed to compare AMESim pneumatic ``pn2hpti`` reference enthalpy flows. - """ - - name: str - molar_mass: float - critical_temperature: float - critical_pressure: float - acentric_factor: float - - @property - def specific_gas_constant(self) -> float: - return UNIVERSAL_GAS_CONSTANT / self.molar_mass - - @property - def a_parameter(self) -> float: - return ( - 0.45724 - * UNIVERSAL_GAS_CONSTANT - * UNIVERSAL_GAS_CONSTANT - * self.critical_temperature - * self.critical_temperature - / self.critical_pressure - ) - - @property - def b_parameter(self) -> float: - return 0.07780 * UNIVERSAL_GAS_CONSTANT * self.critical_temperature / self.critical_pressure - - @property - def kappa(self) -> float: - omega = self.acentric_factor - return 0.37464 + 1.54226 * omega - 0.26992 * omega * omega - - def alpha(self, temperature: float) -> float: - self._validate_temperature(temperature) - reduced_temperature = temperature / self.critical_temperature - return (1.0 + self.kappa * (1.0 - sqrt(reduced_temperature))) ** 2.0 - - def alpha_temperature_derivative(self, temperature: float) -> float: - self._validate_temperature(temperature) - reduced_temperature = temperature / self.critical_temperature - sqrt_reduced_temperature = sqrt(reduced_temperature) - alpha_base = 1.0 + self.kappa * (1.0 - sqrt_reduced_temperature) - return -( - alpha_base - * self.kappa - / (self.critical_temperature * sqrt_reduced_temperature) - ) - - def attractive_parameter(self, temperature: float) -> float: - return self.a_parameter * self.alpha(temperature) - - def attractive_parameter_temperature_derivative(self, temperature: float) -> float: - return self.a_parameter * self.alpha_temperature_derivative(temperature) - - def pressure_from_molar_volume(self, temperature: float, molar_volume: float) -> float: - self._validate_temperature(temperature) - if molar_volume <= self.b_parameter: - raise ValueError("Molar volume must be larger than Peng-Robinson b parameter.") - a_alpha = self.attractive_parameter(temperature) - b = self.b_parameter - repulsive = UNIVERSAL_GAS_CONSTANT * temperature / (molar_volume - b) - attractive = a_alpha / (molar_volume * (molar_volume + b) + b * (molar_volume - b)) - return repulsive - attractive - - def pressure_from_density(self, temperature: float, density: float) -> float: - if density <= 0.0: - raise ValueError("Density must be positive.") - return self.pressure_from_molar_volume(temperature, self.molar_mass / density) - - def reduced_parameters(self, pressure: float, temperature: float) -> tuple[float, float]: - self._validate_pressure_temperature(pressure, temperature) - a_alpha = self.attractive_parameter(temperature) - b = self.b_parameter - A = a_alpha * pressure / (UNIVERSAL_GAS_CONSTANT * UNIVERSAL_GAS_CONSTANT * temperature * temperature) - B = b * pressure / (UNIVERSAL_GAS_CONSTANT * temperature) - return A, B - - def compressibility_roots(self, pressure: float, temperature: float) -> tuple[float, ...]: - A, B = self.reduced_parameters(pressure, temperature) - coefficients = ( - -(1.0 - B), - A - 3.0 * B * B - 2.0 * B, - -(A * B - B * B - B * B * B), - ) - roots = _real_cubic_roots(*coefficients) - physical_roots = tuple(sorted(root for root in roots if root > B and isfinite(root))) - if not physical_roots: - raise ValueError("Peng-Robinson cubic produced no physical compressibility root.") - return physical_roots - - def compressibility_factor( - self, - pressure: float, - temperature: float, - phase: str = "vapor", - ) -> float: - roots = self.compressibility_roots(pressure, temperature) - if phase == "vapor": - return roots[-1] - if phase == "liquid": - return roots[0] - if phase == "stable-single-root": - return roots[-1] - raise ValueError(f"Unsupported phase selector: {phase!r}") - - def molar_volume( - self, - pressure: float, - temperature: float, - phase: str = "vapor", - ) -> float: - z = self.compressibility_factor(pressure, temperature, phase=phase) - return z * UNIVERSAL_GAS_CONSTANT * temperature / pressure - - def density( - self, - pressure: float, - temperature: float, - phase: str = "vapor", - ) -> float: - return self.molar_mass / self.molar_volume(pressure, temperature, phase=phase) - - def residual_specific_enthalpy( - self, - pressure: float, - temperature: float, - phase: str = "vapor", - ) -> float: - """Return Peng-Robinson enthalpy departure from ideal gas, J/kg.""" - self._validate_pressure_temperature(pressure, temperature) - z = self.compressibility_factor(pressure, temperature, phase=phase) - _, B = self.reduced_parameters(pressure, temperature) - b = self.b_parameter - attractive = self.attractive_parameter(temperature) - d_attractive_d_temperature = ( - self.attractive_parameter_temperature_derivative(temperature) - ) - log_argument = (z + (1.0 + sqrt(2.0)) * B) / ( - z + (1.0 - sqrt(2.0)) * B - ) - residual_molar_enthalpy = ( - UNIVERSAL_GAS_CONSTANT * temperature * (z - 1.0) - + ( - temperature * d_attractive_d_temperature - - attractive - ) - * log(log_argument) - / (2.0 * sqrt(2.0) * b) - ) - return residual_molar_enthalpy / self.molar_mass - - @staticmethod - def _validate_temperature(temperature: float) -> None: - if temperature <= 0.0: - raise ValueError("Temperature must be positive.") - - @classmethod - def _validate_pressure_temperature(cls, pressure: float, temperature: float) -> None: - if pressure <= 0.0: - raise ValueError("Pressure must be positive.") - cls._validate_temperature(temperature) - -HELIUM_PR = PengRobinsonFluid( - name="helium", - molar_mass=0.004002602, - critical_temperature=5.1953, - critical_pressure=227_460.0, - acentric_factor=-0.385, -) - -NITROGEN_PR = PengRobinsonFluid( - name="nitrogen", - molar_mass=0.0280134, - critical_temperature=126.192, - critical_pressure=3.3958e6, - acentric_factor=0.0372, -) - -AIR_PR = PengRobinsonFluid( - name="air", - molar_mass=0.02896513, - critical_temperature=132.5306, - critical_pressure=3.786e6, - acentric_factor=0.0335, -) - - -def _real_cubic_roots(a: float, b: float, c: float) -> tuple[float, ...]: - """Return real roots for x**3 + a*x**2 + b*x + c = 0.""" - - depressed_p = b - a * a / 3.0 - depressed_q = 2.0 * a * a * a / 27.0 - a * b / 3.0 + c - discriminant = (depressed_q / 2.0) ** 2.0 + (depressed_p / 3.0) ** 3.0 - offset = -a / 3.0 - tolerance = 1e-14 - - if discriminant > tolerance: - sqrt_discriminant = sqrt(discriminant) - u = _real_cube_root(-depressed_q / 2.0 + sqrt_discriminant) - v = _real_cube_root(-depressed_q / 2.0 - sqrt_discriminant) - return (u + v + offset,) - - if abs(discriminant) <= tolerance: - u = _real_cube_root(-depressed_q / 2.0) - return tuple(sorted({2.0 * u + offset, -u + offset})) - - if depressed_p >= 0.0: - raise ValueError("Unexpected cubic state with three real roots and non-negative p.") - radius = 2.0 * sqrt(-depressed_p / 3.0) - argument = (3.0 * depressed_q / (2.0 * depressed_p)) * sqrt(-3.0 / depressed_p) - argument = max(-1.0, min(1.0, argument)) - theta = acos(argument) / 3.0 - roots = [ - radius * cos(theta - 2.0 * pi * index / 3.0) + offset - for index in range(3) - ] - return tuple(sorted(roots)) - - -def _real_cube_root(value: float) -> float: - if value == 0.0: - return 0.0 - return (1.0 if value > 0.0 else -1.0) * abs(value) ** (1.0 / 3.0) diff --git a/PythonModels/core/ports.py b/PythonModels/core/ports.py deleted file mode 100644 index d340db2..0000000 --- a/PythonModels/core/ports.py +++ /dev/null @@ -1,13 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass - - -@dataclass -class PortState: - """Python-side analogue of a Modelica fluid port.""" - - p: float = 0.0 - m_flow: float = 0.0 - h_outflow: float = 0.0 - diff --git a/PythonModels/core/solver.py b/PythonModels/core/solver.py deleted file mode 100644 index 681886c..0000000 --- a/PythonModels/core/solver.py +++ /dev/null @@ -1,318 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass -from typing import Callable, Literal - - -CancellationCheck = Callable[[], bool] -AcceptedStepCallback = Callable[[float], None] -IntegrationStatus = Literal["completed", "cancelled", "failed"] - - -class _IntegrationCancelled(Exception): - pass - - -@dataclass(frozen=True) -class SolveIVPConfig: - t_start: float = 0.0 - t_stop: float = 20.0 - method: str = "BDF" - rtol: float = 1e-6 - atol: float = 1e-10 - max_step: float = 1e-3 - first_step: float | None = None - - -@dataclass(frozen=True) -class ODESolution: - t: list[float] - y: list[list[float]] - success: bool - message: str - status: IntegrationStatus = "completed" - error: Exception | None = None - - -def _vector_add(a: list[float], b: list[float], scale: float = 1.0) -> list[float]: - return [x + scale * y for x, y in zip(a, b)] - - -def _append_solution_sample( - times: list[float], - states: list[list[float]], - time: float, - state: list[float], -) -> None: - if times and time <= times[-1] + 1e-12: - return - times.append(float(time)) - for index, value in enumerate(state): - states[index].append(float(value)) - - -def _runge_kutta_4( - rhs: Callable[[float, list[float]], list[float]], - initial_state: list[float], - config: SolveIVPConfig, - t_eval: list[float] | None, - cancel_check: CancellationCheck | None = None, - accepted_step_callback: AcceptedStepCallback | None = None, -) -> ODESolution: - if t_eval is None: - point_count = max( - 2, - int((config.t_stop - config.t_start) / max(config.max_step, 1e-6)) + 1, - ) - step = (config.t_stop - config.t_start) / (point_count - 1) - t_eval = [config.t_start + index * step for index in range(point_count)] - - state = list(initial_state) - states = [[value] for value in state] - times = [float(t_eval[0])] - current_time = float(t_eval[0]) - status: IntegrationStatus = "completed" - message = "Integrated with built-in RK4 fallback because SciPy is unavailable." - error: Exception | None = None - - try: - for target_time in t_eval[1:]: - while current_time < target_time - 1e-15: - if cancel_check is not None and cancel_check(): - raise _IntegrationCancelled - dt = min(config.max_step, target_time - current_time) - k1 = rhs(current_time, state) - k2 = rhs(current_time + 0.5 * dt, _vector_add(state, k1, 0.5 * dt)) - k3 = rhs(current_time + 0.5 * dt, _vector_add(state, k2, 0.5 * dt)) - k4 = rhs(current_time + dt, _vector_add(state, k3, dt)) - state = [ - value + (dt / 6.0) * (a + 2.0 * b + 2.0 * c + d) - for value, a, b, c, d in zip(state, k1, k2, k3, k4) - ] - current_time += dt - if accepted_step_callback is not None: - accepted_step_callback(current_time) - - _append_solution_sample(times, states, target_time, state) - except _IntegrationCancelled: - status = "cancelled" - message = "Simulation was stopped before reaching the requested end time." - _append_solution_sample(times, states, current_time, state) - except Exception as exc: - status = "failed" - message = str(exc) - error = exc - _append_solution_sample(times, states, current_time, state) - - return ODESolution( - t=times, - y=states, - success=status == "completed", - message=message, - status=status, - error=error, - ) - - -def _integrate_scipy_stepwise( - rhs: Callable[[float, list[float]], list[float]], - initial_state: list[float], - config: SolveIVPConfig, - t_eval: list[float] | None, - cancel_check: CancellationCheck, - accepted_step_callback: AcceptedStepCallback | None, -) -> ODESolution: - import numpy as np - from scipy.integrate import BDF, DOP853, LSODA, RK23, RK45, Radau - - solver_types = { - "BDF": BDF, - "DOP853": DOP853, - "LSODA": LSODA, - "RK23": RK23, - "RK45": RK45, - "Radau": Radau, - } - solver_type = solver_types.get(config.method) - if solver_type is None: - raise ValueError(f"Unsupported integration method: {config.method}") - - times = [float(config.t_start)] - states = [[float(value)] for value in initial_state] - last_accepted_time = float(config.t_start) - last_accepted_state = [float(value) for value in initial_state] - sample_times = list(t_eval or []) - sample_index = 0 - while ( - sample_index < len(sample_times) - and sample_times[sample_index] <= config.t_start + 1e-12 - ): - sample_index += 1 - - def cancellable_rhs(time, state): - if cancel_check(): - raise _IntegrationCancelled - return rhs(float(time), [float(value) for value in state]) - - if cancel_check(): - return ODESolution( - t=times, - y=states, - success=False, - message="Simulation was stopped before integration started.", - status="cancelled", - ) - - solver_options = { - "rtol": config.rtol, - "atol": config.atol, - "max_step": config.max_step, - } - if config.first_step is not None: - solver_options["first_step"] = config.first_step - - try: - solver = solver_type( - cancellable_rhs, - config.t_start, - np.asarray(initial_state, dtype=float), - config.t_stop, - **solver_options, - ) - except _IntegrationCancelled: - return ODESolution( - t=times, - y=states, - success=False, - message="Simulation was stopped before integration started.", - status="cancelled", - ) - except Exception as exc: - return ODESolution( - t=times, - y=states, - success=False, - message=str(exc), - status="failed", - error=exc, - ) - - status: IntegrationStatus = "completed" - message = "The solver successfully reached the end of the integration interval." - error: Exception | None = None - - while solver.status == "running": - if cancel_check(): - status = "cancelled" - message = "Simulation was stopped before reaching the requested end time." - break - try: - step_message = solver.step() - except _IntegrationCancelled: - status = "cancelled" - message = "Simulation was stopped before reaching the requested end time." - break - except Exception as exc: - status = "failed" - message = str(exc) - error = exc - break - - if solver.status == "failed": - status = "failed" - message = str(step_message or "Integration step failed.") - break - - last_accepted_time = float(solver.t) - last_accepted_state = [float(value) for value in solver.y] - if sample_times: - dense_output = solver.dense_output() - while ( - sample_index < len(sample_times) - and sample_times[sample_index] <= last_accepted_time + 1e-12 - ): - sample_time = float(sample_times[sample_index]) - sample_state = [float(value) for value in dense_output(sample_time)] - _append_solution_sample(times, states, sample_time, sample_state) - sample_index += 1 - else: - _append_solution_sample( - times, - states, - last_accepted_time, - last_accepted_state, - ) - if accepted_step_callback is not None: - accepted_step_callback(last_accepted_time) - - if status != "completed": - _append_solution_sample( - times, - states, - last_accepted_time, - last_accepted_state, - ) - - return ODESolution( - t=times, - y=states, - success=status == "completed", - message=message, - status=status, - error=error, - ) - - -def integrate_ode( - rhs: Callable[[float, list[float]], list[float]], - initial_state: list[float], - config: SolveIVPConfig, - t_eval: list[float] | None = None, - cancel_check: CancellationCheck | None = None, - accepted_step_callback: AcceptedStepCallback | None = None, -): - """Thin wrapper around scipy.integrate.solve_ivp with a pure-Python fallback.""" - - if abs(config.t_stop - config.t_start) <= 1e-15: - return ODESolution( - t=[float(config.t_start)], - y=[[value] for value in initial_state], - success=True, - message="Skipped integration because t_start equals t_stop.", - ) - - try: - from scipy.integrate import solve_ivp - except ImportError: - return _runge_kutta_4( - rhs, - initial_state, - config, - t_eval, - cancel_check, - accepted_step_callback, - ) - - if cancel_check is not None: - return _integrate_scipy_stepwise( - rhs, - initial_state, - config, - t_eval, - cancel_check, - accepted_step_callback, - ) - - solve_options = { - "fun": rhs, - "t_span": (config.t_start, config.t_stop), - "y0": initial_state, - "method": config.method, - "rtol": config.rtol, - "atol": config.atol, - "max_step": config.max_step, - "t_eval": t_eval, - } - if config.first_step is not None: - solve_options["first_step"] = config.first_step - return solve_ivp(**solve_options) diff --git a/PythonModels/core/state.py b/PythonModels/core/state.py deleted file mode 100644 index e4cf3b3..0000000 --- a/PythonModels/core/state.py +++ /dev/null @@ -1,21 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass - - -@dataclass -class VolumeState: - """Primary dynamic state for rigid adiabatic control volumes.""" - - m: float - U: float - - def as_vector(self) -> list[float]: - return [self.m, self.U] - - @classmethod - def from_vector(cls, values: list[float]) -> "VolumeState": - if len(values) != 2: - raise ValueError("VolumeState requires exactly two values: [m, U].") - return cls(m=values[0], U=values[1]) - diff --git a/PythonModels/reporting/__init__.py b/PythonModels/reporting/__init__.py deleted file mode 100644 index 9ca6880..0000000 --- a/PythonModels/reporting/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -from PythonModels.reporting.testmodel_outputs import ( - COMPARISON_KEYS, - MODELICA_COMPARISON_COLUMNS, - PRIMARY_KEYS, - TestModelArtifacts, - export_testmodel_artifacts, - format_testmodel_run_report, - load_modelica_series, - write_testmodel_run_report, - write_modelica_comparison, -) - -__all__ = [ - "COMPARISON_KEYS", - "MODELICA_COMPARISON_COLUMNS", - "PRIMARY_KEYS", - "TestModelArtifacts", - "export_testmodel_artifacts", - "format_testmodel_run_report", - "load_modelica_series", - "write_testmodel_run_report", - "write_modelica_comparison", -] diff --git a/PythonModels/reporting/amesim_results.py b/PythonModels/reporting/amesim_results.py deleted file mode 100644 index 449b839..0000000 --- a/PythonModels/reporting/amesim_results.py +++ /dev/null @@ -1,263 +0,0 @@ -from __future__ import annotations - -import re -import struct -import tarfile -from dataclasses import dataclass -from pathlib import Path - - -class AmesimResultsError(ValueError): - """Raised when AMESim result files cannot be parsed consistently.""" - - -@dataclass(frozen=True) -class AmesimVariable: - index: int - label: str - data_path: str | None - param_id: int | None - hidden: bool - - -@dataclass(frozen=True) -class AmesimResults: - times: tuple[float, ...] - variables: tuple[AmesimVariable, ...] - saved_variable_indices: tuple[int, ...] - series_by_data_path: dict[str, tuple[float, ...]] - final_values_by_data_path: dict[str, float] - - @property - def point_count(self) -> int: - return len(self.times) - - @property - def saved_variable_count(self) -> int: - return len(self.saved_variable_indices) - - def series(self, data_path: str) -> tuple[float, ...]: - return self.series_by_data_path[data_path] - - def final_value(self, data_path: str) -> float: - return self.final_values_by_data_path[data_path] - - -_DATA_PATH_RE = re.compile(r"Data_Path=(\S+)") -_PARAM_ID_RE = re.compile(r"Param_Id=(\d+)") - - -def load_test_mql_amesim_results( - archive_path: str | Path, - *, - time_stop_s: float | None = None, -) -> AmesimResults: - return load_amesim_results_from_archive( - archive_path=archive_path, - var_member=None, - results_member=None, - time_stop_s=time_stop_s, - ) - - -def load_amesim_results_from_archive( - *, - archive_path: str | Path, - var_member: str | None, - results_member: str | None, - time_stop_s: float | None = None, -) -> AmesimResults: - with tarfile.open(archive_path) as archive: - var_member, results_member = _resolve_result_members( - archive, - var_member=var_member, - results_member=results_member, - ) - var_file = archive.extractfile(var_member) - results_file = archive.extractfile(results_member) - if var_file is None: - raise AmesimResultsError(f"Missing AMESim variable member: {var_member}") - if results_file is None: - raise AmesimResultsError(f"Missing AMESim results member: {results_member}") - var_lines = var_file.read().decode("latin1").splitlines() - variables = tuple( - _parse_variable_line(index, line) for index, line in enumerate(var_lines) - ) - if time_stop_s is not None: - return _parse_amesim_results_window( - results_file, - variables, - time_stop_s=time_stop_s, - ) - results_data = results_file.read() - return parse_amesim_results_bytes(results_data, variables) - - -def _resolve_result_members( - archive: tarfile.TarFile, - *, - var_member: str | None, - results_member: str | None, -) -> tuple[str, str]: - member_names = set(archive.getnames()) - if var_member is not None or results_member is not None: - if var_member is None or results_member is None: - raise AmesimResultsError( - "var_member and results_member must either both be set or both be omitted." - ) - return var_member, results_member - - preferred = ("test_mql_.var", "test_mql_.results") - if preferred[0] in member_names and preferred[1] in member_names: - return preferred - - pairs = sorted( - (name, f"{name[:-4]}.results") - for name in member_names - if name.endswith(".var") and f"{name[:-4]}.results" in member_names - ) - if len(pairs) != 1: - raise AmesimResultsError( - "Unable to identify a unique AMESim .var/.results member pair." - ) - return pairs[0] - - -def _parse_amesim_results_window( - results_file, - variables: tuple[AmesimVariable, ...], - *, - time_stop_s: float, -) -> AmesimResults: - header = results_file.read(8) - if len(header) < 8: - raise AmesimResultsError("AMESim results data is too small.") - point_count, encoded_saved_variable_count = struct.unpack("<2i", header) - saved_variable_count = abs(encoded_saved_variable_count) - if point_count <= 0 or saved_variable_count <= 0: - raise AmesimResultsError("Invalid AMESim results header.") - - mapping_data = results_file.read(saved_variable_count * 4) - if len(mapping_data) != saved_variable_count * 4: - raise AmesimResultsError("AMESim results variable mapping is truncated.") - saved_variable_indices = struct.unpack( - f"<{saved_variable_count}i", - mapping_data, - ) - if any(index < 0 or index >= len(variables) for index in saved_variable_indices): - raise AmesimResultsError( - "AMESim results variable mapping references unknown .var rows." - ) - - row_length = 1 + saved_variable_count - row_byte_count = row_length * 8 - times: list[float] = [] - series_lists: dict[str, list[float]] = {} - saved_paths: list[tuple[int, str]] = [] - for column, variable_index in enumerate(saved_variable_indices, start=1): - data_path = variables[variable_index].data_path - if data_path is None: - continue - series_lists[data_path] = [] - saved_paths.append((column, data_path)) - - for _row_index in range(point_count): - row = results_file.read(row_byte_count) - if len(row) != row_byte_count: - raise AmesimResultsError("AMESim results matrix is truncated.") - time_s = struct.unpack_from(" time_stop_s + 1.0e-12: - break - - return AmesimResults( - times=tuple(times), - variables=variables, - saved_variable_indices=tuple(saved_variable_indices), - series_by_data_path={ - data_path: tuple(values) for data_path, values in series_lists.items() - }, - final_values_by_data_path={}, - ) - - -def parse_amesim_results_bytes( - results_data: bytes, - variables: tuple[AmesimVariable, ...], -) -> AmesimResults: - if len(results_data) < 8: - raise AmesimResultsError("AMESim results data is too small.") - point_count, encoded_saved_variable_count = struct.unpack_from("<2i", results_data, 0) - saved_variable_count = abs(encoded_saved_variable_count) - if point_count <= 0 or saved_variable_count <= 0: - raise AmesimResultsError("Invalid AMESim results header.") - - mapping_offset = 8 - mapping_size = saved_variable_count * 4 - data_offset = mapping_offset + mapping_size - saved_variable_indices = struct.unpack_from( - f"<{saved_variable_count}i", - results_data, - mapping_offset, - ) - if any(index < 0 or index >= len(variables) for index in saved_variable_indices): - raise AmesimResultsError( - "AMESim results variable mapping references unknown .var rows." - ) - - row_length = 1 + saved_variable_count - main_value_count = point_count * row_length - main_byte_count = main_value_count * 8 - main_end = data_offset + main_byte_count - if main_end > len(results_data): - raise AmesimResultsError("AMESim results matrix is truncated.") - - main_values = struct.unpack_from(f"<{main_value_count}d", results_data, data_offset) - times = tuple(main_values[row * row_length] for row in range(point_count)) - series_by_data_path: dict[str, tuple[float, ...]] = {} - for column, variable_index in enumerate(saved_variable_indices, start=1): - variable = variables[variable_index] - if variable.data_path is None: - continue - series_by_data_path[variable.data_path] = tuple( - main_values[row * row_length + column] - for row in range(point_count) - ) - - final_values_by_data_path: dict[str, float] = {} - trailing_bytes = len(results_data) - main_end - expected_final_bytes = (1 + len(variables)) * 8 - if trailing_bytes >= expected_final_bytes: - final_values = struct.unpack_from(f"<{1 + len(variables)}d", results_data, main_end) - for variable, value in zip(variables, final_values[1:]): - if variable.data_path is not None: - final_values_by_data_path[variable.data_path] = value - - return AmesimResults( - times=times, - variables=variables, - saved_variable_indices=tuple(saved_variable_indices), - series_by_data_path=series_by_data_path, - final_values_by_data_path=final_values_by_data_path, - ) - - -def _parse_variable_line(index: int, line: str) -> AmesimVariable: - data_path_match = _DATA_PATH_RE.search(line) - param_id_match = _PARAM_ID_RE.search(line) - label = line - if data_path_match is not None: - label = line[: data_path_match.start()].strip() - return AmesimVariable( - index=index, - label=label, - data_path=data_path_match.group(1) if data_path_match else None, - param_id=int(param_id_match.group(1)) if param_id_match else None, - hidden="HIDDEN" in line, - ) diff --git a/PythonModels/reporting/test_mql_chamber_observations.py b/PythonModels/reporting/test_mql_chamber_observations.py deleted file mode 100644 index fd60209..0000000 --- a/PythonModels/reporting/test_mql_chamber_observations.py +++ /dev/null @@ -1,195 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass - -from PythonModels.reporting.amesim_results import AmesimResults -from PythonModels.reporting.test_mql_variables import ( - TestMqlVariableBinding, - TestMqlVariableCatalog, - build_test_mql_variable_catalog, -) -from PythonModels.systems.test_mql_pneumatic import ( - TestMqlPneumaticAssembly, - build_test_mql_pneumatic_assembly, -) - - -@dataclass(frozen=True) -class TestMqlChamberObservation: - time: float - pressure_pa: float - temperature_k: float - gas_mass_g: float - volume_cm3: float | None - - -@dataclass(frozen=True) -class TestMqlChamberBinding: - alias: str - submodel: str - pressure_path: str - temperature_path: str - gas_mass_path: str - pressure_duplicate_paths: tuple[str, ...] - temperature_duplicate_paths: tuple[str, ...] - volume_path: str | None - - @property - def is_variable(self) -> bool: - return self.volume_path is not None - - def observation_at(self, results: AmesimResults, index: int) -> TestMqlChamberObservation: - return TestMqlChamberObservation( - time=results.times[index], - pressure_pa=results.series(self.pressure_path)[index], - temperature_k=results.series(self.temperature_path)[index], - gas_mass_g=results.series(self.gas_mass_path)[index], - volume_cm3=( - results.series(self.volume_path)[index] - if self.volume_path is not None - else None - ), - ) - - -@dataclass(frozen=True) -class TestMqlChamberObservationCatalog: - bindings: tuple[TestMqlChamberBinding, ...] - - @property - def fixed_count(self) -> int: - return sum(1 for binding in self.bindings if binding.submodel == "PNCH023") - - @property - def variable_count(self) -> int: - return sum(1 for binding in self.bindings if binding.submodel == "PNCH012") - - def by_alias(self, alias: str) -> TestMqlChamberBinding: - for binding in self.bindings: - if binding.alias == alias: - return binding - raise KeyError(alias) - - -def build_test_mql_chamber_observation_catalog( - results: AmesimResults, - *, - variable_catalog: TestMqlVariableCatalog | None = None, - assembly: TestMqlPneumaticAssembly | None = None, -) -> TestMqlChamberObservationCatalog: - variable_catalog = variable_catalog or build_test_mql_variable_catalog(results) - assembly = assembly or build_test_mql_pneumatic_assembly() - chamber_aliases = { - **{alias: "PNCH023" for alias in assembly.fixed_chambers}, - **{alias: "PNCH012" for alias in assembly.variable_chambers}, - } - bindings = [] - for alias, submodel in chamber_aliases.items(): - variables = tuple( - variable - for variable in variable_catalog.variables - if variable.owner_alias == alias - ) - pressure = _primary_observable(variables, "press", expected_units="Pa") - temperature = _primary_observable(variables, "temp", expected_units="K") - gas_mass = _required_path( - variables, - "mgas1" if submodel == "PNCH012" else "mgas", - expected_units="g", - ) - volume = _optional_path(variables, "vol", expected_units="cm**3") - bindings.append( - TestMqlChamberBinding( - alias=alias, - submodel=submodel, - pressure_path=pressure.data_path, - temperature_path=temperature.data_path, - gas_mass_path=gas_mass, - pressure_duplicate_paths=_duplicate_paths(variables, "press", expected_units="Pa"), - temperature_duplicate_paths=_duplicate_paths(variables, "temp", expected_units="K"), - volume_path=volume, - ) - ) - return TestMqlChamberObservationCatalog( - bindings=tuple(sorted(bindings, key=lambda binding: binding.alias)) - ) - - -def _primary_observable( - variables: tuple[TestMqlVariableBinding, ...], - signal_prefix: str, - *, - expected_units: str, -) -> TestMqlVariableBinding: - matches = tuple( - variable - for variable in variables - if variable.signal_name == signal_prefix - and "duplicate" not in variable.label - ) - variable = _single(matches, f"primary {signal_prefix}") - _assert_units(variable, expected_units) - return variable - - -def _duplicate_paths( - variables: tuple[TestMqlVariableBinding, ...], - signal_prefix: str, - *, - expected_units: str, -) -> tuple[str, ...]: - matches = tuple( - variable - for variable in variables - if variable.signal_name.startswith(signal_prefix) - and variable.signal_name != signal_prefix - and "duplicate" in variable.label - ) - for variable in matches: - _assert_units(variable, expected_units) - return tuple(variable.data_path for variable in matches) - - -def _required_path( - variables: tuple[TestMqlVariableBinding, ...], - signal_name: str, - *, - expected_units: str, -) -> str: - variable = _single( - tuple(variable for variable in variables if variable.signal_name == signal_name), - signal_name, - ) - _assert_units(variable, expected_units) - return variable.data_path - - -def _optional_path( - variables: tuple[TestMqlVariableBinding, ...], - signal_name: str, - *, - expected_units: str, -) -> str | None: - matches = tuple(variable for variable in variables if variable.signal_name == signal_name) - if not matches: - return None - variable = _single(matches, signal_name) - _assert_units(variable, expected_units) - return variable.data_path - - -def _single( - matches: tuple[TestMqlVariableBinding, ...], - description: str, -) -> TestMqlVariableBinding: - if len(matches) != 1: - raise ValueError(f"Expected one {description} variable, found {len(matches)}.") - return matches[0] - - -def _assert_units(variable: TestMqlVariableBinding, expected_units: str) -> None: - if variable.units != expected_units: - raise ValueError( - f"Unexpected units for {variable.data_path}: " - f"{variable.units!r}, expected {expected_units!r}." - ) diff --git a/PythonModels/reporting/test_mql_comparison.py b/PythonModels/reporting/test_mql_comparison.py deleted file mode 100644 index 3d65cf2..0000000 --- a/PythonModels/reporting/test_mql_comparison.py +++ /dev/null @@ -1,237 +0,0 @@ -from __future__ import annotations - -from bisect import bisect_left -import csv -from dataclasses import dataclass -from pathlib import Path - -from PythonModels.reporting.amesim_results import AmesimResults - - -DEFAULT_TEST_MQL_ALIGNMENT_PATHS = ( - "temp3@pn_c1_8", - "press3@pn_c1_8", - "vvol1@pn_brp2_8", - "vol1@pn_brp2_8", -) - - -@dataclass(frozen=True) -class TestMqlComparisonMetric: - data_path: str - sample_count: int - max_abs_error: float - mean_abs_error: float - max_rel_error: float - final_abs_error: float - - -@dataclass(frozen=True) -class TestMqlComparisonResult: - metrics: tuple[TestMqlComparisonMetric, ...] - - def metric(self, data_path: str) -> TestMqlComparisonMetric: - for metric in self.metrics: - if metric.data_path == data_path: - return metric - raise KeyError(data_path) - - @property - def max_abs_error(self) -> float: - return max((metric.max_abs_error for metric in self.metrics), default=0.0) - - @property - def max_rel_error(self) -> float: - return max((metric.max_rel_error for metric in self.metrics), default=0.0) - - -class TestMqlComparisonError(ValueError): - """Raised when Python and AMESim series cannot be aligned.""" - - -def compare_test_mql_series( - *, - python_times: tuple[float, ...] | list[float], - python_series_by_data_path: dict[str, tuple[float, ...] | list[float]], - amesim_results: AmesimResults, - data_paths: tuple[str, ...] | list[str] | None = None, - relative_floor: float = 1.0e-12, -) -> TestMqlComparisonResult: - _validate_time_axis(python_times) - selected_paths = _select_data_paths(python_series_by_data_path, amesim_results, data_paths) - metrics = [] - for data_path in selected_paths: - python_values = tuple(float(value) for value in python_series_by_data_path[data_path]) - if len(python_values) != len(python_times): - raise TestMqlComparisonError( - f"Python series length mismatch for {data_path!r}: " - f"{len(python_values)} values for {len(python_times)} time samples." - ) - amesim_values = amesim_results.series(data_path) - abs_errors = [] - rel_errors = [] - for time_value, python_value in zip(python_times, python_values): - amesim_value = interpolate_series_value(amesim_results.times, amesim_values, time_value) - abs_error = abs(python_value - amesim_value) - abs_errors.append(abs_error) - rel_errors.append(abs_error / max(abs(amesim_value), relative_floor)) - final_amesim_value = interpolate_series_value( - amesim_results.times, - amesim_values, - float(python_times[-1]), - ) - metrics.append( - TestMqlComparisonMetric( - data_path=data_path, - sample_count=len(python_times), - max_abs_error=max(abs_errors, default=0.0), - mean_abs_error=sum(abs_errors) / max(len(abs_errors), 1), - max_rel_error=max(rel_errors, default=0.0), - final_abs_error=abs(python_values[-1] - final_amesim_value), - ) - ) - return TestMqlComparisonResult(metrics=tuple(metrics)) - - -def write_test_mql_amesim_baseline_csv( - output_dir: Path, - amesim_results: AmesimResults, - data_paths: tuple[str, ...] | list[str] = DEFAULT_TEST_MQL_ALIGNMENT_PATHS, -) -> Path: - output_dir.mkdir(parents=True, exist_ok=True) - csv_path = output_dir / "test_mql_amesim_baseline.csv" - _validate_amesim_data_paths(amesim_results, data_paths) - with csv_path.open("w", newline="", encoding="utf-8") as handle: - writer = csv.writer(handle) - writer.writerow(["time_s", *data_paths]) - for index, time_value in enumerate(amesim_results.times): - writer.writerow( - [time_value, *(amesim_results.series(data_path)[index] for data_path in data_paths)] - ) - return csv_path - - -def write_test_mql_comparison_csv( - *, - output_dir: Path, - python_times: tuple[float, ...] | list[float], - python_series_by_data_path: dict[str, tuple[float, ...] | list[float]], - amesim_results: AmesimResults, - data_paths: tuple[str, ...] | list[str] | None = None, -) -> tuple[Path, Path, TestMqlComparisonResult]: - output_dir.mkdir(parents=True, exist_ok=True) - selected_paths = _select_data_paths(python_series_by_data_path, amesim_results, data_paths) - comparison = compare_test_mql_series( - python_times=python_times, - python_series_by_data_path=python_series_by_data_path, - amesim_results=amesim_results, - data_paths=selected_paths, - ) - csv_path = output_dir / "test_mql_amesim_comparison.csv" - summary_path = output_dir / "test_mql_amesim_comparison_summary.txt" - - with csv_path.open("w", newline="", encoding="utf-8") as handle: - writer = csv.writer(handle) - header = ["time_s"] - for data_path in selected_paths: - header.extend( - [ - f"python.{data_path}", - f"amesim.{data_path}", - f"abs_error.{data_path}", - f"rel_error.{data_path}", - ] - ) - writer.writerow(header) - for index, time_value in enumerate(python_times): - row = [time_value] - for data_path in selected_paths: - python_value = float(python_series_by_data_path[data_path][index]) - amesim_value = interpolate_series_value( - amesim_results.times, - amesim_results.series(data_path), - float(time_value), - ) - abs_error = abs(python_value - amesim_value) - rel_error = abs_error / max(abs(amesim_value), 1.0e-12) - row.extend([python_value, amesim_value, abs_error, rel_error]) - writer.writerow(row) - - summary_lines = [ - ( - f"{metric.data_path}: samples={metric.sample_count}, " - f"max_abs_error={metric.max_abs_error:.12g}, " - f"mean_abs_error={metric.mean_abs_error:.12g}, " - f"max_rel_error={metric.max_rel_error:.12%}, " - f"final_abs_error={metric.final_abs_error:.12g}" - ) - for metric in comparison.metrics - ] - summary_path.write_text("\n".join(summary_lines) + "\n", encoding="utf-8") - return csv_path, summary_path, comparison - - -def interpolate_series_value( - time_values: tuple[float, ...] | list[float], - values: tuple[float, ...] | list[float], - target_time: float, -) -> float: - if len(time_values) != len(values): - raise TestMqlComparisonError("time and value series lengths differ.") - if not time_values: - raise TestMqlComparisonError("cannot interpolate an empty series.") - if target_time <= time_values[0]: - return float(values[0]) - if target_time >= time_values[-1]: - return float(values[-1]) - - right_index = bisect_left(time_values, target_time) - if right_index < len(time_values) and abs(time_values[right_index] - target_time) <= 1.0e-12: - return float(values[right_index]) - - left_index = right_index - 1 - left_time = float(time_values[left_index]) - right_time = float(time_values[right_index]) - fraction = (target_time - left_time) / (right_time - left_time) - return float(values[left_index]) + fraction * (float(values[right_index]) - float(values[left_index])) - - -def _select_data_paths( - python_series_by_data_path: dict[str, tuple[float, ...] | list[float]], - amesim_results: AmesimResults, - data_paths: tuple[str, ...] | list[str] | None, -) -> tuple[str, ...]: - if data_paths is None: - data_paths = tuple( - data_path - for data_path in python_series_by_data_path - if data_path in amesim_results.series_by_data_path - ) - selected_paths = tuple(data_paths) - if not selected_paths: - raise TestMqlComparisonError("no common Data_Path values are available for comparison.") - missing_python = [data_path for data_path in selected_paths if data_path not in python_series_by_data_path] - if missing_python: - raise TestMqlComparisonError(f"Python series missing Data_Path values: {missing_python}") - _validate_amesim_data_paths(amesim_results, selected_paths) - return selected_paths - - -def _validate_amesim_data_paths( - amesim_results: AmesimResults, - data_paths: tuple[str, ...] | list[str], -) -> None: - missing_amesim = [data_path for data_path in data_paths if data_path not in amesim_results.series_by_data_path] - if missing_amesim: - raise TestMqlComparisonError(f"AMESim results missing Data_Path values: {missing_amesim}") - - -def _validate_time_axis(time_values: tuple[float, ...] | list[float]) -> None: - if not time_values: - raise TestMqlComparisonError("Python time axis is empty.") - previous = float(time_values[0]) - for value in time_values[1:]: - value = float(value) - if value < previous: - raise TestMqlComparisonError("Python time axis must be monotonically increasing.") - previous = value diff --git a/PythonModels/reporting/test_mql_line_observations.py b/PythonModels/reporting/test_mql_line_observations.py deleted file mode 100644 index 0a85c8a..0000000 --- a/PythonModels/reporting/test_mql_line_observations.py +++ /dev/null @@ -1,211 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass - -from PythonModels.reporting.amesim_results import AmesimResults -from PythonModels.reporting.test_mql_variables import ( - TestMqlVariableBinding, - TestMqlVariableCatalog, - build_test_mql_variable_catalog, -) -from PythonModels.systems.test_mql_lines import ( - TestMqlLineAssembly, - build_test_mql_line_assembly, -) - - -G_PER_S_TO_KG_PER_S = 1.0e-3 - - -@dataclass(frozen=True) -class TestMqlLineObservation: - time: float - mass_flows_kg_s: dict[str, float] - enthalpy_flows_w: dict[str, float] - pressures_pa: dict[str, float] - temperatures_k: dict[str, float] - gas_mass_g: float | None - reynolds_number: float - mass_flow_parameter: float - gas_velocity_m_s: float - friction_factor: float - - -@dataclass(frozen=True) -class TestMqlLineObservationBinding: - alias: str - submodel: str - pattern: str - mass_flow_paths: tuple[str, ...] - enthalpy_flow_paths: tuple[str, ...] - pressure_paths: tuple[str, ...] - temperature_paths: tuple[str, ...] - gas_mass_path: str | None - reynolds_path: str - mass_flow_parameter_path: str - gas_velocity_path: str - friction_factor_path: str - - def mass_flow_kg_s_series( - self, - results: AmesimResults, - data_path: str | None = None, - ) -> tuple[float, ...]: - path = data_path or self.mass_flow_paths[0] - if path not in self.mass_flow_paths: - raise KeyError(path) - return tuple(value * G_PER_S_TO_KG_PER_S for value in results.series(path)) - - def observation_at(self, results: AmesimResults, index: int) -> TestMqlLineObservation: - return TestMqlLineObservation( - time=results.times[index], - mass_flows_kg_s={ - path: results.series(path)[index] * G_PER_S_TO_KG_PER_S - for path in self.mass_flow_paths - }, - enthalpy_flows_w={ - path: results.series(path)[index] - for path in self.enthalpy_flow_paths - }, - pressures_pa={ - path: results.series(path)[index] - for path in self.pressure_paths - }, - temperatures_k={ - path: results.series(path)[index] - for path in self.temperature_paths - }, - gas_mass_g=( - results.series(self.gas_mass_path)[index] - if self.gas_mass_path is not None - else None - ), - reynolds_number=results.series(self.reynolds_path)[index], - mass_flow_parameter=results.series(self.mass_flow_parameter_path)[index], - gas_velocity_m_s=results.series(self.gas_velocity_path)[index], - friction_factor=results.series(self.friction_factor_path)[index], - ) - - -@dataclass(frozen=True) -class TestMqlLineObservationCatalog: - bindings: tuple[TestMqlLineObservationBinding, ...] - - @property - def line_count(self) -> int: - return len(self.bindings) - - def by_alias(self, alias: str) -> TestMqlLineObservationBinding: - for binding in self.bindings: - if binding.alias == alias: - return binding - raise KeyError(alias) - - def by_submodel(self, submodel: str) -> tuple[TestMqlLineObservationBinding, ...]: - return tuple(binding for binding in self.bindings if binding.submodel == submodel) - - -def build_test_mql_line_observation_catalog( - results: AmesimResults, - *, - variable_catalog: TestMqlVariableCatalog | None = None, - line_assembly: TestMqlLineAssembly | None = None, -) -> TestMqlLineObservationCatalog: - variable_catalog = variable_catalog or build_test_mql_variable_catalog(results) - line_assembly = line_assembly or build_test_mql_line_assembly(results, variable_catalog) - bindings = [] - for line in line_assembly.lines: - variables = tuple( - variable - for variable in variable_catalog.variables - if variable.owner_alias == line.alias - ) - bindings.append( - TestMqlLineObservationBinding( - alias=line.alias, - submodel=line.submodel, - pattern=line.pattern, - mass_flow_paths=_paths_with_prefix(variables, "dm", expected_units="g/s"), - enthalpy_flow_paths=_paths_with_prefix(variables, "dh", expected_units="J/s"), - pressure_paths=_paths_with_prefix(variables, "p", expected_units="Pa"), - temperature_paths=_paths_with_prefix(variables, "t", expected_units="K"), - gas_mass_path=_optional_path(variables, "mgas", expected_units="g"), - reynolds_path=_required_path(variables, "re", expected_units=None), - mass_flow_parameter_path=_required_path( - variables, - "cm", - expected_units="(kg*K/J)**(1/2)", - ), - gas_velocity_path=_required_path(variables, "v", expected_units="m/s"), - friction_factor_path=_required_path(variables, "ff", expected_units=None), - ) - ) - return TestMqlLineObservationCatalog(bindings=tuple(bindings)) - - -def _paths_with_prefix( - variables: tuple[TestMqlVariableBinding, ...], - prefix: str, - *, - expected_units: str | None, -) -> tuple[str, ...]: - matches = tuple( - variable - for variable in variables - if variable.signal_name.startswith(prefix) - ) - for variable in matches: - _assert_units(variable, expected_units) - return tuple(variable.data_path for variable in matches) - - -def _required_path( - variables: tuple[TestMqlVariableBinding, ...], - signal_name: str, - *, - expected_units: str | None, -) -> str: - variable = _single_signal(variables, signal_name) - _assert_units(variable, expected_units) - return variable.data_path - - -def _optional_path( - variables: tuple[TestMqlVariableBinding, ...], - signal_name: str, - *, - expected_units: str | None, -) -> str | None: - matches = tuple(variable for variable in variables if variable.signal_name == signal_name) - if not matches: - return None - variable = _single(matches, signal_name) - _assert_units(variable, expected_units) - return variable.data_path - - -def _single_signal( - variables: tuple[TestMqlVariableBinding, ...], - signal_name: str, -) -> TestMqlVariableBinding: - return _single( - tuple(variable for variable in variables if variable.signal_name == signal_name), - signal_name, - ) - - -def _single( - matches: tuple[TestMqlVariableBinding, ...], - description: str, -) -> TestMqlVariableBinding: - if len(matches) != 1: - raise ValueError(f"Expected one {description} variable, found {len(matches)}.") - return matches[0] - - -def _assert_units(variable: TestMqlVariableBinding, expected_units: str | None) -> None: - if variable.units != expected_units: - raise ValueError( - f"Unexpected units for {variable.data_path}: " - f"{variable.units!r}, expected {expected_units!r}." - ) diff --git a/PythonModels/reporting/test_mql_mechanical_observations.py b/PythonModels/reporting/test_mql_mechanical_observations.py deleted file mode 100644 index 645be29..0000000 --- a/PythonModels/reporting/test_mql_mechanical_observations.py +++ /dev/null @@ -1,395 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass - -from PythonModels.reporting.amesim_results import AmesimResults -from PythonModels.reporting.test_mql_variables import ( - TestMqlVariableBinding, - TestMqlVariableCatalog, - build_test_mql_variable_catalog, -) -from PythonModels.systems.test_mql_mechanical import ( - TestMqlMechanicalAssembly, - build_test_mql_mechanical_assembly, -) - - -@dataclass(frozen=True) -class TestMqlPistonObservation: - time: float - chamber_volume_cm3: float - chamber_volume_rate_l_min: float - chamber_length_mm: float - force_port_2_n: float - force_port_3_n: float - displacement_port_2_m: float - velocity_port_2_m_s: float - displacement_port_3_m: float - velocity_port_3_m_s: float - - -@dataclass(frozen=True) -class TestMqlMassEndstopObservation: - time: float - displacement_m: float - velocity_m_s: float - acceleration_m_s2: float - lower_contact_force_n: float - upper_contact_force_n: float - viscous_friction_force_n: float - dry_friction_force_n: float - stick_flag: float - - -@dataclass(frozen=True) -class TestMqlElasticEndstopObservation: - time: float - force_n: float - duplicate_force_n: float - gap_mm: float - stiffness_n_m: float - - -@dataclass(frozen=True) -class TestMqlForceSourceObservation: - time: float - force_n: float - - -@dataclass(frozen=True) -class TestMqlForceConnectorObservation: - time: float - force_n: float - - -@dataclass(frozen=True) -class TestMqlMechanicalNodeObservation: - time: float - velocities_m_s: dict[int, float] - displacements_m: dict[int, float] - total_force_n: float - - -@dataclass(frozen=True) -class TestMqlPistonObservationBinding: - alias: str - volume_path: str - volume_rate_path: str - length_path: str - force_port_2_path: str - force_port_3_path: str - displacement_port_2_path: str - velocity_port_2_path: str - displacement_port_3_path: str - velocity_port_3_path: str - - def observation_at(self, results: AmesimResults, index: int) -> TestMqlPistonObservation: - return TestMqlPistonObservation( - time=results.times[index], - chamber_volume_cm3=results.series(self.volume_path)[index], - chamber_volume_rate_l_min=results.series(self.volume_rate_path)[index], - chamber_length_mm=results.series(self.length_path)[index], - force_port_2_n=results.series(self.force_port_2_path)[index], - force_port_3_n=results.series(self.force_port_3_path)[index], - displacement_port_2_m=results.series(self.displacement_port_2_path)[index], - velocity_port_2_m_s=results.series(self.velocity_port_2_path)[index], - displacement_port_3_m=results.series(self.displacement_port_3_path)[index], - velocity_port_3_m_s=results.series(self.velocity_port_3_path)[index], - ) - - -@dataclass(frozen=True) -class TestMqlMassEndstopObservationBinding: - alias: str - displacement_path: str - velocity_path: str - acceleration_path: str - displacement_duplicate_path: str - velocity_duplicate_path: str - acceleration_duplicate_path: str - lower_contact_force_path: str - upper_contact_force_path: str - viscous_friction_force_path: str - dry_friction_force_path: str - stick_flag_path: str - - def observation_at(self, results: AmesimResults, index: int) -> TestMqlMassEndstopObservation: - return TestMqlMassEndstopObservation( - time=results.times[index], - displacement_m=results.series(self.displacement_path)[index], - velocity_m_s=results.series(self.velocity_path)[index], - acceleration_m_s2=results.series(self.acceleration_path)[index], - lower_contact_force_n=results.series(self.lower_contact_force_path)[index], - upper_contact_force_n=results.series(self.upper_contact_force_path)[index], - viscous_friction_force_n=results.series(self.viscous_friction_force_path)[index], - dry_friction_force_n=results.series(self.dry_friction_force_path)[index], - stick_flag=results.series(self.stick_flag_path)[index], - ) - - -@dataclass(frozen=True) -class TestMqlElasticEndstopObservationBinding: - alias: str - force_path: str - duplicate_force_path: str - gap_path: str - stiffness_path: str - - def observation_at(self, results: AmesimResults, index: int) -> TestMqlElasticEndstopObservation: - return TestMqlElasticEndstopObservation( - time=results.times[index], - force_n=results.series(self.force_path)[index], - duplicate_force_n=results.series(self.duplicate_force_path)[index], - gap_mm=results.series(self.gap_path)[index], - stiffness_n_m=results.series(self.stiffness_path)[index], - ) - - -@dataclass(frozen=True) -class TestMqlForceSourceObservationBinding: - alias: str - force_path: str - - def observation_at(self, results: AmesimResults, index: int) -> TestMqlForceSourceObservation: - return TestMqlForceSourceObservation( - time=results.times[index], - force_n=results.series(self.force_path)[index], - ) - - -@dataclass(frozen=True) -class TestMqlForceConnectorObservationBinding: - alias: str - force_path: str - - def observation_at(self, results: AmesimResults, index: int) -> TestMqlForceConnectorObservation: - return TestMqlForceConnectorObservation( - time=results.times[index], - force_n=results.series(self.force_path)[index], - ) - - -@dataclass(frozen=True) -class TestMqlMechanicalNodeObservationBinding: - alias: str - velocity_paths_by_port: dict[int, str] - displacement_paths_by_port: dict[int, str] - total_force_path: str - - def observation_at(self, results: AmesimResults, index: int) -> TestMqlMechanicalNodeObservation: - return TestMqlMechanicalNodeObservation( - time=results.times[index], - velocities_m_s={ - port: results.series(path)[index] - for port, path in self.velocity_paths_by_port.items() - }, - displacements_m={ - port: results.series(path)[index] - for port, path in self.displacement_paths_by_port.items() - }, - total_force_n=results.series(self.total_force_path)[index], - ) - - -@dataclass(frozen=True) -class TestMqlMechanicalObservationCatalog: - pistons: dict[str, TestMqlPistonObservationBinding] - masses: dict[str, TestMqlMassEndstopObservationBinding] - elastic_endstops: dict[str, TestMqlElasticEndstopObservationBinding] - zero_force_sources: dict[str, TestMqlForceSourceObservationBinding] - force_connectors: dict[str, TestMqlForceConnectorObservationBinding] - mechanical_nodes: dict[str, TestMqlMechanicalNodeObservationBinding] - - @property - def binding_count(self) -> int: - return ( - len(self.pistons) - + len(self.masses) - + len(self.elastic_endstops) - + len(self.zero_force_sources) - + len(self.force_connectors) - + len(self.mechanical_nodes) - ) - - -def build_test_mql_mechanical_observation_catalog( - results: AmesimResults, - *, - variable_catalog: TestMqlVariableCatalog | None = None, - mechanical_assembly: TestMqlMechanicalAssembly | None = None, -) -> TestMqlMechanicalObservationCatalog: - variable_catalog = variable_catalog or build_test_mql_variable_catalog(results) - mechanical_assembly = mechanical_assembly or build_test_mql_mechanical_assembly( - amesim_results=results, - variable_catalog=variable_catalog, - ) - return TestMqlMechanicalObservationCatalog( - pistons={ - alias: _build_piston_binding(alias, variable_catalog) - for alias in mechanical_assembly.pistons - }, - masses={ - alias: _build_mass_binding(alias, variable_catalog) - for alias in mechanical_assembly.masses - }, - elastic_endstops={ - alias: _build_elastic_endstop_binding(alias, variable_catalog) - for alias in mechanical_assembly.elastic_endstops - }, - zero_force_sources={ - alias: _build_zero_force_source_binding(alias, variable_catalog) - for alias in mechanical_assembly.zero_force_sources - }, - force_connectors={ - alias: _build_force_connector_binding(alias, variable_catalog) - for alias in mechanical_assembly.force_connectors - }, - mechanical_nodes={ - alias: _build_mechanical_node_binding(alias, variable_catalog) - for alias in mechanical_assembly.mechanical_nodes - }, - ) - - -def _build_piston_binding( - alias: str, - variable_catalog: TestMqlVariableCatalog, -) -> TestMqlPistonObservationBinding: - variables = _owner_variables(variable_catalog, alias) - return TestMqlPistonObservationBinding( - alias=alias, - volume_path=_required_path(variables, "vol1", expected_units="cm**3"), - volume_rate_path=_required_path(variables, "vvol1", expected_units="L/min"), - length_path=_required_path(variables, "length", expected_units="mm"), - force_port_2_path=_required_path(variables, "f2", expected_units="N"), - force_port_3_path=_required_path(variables, "f3", expected_units="N"), - displacement_port_2_path=_required_path(variables, "x5", expected_units="m"), - velocity_port_2_path=_required_path(variables, "v5", expected_units="m/s"), - displacement_port_3_path=_required_path(variables, "x4", expected_units="m"), - velocity_port_3_path=_required_path(variables, "v4", expected_units="m/s"), - ) - - -def _build_mass_binding( - alias: str, - variable_catalog: TestMqlVariableCatalog, -) -> TestMqlMassEndstopObservationBinding: - variables = _owner_variables(variable_catalog, alias) - return TestMqlMassEndstopObservationBinding( - alias=alias, - displacement_path=_required_path(variables, "x1", expected_units="m"), - velocity_path=_required_path(variables, "v1", expected_units="m/s"), - acceleration_path=_required_path(variables, "acc1", expected_units="m/s/s"), - displacement_duplicate_path=_required_path(variables, "x1dup", expected_units="m"), - velocity_duplicate_path=_required_path(variables, "v1dup", expected_units="m/s"), - acceleration_duplicate_path=_required_path(variables, "acc1dup", expected_units="m/s/s"), - lower_contact_force_path=_required_path(variables, "Fmin", expected_units="N"), - upper_contact_force_path=_required_path(variables, "Fmax", expected_units="N"), - viscous_friction_force_path=_required_path(variables, "Fvisc", expected_units="N"), - dry_friction_force_path=_required_path(variables, "Ffric", expected_units="N"), - stick_flag_path=_required_path(variables, "stick", expected_units=None), - ) - - -def _build_elastic_endstop_binding( - alias: str, - variable_catalog: TestMqlVariableCatalog, -) -> TestMqlElasticEndstopObservationBinding: - variables = _owner_variables(variable_catalog, alias) - return TestMqlElasticEndstopObservationBinding( - alias=alias, - force_path=_required_path(variables, "f1", expected_units="N"), - duplicate_force_path=_required_path(variables, "f2", expected_units="N"), - gap_path=_required_path(variables, "gap", expected_units="mm"), - stiffness_path=_required_path(variables, "kval", expected_units="N/m"), - ) - - -def _build_zero_force_source_binding( - alias: str, - variable_catalog: TestMqlVariableCatalog, -) -> TestMqlForceSourceObservationBinding: - variables = _owner_variables(variable_catalog, alias) - return TestMqlForceSourceObservationBinding( - alias=alias, - force_path=_required_path(variables, "fzero", expected_units="N"), - ) - - -def _build_force_connector_binding( - alias: str, - variable_catalog: TestMqlVariableCatalog, -) -> TestMqlForceConnectorObservationBinding: - variables = _owner_variables(variable_catalog, alias) - return TestMqlForceConnectorObservationBinding( - alias=alias, - force_path=_required_path(variables, "force", expected_units="N"), - ) - - -def _build_mechanical_node_binding( - alias: str, - variable_catalog: TestMqlVariableCatalog, -) -> TestMqlMechanicalNodeObservationBinding: - variables = _owner_variables(variable_catalog, alias) - velocity_paths_by_port = {} - displacement_paths_by_port = {} - for port in range(1, 9): - velocity_paths_by_port[port] = _required_path( - variables, - f"p{port}__vt", - expected_units="m/s", - ) - displacement_paths_by_port[port] = _required_path( - variables, - f"p{port}__xt", - expected_units="m", - ) - return TestMqlMechanicalNodeObservationBinding( - alias=alias, - velocity_paths_by_port=velocity_paths_by_port, - displacement_paths_by_port=displacement_paths_by_port, - total_force_path=_required_path(variables, "tforce", expected_units="N"), - ) - - -def _owner_variables( - variable_catalog: TestMqlVariableCatalog, - alias: str, -) -> tuple[TestMqlVariableBinding, ...]: - return tuple( - variable - for variable in variable_catalog.variables - if variable.owner_alias == alias - ) - - -def _required_path( - variables: tuple[TestMqlVariableBinding, ...], - signal_name: str, - *, - expected_units: str | None, -) -> str: - variable = _single( - tuple(variable for variable in variables if variable.signal_name == signal_name), - signal_name, - ) - _assert_units(variable, expected_units) - return variable.data_path - - -def _single( - matches: tuple[TestMqlVariableBinding, ...], - description: str, -) -> TestMqlVariableBinding: - if len(matches) != 1: - raise ValueError(f"Expected one {description} variable, found {len(matches)}.") - return matches[0] - - -def _assert_units(variable: TestMqlVariableBinding, expected_units: str | None) -> None: - if variable.units != expected_units: - raise ValueError( - f"Unexpected units for {variable.data_path}: " - f"{variable.units!r}, expected {expected_units!r}." - ) diff --git a/PythonModels/reporting/test_mql_observations.py b/PythonModels/reporting/test_mql_observations.py deleted file mode 100644 index fe48070..0000000 --- a/PythonModels/reporting/test_mql_observations.py +++ /dev/null @@ -1,210 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass - -from PythonModels.reporting.amesim_results import AmesimResults -from PythonModels.reporting.test_mql_chamber_observations import ( - TestMqlChamberObservationCatalog, - build_test_mql_chamber_observation_catalog, -) -from PythonModels.reporting.test_mql_line_observations import ( - TestMqlLineObservationCatalog, - build_test_mql_line_observation_catalog, -) -from PythonModels.reporting.test_mql_mechanical_observations import ( - TestMqlMechanicalObservationCatalog, - build_test_mql_mechanical_observation_catalog, -) -from PythonModels.reporting.test_mql_orifice_observations import ( - TestMqlOrificeObservationCatalog, - build_test_mql_orifice_observation_catalog, -) -from PythonModels.reporting.test_mql_variables import ( - TestMqlVariableCatalog, - build_test_mql_variable_catalog, -) - - -@dataclass(frozen=True) -class TestMqlObservationCatalog: - variable_catalog: TestMqlVariableCatalog - chambers: TestMqlChamberObservationCatalog - orifices: TestMqlOrificeObservationCatalog - lines: TestMqlLineObservationCatalog - mechanical: TestMqlMechanicalObservationCatalog - - @property - def binding_count(self) -> int: - return ( - len(self.chambers.bindings) - + len(self.orifices.bindings) - + self.lines.line_count - + self.mechanical.binding_count - ) - - def data_paths_by_domain(self) -> dict[str, tuple[str, ...]]: - return { - "chambers": _sorted_unique(_chamber_data_paths(self.chambers)), - "orifices": _sorted_unique(_orifice_data_paths(self.orifices)), - "lines": _sorted_unique(_line_data_paths(self.lines)), - "mechanical": _sorted_unique(_mechanical_data_paths(self.mechanical)), - } - - def data_paths(self) -> tuple[str, ...]: - paths = [] - for domain_paths in self.data_paths_by_domain().values(): - paths.extend(domain_paths) - return _sorted_unique(paths) - - def baseline_series_by_data_path( - self, - results: AmesimResults, - data_paths: tuple[str, ...] | list[str] | None = None, - ) -> dict[str, tuple[float, ...]]: - selected_paths = tuple(data_paths) if data_paths is not None else self.data_paths() - _validate_observed_paths(self, selected_paths) - return {data_path: results.series(data_path) for data_path in selected_paths} - - -def build_test_mql_observation_catalog(results: AmesimResults) -> TestMqlObservationCatalog: - variable_catalog = build_test_mql_variable_catalog(results) - return TestMqlObservationCatalog( - variable_catalog=variable_catalog, - chambers=build_test_mql_chamber_observation_catalog( - results, - variable_catalog=variable_catalog, - ), - orifices=build_test_mql_orifice_observation_catalog( - results, - variable_catalog=variable_catalog, - ), - lines=build_test_mql_line_observation_catalog( - results, - variable_catalog=variable_catalog, - ), - mechanical=build_test_mql_mechanical_observation_catalog( - results, - variable_catalog=variable_catalog, - ), - ) - - -def _chamber_data_paths(catalog: TestMqlChamberObservationCatalog) -> tuple[str, ...]: - paths = [] - for binding in catalog.bindings: - paths.extend( - [ - binding.pressure_path, - binding.temperature_path, - binding.gas_mass_path, - *binding.pressure_duplicate_paths, - *binding.temperature_duplicate_paths, - ] - ) - if binding.volume_path is not None: - paths.append(binding.volume_path) - return tuple(paths) - - -def _orifice_data_paths(catalog: TestMqlOrificeObservationCatalog) -> tuple[str, ...]: - paths = [] - for binding in catalog.bindings: - paths.extend( - [ - binding.primary_mass_flow_path, - binding.primary_enthalpy_flow_path, - binding.reversed_mass_flow_path, - binding.reversed_enthalpy_flow_path, - binding.mass_flow_parameter_path, - binding.gas_velocity_path, - ] - ) - if binding.opening_path is not None: - paths.append(binding.opening_path) - return tuple(paths) - - -def _line_data_paths(catalog: TestMqlLineObservationCatalog) -> tuple[str, ...]: - paths = [] - for binding in catalog.bindings: - paths.extend(binding.mass_flow_paths) - paths.extend(binding.enthalpy_flow_paths) - paths.extend(binding.pressure_paths) - paths.extend(binding.temperature_paths) - if binding.gas_mass_path is not None: - paths.append(binding.gas_mass_path) - paths.extend( - [ - binding.reynolds_path, - binding.mass_flow_parameter_path, - binding.gas_velocity_path, - binding.friction_factor_path, - ] - ) - return tuple(paths) - - -def _mechanical_data_paths(catalog: TestMqlMechanicalObservationCatalog) -> tuple[str, ...]: - paths = [] - for binding in catalog.pistons.values(): - paths.extend( - [ - binding.volume_path, - binding.volume_rate_path, - binding.length_path, - binding.force_port_2_path, - binding.force_port_3_path, - binding.displacement_port_2_path, - binding.velocity_port_2_path, - binding.displacement_port_3_path, - binding.velocity_port_3_path, - ] - ) - for binding in catalog.masses.values(): - paths.extend( - [ - binding.displacement_path, - binding.velocity_path, - binding.acceleration_path, - binding.displacement_duplicate_path, - binding.velocity_duplicate_path, - binding.acceleration_duplicate_path, - binding.lower_contact_force_path, - binding.upper_contact_force_path, - binding.viscous_friction_force_path, - binding.dry_friction_force_path, - binding.stick_flag_path, - ] - ) - for binding in catalog.elastic_endstops.values(): - paths.extend( - [ - binding.force_path, - binding.duplicate_force_path, - binding.gap_path, - binding.stiffness_path, - ] - ) - for binding in catalog.zero_force_sources.values(): - paths.append(binding.force_path) - for binding in catalog.force_connectors.values(): - paths.append(binding.force_path) - for binding in catalog.mechanical_nodes.values(): - paths.extend(binding.velocity_paths_by_port.values()) - paths.extend(binding.displacement_paths_by_port.values()) - paths.append(binding.total_force_path) - return tuple(paths) - - -def _validate_observed_paths( - catalog: TestMqlObservationCatalog, - data_paths: tuple[str, ...], -) -> None: - observed_paths = set(catalog.data_paths()) - missing = [data_path for data_path in data_paths if data_path not in observed_paths] - if missing: - raise KeyError(f"Data_Path values are not in the test_mql observation catalog: {missing}") - - -def _sorted_unique(data_paths: tuple[str, ...] | list[str]) -> tuple[str, ...]: - return tuple(sorted(set(data_paths))) diff --git a/PythonModels/reporting/test_mql_orifice_observations.py b/PythonModels/reporting/test_mql_orifice_observations.py deleted file mode 100644 index defe414..0000000 --- a/PythonModels/reporting/test_mql_orifice_observations.py +++ /dev/null @@ -1,194 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass - -from PythonModels.reporting.amesim_results import AmesimResults -from PythonModels.reporting.test_mql_variables import ( - TestMqlVariableBinding, - TestMqlVariableCatalog, - build_test_mql_variable_catalog, -) -from PythonModels.systems.test_mql_pneumatic import ( - TestMqlPneumaticAssembly, - build_test_mql_pneumatic_assembly, -) - - -G_PER_S_TO_KG_PER_S = 1.0e-3 - - -@dataclass(frozen=True) -class TestMqlOrificeObservation: - time: float - mass_flow_kg_s: float - enthalpy_flow_w: float - mass_flow_parameter: float - gas_velocity_m_s: float - opening: float - effective_area_m2: float - - -@dataclass(frozen=True) -class TestMqlOrificeBinding: - alias: str - submodel: str - nominal_area_m2: float - flow_coefficient: float - primary_mass_flow_path: str - primary_enthalpy_flow_path: str - reversed_mass_flow_path: str - reversed_enthalpy_flow_path: str - mass_flow_parameter_path: str - gas_velocity_path: str - opening_path: str | None - - @property - def is_variable(self) -> bool: - return self.opening_path is not None - - def opening_series(self, results: AmesimResults) -> tuple[float, ...]: - if self.opening_path is None: - return tuple(1.0 for _ in results.times) - return tuple(results.series(self.opening_path)) - - def mass_flow_kg_s_series(self, results: AmesimResults) -> tuple[float, ...]: - return tuple(value * G_PER_S_TO_KG_PER_S for value in results.series(self.primary_mass_flow_path)) - - def reversed_mass_flow_kg_s_series(self, results: AmesimResults) -> tuple[float, ...]: - return tuple(value * G_PER_S_TO_KG_PER_S for value in results.series(self.reversed_mass_flow_path)) - - def effective_area_series(self, results: AmesimResults) -> tuple[float, ...]: - return tuple(self.nominal_area_m2 * max(opening, 0.0) for opening in self.opening_series(results)) - - def observation_at(self, results: AmesimResults, index: int) -> TestMqlOrificeObservation: - opening = self.opening_series(results)[index] - return TestMqlOrificeObservation( - time=results.times[index], - mass_flow_kg_s=results.series(self.primary_mass_flow_path)[index] * G_PER_S_TO_KG_PER_S, - enthalpy_flow_w=results.series(self.primary_enthalpy_flow_path)[index], - mass_flow_parameter=results.series(self.mass_flow_parameter_path)[index], - gas_velocity_m_s=results.series(self.gas_velocity_path)[index], - opening=opening, - effective_area_m2=self.nominal_area_m2 * max(opening, 0.0), - ) - - -@dataclass(frozen=True) -class TestMqlOrificeObservationCatalog: - bindings: tuple[TestMqlOrificeBinding, ...] - - @property - def fixed_count(self) -> int: - return sum(1 for binding in self.bindings if binding.submodel == "PNOR001") - - @property - def variable_count(self) -> int: - return sum(1 for binding in self.bindings if binding.submodel == "PNVO001") - - def by_alias(self, alias: str) -> TestMqlOrificeBinding: - for binding in self.bindings: - if binding.alias == alias: - return binding - raise KeyError(alias) - - -def build_test_mql_orifice_observation_catalog( - results: AmesimResults, - *, - variable_catalog: TestMqlVariableCatalog | None = None, - assembly: TestMqlPneumaticAssembly | None = None, -) -> TestMqlOrificeObservationCatalog: - variable_catalog = variable_catalog or build_test_mql_variable_catalog(results) - assembly = assembly or build_test_mql_pneumatic_assembly() - bindings = [] - for alias, orifice in { - **assembly.fixed_orifices, - **assembly.variable_orifices, - }.items(): - owner_variables = tuple( - variable - for variable in variable_catalog.variables - if variable.owner_alias == alias - ) - primary_mass_flow = _find_primary(owner_variables, signal_prefix="dm") - primary_enthalpy_flow = _find_primary(owner_variables, signal_prefix="dh") - reversed_mass_flow = _find_reversed(owner_variables, signal_prefix="dm") - reversed_enthalpy_flow = _find_reversed(owner_variables, signal_prefix="dh") - mass_flow_parameter = _find_by_signal(owner_variables, "cm") - gas_velocity = _find_by_signal(owner_variables, "gasvel") - opening = _find_optional_by_signal(owner_variables, "xv") - bindings.append( - TestMqlOrificeBinding( - alias=alias, - submodel=primary_mass_flow.submodel, - nominal_area_m2=orifice.area, - flow_coefficient=orifice.flow_coefficient, - primary_mass_flow_path=primary_mass_flow.data_path, - primary_enthalpy_flow_path=primary_enthalpy_flow.data_path, - reversed_mass_flow_path=reversed_mass_flow.data_path, - reversed_enthalpy_flow_path=reversed_enthalpy_flow.data_path, - mass_flow_parameter_path=mass_flow_parameter.data_path, - gas_velocity_path=gas_velocity.data_path, - opening_path=opening.data_path if opening is not None else None, - ) - ) - return TestMqlOrificeObservationCatalog( - bindings=tuple(sorted(bindings, key=lambda binding: binding.alias)) - ) - - -def _find_primary( - variables: tuple[TestMqlVariableBinding, ...], - *, - signal_prefix: str, -) -> TestMqlVariableBinding: - matches = [ - variable - for variable in variables - if variable.signal_name.startswith(signal_prefix) - and "sign reversed duplicate" not in variable.label - ] - return _single(matches, f"primary {signal_prefix}") - - -def _find_reversed( - variables: tuple[TestMqlVariableBinding, ...], - *, - signal_prefix: str, -) -> TestMqlVariableBinding: - matches = [ - variable - for variable in variables - if variable.signal_name.startswith(signal_prefix) - and "sign reversed duplicate" in variable.label - ] - return _single(matches, f"reversed {signal_prefix}") - - -def _find_by_signal( - variables: tuple[TestMqlVariableBinding, ...], - signal_name: str, -) -> TestMqlVariableBinding: - return _single( - [variable for variable in variables if variable.signal_name == signal_name], - signal_name, - ) - - -def _find_optional_by_signal( - variables: tuple[TestMqlVariableBinding, ...], - signal_name: str, -) -> TestMqlVariableBinding | None: - matches = [variable for variable in variables if variable.signal_name == signal_name] - if not matches: - return None - return _single(matches, signal_name) - - -def _single( - matches: list[TestMqlVariableBinding], - description: str, -) -> TestMqlVariableBinding: - if len(matches) != 1: - raise ValueError(f"Expected one {description} variable, found {len(matches)}.") - return matches[0] diff --git a/PythonModels/reporting/test_mql_output_schema.py b/PythonModels/reporting/test_mql_output_schema.py deleted file mode 100644 index 3a5661c..0000000 --- a/PythonModels/reporting/test_mql_output_schema.py +++ /dev/null @@ -1,100 +0,0 @@ -from __future__ import annotations - -from collections import Counter -from dataclasses import dataclass - -from PythonModels.reporting.amesim_results import AmesimResults -from PythonModels.reporting.test_mql_observations import ( - TestMqlObservationCatalog, - build_test_mql_observation_catalog, -) -from PythonModels.reporting.test_mql_variables import TestMqlVariableBinding - - -@dataclass(frozen=True) -class TestMqlOutputSignal: - data_path: str - domain: str - owner_alias: str - owner_kind: str - submodel: str - signal_name: str - units: str | None - amesim_index: int - saved: bool - - -@dataclass(frozen=True) -class TestMqlOutputSchema: - signals: tuple[TestMqlOutputSignal, ...] - - @property - def signal_count(self) -> int: - return len(self.signals) - - def by_data_path(self, data_path: str) -> TestMqlOutputSignal: - for signal in self.signals: - if signal.data_path == data_path: - return signal - raise KeyError(data_path) - - def data_paths(self) -> tuple[str, ...]: - return tuple(signal.data_path for signal in self.signals) - - def data_paths_by_domain(self, domain: str) -> tuple[str, ...]: - return tuple(signal.data_path for signal in self.signals if signal.domain == domain) - - def counts_by_domain(self) -> dict[str, int]: - return dict(Counter(signal.domain for signal in self.signals)) - - def counts_by_submodel(self) -> dict[str, int]: - return dict(Counter(signal.submodel for signal in self.signals)) - - def counts_by_owner_kind(self) -> dict[str, int]: - return dict(Counter(signal.owner_kind for signal in self.signals)) - - def counts_by_units(self) -> dict[str | None, int]: - return dict(Counter(signal.units for signal in self.signals)) - - -def build_test_mql_output_schema( - results: AmesimResults, - *, - observation_catalog: TestMqlObservationCatalog | None = None, -) -> TestMqlOutputSchema: - observation_catalog = observation_catalog or build_test_mql_observation_catalog(results) - domain_by_data_path = _domain_by_data_path(observation_catalog) - signals = [] - for data_path in sorted(domain_by_data_path): - variable = observation_catalog.variable_catalog.by_data_path(data_path) - signals.append(_signal_from_variable(variable, domain_by_data_path[data_path])) - return TestMqlOutputSchema(signals=tuple(signals)) - - -def _domain_by_data_path( - observation_catalog: TestMqlObservationCatalog, -) -> dict[str, str]: - domain_by_data_path = {} - for domain, data_paths in observation_catalog.data_paths_by_domain().items(): - for data_path in data_paths: - if data_path in domain_by_data_path: - raise ValueError(f"Data_Path {data_path!r} is assigned to multiple domains.") - domain_by_data_path[data_path] = domain - return domain_by_data_path - - -def _signal_from_variable( - variable: TestMqlVariableBinding, - domain: str, -) -> TestMqlOutputSignal: - return TestMqlOutputSignal( - data_path=variable.data_path, - domain=domain, - owner_alias=variable.owner_alias, - owner_kind=variable.owner_kind, - submodel=variable.submodel, - signal_name=variable.signal_name, - units=variable.units, - amesim_index=variable.index, - saved=variable.saved, - ) diff --git a/PythonModels/reporting/test_mql_output_validation.py b/PythonModels/reporting/test_mql_output_validation.py deleted file mode 100644 index 39333e2..0000000 --- a/PythonModels/reporting/test_mql_output_validation.py +++ /dev/null @@ -1,161 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass -from math import isfinite - -from PythonModels.reporting.amesim_results import AmesimResults -from PythonModels.reporting.test_mql_comparison import ( - TestMqlComparisonResult, - compare_test_mql_series, -) -from PythonModels.reporting.test_mql_output_schema import TestMqlOutputSchema - - -class TestMqlOutputValidationError(ValueError): - """Raised when a Python test_mql output does not satisfy the AMESim output contract.""" - - -@dataclass(frozen=True) -class TestMqlValidatedOutput: - times: tuple[float, ...] - series_by_data_path: dict[str, tuple[float, ...]] - data_paths: tuple[str, ...] - - def series(self, data_path: str) -> tuple[float, ...]: - if data_path not in self.series_by_data_path: - raise KeyError(data_path) - return self.series_by_data_path[data_path] - - -def validate_test_mql_output( - *, - times: tuple[float, ...] | list[float], - series_by_data_path: dict[str, tuple[float, ...] | list[float]], - schema: TestMqlOutputSchema, - data_paths: tuple[str, ...] | list[str] | None = None, - allow_extra_paths: bool = False, - require_all_schema_paths: bool = False, -) -> TestMqlValidatedOutput: - validated_times = _validate_time_axis(times) - selected_paths = _select_paths( - series_by_data_path=series_by_data_path, - schema=schema, - data_paths=data_paths, - allow_extra_paths=allow_extra_paths, - require_all_schema_paths=require_all_schema_paths, - ) - validated_series = { - data_path: _validate_series( - data_path=data_path, - values=series_by_data_path[data_path], - expected_count=len(validated_times), - ) - for data_path in selected_paths - } - return TestMqlValidatedOutput( - times=validated_times, - series_by_data_path=validated_series, - data_paths=selected_paths, - ) - - -def compare_validated_test_mql_output( - *, - times: tuple[float, ...] | list[float], - series_by_data_path: dict[str, tuple[float, ...] | list[float]], - schema: TestMqlOutputSchema, - amesim_results: AmesimResults, - data_paths: tuple[str, ...] | list[str] | None = None, - allow_extra_paths: bool = False, - require_all_schema_paths: bool = False, - relative_floor: float = 1.0e-12, -) -> TestMqlComparisonResult: - validated = validate_test_mql_output( - times=times, - series_by_data_path=series_by_data_path, - schema=schema, - data_paths=data_paths, - allow_extra_paths=allow_extra_paths, - require_all_schema_paths=require_all_schema_paths, - ) - return compare_test_mql_series( - python_times=validated.times, - python_series_by_data_path=validated.series_by_data_path, - amesim_results=amesim_results, - data_paths=validated.data_paths, - relative_floor=relative_floor, - ) - - -def _validate_time_axis(times: tuple[float, ...] | list[float]) -> tuple[float, ...]: - if not times: - raise TestMqlOutputValidationError("Python time axis is empty.") - validated = tuple(_finite_float("time", value) for value in times) - previous = validated[0] - for value in validated[1:]: - if value < previous: - raise TestMqlOutputValidationError("Python time axis must be monotonically increasing.") - previous = value - return validated - - -def _select_paths( - *, - series_by_data_path: dict[str, tuple[float, ...] | list[float]], - schema: TestMqlOutputSchema, - data_paths: tuple[str, ...] | list[str] | None, - allow_extra_paths: bool, - require_all_schema_paths: bool, -) -> tuple[str, ...]: - schema_paths = set(schema.data_paths()) - provided_paths = set(series_by_data_path) - if not allow_extra_paths: - extra_paths = sorted(provided_paths - schema_paths) - if extra_paths: - raise TestMqlOutputValidationError( - f"Python output contains Data_Path values outside test_mql schema: {extra_paths}" - ) - if require_all_schema_paths: - missing_schema_paths = sorted(schema_paths - provided_paths) - if missing_schema_paths: - raise TestMqlOutputValidationError( - f"Python output is missing required test_mql schema Data_Path values: {missing_schema_paths}" - ) - selected_paths = tuple(data_paths) if data_paths is not None else tuple(sorted(provided_paths & schema_paths)) - if not selected_paths: - raise TestMqlOutputValidationError("no test_mql schema Data_Path values are available.") - unknown_selected = [data_path for data_path in selected_paths if data_path not in schema_paths] - if unknown_selected: - raise TestMqlOutputValidationError( - f"Requested Data_Path values are outside test_mql schema: {unknown_selected}" - ) - missing_selected = [data_path for data_path in selected_paths if data_path not in series_by_data_path] - if missing_selected: - raise TestMqlOutputValidationError( - f"Python output is missing selected Data_Path values: {missing_selected}" - ) - return selected_paths - - -def _validate_series( - *, - data_path: str, - values: tuple[float, ...] | list[float], - expected_count: int, -) -> tuple[float, ...]: - if len(values) != expected_count: - raise TestMqlOutputValidationError( - f"Python series length mismatch for {data_path!r}: " - f"{len(values)} values for {expected_count} time samples." - ) - return tuple(_finite_float(data_path, value) for value in values) - - -def _finite_float(label: str, value: float) -> float: - try: - numeric_value = float(value) - except (TypeError, ValueError) as exc: - raise TestMqlOutputValidationError(f"{label!r} contains a non-numeric value: {value!r}") from exc - if not isfinite(numeric_value): - raise TestMqlOutputValidationError(f"{label!r} contains a non-finite value: {value!r}") - return numeric_value diff --git a/PythonModels/reporting/test_mql_variables.py b/PythonModels/reporting/test_mql_variables.py deleted file mode 100644 index c453928..0000000 --- a/PythonModels/reporting/test_mql_variables.py +++ /dev/null @@ -1,111 +0,0 @@ -from __future__ import annotations - -import re -from collections import Counter -from dataclasses import dataclass - -from PythonModels.reporting.amesim_results import AmesimResults, AmesimVariable -from PythonModels.systems.test_mql import COMPONENT_SPECS, CONNECTION_SPECS - - -_UNIT_RE = re.compile(r"\[([^\]]+)\]\s*$") - - -@dataclass(frozen=True) -class TestMqlVariableBinding: - index: int - data_path: str - signal_name: str - owner_alias: str - owner_kind: str - submodel: str - label: str - units: str | None - saved: bool - - -@dataclass(frozen=True) -class TestMqlVariableCatalog: - variables: tuple[TestMqlVariableBinding, ...] - - @property - def data_path_count(self) -> int: - return len(self.variables) - - @property - def saved_data_path_count(self) -> int: - return sum(1 for variable in self.variables if variable.saved) - - def by_data_path(self, data_path: str) -> TestMqlVariableBinding: - for variable in self.variables: - if variable.data_path == data_path: - return variable - raise KeyError(data_path) - - def counts_by_submodel(self) -> dict[str, int]: - return dict(Counter(variable.submodel for variable in self.variables)) - - def counts_by_owner_kind(self) -> dict[str, int]: - return dict(Counter(variable.owner_kind for variable in self.variables)) - - def data_paths_for_owner(self, owner_alias: str) -> tuple[str, ...]: - return tuple( - variable.data_path - for variable in self.variables - if variable.owner_alias == owner_alias - ) - - -def build_test_mql_variable_catalog(amesim_results: AmesimResults) -> TestMqlVariableCatalog: - owner_map = _build_owner_map() - saved_indices = set(amesim_results.saved_variable_indices) - bindings = [] - for variable in amesim_results.variables: - if variable.data_path is None: - continue - signal_name, owner_alias = split_data_path(variable.data_path) - owner_kind, submodel = owner_map[owner_alias] - bindings.append( - TestMqlVariableBinding( - index=variable.index, - data_path=variable.data_path, - signal_name=signal_name, - owner_alias=owner_alias, - owner_kind=owner_kind, - submodel=submodel, - label=variable.label, - units=_extract_units(variable), - saved=variable.index in saved_indices, - ) - ) - return TestMqlVariableCatalog(variables=tuple(bindings)) - - -def split_data_path(data_path: str) -> tuple[str, str]: - if "@" not in data_path: - raise ValueError(f"AMESim Data_Path does not contain an owner alias: {data_path!r}") - signal_name, owner_alias = data_path.rsplit("@", 1) - if not signal_name or not owner_alias: - raise ValueError(f"Invalid AMESim Data_Path: {data_path!r}") - return signal_name, owner_alias - - -def _build_owner_map() -> dict[str, tuple[str, str]]: - owner_map = { - str(spec["alias"]): ("component", str(spec["submodel"])) - for spec in COMPONENT_SPECS - } - owner_map.update( - { - str(spec["alias"]): ("connection", str(spec["submodel"])) - for spec in CONNECTION_SPECS - } - ) - return owner_map - - -def _extract_units(variable: AmesimVariable) -> str | None: - match = _UNIT_RE.search(variable.label) - if match is None: - return None - return match.group(1) diff --git a/PythonModels/reporting/testmodel_outputs.py b/PythonModels/reporting/testmodel_outputs.py deleted file mode 100644 index 9338b1c..0000000 --- a/PythonModels/reporting/testmodel_outputs.py +++ /dev/null @@ -1,393 +0,0 @@ -from __future__ import annotations - -from bisect import bisect_left -import csv -from dataclasses import dataclass -from pathlib import Path -from typing import Any - - -PRIMARY_KEYS = ( - "mytank.p", - "mytank.T", - "mycylinder.p", - "mycylinder.T", -) - -MODELICA_COMPARISON_COLUMNS = { - "mytank.p": "mytank.p", - "mytank.T": "mytank.T", - "mycylinder.p": "mycylinder.p", - "mycylinder.T": "mycylinder.T", - "branch.upper_branch.p": "mypipe.p", - "branch.upper_branch.in": "myorifice.port_a.m_flow", - "branch.upper_branch.out": "mytee1.port_out2.m_flow", - "branch.lower_branch.p": "mypipe1.p", - "branch.lower_branch.in": "myorifice1.port_a.m_flow", - "branch.lower_branch.out": "mytee1.port_out1.m_flow", -} - -COMPARISON_KEYS = tuple(MODELICA_COMPARISON_COLUMNS.keys()) - - -def _branch_series_values( - series: dict[str, list[float]], - branch_name: str, - legacy_key: str, -) -> list[float]: - generic_key = f"branch.{branch_name}.{legacy_key.split('.')[-1]}" - if generic_key in series: - return series[generic_key] - return series[legacy_key] - - -@dataclass(frozen=True) -class TestModelArtifacts: - primary_csv_path: Path - temperature_csv_path: Path - temperature_svg_path: Path - run_report_path: Path - comparison_csv_path: Path | None = None - comparison_summary_path: Path | None = None - - -def format_testmodel_run_report( - *, - network_summary: str, - initialization: Any, - raw_initial_state: tuple[float, ...], - consistent_initial_state: tuple[float, ...], - solution: Any, - series: dict[str, list[float]], - solve_diagnostics: Any, - artifacts: TestModelArtifacts, - comparison_summary: dict[str, tuple[float, float]] | None, -) -> str: - lines = [ - network_summary, - "", - f"Initialization converged: {initialization.converged}", - f"Initialization iterations: {initialization.iterations}", - f"Initialization max state delta: {initialization.max_state_delta:.6e}", - f"Initialization max flow delta: {initialization.max_flow_delta:.6e}", - f"Initialization max enthalpy delta: {initialization.max_enthalpy_delta:.6e}", - ( - "Initialization downstream pressure spread: " - f"{initialization.downstream_pressure_spread:.6e}" - ), - "", - "Raw initial state vector:", - str(list(raw_initial_state)), - "", - "Constraint-consistent initial state vector:", - str(list(consistent_initial_state)), - "", - f"Solver success: {solution.success}", - f"Solver message: {solution.message}", - f"Final time: {solution.t[-1]:.2f} s", - f"Final tank pressure: {series['mytank.p'][-1]:.3f} Pa", - f"Final tank temperature: {series['mytank.T'][-1]:.3f} K", - f"Final cylinder pressure: {series['mycylinder.p'][-1]:.3f} Pa", - ( - "Final branch inflow: " - f"{_branch_series_values(series, 'upper_branch', 'branch_upper.in')[-1] + _branch_series_values(series, 'lower_branch', 'branch_lower.in')[-1]:.6f} kg/s" - ), - ] - - if solve_diagnostics is not None: - lines.extend( - [ - "", - "Final closure solve diagnostics:", - ( - "Upper branch inlet solve: " - f"converged={solve_diagnostics.upper_branch_inlet.converged}, " - f"iterations={solve_diagnostics.upper_branch_inlet.iterations}, " - f"residual={solve_diagnostics.upper_branch_inlet.residual:.6e}" - ), - ( - "Lower branch inlet solve: " - f"converged={solve_diagnostics.lower_branch_inlet.converged}, " - f"iterations={solve_diagnostics.lower_branch_inlet.iterations}, " - f"residual={solve_diagnostics.lower_branch_inlet.residual:.6e}" - ), - ] - ) - if solve_diagnostics.downstream_pressure_projection is not None: - lines.append( - "Downstream pressure projection: " - f"converged={solve_diagnostics.downstream_pressure_projection.converged}, " - f"iterations={solve_diagnostics.downstream_pressure_projection.iterations}, " - f"residual={solve_diagnostics.downstream_pressure_projection.residual:.6e}" - ) - - lines.extend( - [ - f"Primary series CSV: {artifacts.primary_csv_path}", - f"Temperature CSV: {artifacts.temperature_csv_path}", - f"Temperature plot: {artifacts.temperature_svg_path}", - f"Run report TXT: {artifacts.run_report_path}", - ] - ) - - if ( - artifacts.comparison_csv_path is not None - and artifacts.comparison_summary_path is not None - ): - lines.extend( - [ - f"Modelica comparison CSV: {artifacts.comparison_csv_path}", - f"Modelica comparison summary: {artifacts.comparison_summary_path}", - ] - ) - if comparison_summary is not None: - for key, (max_abs_error, max_rel_error) in comparison_summary.items(): - lines.append( - f"{key} max abs error: {max_abs_error:.6f}, " - f"max rel error: {max_rel_error:.6%}" - ) - - return "\n".join(lines) + "\n" - - -def write_testmodel_run_report(output_dir: Path, report_text: str) -> Path: - report_path = output_dir / "testmodel_run_report.txt" - report_path.write_text(report_text, encoding="utf-8") - return report_path - - -def _write_primary_series_csv(output_dir: Path, series: dict[str, list[float]]) -> Path: - csv_path = output_dir / "testmodel_primary_series.csv" - with csv_path.open("w", newline="", encoding="utf-8") as handle: - writer = csv.writer(handle) - writer.writerow(["time_s", *PRIMARY_KEYS]) - for index, time_value in enumerate(series["time"]): - writer.writerow([time_value, *(series[key][index] for key in PRIMARY_KEYS)]) - return csv_path - - -def _write_temperature_csv(output_dir: Path, time_values: list[float], temperatures: list[float]) -> Path: - csv_path = output_dir / "testmodel_tank_temperature.csv" - with csv_path.open("w", newline="", encoding="utf-8") as handle: - writer = csv.writer(handle) - writer.writerow(["time_s", "mytank_T_K"]) - writer.writerows(zip(time_values, temperatures)) - return csv_path - - -def _write_temperature_svg(output_dir: Path, time_values: list[float], temperatures: list[float]) -> Path: - svg_path = output_dir / "testmodel_tank_temperature.svg" - - width = 900 - height = 520 - left = 90 - right = 40 - top = 60 - bottom = 70 - plot_width = width - left - right - plot_height = height - top - bottom - - min_time = min(time_values) - max_time = max(time_values) - min_temp = min(temperatures) - max_temp = max(temperatures) - temp_padding = max(1.0, (max_temp - min_temp) * 0.08) - min_temp -= temp_padding - max_temp += temp_padding - - def scale_x(value: float) -> float: - return left + (value - min_time) / max(max_time - min_time, 1e-12) * plot_width - - def scale_y(value: float) -> float: - return top + (max_temp - value) / max(max_temp - min_temp, 1e-12) * plot_height - - points = " ".join( - f"{scale_x(time_value):.2f},{scale_y(temperature):.2f}" - for time_value, temperature in zip(time_values, temperatures) - ) - - x_ticks = 5 - y_ticks = 5 - x_tick_markup = [] - y_tick_markup = [] - - for index in range(x_ticks + 1): - fraction = index / x_ticks - time_value = min_time + fraction * (max_time - min_time) - x = left + fraction * plot_width - x_tick_markup.append( - f'' - ) - x_tick_markup.append( - f'' - f"{time_value:.1f}" - ) - - for index in range(y_ticks + 1): - fraction = index / y_ticks - temp_value = min_temp + fraction * (max_temp - min_temp) - y = top + plot_height - fraction * plot_height - y_tick_markup.append( - f'' - ) - y_tick_markup.append( - f'' - f"{temp_value:.1f}" - ) - - svg_content = f""" - - Python Testmodel Tank Temperature - Time (s) - Temperature (K) - - {''.join(x_tick_markup)} - {''.join(y_tick_markup)} - - -""" - svg_path.write_text(svg_content, encoding="utf-8") - return svg_path - - -def load_modelica_series(csv_path: Path, variable_names: tuple[str, ...]) -> dict[str, list[float]]: - series = {"time": []} - for variable_name in variable_names: - series[variable_name] = [] - - with csv_path.open("r", newline="", encoding="utf-8") as handle: - reader = csv.DictReader(handle) - available_variable_names = tuple( - variable_name - for variable_name in variable_names - if MODELICA_COMPARISON_COLUMNS.get(variable_name, variable_name) in (reader.fieldnames or ()) - ) - for row in reader: - series["time"].append(float(row["time"])) - for variable_name in available_variable_names: - modelica_column = MODELICA_COMPARISON_COLUMNS.get(variable_name, variable_name) - series[variable_name].append(float(row[modelica_column])) - - return series - - -def _interpolate_series_value(time_values: list[float], values: list[float], target_time: float) -> float: - if target_time <= time_values[0]: - return values[0] - if target_time >= time_values[-1]: - return values[-1] - - right_index = bisect_left(time_values, target_time) - if right_index < len(time_values) and abs(time_values[right_index] - target_time) <= 1e-12: - return values[right_index] - - left_index = right_index - 1 - left_time = time_values[left_index] - right_time = time_values[right_index] - fraction = (target_time - left_time) / (right_time - left_time) - return values[left_index] + fraction * (values[right_index] - values[left_index]) - - -def write_modelica_comparison( - output_dir: Path, - python_series: dict[str, list[float]], - modelica_series: dict[str, list[float]], -) -> tuple[Path, Path, dict[str, tuple[float, float]]]: - comparison_csv_path = output_dir / "testmodel_modelica_comparison.csv" - summary_path = output_dir / "testmodel_modelica_comparison_summary.txt" - summary: dict[str, tuple[float, float]] = {} - - with comparison_csv_path.open("w", newline="", encoding="utf-8") as handle: - writer = csv.writer(handle) - header = ["time_s"] - comparison_keys = tuple( - key - for key in COMPARISON_KEYS - if key in python_series and key in modelica_series and modelica_series[key] - ) - for key in comparison_keys: - header.extend( - [ - f"python.{key}", - f"modelica.{key}", - f"abs_error.{key}", - f"rel_error.{key}", - ] - ) - writer.writerow(header) - - max_abs_errors = {key: 0.0 for key in comparison_keys} - max_rel_errors = {key: 0.0 for key in comparison_keys} - - for index, time_value in enumerate(python_series["time"]): - row = [time_value] - for key in comparison_keys: - python_value = python_series[key][index] - modelica_value = _interpolate_series_value( - modelica_series["time"], - modelica_series[key], - time_value, - ) - abs_error = abs(python_value - modelica_value) - rel_error = abs_error / max(abs(modelica_value), 1e-9) - max_abs_errors[key] = max(max_abs_errors[key], abs_error) - max_rel_errors[key] = max(max_rel_errors[key], rel_error) - row.extend([python_value, modelica_value, abs_error, rel_error]) - writer.writerow(row) - - summary_lines = [] - for key in comparison_keys: - summary[key] = (max_abs_errors[key], max_rel_errors[key]) - summary_lines.append( - f"{key}: max_abs_error={max_abs_errors[key]:.6f}, " - f"max_rel_error={max_rel_errors[key]:.6%}" - ) - summary_path.write_text("\n".join(summary_lines) + "\n", encoding="utf-8") - return comparison_csv_path, summary_path, summary - - -def export_testmodel_artifacts( - *, - output_dir: Path, - series: dict[str, list[float]], - modelica_series: dict[str, list[float]] | None = None, -) -> tuple[TestModelArtifacts, dict[str, tuple[float, float]] | None]: - output_dir.mkdir(parents=True, exist_ok=True) - - primary_csv_path = _write_primary_series_csv(output_dir, series) - temperature_csv_path = _write_temperature_csv( - output_dir, - series["time"], - series["mytank.T"], - ) - temperature_svg_path = _write_temperature_svg( - output_dir, - series["time"], - series["mytank.T"], - ) - - comparison_csv_path = None - comparison_summary_path = None - comparison_summary = None - if modelica_series is not None: - ( - comparison_csv_path, - comparison_summary_path, - comparison_summary, - ) = write_modelica_comparison(output_dir, series, modelica_series) - - return ( - TestModelArtifacts( - primary_csv_path=primary_csv_path, - temperature_csv_path=temperature_csv_path, - temperature_svg_path=temperature_svg_path, - run_report_path=output_dir / "testmodel_run_report.txt", - comparison_csv_path=comparison_csv_path, - comparison_summary_path=comparison_summary_path, - ), - comparison_summary, - ) diff --git a/PythonModels/runs/test_mql_20260715_085526_745726/test_mql_model_summary.txt b/PythonModels/runs/test_mql_20260715_085526_745726/test_mql_model_summary.txt deleted file mode 100644 index 1ad5ff1..0000000 --- a/PythonModels/runs/test_mql_20260715_085526_745726/test_mql_model_summary.txt +++ /dev/null @@ -1,31 +0,0 @@ -Model: test_mql -Source archive: /home/huojr/projects/SystemSimulationApp/AmesimModels/test_mql.ame -Components: 117 -Connections: 84 -Continuous states in AMESim modelinfo: 132 -Discrete states in AMESim modelinfo: 24 -Global parameters: - - D1: 20 - - D2: 20 - - D3: 14 - - P0: 153 - - Pdq: 1 - - V: 15 - - cf: 0.45 -Component submodels: - - F000: 16 - - FORC: 2 - - LMECHN1: 2 - - LSTP00A: 8 - - MECMAS21: 10 - - P4NODE2: 8 - - PN3NODE2: 8 - - PNCH012: 8 - - PNCH023: 4 - - PNGD00: 1 - - PNOR001: 8 - - PNPL01: 16 - - PNRP17: 8 - - PNVO001: 8 - - STEP0: 8 - - UD00: 2 diff --git a/PythonModels/runs/test_mql_20260715_085712_158379/test_mql_model_summary.txt b/PythonModels/runs/test_mql_20260715_085712_158379/test_mql_model_summary.txt deleted file mode 100644 index 1ad5ff1..0000000 --- a/PythonModels/runs/test_mql_20260715_085712_158379/test_mql_model_summary.txt +++ /dev/null @@ -1,31 +0,0 @@ -Model: test_mql -Source archive: /home/huojr/projects/SystemSimulationApp/AmesimModels/test_mql.ame -Components: 117 -Connections: 84 -Continuous states in AMESim modelinfo: 132 -Discrete states in AMESim modelinfo: 24 -Global parameters: - - D1: 20 - - D2: 20 - - D3: 14 - - P0: 153 - - Pdq: 1 - - V: 15 - - cf: 0.45 -Component submodels: - - F000: 16 - - FORC: 2 - - LMECHN1: 2 - - LSTP00A: 8 - - MECMAS21: 10 - - P4NODE2: 8 - - PN3NODE2: 8 - - PNCH012: 8 - - PNCH023: 4 - - PNGD00: 1 - - PNOR001: 8 - - PNPL01: 16 - - PNRP17: 8 - - PNVO001: 8 - - STEP0: 8 - - UD00: 2 diff --git a/PythonModels/runs/test_mql_20260715_091524_614886/test_mql_model_summary.txt b/PythonModels/runs/test_mql_20260715_091524_614886/test_mql_model_summary.txt deleted file mode 100644 index 1ad5ff1..0000000 --- a/PythonModels/runs/test_mql_20260715_091524_614886/test_mql_model_summary.txt +++ /dev/null @@ -1,31 +0,0 @@ -Model: test_mql -Source archive: /home/huojr/projects/SystemSimulationApp/AmesimModels/test_mql.ame -Components: 117 -Connections: 84 -Continuous states in AMESim modelinfo: 132 -Discrete states in AMESim modelinfo: 24 -Global parameters: - - D1: 20 - - D2: 20 - - D3: 14 - - P0: 153 - - Pdq: 1 - - V: 15 - - cf: 0.45 -Component submodels: - - F000: 16 - - FORC: 2 - - LMECHN1: 2 - - LSTP00A: 8 - - MECMAS21: 10 - - P4NODE2: 8 - - PN3NODE2: 8 - - PNCH012: 8 - - PNCH023: 4 - - PNGD00: 1 - - PNOR001: 8 - - PNPL01: 16 - - PNRP17: 8 - - PNVO001: 8 - - STEP0: 8 - - UD00: 2 diff --git a/PythonModels/runs/test_mql_20260720_084308_403642/test_mql_model_summary.txt b/PythonModels/runs/test_mql_20260720_084308_403642/test_mql_model_summary.txt deleted file mode 100644 index 1ad5ff1..0000000 --- a/PythonModels/runs/test_mql_20260720_084308_403642/test_mql_model_summary.txt +++ /dev/null @@ -1,31 +0,0 @@ -Model: test_mql -Source archive: /home/huojr/projects/SystemSimulationApp/AmesimModels/test_mql.ame -Components: 117 -Connections: 84 -Continuous states in AMESim modelinfo: 132 -Discrete states in AMESim modelinfo: 24 -Global parameters: - - D1: 20 - - D2: 20 - - D3: 14 - - P0: 153 - - Pdq: 1 - - V: 15 - - cf: 0.45 -Component submodels: - - F000: 16 - - FORC: 2 - - LMECHN1: 2 - - LSTP00A: 8 - - MECMAS21: 10 - - P4NODE2: 8 - - PN3NODE2: 8 - - PNCH012: 8 - - PNCH023: 4 - - PNGD00: 1 - - PNOR001: 8 - - PNPL01: 16 - - PNRP17: 8 - - PNVO001: 8 - - STEP0: 8 - - UD00: 2 diff --git a/PythonModels/runs/test_mql_20260720_085120_858664/test_mql_model_summary.txt b/PythonModels/runs/test_mql_20260720_085120_858664/test_mql_model_summary.txt deleted file mode 100644 index 1ad5ff1..0000000 --- a/PythonModels/runs/test_mql_20260720_085120_858664/test_mql_model_summary.txt +++ /dev/null @@ -1,31 +0,0 @@ -Model: test_mql -Source archive: /home/huojr/projects/SystemSimulationApp/AmesimModels/test_mql.ame -Components: 117 -Connections: 84 -Continuous states in AMESim modelinfo: 132 -Discrete states in AMESim modelinfo: 24 -Global parameters: - - D1: 20 - - D2: 20 - - D3: 14 - - P0: 153 - - Pdq: 1 - - V: 15 - - cf: 0.45 -Component submodels: - - F000: 16 - - FORC: 2 - - LMECHN1: 2 - - LSTP00A: 8 - - MECMAS21: 10 - - P4NODE2: 8 - - PN3NODE2: 8 - - PNCH012: 8 - - PNCH023: 4 - - PNGD00: 1 - - PNOR001: 8 - - PNPL01: 16 - - PNRP17: 8 - - PNVO001: 8 - - STEP0: 8 - - UD00: 2 diff --git a/PythonModels/runs/test_mql_baseline_20260716_024434_287016/test_mql_baseline_summary.txt b/PythonModels/runs/test_mql_baseline_20260716_024434_287016/test_mql_baseline_summary.txt deleted file mode 100644 index e48fb2a..0000000 --- a/PythonModels/runs/test_mql_baseline_20260716_024434_287016/test_mql_baseline_summary.txt +++ /dev/null @@ -1,8 +0,0 @@ -Model: test_mql -Mode: AMESim baseline passthrough -Samples: 1002 -Output schema signals: 858 -Compared signals: 858 -Observation bindings: 114 -Max absolute error: 0.0 -Max relative error: 0.0 diff --git a/PythonModels/runs/test_mql_baseline_20260720_084318_747036/test_mql_baseline_summary.txt b/PythonModels/runs/test_mql_baseline_20260720_084318_747036/test_mql_baseline_summary.txt deleted file mode 100644 index e48fb2a..0000000 --- a/PythonModels/runs/test_mql_baseline_20260720_084318_747036/test_mql_baseline_summary.txt +++ /dev/null @@ -1,8 +0,0 @@ -Model: test_mql -Mode: AMESim baseline passthrough -Samples: 1002 -Output schema signals: 858 -Compared signals: 858 -Observation bindings: 114 -Max absolute error: 0.0 -Max relative error: 0.0 diff --git a/PythonModels/scripts/run_test_mql.py b/PythonModels/scripts/run_test_mql.py deleted file mode 100644 index 0fd7977..0000000 --- a/PythonModels/scripts/run_test_mql.py +++ /dev/null @@ -1,74 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass, field -from datetime import UTC, datetime -from pathlib import Path - -from PythonModels.systems.test_mql import TestMqlRunConfig, TestMqlSystem - - -@dataclass(frozen=True) -class TestMqlPathConfig: - output_dir: Path | None = None - - -@dataclass(frozen=True) -class TestMqlExecutionConfig: - write_summary: bool = True - - -@dataclass(frozen=True) -class TestMqlScriptConfig: - run: TestMqlRunConfig = field(default_factory=TestMqlRunConfig) - paths: TestMqlPathConfig = field(default_factory=TestMqlPathConfig) - execution: TestMqlExecutionConfig = field(default_factory=TestMqlExecutionConfig) - - -def _default_output_dir() -> Path: - pythonmodels_root = Path(__file__).resolve().parents[1] - timestamp = datetime.now(UTC).strftime("test_mql_%Y%m%d_%H%M%S_%f") - return pythonmodels_root / "runs" / timestamp - - -def format_test_mql_summary(system: TestMqlSystem) -> str: - snapshot = system.snapshot() - lines = [ - "Model: test_mql", - f"Source archive: {system.archive_path}", - f"Components: {snapshot.component_count}", - f"Connections: {snapshot.connection_count}", - f"Continuous states in AMESim modelinfo: {snapshot.continuous_state_count}", - f"Discrete states in AMESim modelinfo: {snapshot.discrete_state_count}", - "Global parameters:", - ] - for name, value in sorted(snapshot.global_parameters.items()): - lines.append(f" - {name}: {value}") - lines.append("Component submodels:") - for name, count in sorted(snapshot.submodel_counts.items()): - lines.append(f" - {name}: {count}") - return "\n".join(lines) + "\n" - - -def run_test_mql(config: TestMqlScriptConfig | None = None): - config = config or TestMqlScriptConfig() - system = TestMqlSystem() - result = system.simulate(config.run) - output_dir = config.paths.output_dir or _default_output_dir() - if config.execution.write_summary: - output_dir.mkdir(parents=True, exist_ok=True) - (output_dir / "test_mql_model_summary.txt").write_text( - format_test_mql_summary(system), - encoding="utf-8", - ) - return system, result, output_dir - - -def main() -> None: - system, result, output_dir = run_test_mql() - print(format_test_mql_summary(system), end="") - print(f"Samples: {len(result.t)}") - print(f"Output directory: {output_dir}") - - -if __name__ == "__main__": - main() diff --git a/PythonModels/scripts/run_testmodel.py b/PythonModels/scripts/run_testmodel.py deleted file mode 100644 index 4834677..0000000 --- a/PythonModels/scripts/run_testmodel.py +++ /dev/null @@ -1,219 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass, field -from datetime import UTC, datetime -from pathlib import Path - -from PythonModels.reporting import ( - COMPARISON_KEYS, - PRIMARY_KEYS, - TestModelArtifacts, - export_testmodel_artifacts, - format_testmodel_run_report, - load_modelica_series, - write_testmodel_run_report, -) -from PythonModels.core.solver import SolveIVPConfig -from PythonModels.systems.testmodel import ( - InitializationDiagnostics, - TestModelConfig, - TestModelSystem, -) -from PythonModels.systems.testmodel_closure import TestModelSolveDiagnostics - - -@dataclass(frozen=True) -class TestModelSamplingConfig: - step: float = 0.1 - - -@dataclass(frozen=True) -class TestModelPathConfig: - output_dir: Path | None = None - modelica_result_path: Path | None = None - - -@dataclass(frozen=True) -class TestModelExecutionConfig: - use_modelica_reference_if_available: bool = True - - -@dataclass(frozen=True) -class TestModelRunConfig: - model: TestModelConfig = field(default_factory=TestModelConfig) - solver: SolveIVPConfig = field(default_factory=SolveIVPConfig) - sampling: TestModelSamplingConfig = field(default_factory=TestModelSamplingConfig) - paths: TestModelPathConfig = field(default_factory=TestModelPathConfig) - execution: TestModelExecutionConfig = field(default_factory=TestModelExecutionConfig) - - @property - def sample_step(self) -> float: - return self.sampling.step - - def sample_times(self) -> list[float]: - return _sample_times( - self.solver.t_start, - self.solver.t_stop, - step=self.sampling.step, - ) - - -@dataclass(frozen=True) -class PreparedTestModelRun: - run_config: TestModelRunConfig - repo_root: Path - output_dir: Path - modelica_result_path: Path - t_eval: tuple[float, ...] - use_modelica_reference_if_available: bool - modelica_reference_exists: bool - - -@dataclass(frozen=True) -class TestModelRunResult: - run_config: TestModelRunConfig - prepared_run: PreparedTestModelRun - system: TestModelSystem - initialization: InitializationDiagnostics - raw_initial_state: tuple[float, ...] - consistent_initial_state: tuple[float, ...] - solution: object - series: dict[str, list[float]] - solve_diagnostics: TestModelSolveDiagnostics | None - artifacts: TestModelArtifacts - comparison_summary: dict[str, tuple[float, float]] | None - used_modelica_reference: bool - - -def _sample_times(t_start: float, t_stop: float, step: float) -> list[float]: - point_count = int(round((t_stop - t_start) / step)) - return [t_start + index * step for index in range(point_count + 1)] - - -def _default_run_output_dir(pythonmodels_root: Path) -> Path: - timestamp = datetime.now(UTC).strftime("testmodel_%Y%m%d_%H%M%S_%f") - return pythonmodels_root / "runs" / timestamp - - -def prepare_testmodel_run( - *, - run_config: TestModelRunConfig | None = None, - output_dir: Path | None = None, - modelica_result_path: Path | None = None, -) -> PreparedTestModelRun: - run_config = run_config or TestModelRunConfig() - repo_root = Path(__file__).resolve().parents[2] - pythonmodels_root = Path(__file__).resolve().parents[1] - resolved_output_dir = ( - output_dir - or run_config.paths.output_dir - or _default_run_output_dir(pythonmodels_root) - ) - resolved_modelica_result_path = ( - modelica_result_path - or run_config.paths.modelica_result_path - or repo_root / "ModelicaModels" / "Simulation" / "Testmodel_res.csv" - ) - t_eval = tuple(run_config.sample_times()) - return PreparedTestModelRun( - run_config=run_config, - repo_root=repo_root, - output_dir=resolved_output_dir, - modelica_result_path=resolved_modelica_result_path, - t_eval=t_eval, - use_modelica_reference_if_available=run_config.execution.use_modelica_reference_if_available, - modelica_reference_exists=resolved_modelica_result_path.exists(), - ) - - -def run_prepared_testmodel(prepared_run: PreparedTestModelRun) -> TestModelRunResult: - run_config = prepared_run.run_config - system = TestModelSystem(config=run_config.model) - raw_initial_state = tuple(system.initial_state_vector()) - initialization = system.initialize_consistent_state() - consistent_initial_state = tuple(initialization.state_vector) - solution = system.simulate(config=run_config.solver, t_eval=list(prepared_run.t_eval)) - series = system.evaluate_solution(solution) - solve_diagnostics = system.last_solve_diagnostics - - modelica_series = None - used_modelica_reference = False - if ( - prepared_run.use_modelica_reference_if_available - and prepared_run.modelica_reference_exists - ): - modelica_series = load_modelica_series( - prepared_run.modelica_result_path, - COMPARISON_KEYS, - ) - used_modelica_reference = True - - artifacts, comparison_summary = export_testmodel_artifacts( - output_dir=prepared_run.output_dir, - series=series, - modelica_series=modelica_series, - ) - report_text = format_testmodel_run_report( - network_summary=system.network.summary(), - initialization=initialization, - raw_initial_state=raw_initial_state, - consistent_initial_state=consistent_initial_state, - solution=solution, - series=series, - solve_diagnostics=solve_diagnostics, - artifacts=artifacts, - comparison_summary=comparison_summary, - ) - write_testmodel_run_report(prepared_run.output_dir, report_text) - - return TestModelRunResult( - run_config=run_config, - prepared_run=prepared_run, - system=system, - initialization=initialization, - raw_initial_state=raw_initial_state, - consistent_initial_state=consistent_initial_state, - solution=solution, - series=series, - solve_diagnostics=solve_diagnostics, - artifacts=artifacts, - comparison_summary=comparison_summary, - used_modelica_reference=used_modelica_reference, - ) - - -def run_testmodel( - *, - run_config: TestModelRunConfig | None = None, - output_dir: Path | None = None, - modelica_result_path: Path | None = None, -) -> TestModelRunResult: - prepared_run = prepare_testmodel_run( - run_config=run_config, - output_dir=output_dir, - modelica_result_path=modelica_result_path, - ) - return run_prepared_testmodel(prepared_run) - - -def main() -> None: - run_config = TestModelRunConfig() - result = run_testmodel(run_config=run_config) - print( - format_testmodel_run_report( - network_summary=result.system.network.summary(), - initialization=result.initialization, - raw_initial_state=result.raw_initial_state, - consistent_initial_state=result.consistent_initial_state, - solution=result.solution, - series=result.series, - solve_diagnostics=result.solve_diagnostics, - artifacts=result.artifacts, - comparison_summary=result.comparison_summary, - ), - end="", - ) - - -if __name__ == "__main__": - main() diff --git a/PythonModels/systems/__init__.py b/PythonModels/systems/__init__.py deleted file mode 100644 index 187800c..0000000 --- a/PythonModels/systems/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -"""System assembly modules.""" - diff --git a/PythonModels/systems/test_mql.py b/PythonModels/systems/test_mql.py deleted file mode 100644 index 1bd736b..0000000 --- a/PythonModels/systems/test_mql.py +++ /dev/null @@ -1,6334 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass, field -from pathlib import Path -from types import SimpleNamespace - -from PythonModels.core.base import Component -from PythonModels.core.network import SimulationNetwork -from PythonModels.core.solver import SolveIVPConfig, integrate_ode - - -MODEL_NAME = "test_mql" -AMESIM_ARCHIVE_RELATIVE_PATH = Path("AmesimModels/test_mql.ame") - -# Extracted from AmesimModels/test_mql.ame on the model-development branch. -# This is a structural Python port scaffold: it preserves the AMESim component -# inventory, parameters, and connection topology without changing the existing -# Testmodel implementation. The detailed AMESim submodel equations should be -# replaced incrementally per submodel family. -MODEL_INFO = {'num_states': 132, 'num_discrete_states': 24, 'is_explicit': 1} -SIMULATION_SETTINGS = {'raw': '0 10 0.01 1e+30 1e-07 0.001 1 0.1\r\n0 0 0 0 8 0 0 0 0 0', - 't_start': 0.0, - 't_stop': 10.0, - 'print_interval': 0.01, - 'max_step': 0.001, - 'sample_step': 0.1} -GLOBAL_PARAMETERS = {'D2': '20', 'D1': '20', 'D3': '14', 'Pdq': '1', 'P0': '153', 'V': '15', 'cf': '0.45'} -SUBMODEL_COUNTS = {'PNRP17': 8, - 'F000': 16, - 'MECMAS21': 10, - 'LMECHN1': 2, - 'LSTP00A': 8, - 'PNCH012': 8, - 'PNPL01': 16, - 'FORC': 2, - 'PNCH023': 4, - 'PNOR001': 8, - 'PN3NODE2': 8, - 'P4NODE2': 8, - 'PNVO001': 8, - 'STEP0': 8, - 'UD00': 2, - 'PNGD00': 1} -LINE_SUBMODEL_COUNTS = {'DIRECT': 44, 'PNL0001': 20, 'PNL0003': 8, 'PNL0002': 8, 'PNL00R': 4} -COMPONENT_SPECS = [{'index': 0, - 'alias': 'pn_brp2_8', - 'component_name': 'pn_brp2', - 'library_id': 'pcd.pn_brp2', - 'submodel': 'PNRP17', - 'label': 'pneumatic piston with moving body', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}, - {'name': 'port_2', 'type': 'lshaft', 'face': '2'}, - {'name': 'port_3', 'type': 'lshaft', 'face': '2'}, - {'name': 'port_4', 'type': 'lshaft', 'face': '3'}, - {'name': 'port_5', 'type': 'lshaft', 'face': '3'}], - 'parameters': [{'name': 'dp', - 'title': 'piston diameter', - 'value': '2.00000000000000e+02', - 'units': 'mm'}, - {'name': 'dr', - 'title': 'rod diameter', - 'value': '1.00000000000000e+00', - 'units': 'mm'}, - {'name': 'x0', - 'title': 'chamber length at zero relative displacement', - 'value': '0.00000000000000e+00', - 'units': 'mm'}, - {'name': 'gi', 'title': 'gas type index', 'value': '1', 'units': ''}]}, - {'index': 1, - 'alias': 'pn_brp2_9', - 'component_name': 'pn_brp2', - 'library_id': 'pcd.pn_brp2', - 'submodel': 'PNRP17', - 'label': 'pneumatic piston with moving body', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}, - {'name': 'port_2', 'type': 'lshaft', 'face': '2'}, - {'name': 'port_3', 'type': 'lshaft', 'face': '2'}, - {'name': 'port_4', 'type': 'lshaft', 'face': '3'}, - {'name': 'port_5', 'type': 'lshaft', 'face': '3'}], - 'parameters': [{'name': 'dp', - 'title': 'piston diameter', - 'value': '2.00000000000000e+02', - 'units': 'mm'}, - {'name': 'dr', - 'title': 'rod diameter', - 'value': '1.00000000000000e+00', - 'units': 'mm'}, - {'name': 'x0', - 'title': 'chamber length at zero relative displacement', - 'value': '0.00000000000000e+00', - 'units': 'mm'}, - {'name': 'gi', 'title': 'gas type index', 'value': '1', 'units': ''}]}, - {'index': 2, - 'alias': 'pn_brp2_10', - 'component_name': 'pn_brp2', - 'library_id': 'pcd.pn_brp2', - 'submodel': 'PNRP17', - 'label': 'pneumatic piston with moving body', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}, - {'name': 'port_2', 'type': 'lshaft', 'face': '2'}, - {'name': 'port_3', 'type': 'lshaft', 'face': '2'}, - {'name': 'port_4', 'type': 'lshaft', 'face': '3'}, - {'name': 'port_5', 'type': 'lshaft', 'face': '3'}], - 'parameters': [{'name': 'dp', - 'title': 'piston diameter', - 'value': '2.00000000000000e+02', - 'units': 'mm'}, - {'name': 'dr', - 'title': 'rod diameter', - 'value': '1.00000000000000e+00', - 'units': 'mm'}, - {'name': 'x0', - 'title': 'chamber length at zero relative displacement', - 'value': '0.00000000000000e+00', - 'units': 'mm'}, - {'name': 'gi', 'title': 'gas type index', 'value': '1', 'units': ''}]}, - {'index': 3, - 'alias': 'pn_brp2_11', - 'component_name': 'pn_brp2', - 'library_id': 'pcd.pn_brp2', - 'submodel': 'PNRP17', - 'label': 'pneumatic piston with moving body', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}, - {'name': 'port_2', 'type': 'lshaft', 'face': '2'}, - {'name': 'port_3', 'type': 'lshaft', 'face': '2'}, - {'name': 'port_4', 'type': 'lshaft', 'face': '3'}, - {'name': 'port_5', 'type': 'lshaft', 'face': '3'}], - 'parameters': [{'name': 'dp', - 'title': 'piston diameter', - 'value': '2.00000000000000e+02', - 'units': 'mm'}, - {'name': 'dr', - 'title': 'rod diameter', - 'value': '1.00000000000000e+00', - 'units': 'mm'}, - {'name': 'x0', - 'title': 'chamber length at zero relative displacement', - 'value': '0.00000000000000e+00', - 'units': 'mm'}, - {'name': 'gi', 'title': 'gas type index', 'value': '1', 'units': ''}]}, - {'index': 4, - 'alias': 'pn_brp2_12', - 'component_name': 'pn_brp2', - 'library_id': 'pcd.pn_brp2', - 'submodel': 'PNRP17', - 'label': 'pneumatic piston with moving body', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}, - {'name': 'port_2', 'type': 'lshaft', 'face': '2'}, - {'name': 'port_3', 'type': 'lshaft', 'face': '2'}, - {'name': 'port_4', 'type': 'lshaft', 'face': '3'}, - {'name': 'port_5', 'type': 'lshaft', 'face': '3'}], - 'parameters': [{'name': 'dp', - 'title': 'piston diameter', - 'value': '2.00000000000000e+02', - 'units': 'mm'}, - {'name': 'dr', - 'title': 'rod diameter', - 'value': '1.00000000000000e+00', - 'units': 'mm'}, - {'name': 'x0', - 'title': 'chamber length at zero relative displacement', - 'value': '0.00000000000000e+00', - 'units': 'mm'}, - {'name': 'gi', 'title': 'gas type index', 'value': '1', 'units': ''}]}, - {'index': 5, - 'alias': 'pn_brp2_13', - 'component_name': 'pn_brp2', - 'library_id': 'pcd.pn_brp2', - 'submodel': 'PNRP17', - 'label': 'pneumatic piston with moving body', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}, - {'name': 'port_2', 'type': 'lshaft', 'face': '2'}, - {'name': 'port_3', 'type': 'lshaft', 'face': '2'}, - {'name': 'port_4', 'type': 'lshaft', 'face': '3'}, - {'name': 'port_5', 'type': 'lshaft', 'face': '3'}], - 'parameters': [{'name': 'dp', - 'title': 'piston diameter', - 'value': '2.00000000000000e+02', - 'units': 'mm'}, - {'name': 'dr', - 'title': 'rod diameter', - 'value': '1.00000000000000e+00', - 'units': 'mm'}, - {'name': 'x0', - 'title': 'chamber length at zero relative displacement', - 'value': '0.00000000000000e+00', - 'units': 'mm'}, - {'name': 'gi', 'title': 'gas type index', 'value': '1', 'units': ''}]}, - {'index': 6, - 'alias': 'pn_brp2_14', - 'component_name': 'pn_brp2', - 'library_id': 'pcd.pn_brp2', - 'submodel': 'PNRP17', - 'label': 'pneumatic piston with moving body', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}, - {'name': 'port_2', 'type': 'lshaft', 'face': '2'}, - {'name': 'port_3', 'type': 'lshaft', 'face': '2'}, - {'name': 'port_4', 'type': 'lshaft', 'face': '3'}, - {'name': 'port_5', 'type': 'lshaft', 'face': '3'}], - 'parameters': [{'name': 'dp', - 'title': 'piston diameter', - 'value': '2.00000000000000e+02', - 'units': 'mm'}, - {'name': 'dr', - 'title': 'rod diameter', - 'value': '1.00000000000000e+00', - 'units': 'mm'}, - {'name': 'x0', - 'title': 'chamber length at zero relative displacement', - 'value': '0.00000000000000e+00', - 'units': 'mm'}, - {'name': 'gi', 'title': 'gas type index', 'value': '1', 'units': ''}]}, - {'index': 7, - 'alias': 'pn_brp2_15', - 'component_name': 'pn_brp2', - 'library_id': 'pcd.pn_brp2', - 'submodel': 'PNRP17', - 'label': 'pneumatic piston with moving body', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}, - {'name': 'port_2', 'type': 'lshaft', 'face': '2'}, - {'name': 'port_3', 'type': 'lshaft', 'face': '2'}, - {'name': 'port_4', 'type': 'lshaft', 'face': '3'}, - {'name': 'port_5', 'type': 'lshaft', 'face': '3'}], - 'parameters': [{'name': 'dp', - 'title': 'piston diameter', - 'value': '2.00000000000000e+02', - 'units': 'mm'}, - {'name': 'dr', - 'title': 'rod diameter', - 'value': '1.00000000000000e+00', - 'units': 'mm'}, - {'name': 'x0', - 'title': 'chamber length at zero relative displacement', - 'value': '0.00000000000000e+00', - 'units': 'mm'}, - {'name': 'gi', 'title': 'gas type index', 'value': '1', 'units': ''}]}, - {'index': 8, - 'alias': 'zeroforcesource_17', - 'component_name': 'zeroforcesource', - 'library_id': 'meca.zeroforcesource', - 'submodel': 'F000', - 'label': 'zero force source', - 'ports': [{'name': 'port_1', 'type': 'lshaft', 'face': '3'}], - 'parameters': []}, - {'index': 9, - 'alias': 'mass_friction_endstops_10', - 'component_name': 'mass_friction_endstops', - 'library_id': 'meca.mass_friction_endstops', - 'submodel': 'MECMAS21', - 'label': '1D translation mass', - 'ports': [{'name': 'port_1', 'type': 'lshaft', 'face': '3'}, - {'name': 'port_2', 'type': 'lshaft', 'face': '2'}], - 'parameters': [{'name': 'mass', 'title': 'mass', 'value': '5.00000000000000e+01', 'units': 'kg'}, - {'name': 'fstick', - 'title': 'stiction force', - 'value': '0.00000000000000e+00', - 'units': 'N'}, - {'name': 'fcoul', - 'title': 'Coulomb friction force', - 'value': '0.00000000000000e+00', - 'units': 'N'}, - {'name': 'rvisc', - 'title': 'coefficient of viscous friction', - 'value': '0.00000000000000e+00', - 'units': 'N/(m/s)'}, - {'name': 'wind', - 'title': 'coefficient of windage', - 'value': '0.00000000000000e+00', - 'units': 'N/(m/s)**2'}, - {'name': 'dvel', - 'title': 'stick velocity threshold', - 'value': '1.00000000000000e-06', - 'units': 'm/s'}, - {'name': 'restdvel', - 'title': 'velocity threshold', - 'value': '1.00000000000000e-06', - 'units': 'm/s'}, - {'name': 'restcoeff', - 'title': 'restitution coefficient', - 'value': '6.50000000000000e-01', - 'units': 'null'}, - {'name': 'astrib', - 'title': 'Stribeck constant', - 'value': '1.00000000000000e-03', - 'units': 'm/s'}, - {'name': 'xmin', - 'title': 'lower displacement limit', - 'value': '-1.00000000000000e+00', - 'units': 'm'}, - {'name': 'Kbmin', - 'title': 'lower limit stiffness', - 'value': '1.00000000000000e+06', - 'units': 'N/mm'}, - {'name': 'Dbmin', - 'title': 'lower limit contact damping coefficient', - 'value': '1.00000000000000e+01', - 'units': 'N/(mm/s)'}, - {'name': 'Pdmin', - 'title': 'lower limit penetration for full damping', - 'value': '1.00000000000000e-01', - 'units': 'mm'}, - {'name': 'xmax', - 'title': 'higher displacement limit', - 'value': '8.00000000000000e-01', - 'units': 'm'}, - {'name': 'Kbmax', - 'title': 'higher limit stiffness', - 'value': '1.00000000000000e+06', - 'units': 'N/mm'}, - {'name': 'Dbmax', - 'title': 'higher limit contact damping coefficient', - 'value': '1.00000000000000e+01', - 'units': 'N/(mm/s)'}, - {'name': 'Pdmax', - 'title': 'higher limit penetration for full damping', - 'value': '1.00000000000000e-01', - 'units': 'mm'}, - {'name': 'theta', - 'title': 'inclination (+90 port 1 lowest, -90 port 1 highest)', - 'value': '0.00000000000000e+00', - 'units': 'degree'}, - {'name': 'useFriction', 'title': 'use friction', 'value': '1', 'units': ''}, - {'name': 'stoptype', 'title': 'endstop type', 'value': '4', 'units': ''}, - {'name': 'discContactOption', - 'title': 'negative contact force', - 'value': '1', - 'units': ''}, - {'name': 'strib', 'title': 'Stribeck effect', 'value': '1', 'units': ''}, - {'name': 'frictionType', 'title': 'friction type', 'value': '1', 'units': ''}]}, - {'index': 10, - 'alias': 'zeroforcesource_18', - 'component_name': 'zeroforcesource', - 'library_id': 'meca.zeroforcesource', - 'submodel': 'F000', - 'label': 'zero force source', - 'ports': [{'name': 'port_1', 'type': 'lshaft', 'face': '3'}], - 'parameters': []}, - {'index': 11, - 'alias': 'mass_friction_endstops_11', - 'component_name': 'mass_friction_endstops', - 'library_id': 'meca.mass_friction_endstops', - 'submodel': 'MECMAS21', - 'label': '1D translation mass', - 'ports': [{'name': 'port_1', 'type': 'lshaft', 'face': '3'}, - {'name': 'port_2', 'type': 'lshaft', 'face': '2'}], - 'parameters': [{'name': 'mass', 'title': 'mass', 'value': '5.00000000000000e+01', 'units': 'kg'}, - {'name': 'fstick', - 'title': 'stiction force', - 'value': '0.00000000000000e+00', - 'units': 'N'}, - {'name': 'fcoul', - 'title': 'Coulomb friction force', - 'value': '0.00000000000000e+00', - 'units': 'N'}, - {'name': 'rvisc', - 'title': 'coefficient of viscous friction', - 'value': '0.00000000000000e+00', - 'units': 'N/(m/s)'}, - {'name': 'wind', - 'title': 'coefficient of windage', - 'value': '0.00000000000000e+00', - 'units': 'N/(m/s)**2'}, - {'name': 'dvel', - 'title': 'stick velocity threshold', - 'value': '1.00000000000000e-06', - 'units': 'm/s'}, - {'name': 'restdvel', - 'title': 'velocity threshold', - 'value': '1.00000000000000e-06', - 'units': 'm/s'}, - {'name': 'restcoeff', - 'title': 'restitution coefficient', - 'value': '6.50000000000000e-01', - 'units': 'null'}, - {'name': 'astrib', - 'title': 'Stribeck constant', - 'value': '1.00000000000000e-03', - 'units': 'm/s'}, - {'name': 'xmin', - 'title': 'lower displacement limit', - 'value': '-1.00000000000000e+00', - 'units': 'm'}, - {'name': 'Kbmin', - 'title': 'lower limit stiffness', - 'value': '1.00000000000000e+06', - 'units': 'N/mm'}, - {'name': 'Dbmin', - 'title': 'lower limit contact damping coefficient', - 'value': '1.00000000000000e+01', - 'units': 'N/(mm/s)'}, - {'name': 'Pdmin', - 'title': 'lower limit penetration for full damping', - 'value': '1.00000000000000e-01', - 'units': 'mm'}, - {'name': 'xmax', - 'title': 'higher displacement limit', - 'value': '8.00000000000000e-01', - 'units': 'm'}, - {'name': 'Kbmax', - 'title': 'higher limit stiffness', - 'value': '1.00000000000000e+06', - 'units': 'N/mm'}, - {'name': 'Dbmax', - 'title': 'higher limit contact damping coefficient', - 'value': '1.00000000000000e+01', - 'units': 'N/(mm/s)'}, - {'name': 'Pdmax', - 'title': 'higher limit penetration for full damping', - 'value': '1.00000000000000e-01', - 'units': 'mm'}, - {'name': 'theta', - 'title': 'inclination (+90 port 1 lowest, -90 port 1 highest)', - 'value': '0.00000000000000e+00', - 'units': 'degree'}, - {'name': 'useFriction', 'title': 'use friction', 'value': '1', 'units': ''}, - {'name': 'stoptype', 'title': 'endstop type', 'value': '4', 'units': ''}, - {'name': 'discContactOption', - 'title': 'negative contact force', - 'value': '1', - 'units': ''}, - {'name': 'strib', 'title': 'Stribeck effect', 'value': '1', 'units': ''}, - {'name': 'frictionType', 'title': 'friction type', 'value': '1', 'units': ''}]}, - {'index': 12, - 'alias': 'zeroforcesource_19', - 'component_name': 'zeroforcesource', - 'library_id': 'meca.zeroforcesource', - 'submodel': 'F000', - 'label': 'zero force source', - 'ports': [{'name': 'port_1', 'type': 'lshaft', 'face': '3'}], - 'parameters': []}, - {'index': 13, - 'alias': 'mass_friction_endstops_12', - 'component_name': 'mass_friction_endstops', - 'library_id': 'meca.mass_friction_endstops', - 'submodel': 'MECMAS21', - 'label': '1D translation mass', - 'ports': [{'name': 'port_1', 'type': 'lshaft', 'face': '3'}, - {'name': 'port_2', 'type': 'lshaft', 'face': '2'}], - 'parameters': [{'name': 'mass', 'title': 'mass', 'value': '5.00000000000000e+01', 'units': 'kg'}, - {'name': 'fstick', - 'title': 'stiction force', - 'value': '0.00000000000000e+00', - 'units': 'N'}, - {'name': 'fcoul', - 'title': 'Coulomb friction force', - 'value': '0.00000000000000e+00', - 'units': 'N'}, - {'name': 'rvisc', - 'title': 'coefficient of viscous friction', - 'value': '0.00000000000000e+00', - 'units': 'N/(m/s)'}, - {'name': 'wind', - 'title': 'coefficient of windage', - 'value': '0.00000000000000e+00', - 'units': 'N/(m/s)**2'}, - {'name': 'dvel', - 'title': 'stick velocity threshold', - 'value': '1.00000000000000e-06', - 'units': 'm/s'}, - {'name': 'restdvel', - 'title': 'velocity threshold', - 'value': '1.00000000000000e-06', - 'units': 'm/s'}, - {'name': 'restcoeff', - 'title': 'restitution coefficient', - 'value': '6.50000000000000e-01', - 'units': 'null'}, - {'name': 'astrib', - 'title': 'Stribeck constant', - 'value': '1.00000000000000e-03', - 'units': 'm/s'}, - {'name': 'xmin', - 'title': 'lower displacement limit', - 'value': '-1.00000000000000e+00', - 'units': 'm'}, - {'name': 'Kbmin', - 'title': 'lower limit stiffness', - 'value': '1.00000000000000e+06', - 'units': 'N/mm'}, - {'name': 'Dbmin', - 'title': 'lower limit contact damping coefficient', - 'value': '1.00000000000000e+01', - 'units': 'N/(mm/s)'}, - {'name': 'Pdmin', - 'title': 'lower limit penetration for full damping', - 'value': '1.00000000000000e-01', - 'units': 'mm'}, - {'name': 'xmax', - 'title': 'higher displacement limit', - 'value': '8.00000000000000e-01', - 'units': 'm'}, - {'name': 'Kbmax', - 'title': 'higher limit stiffness', - 'value': '1.00000000000000e+06', - 'units': 'N/mm'}, - {'name': 'Dbmax', - 'title': 'higher limit contact damping coefficient', - 'value': '1.00000000000000e+01', - 'units': 'N/(mm/s)'}, - {'name': 'Pdmax', - 'title': 'higher limit penetration for full damping', - 'value': '1.00000000000000e-01', - 'units': 'mm'}, - {'name': 'theta', - 'title': 'inclination (+90 port 1 lowest, -90 port 1 highest)', - 'value': '0.00000000000000e+00', - 'units': 'degree'}, - {'name': 'useFriction', 'title': 'use friction', 'value': '1', 'units': ''}, - {'name': 'stoptype', 'title': 'endstop type', 'value': '4', 'units': ''}, - {'name': 'discContactOption', - 'title': 'negative contact force', - 'value': '1', - 'units': ''}, - {'name': 'strib', 'title': 'Stribeck effect', 'value': '1', 'units': ''}, - {'name': 'frictionType', 'title': 'friction type', 'value': '1', 'units': ''}]}, - {'index': 14, - 'alias': 'zeroforcesource_20', - 'component_name': 'zeroforcesource', - 'library_id': 'meca.zeroforcesource', - 'submodel': 'F000', - 'label': 'zero force source', - 'ports': [{'name': 'port_1', 'type': 'lshaft', 'face': '3'}], - 'parameters': []}, - {'index': 15, - 'alias': 'mass_friction_endstops_13', - 'component_name': 'mass_friction_endstops', - 'library_id': 'meca.mass_friction_endstops', - 'submodel': 'MECMAS21', - 'label': '1D translation mass', - 'ports': [{'name': 'port_1', 'type': 'lshaft', 'face': '3'}, - {'name': 'port_2', 'type': 'lshaft', 'face': '2'}], - 'parameters': [{'name': 'mass', 'title': 'mass', 'value': '5.00000000000000e+01', 'units': 'kg'}, - {'name': 'fstick', - 'title': 'stiction force', - 'value': '0.00000000000000e+00', - 'units': 'N'}, - {'name': 'fcoul', - 'title': 'Coulomb friction force', - 'value': '0.00000000000000e+00', - 'units': 'N'}, - {'name': 'rvisc', - 'title': 'coefficient of viscous friction', - 'value': '0.00000000000000e+00', - 'units': 'N/(m/s)'}, - {'name': 'wind', - 'title': 'coefficient of windage', - 'value': '0.00000000000000e+00', - 'units': 'N/(m/s)**2'}, - {'name': 'dvel', - 'title': 'stick velocity threshold', - 'value': '1.00000000000000e-06', - 'units': 'm/s'}, - {'name': 'restdvel', - 'title': 'velocity threshold', - 'value': '1.00000000000000e-06', - 'units': 'm/s'}, - {'name': 'restcoeff', - 'title': 'restitution coefficient', - 'value': '6.50000000000000e-01', - 'units': 'null'}, - {'name': 'astrib', - 'title': 'Stribeck constant', - 'value': '1.00000000000000e-03', - 'units': 'm/s'}, - {'name': 'xmin', - 'title': 'lower displacement limit', - 'value': '-1.00000000000000e+00', - 'units': 'm'}, - {'name': 'Kbmin', - 'title': 'lower limit stiffness', - 'value': '1.00000000000000e+06', - 'units': 'N/mm'}, - {'name': 'Dbmin', - 'title': 'lower limit contact damping coefficient', - 'value': '1.00000000000000e+01', - 'units': 'N/(mm/s)'}, - {'name': 'Pdmin', - 'title': 'lower limit penetration for full damping', - 'value': '1.00000000000000e-01', - 'units': 'mm'}, - {'name': 'xmax', - 'title': 'higher displacement limit', - 'value': '8.00000000000000e-01', - 'units': 'm'}, - {'name': 'Kbmax', - 'title': 'higher limit stiffness', - 'value': '1.00000000000000e+06', - 'units': 'N/mm'}, - {'name': 'Dbmax', - 'title': 'higher limit contact damping coefficient', - 'value': '1.00000000000000e+01', - 'units': 'N/(mm/s)'}, - {'name': 'Pdmax', - 'title': 'higher limit penetration for full damping', - 'value': '1.00000000000000e-01', - 'units': 'mm'}, - {'name': 'theta', - 'title': 'inclination (+90 port 1 lowest, -90 port 1 highest)', - 'value': '0.00000000000000e+00', - 'units': 'degree'}, - {'name': 'useFriction', 'title': 'use friction', 'value': '1', 'units': ''}, - {'name': 'stoptype', 'title': 'endstop type', 'value': '4', 'units': ''}, - {'name': 'discContactOption', - 'title': 'negative contact force', - 'value': '1', - 'units': ''}, - {'name': 'strib', 'title': 'Stribeck effect', 'value': '1', 'units': ''}, - {'name': 'frictionType', 'title': 'friction type', 'value': '1', 'units': ''}]}, - {'index': 16, - 'alias': 'zeroforcesource_21', - 'component_name': 'zeroforcesource', - 'library_id': 'meca.zeroforcesource', - 'submodel': 'F000', - 'label': 'zero force source', - 'ports': [{'name': 'port_1', 'type': 'lshaft', 'face': '3'}], - 'parameters': []}, - {'index': 17, - 'alias': 'mass_friction_endstops_14', - 'component_name': 'mass_friction_endstops', - 'library_id': 'meca.mass_friction_endstops', - 'submodel': 'MECMAS21', - 'label': '1D translation mass', - 'ports': [{'name': 'port_1', 'type': 'lshaft', 'face': '3'}, - {'name': 'port_2', 'type': 'lshaft', 'face': '2'}], - 'parameters': [{'name': 'mass', 'title': 'mass', 'value': '5.00000000000000e+01', 'units': 'kg'}, - {'name': 'fstick', - 'title': 'stiction force', - 'value': '0.00000000000000e+00', - 'units': 'N'}, - {'name': 'fcoul', - 'title': 'Coulomb friction force', - 'value': '0.00000000000000e+00', - 'units': 'N'}, - {'name': 'rvisc', - 'title': 'coefficient of viscous friction', - 'value': '0.00000000000000e+00', - 'units': 'N/(m/s)'}, - {'name': 'wind', - 'title': 'coefficient of windage', - 'value': '0.00000000000000e+00', - 'units': 'N/(m/s)**2'}, - {'name': 'dvel', - 'title': 'stick velocity threshold', - 'value': '1.00000000000000e-06', - 'units': 'm/s'}, - {'name': 'restdvel', - 'title': 'velocity threshold', - 'value': '1.00000000000000e-06', - 'units': 'm/s'}, - {'name': 'restcoeff', - 'title': 'restitution coefficient', - 'value': '6.50000000000000e-01', - 'units': 'null'}, - {'name': 'astrib', - 'title': 'Stribeck constant', - 'value': '1.00000000000000e-03', - 'units': 'm/s'}, - {'name': 'xmin', - 'title': 'lower displacement limit', - 'value': '-1.00000000000000e+00', - 'units': 'm'}, - {'name': 'Kbmin', - 'title': 'lower limit stiffness', - 'value': '1.00000000000000e+06', - 'units': 'N/mm'}, - {'name': 'Dbmin', - 'title': 'lower limit contact damping coefficient', - 'value': '1.00000000000000e+01', - 'units': 'N/(mm/s)'}, - {'name': 'Pdmin', - 'title': 'lower limit penetration for full damping', - 'value': '1.00000000000000e-01', - 'units': 'mm'}, - {'name': 'xmax', - 'title': 'higher displacement limit', - 'value': '8.00000000000000e-01', - 'units': 'm'}, - {'name': 'Kbmax', - 'title': 'higher limit stiffness', - 'value': '1.00000000000000e+06', - 'units': 'N/mm'}, - {'name': 'Dbmax', - 'title': 'higher limit contact damping coefficient', - 'value': '1.00000000000000e+01', - 'units': 'N/(mm/s)'}, - {'name': 'Pdmax', - 'title': 'higher limit penetration for full damping', - 'value': '1.00000000000000e-01', - 'units': 'mm'}, - {'name': 'theta', - 'title': 'inclination (+90 port 1 lowest, -90 port 1 highest)', - 'value': '0.00000000000000e+00', - 'units': 'degree'}, - {'name': 'useFriction', 'title': 'use friction', 'value': '1', 'units': ''}, - {'name': 'stoptype', 'title': 'endstop type', 'value': '4', 'units': ''}, - {'name': 'discContactOption', - 'title': 'negative contact force', - 'value': '1', - 'units': ''}, - {'name': 'strib', 'title': 'Stribeck effect', 'value': '1', 'units': ''}, - {'name': 'frictionType', 'title': 'friction type', 'value': '1', 'units': ''}]}, - {'index': 18, - 'alias': 'zeroforcesource_22', - 'component_name': 'zeroforcesource', - 'library_id': 'meca.zeroforcesource', - 'submodel': 'F000', - 'label': 'zero force source', - 'ports': [{'name': 'port_1', 'type': 'lshaft', 'face': '3'}], - 'parameters': []}, - {'index': 19, - 'alias': 'mass_friction_endstops_15', - 'component_name': 'mass_friction_endstops', - 'library_id': 'meca.mass_friction_endstops', - 'submodel': 'MECMAS21', - 'label': '1D translation mass', - 'ports': [{'name': 'port_1', 'type': 'lshaft', 'face': '3'}, - {'name': 'port_2', 'type': 'lshaft', 'face': '2'}], - 'parameters': [{'name': 'mass', 'title': 'mass', 'value': '5.00000000000000e+01', 'units': 'kg'}, - {'name': 'fstick', - 'title': 'stiction force', - 'value': '0.00000000000000e+00', - 'units': 'N'}, - {'name': 'fcoul', - 'title': 'Coulomb friction force', - 'value': '0.00000000000000e+00', - 'units': 'N'}, - {'name': 'rvisc', - 'title': 'coefficient of viscous friction', - 'value': '0.00000000000000e+00', - 'units': 'N/(m/s)'}, - {'name': 'wind', - 'title': 'coefficient of windage', - 'value': '0.00000000000000e+00', - 'units': 'N/(m/s)**2'}, - {'name': 'dvel', - 'title': 'stick velocity threshold', - 'value': '1.00000000000000e-06', - 'units': 'm/s'}, - {'name': 'restdvel', - 'title': 'velocity threshold', - 'value': '1.00000000000000e-06', - 'units': 'm/s'}, - {'name': 'restcoeff', - 'title': 'restitution coefficient', - 'value': '6.50000000000000e-01', - 'units': 'null'}, - {'name': 'astrib', - 'title': 'Stribeck constant', - 'value': '1.00000000000000e-03', - 'units': 'm/s'}, - {'name': 'xmin', - 'title': 'lower displacement limit', - 'value': '-1.00000000000000e+00', - 'units': 'm'}, - {'name': 'Kbmin', - 'title': 'lower limit stiffness', - 'value': '1.00000000000000e+06', - 'units': 'N/mm'}, - {'name': 'Dbmin', - 'title': 'lower limit contact damping coefficient', - 'value': '1.00000000000000e+01', - 'units': 'N/(mm/s)'}, - {'name': 'Pdmin', - 'title': 'lower limit penetration for full damping', - 'value': '1.00000000000000e-01', - 'units': 'mm'}, - {'name': 'xmax', - 'title': 'higher displacement limit', - 'value': '8.00000000000000e-01', - 'units': 'm'}, - {'name': 'Kbmax', - 'title': 'higher limit stiffness', - 'value': '1.00000000000000e+06', - 'units': 'N/mm'}, - {'name': 'Dbmax', - 'title': 'higher limit contact damping coefficient', - 'value': '1.00000000000000e+01', - 'units': 'N/(mm/s)'}, - {'name': 'Pdmax', - 'title': 'higher limit penetration for full damping', - 'value': '1.00000000000000e-01', - 'units': 'mm'}, - {'name': 'theta', - 'title': 'inclination (+90 port 1 lowest, -90 port 1 highest)', - 'value': '0.00000000000000e+00', - 'units': 'degree'}, - {'name': 'useFriction', 'title': 'use friction', 'value': '1', 'units': ''}, - {'name': 'stoptype', 'title': 'endstop type', 'value': '4', 'units': ''}, - {'name': 'discContactOption', - 'title': 'negative contact force', - 'value': '1', - 'units': ''}, - {'name': 'strib', 'title': 'Stribeck effect', 'value': '1', 'units': ''}, - {'name': 'frictionType', 'title': 'friction type', 'value': '1', 'units': ''}]}, - {'index': 20, - 'alias': 'zeroforcesource_23', - 'component_name': 'zeroforcesource', - 'library_id': 'meca.zeroforcesource', - 'submodel': 'F000', - 'label': 'zero force source', - 'ports': [{'name': 'port_1', 'type': 'lshaft', 'face': '3'}], - 'parameters': []}, - {'index': 21, - 'alias': 'mass_friction_endstops_16', - 'component_name': 'mass_friction_endstops', - 'library_id': 'meca.mass_friction_endstops', - 'submodel': 'MECMAS21', - 'label': '1D translation mass', - 'ports': [{'name': 'port_1', 'type': 'lshaft', 'face': '3'}, - {'name': 'port_2', 'type': 'lshaft', 'face': '2'}], - 'parameters': [{'name': 'mass', 'title': 'mass', 'value': '5.00000000000000e+01', 'units': 'kg'}, - {'name': 'fstick', - 'title': 'stiction force', - 'value': '0.00000000000000e+00', - 'units': 'N'}, - {'name': 'fcoul', - 'title': 'Coulomb friction force', - 'value': '0.00000000000000e+00', - 'units': 'N'}, - {'name': 'rvisc', - 'title': 'coefficient of viscous friction', - 'value': '0.00000000000000e+00', - 'units': 'N/(m/s)'}, - {'name': 'wind', - 'title': 'coefficient of windage', - 'value': '0.00000000000000e+00', - 'units': 'N/(m/s)**2'}, - {'name': 'dvel', - 'title': 'stick velocity threshold', - 'value': '1.00000000000000e-06', - 'units': 'm/s'}, - {'name': 'restdvel', - 'title': 'velocity threshold', - 'value': '1.00000000000000e-06', - 'units': 'm/s'}, - {'name': 'restcoeff', - 'title': 'restitution coefficient', - 'value': '6.50000000000000e-01', - 'units': 'null'}, - {'name': 'astrib', - 'title': 'Stribeck constant', - 'value': '1.00000000000000e-03', - 'units': 'm/s'}, - {'name': 'xmin', - 'title': 'lower displacement limit', - 'value': '-1.00000000000000e+00', - 'units': 'm'}, - {'name': 'Kbmin', - 'title': 'lower limit stiffness', - 'value': '1.00000000000000e+06', - 'units': 'N/mm'}, - {'name': 'Dbmin', - 'title': 'lower limit contact damping coefficient', - 'value': '1.00000000000000e+01', - 'units': 'N/(mm/s)'}, - {'name': 'Pdmin', - 'title': 'lower limit penetration for full damping', - 'value': '1.00000000000000e-01', - 'units': 'mm'}, - {'name': 'xmax', - 'title': 'higher displacement limit', - 'value': '8.00000000000000e-01', - 'units': 'm'}, - {'name': 'Kbmax', - 'title': 'higher limit stiffness', - 'value': '1.00000000000000e+06', - 'units': 'N/mm'}, - {'name': 'Dbmax', - 'title': 'higher limit contact damping coefficient', - 'value': '1.00000000000000e+01', - 'units': 'N/(mm/s)'}, - {'name': 'Pdmax', - 'title': 'higher limit penetration for full damping', - 'value': '1.00000000000000e-01', - 'units': 'mm'}, - {'name': 'theta', - 'title': 'inclination (+90 port 1 lowest, -90 port 1 highest)', - 'value': '0.00000000000000e+00', - 'units': 'degree'}, - {'name': 'useFriction', 'title': 'use friction', 'value': '1', 'units': ''}, - {'name': 'stoptype', 'title': 'endstop type', 'value': '4', 'units': ''}, - {'name': 'discContactOption', - 'title': 'negative contact force', - 'value': '1', - 'units': ''}, - {'name': 'strib', 'title': 'Stribeck effect', 'value': '1', 'units': ''}, - {'name': 'frictionType', 'title': 'friction type', 'value': '1', 'units': ''}]}, - {'index': 22, - 'alias': 'zeroforcesource_24', - 'component_name': 'zeroforcesource', - 'library_id': 'meca.zeroforcesource', - 'submodel': 'F000', - 'label': 'zero force source', - 'ports': [{'name': 'port_1', 'type': 'lshaft', 'face': '3'}], - 'parameters': []}, - {'index': 23, - 'alias': 'mass_friction_endstops_17', - 'component_name': 'mass_friction_endstops', - 'library_id': 'meca.mass_friction_endstops', - 'submodel': 'MECMAS21', - 'label': '1D translation mass', - 'ports': [{'name': 'port_1', 'type': 'lshaft', 'face': '3'}, - {'name': 'port_2', 'type': 'lshaft', 'face': '2'}], - 'parameters': [{'name': 'mass', 'title': 'mass', 'value': '5.00000000000000e+01', 'units': 'kg'}, - {'name': 'fstick', - 'title': 'stiction force', - 'value': '0.00000000000000e+00', - 'units': 'N'}, - {'name': 'fcoul', - 'title': 'Coulomb friction force', - 'value': '0.00000000000000e+00', - 'units': 'N'}, - {'name': 'rvisc', - 'title': 'coefficient of viscous friction', - 'value': '0.00000000000000e+00', - 'units': 'N/(m/s)'}, - {'name': 'wind', - 'title': 'coefficient of windage', - 'value': '0.00000000000000e+00', - 'units': 'N/(m/s)**2'}, - {'name': 'dvel', - 'title': 'stick velocity threshold', - 'value': '1.00000000000000e-06', - 'units': 'm/s'}, - {'name': 'restdvel', - 'title': 'velocity threshold', - 'value': '1.00000000000000e-06', - 'units': 'm/s'}, - {'name': 'restcoeff', - 'title': 'restitution coefficient', - 'value': '6.50000000000000e-01', - 'units': 'null'}, - {'name': 'astrib', - 'title': 'Stribeck constant', - 'value': '1.00000000000000e-03', - 'units': 'm/s'}, - {'name': 'xmin', - 'title': 'lower displacement limit', - 'value': '-1.00000000000000e+00', - 'units': 'm'}, - {'name': 'Kbmin', - 'title': 'lower limit stiffness', - 'value': '1.00000000000000e+06', - 'units': 'N/mm'}, - {'name': 'Dbmin', - 'title': 'lower limit contact damping coefficient', - 'value': '1.00000000000000e+01', - 'units': 'N/(mm/s)'}, - {'name': 'Pdmin', - 'title': 'lower limit penetration for full damping', - 'value': '1.00000000000000e-01', - 'units': 'mm'}, - {'name': 'xmax', - 'title': 'higher displacement limit', - 'value': '8.00000000000000e-01', - 'units': 'm'}, - {'name': 'Kbmax', - 'title': 'higher limit stiffness', - 'value': '1.00000000000000e+06', - 'units': 'N/mm'}, - {'name': 'Dbmax', - 'title': 'higher limit contact damping coefficient', - 'value': '1.00000000000000e+01', - 'units': 'N/(mm/s)'}, - {'name': 'Pdmax', - 'title': 'higher limit penetration for full damping', - 'value': '1.00000000000000e-01', - 'units': 'mm'}, - {'name': 'theta', - 'title': 'inclination (+90 port 1 lowest, -90 port 1 highest)', - 'value': '0.00000000000000e+00', - 'units': 'degree'}, - {'name': 'useFriction', 'title': 'use friction', 'value': '1', 'units': ''}, - {'name': 'stoptype', 'title': 'endstop type', 'value': '4', 'units': ''}, - {'name': 'discContactOption', - 'title': 'negative contact force', - 'value': '1', - 'units': ''}, - {'name': 'strib', 'title': 'Stribeck effect', 'value': '1', 'units': ''}, - {'name': 'frictionType', 'title': 'friction type', 'value': '1', 'units': ''}]}, - {'index': 24, - 'alias': 'dynamic_mechanical_node_alternative_2', - 'component_name': 'dynamic_mechanical_node_alternative', - 'library_id': 'meca.dynamic_mechanical_node_alternative', - 'submodel': 'LMECHN1', - 'label': 'dynamic linear mechanical node (velocity and displacement)', - 'ports': [{'name': 'port_1', 'type': 'lshaft', 'face': '2'}, - {'name': 'port_2', 'type': 'lshaft', 'face': '2'}, - {'name': 'port_3', 'type': 'lshaft', 'face': '2'}, - {'name': 'port_4', 'type': 'lshaft', 'face': '2'}, - {'name': 'port_5', 'type': 'lshaft', 'face': '2'}, - {'name': 'port_6', 'type': 'lshaft', 'face': '2'}, - {'name': 'port_7', 'type': 'lshaft', 'face': '2'}, - {'name': 'port_8', 'type': 'lshaft', 'face': '2'}, - {'name': 'port_9', 'type': 'lshaft', 'face': '3'}], - 'parameters': [{'name': 'v1', 'title': 'number of ports at right', 'value': '8', 'units': ''}, - {'name': 'sum', 'title': 'node sum', 'value': '1', 'units': ''}]}, - {'index': 25, - 'alias': 'mass_friction_endstops_18', - 'component_name': 'mass_friction_endstops', - 'library_id': 'meca.mass_friction_endstops', - 'submodel': 'MECMAS21', - 'label': '1D translation mass', - 'ports': [{'name': 'port_1', 'type': 'lshaft', 'face': '3'}, - {'name': 'port_2', 'type': 'lshaft', 'face': '2'}], - 'parameters': [{'name': 'mass', 'title': 'mass', 'value': '1.70000000000000e+05', 'units': 'kg'}, - {'name': 'fstick', - 'title': 'stiction force', - 'value': '0.00000000000000e+00', - 'units': 'N'}, - {'name': 'fcoul', - 'title': 'Coulomb friction force', - 'value': '0.00000000000000e+00', - 'units': 'N'}, - {'name': 'rvisc', - 'title': 'coefficient of viscous friction', - 'value': '0.00000000000000e+00', - 'units': 'N/(m/s)'}, - {'name': 'wind', - 'title': 'coefficient of windage', - 'value': '0.00000000000000e+00', - 'units': 'N/(m/s)**2'}, - {'name': 'dvel', - 'title': 'stick velocity threshold', - 'value': '1.00000000000000e-06', - 'units': 'm/s'}, - {'name': 'restdvel', - 'title': 'velocity threshold', - 'value': '1.00000000000000e-06', - 'units': 'm/s'}, - {'name': 'restcoeff', - 'title': 'restitution coefficient', - 'value': '6.50000000000000e-01', - 'units': 'null'}, - {'name': 'astrib', - 'title': 'Stribeck constant', - 'value': '1.00000000000000e-03', - 'units': 'm/s'}, - {'name': 'xmin', - 'title': 'lower displacement limit', - 'value': '0.00000000000000e+00', - 'units': 'm'}, - {'name': 'Kbmin', - 'title': 'lower limit stiffness', - 'value': '1.00000000000000e+06', - 'units': 'N/mm'}, - {'name': 'Dbmin', - 'title': 'lower limit contact damping coefficient', - 'value': '1.00000000000000e+01', - 'units': 'N/(mm/s)'}, - {'name': 'Pdmin', - 'title': 'lower limit penetration for full damping', - 'value': '1.00000000000000e-01', - 'units': 'mm'}, - {'name': 'xmax', - 'title': 'higher displacement limit', - 'value': '3.70000000000000e-01', - 'units': 'm'}, - {'name': 'Kbmax', - 'title': 'higher limit stiffness', - 'value': '1.00000000000000e+06', - 'units': 'N/mm'}, - {'name': 'Dbmax', - 'title': 'higher limit contact damping coefficient', - 'value': '1.00000000000000e+01', - 'units': 'N/(mm/s)'}, - {'name': 'Pdmax', - 'title': 'higher limit penetration for full damping', - 'value': '1.00000000000000e-01', - 'units': 'mm'}, - {'name': 'theta', - 'title': 'inclination (+90 port 1 lowest, -90 port 1 highest)', - 'value': '0.00000000000000e+00', - 'units': 'degree'}, - {'name': 'useFriction', 'title': 'use friction', 'value': '1', 'units': ''}, - {'name': 'stoptype', 'title': 'endstop type', 'value': '1', 'units': ''}, - {'name': 'discContactOption', - 'title': 'negative contact force', - 'value': '1', - 'units': ''}, - {'name': 'strib', 'title': 'Stribeck effect', 'value': '1', 'units': ''}, - {'name': 'frictionType', 'title': 'friction type', 'value': '1', 'units': ''}]}, - {'index': 26, - 'alias': 'elasticendstop_8', - 'component_name': 'elasticendstop', - 'library_id': 'meca.elasticendstop', - 'submodel': 'LSTP00A', - 'label': 'elastic contact (no states)', - 'ports': [{'name': 'port_1', 'type': 'lshaft', 'face': '2'}, - {'name': 'port_2', 'type': 'lshaft', 'face': '3'}], - 'parameters': [{'name': 'na', - 'title': 'number of active coils', - 'value': '1.00000000000000e+01', - 'units': 'null'}, - {'name': 'gap0', - 'title': 'gap or clearance with both displacements zero', - 'value': '0.00000000000000e+00', - 'units': 'mm'}, - {'name': 'kcont', - 'title': 'contact stiffness', - 'value': '1.00000000000000e+11', - 'units': 'N/m'}, - {'name': 'G', - 'title': 'material shear modulus', - 'value': '8.57000000000000e+10', - 'units': 'N/m**2'}, - {'name': 'sdiam', - 'title': 'spring diameter', - 'value': '2.00000000000000e+01', - 'units': 'mm'}, - {'name': 'wdiam', - 'title': 'wire diameter', - 'value': '2.00000000000000e+00', - 'units': 'mm'}, - {'name': 'rcont', - 'title': 'contact damping', - 'value': '1.00000000000000e+11', - 'units': 'N/(m/s)'}, - {'name': 'Pdis', - 'title': 'penetration for full damping', - 'value': '1.00000000000000e-04', - 'units': 'mm'}, - {'name': 'stiffmode', 'title': 'spring stiffness mode', 'value': '1', 'units': ''}, - {'name': 'discContactOption', - 'title': 'negative contact force', - 'value': '1', - 'units': ''}]}, - {'index': 27, - 'alias': 'elasticendstop_9', - 'component_name': 'elasticendstop', - 'library_id': 'meca.elasticendstop', - 'submodel': 'LSTP00A', - 'label': 'elastic contact (no states)', - 'ports': [{'name': 'port_1', 'type': 'lshaft', 'face': '2'}, - {'name': 'port_2', 'type': 'lshaft', 'face': '3'}], - 'parameters': [{'name': 'na', - 'title': 'number of active coils', - 'value': '1.00000000000000e+01', - 'units': 'null'}, - {'name': 'gap0', - 'title': 'gap or clearance with both displacements zero', - 'value': '0.00000000000000e+00', - 'units': 'mm'}, - {'name': 'kcont', - 'title': 'contact stiffness', - 'value': '1.00000000000000e+11', - 'units': 'N/m'}, - {'name': 'G', - 'title': 'material shear modulus', - 'value': '8.57000000000000e+10', - 'units': 'N/m**2'}, - {'name': 'sdiam', - 'title': 'spring diameter', - 'value': '2.00000000000000e+01', - 'units': 'mm'}, - {'name': 'wdiam', - 'title': 'wire diameter', - 'value': '2.00000000000000e+00', - 'units': 'mm'}, - {'name': 'rcont', - 'title': 'contact damping', - 'value': '1.00000000000000e+11', - 'units': 'N/(m/s)'}, - {'name': 'Pdis', - 'title': 'penetration for full damping', - 'value': '1.00000000000000e-04', - 'units': 'mm'}, - {'name': 'stiffmode', 'title': 'spring stiffness mode', 'value': '1', 'units': ''}, - {'name': 'discContactOption', - 'title': 'negative contact force', - 'value': '1', - 'units': ''}]}, - {'index': 28, - 'alias': 'elasticendstop_10', - 'component_name': 'elasticendstop', - 'library_id': 'meca.elasticendstop', - 'submodel': 'LSTP00A', - 'label': 'elastic contact (no states)', - 'ports': [{'name': 'port_1', 'type': 'lshaft', 'face': '2'}, - {'name': 'port_2', 'type': 'lshaft', 'face': '3'}], - 'parameters': [{'name': 'na', - 'title': 'number of active coils', - 'value': '1.00000000000000e+01', - 'units': 'null'}, - {'name': 'gap0', - 'title': 'gap or clearance with both displacements zero', - 'value': '0.00000000000000e+00', - 'units': 'mm'}, - {'name': 'kcont', - 'title': 'contact stiffness', - 'value': '1.00000000000000e+11', - 'units': 'N/m'}, - {'name': 'G', - 'title': 'material shear modulus', - 'value': '8.57000000000000e+10', - 'units': 'N/m**2'}, - {'name': 'sdiam', - 'title': 'spring diameter', - 'value': '2.00000000000000e+01', - 'units': 'mm'}, - {'name': 'wdiam', - 'title': 'wire diameter', - 'value': '2.00000000000000e+00', - 'units': 'mm'}, - {'name': 'rcont', - 'title': 'contact damping', - 'value': '1.00000000000000e+11', - 'units': 'N/(m/s)'}, - {'name': 'Pdis', - 'title': 'penetration for full damping', - 'value': '1.00000000000000e-04', - 'units': 'mm'}, - {'name': 'stiffmode', 'title': 'spring stiffness mode', 'value': '1', 'units': ''}, - {'name': 'discContactOption', - 'title': 'negative contact force', - 'value': '1', - 'units': ''}]}, - {'index': 29, - 'alias': 'elasticendstop_11', - 'component_name': 'elasticendstop', - 'library_id': 'meca.elasticendstop', - 'submodel': 'LSTP00A', - 'label': 'elastic contact (no states)', - 'ports': [{'name': 'port_1', 'type': 'lshaft', 'face': '2'}, - {'name': 'port_2', 'type': 'lshaft', 'face': '3'}], - 'parameters': [{'name': 'na', - 'title': 'number of active coils', - 'value': '1.00000000000000e+01', - 'units': 'null'}, - {'name': 'gap0', - 'title': 'gap or clearance with both displacements zero', - 'value': '0.00000000000000e+00', - 'units': 'mm'}, - {'name': 'kcont', - 'title': 'contact stiffness', - 'value': '1.00000000000000e+11', - 'units': 'N/m'}, - {'name': 'G', - 'title': 'material shear modulus', - 'value': '8.57000000000000e+10', - 'units': 'N/m**2'}, - {'name': 'sdiam', - 'title': 'spring diameter', - 'value': '2.00000000000000e+01', - 'units': 'mm'}, - {'name': 'wdiam', - 'title': 'wire diameter', - 'value': '2.00000000000000e+00', - 'units': 'mm'}, - {'name': 'rcont', - 'title': 'contact damping', - 'value': '1.00000000000000e+11', - 'units': 'N/(m/s)'}, - {'name': 'Pdis', - 'title': 'penetration for full damping', - 'value': '1.00000000000000e-04', - 'units': 'mm'}, - {'name': 'stiffmode', 'title': 'spring stiffness mode', 'value': '1', 'units': ''}, - {'name': 'discContactOption', - 'title': 'negative contact force', - 'value': '1', - 'units': ''}]}, - {'index': 30, - 'alias': 'elasticendstop_12', - 'component_name': 'elasticendstop', - 'library_id': 'meca.elasticendstop', - 'submodel': 'LSTP00A', - 'label': 'elastic contact (no states)', - 'ports': [{'name': 'port_1', 'type': 'lshaft', 'face': '2'}, - {'name': 'port_2', 'type': 'lshaft', 'face': '3'}], - 'parameters': [{'name': 'na', - 'title': 'number of active coils', - 'value': '1.00000000000000e+01', - 'units': 'null'}, - {'name': 'gap0', - 'title': 'gap or clearance with both displacements zero', - 'value': '0.00000000000000e+00', - 'units': 'mm'}, - {'name': 'kcont', - 'title': 'contact stiffness', - 'value': '1.00000000000000e+11', - 'units': 'N/m'}, - {'name': 'G', - 'title': 'material shear modulus', - 'value': '8.57000000000000e+10', - 'units': 'N/m**2'}, - {'name': 'sdiam', - 'title': 'spring diameter', - 'value': '2.00000000000000e+01', - 'units': 'mm'}, - {'name': 'wdiam', - 'title': 'wire diameter', - 'value': '2.00000000000000e+00', - 'units': 'mm'}, - {'name': 'rcont', - 'title': 'contact damping', - 'value': '1.00000000000000e+11', - 'units': 'N/(m/s)'}, - {'name': 'Pdis', - 'title': 'penetration for full damping', - 'value': '1.00000000000000e-04', - 'units': 'mm'}, - {'name': 'stiffmode', 'title': 'spring stiffness mode', 'value': '1', 'units': ''}, - {'name': 'discContactOption', - 'title': 'negative contact force', - 'value': '1', - 'units': ''}]}, - {'index': 31, - 'alias': 'elasticendstop_13', - 'component_name': 'elasticendstop', - 'library_id': 'meca.elasticendstop', - 'submodel': 'LSTP00A', - 'label': 'elastic contact (no states)', - 'ports': [{'name': 'port_1', 'type': 'lshaft', 'face': '2'}, - {'name': 'port_2', 'type': 'lshaft', 'face': '3'}], - 'parameters': [{'name': 'na', - 'title': 'number of active coils', - 'value': '1.00000000000000e+01', - 'units': 'null'}, - {'name': 'gap0', - 'title': 'gap or clearance with both displacements zero', - 'value': '0.00000000000000e+00', - 'units': 'mm'}, - {'name': 'kcont', - 'title': 'contact stiffness', - 'value': '1.00000000000000e+11', - 'units': 'N/m'}, - {'name': 'G', - 'title': 'material shear modulus', - 'value': '8.57000000000000e+10', - 'units': 'N/m**2'}, - {'name': 'sdiam', - 'title': 'spring diameter', - 'value': '2.00000000000000e+01', - 'units': 'mm'}, - {'name': 'wdiam', - 'title': 'wire diameter', - 'value': '2.00000000000000e+00', - 'units': 'mm'}, - {'name': 'rcont', - 'title': 'contact damping', - 'value': '1.00000000000000e+11', - 'units': 'N/(m/s)'}, - {'name': 'Pdis', - 'title': 'penetration for full damping', - 'value': '1.00000000000000e-04', - 'units': 'mm'}, - {'name': 'stiffmode', 'title': 'spring stiffness mode', 'value': '1', 'units': ''}, - {'name': 'discContactOption', - 'title': 'negative contact force', - 'value': '1', - 'units': ''}]}, - {'index': 32, - 'alias': 'elasticendstop_14', - 'component_name': 'elasticendstop', - 'library_id': 'meca.elasticendstop', - 'submodel': 'LSTP00A', - 'label': 'elastic contact (no states)', - 'ports': [{'name': 'port_1', 'type': 'lshaft', 'face': '2'}, - {'name': 'port_2', 'type': 'lshaft', 'face': '3'}], - 'parameters': [{'name': 'na', - 'title': 'number of active coils', - 'value': '1.00000000000000e+01', - 'units': 'null'}, - {'name': 'gap0', - 'title': 'gap or clearance with both displacements zero', - 'value': '0.00000000000000e+00', - 'units': 'mm'}, - {'name': 'kcont', - 'title': 'contact stiffness', - 'value': '1.00000000000000e+11', - 'units': 'N/m'}, - {'name': 'G', - 'title': 'material shear modulus', - 'value': '8.57000000000000e+10', - 'units': 'N/m**2'}, - {'name': 'sdiam', - 'title': 'spring diameter', - 'value': '2.00000000000000e+01', - 'units': 'mm'}, - {'name': 'wdiam', - 'title': 'wire diameter', - 'value': '2.00000000000000e+00', - 'units': 'mm'}, - {'name': 'rcont', - 'title': 'contact damping', - 'value': '1.00000000000000e+11', - 'units': 'N/(m/s)'}, - {'name': 'Pdis', - 'title': 'penetration for full damping', - 'value': '1.00000000000000e-04', - 'units': 'mm'}, - {'name': 'stiffmode', 'title': 'spring stiffness mode', 'value': '1', 'units': ''}, - {'name': 'discContactOption', - 'title': 'negative contact force', - 'value': '1', - 'units': ''}]}, - {'index': 33, - 'alias': 'elasticendstop_15', - 'component_name': 'elasticendstop', - 'library_id': 'meca.elasticendstop', - 'submodel': 'LSTP00A', - 'label': 'elastic contact (no states)', - 'ports': [{'name': 'port_1', 'type': 'lshaft', 'face': '2'}, - {'name': 'port_2', 'type': 'lshaft', 'face': '3'}], - 'parameters': [{'name': 'na', - 'title': 'number of active coils', - 'value': '1.00000000000000e+01', - 'units': 'null'}, - {'name': 'gap0', - 'title': 'gap or clearance with both displacements zero', - 'value': '0.00000000000000e+00', - 'units': 'mm'}, - {'name': 'kcont', - 'title': 'contact stiffness', - 'value': '1.00000000000000e+11', - 'units': 'N/m'}, - {'name': 'G', - 'title': 'material shear modulus', - 'value': '8.57000000000000e+10', - 'units': 'N/m**2'}, - {'name': 'sdiam', - 'title': 'spring diameter', - 'value': '2.00000000000000e+01', - 'units': 'mm'}, - {'name': 'wdiam', - 'title': 'wire diameter', - 'value': '2.00000000000000e+00', - 'units': 'mm'}, - {'name': 'rcont', - 'title': 'contact damping', - 'value': '1.00000000000000e+11', - 'units': 'N/(m/s)'}, - {'name': 'Pdis', - 'title': 'penetration for full damping', - 'value': '1.00000000000000e-04', - 'units': 'mm'}, - {'name': 'stiffmode', 'title': 'spring stiffness mode', 'value': '1', 'units': ''}, - {'name': 'discContactOption', - 'title': 'negative contact force', - 'value': '1', - 'units': ''}]}, - {'index': 34, - 'alias': 'dynamic_mechanical_node_alternative_3', - 'component_name': 'dynamic_mechanical_node_alternative', - 'library_id': 'meca.dynamic_mechanical_node_alternative', - 'submodel': 'LMECHN1', - 'label': 'dynamic linear mechanical node (velocity and displacement)', - 'ports': [{'name': 'port_1', 'type': 'lshaft', 'face': '3'}, - {'name': 'port_2', 'type': 'lshaft', 'face': '3'}, - {'name': 'port_3', 'type': 'lshaft', 'face': '3'}, - {'name': 'port_4', 'type': 'lshaft', 'face': '3'}, - {'name': 'port_5', 'type': 'lshaft', 'face': '3'}, - {'name': 'port_6', 'type': 'lshaft', 'face': '3'}, - {'name': 'port_7', 'type': 'lshaft', 'face': '3'}, - {'name': 'port_8', 'type': 'lshaft', 'face': '3'}, - {'name': 'port_9', 'type': 'lshaft', 'face': '2'}], - 'parameters': [{'name': 'v1', 'title': 'number of ports at right', 'value': '8', 'units': ''}, - {'name': 'sum', 'title': 'node sum', 'value': '1', 'units': ''}]}, - {'index': 35, - 'alias': 'mass_friction_endstops_19', - 'component_name': 'mass_friction_endstops', - 'library_id': 'meca.mass_friction_endstops', - 'submodel': 'MECMAS21', - 'label': '1D translation mass', - 'ports': [{'name': 'port_1', 'type': 'lshaft', 'face': '3'}, - {'name': 'port_2', 'type': 'lshaft', 'face': '2'}], - 'parameters': [{'name': 'mass', 'title': 'mass', 'value': '9.00000000000000e+04', 'units': 'kg'}, - {'name': 'fstick', - 'title': 'stiction force', - 'value': '0.00000000000000e+00', - 'units': 'N'}, - {'name': 'fcoul', - 'title': 'Coulomb friction force', - 'value': '0.00000000000000e+00', - 'units': 'N'}, - {'name': 'rvisc', - 'title': 'coefficient of viscous friction', - 'value': '0.00000000000000e+00', - 'units': 'N/(m/s)'}, - {'name': 'wind', - 'title': 'coefficient of windage', - 'value': '0.00000000000000e+00', - 'units': 'N/(m/s)**2'}, - {'name': 'dvel', - 'title': 'stick velocity threshold', - 'value': '1.00000000000000e-06', - 'units': 'm/s'}, - {'name': 'restdvel', - 'title': 'velocity threshold', - 'value': '1.00000000000000e-06', - 'units': 'm/s'}, - {'name': 'restcoeff', - 'title': 'restitution coefficient', - 'value': '6.50000000000000e-01', - 'units': 'null'}, - {'name': 'astrib', - 'title': 'Stribeck constant', - 'value': '1.00000000000000e-03', - 'units': 'm/s'}, - {'name': 'xmin', - 'title': 'lower displacement limit', - 'value': '-7.20000000000000e-01', - 'units': 'm'}, - {'name': 'Kbmin', - 'title': 'lower limit stiffness', - 'value': '1.00000000000000e+06', - 'units': 'N/mm'}, - {'name': 'Dbmin', - 'title': 'lower limit contact damping coefficient', - 'value': '1.00000000000000e+01', - 'units': 'N/(mm/s)'}, - {'name': 'Pdmin', - 'title': 'lower limit penetration for full damping', - 'value': '1.00000000000000e-01', - 'units': 'mm'}, - {'name': 'xmax', - 'title': 'higher displacement limit', - 'value': '0.00000000000000e+00', - 'units': 'm'}, - {'name': 'Kbmax', - 'title': 'higher limit stiffness', - 'value': '1.00000000000000e+06', - 'units': 'N/mm'}, - {'name': 'Dbmax', - 'title': 'higher limit contact damping coefficient', - 'value': '1.00000000000000e+01', - 'units': 'N/(mm/s)'}, - {'name': 'Pdmax', - 'title': 'higher limit penetration for full damping', - 'value': '1.00000000000000e-01', - 'units': 'mm'}, - {'name': 'theta', - 'title': 'inclination (+90 port 1 lowest, -90 port 1 highest)', - 'value': '0.00000000000000e+00', - 'units': 'degree'}, - {'name': 'useFriction', 'title': 'use friction', 'value': '1', 'units': ''}, - {'name': 'stoptype', 'title': 'endstop type', 'value': '1', 'units': ''}, - {'name': 'discContactOption', - 'title': 'negative contact force', - 'value': '1', - 'units': ''}, - {'name': 'strib', 'title': 'Stribeck effect', 'value': '1', 'units': ''}, - {'name': 'frictionType', 'title': 'friction type', 'value': '1', 'units': ''}]}, - {'index': 36, - 'alias': 'pn_c1_8', - 'component_name': 'pn_c1', - 'library_id': 'pcd.pn_c1', - 'submodel': 'PNCH012', - 'label': 'variable volume pneumatic chamber with heat exchange (preferred)', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '2'}, - {'name': 'port_2', 'type': 'pflow', 'face': '1'}, - {'name': 'port_3', 'type': 'pflow', 'face': '3'}, - {'name': 'port_4', 'type': 'pflow', 'face': '0'}], - 'parameters': [{'name': 'cvol0', 'title': 'dead volume', 'value': 'V', 'units': 'L'}, - {'name': 'kth', - 'title': 'thermal exchange coefficient', - 'value': '1.50000000000000e+03', - 'units': 'J/m**2/K/s'}, - {'name': 'sth', - 'title': 'thermal exchange area', - 'value': '7.00000000000000e-01', - 'units': 'm**2'}, - {'name': 'extemp', - 'title': 'external temperature', - 'value': '2.93150000000000e+02', - 'units': 'K'}, - {'name': 'gi', 'title': 'gas type index', 'value': '1', 'units': ''}]}, - {'index': 37, - 'alias': 'pn_plug_16', - 'component_name': 'pn_plug', - 'library_id': 'pn.pn_plug', - 'submodel': 'PNPL01', - 'label': 'zero pneumatic flow source', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}], - 'parameters': []}, - {'index': 38, - 'alias': 'pn_plug_17', - 'component_name': 'pn_plug', - 'library_id': 'pn.pn_plug', - 'submodel': 'PNPL01', - 'label': 'zero pneumatic flow source', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '0'}], - 'parameters': []}, - {'index': 39, - 'alias': 'pn_c1_9', - 'component_name': 'pn_c1', - 'library_id': 'pcd.pn_c1', - 'submodel': 'PNCH012', - 'label': 'variable volume pneumatic chamber with heat exchange (preferred)', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '2'}, - {'name': 'port_2', 'type': 'pflow', 'face': '1'}, - {'name': 'port_3', 'type': 'pflow', 'face': '3'}, - {'name': 'port_4', 'type': 'pflow', 'face': '0'}], - 'parameters': [{'name': 'cvol0', 'title': 'dead volume', 'value': 'V', 'units': 'L'}, - {'name': 'kth', - 'title': 'thermal exchange coefficient', - 'value': '1.50000000000000e+03', - 'units': 'J/m**2/K/s'}, - {'name': 'sth', - 'title': 'thermal exchange area', - 'value': '7.00000000000000e-01', - 'units': 'm**2'}, - {'name': 'extemp', - 'title': 'external temperature', - 'value': '2.93150000000000e+02', - 'units': 'K'}, - {'name': 'gi', 'title': 'gas type index', 'value': '1', 'units': ''}]}, - {'index': 40, - 'alias': 'pn_plug_18', - 'component_name': 'pn_plug', - 'library_id': 'pn.pn_plug', - 'submodel': 'PNPL01', - 'label': 'zero pneumatic flow source', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}], - 'parameters': []}, - {'index': 41, - 'alias': 'pn_plug_19', - 'component_name': 'pn_plug', - 'library_id': 'pn.pn_plug', - 'submodel': 'PNPL01', - 'label': 'zero pneumatic flow source', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '0'}], - 'parameters': []}, - {'index': 42, - 'alias': 'pn_c1_10', - 'component_name': 'pn_c1', - 'library_id': 'pcd.pn_c1', - 'submodel': 'PNCH012', - 'label': 'variable volume pneumatic chamber with heat exchange (preferred)', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '2'}, - {'name': 'port_2', 'type': 'pflow', 'face': '1'}, - {'name': 'port_3', 'type': 'pflow', 'face': '3'}, - {'name': 'port_4', 'type': 'pflow', 'face': '0'}], - 'parameters': [{'name': 'cvol0', 'title': 'dead volume', 'value': 'V', 'units': 'L'}, - {'name': 'kth', - 'title': 'thermal exchange coefficient', - 'value': '1.50000000000000e+03', - 'units': 'J/m**2/K/s'}, - {'name': 'sth', - 'title': 'thermal exchange area', - 'value': '7.00000000000000e-01', - 'units': 'm**2'}, - {'name': 'extemp', - 'title': 'external temperature', - 'value': '2.93150000000000e+02', - 'units': 'K'}, - {'name': 'gi', 'title': 'gas type index', 'value': '1', 'units': ''}]}, - {'index': 43, - 'alias': 'pn_plug_20', - 'component_name': 'pn_plug', - 'library_id': 'pn.pn_plug', - 'submodel': 'PNPL01', - 'label': 'zero pneumatic flow source', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}], - 'parameters': []}, - {'index': 44, - 'alias': 'pn_plug_21', - 'component_name': 'pn_plug', - 'library_id': 'pn.pn_plug', - 'submodel': 'PNPL01', - 'label': 'zero pneumatic flow source', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '0'}], - 'parameters': []}, - {'index': 45, - 'alias': 'pn_c1_11', - 'component_name': 'pn_c1', - 'library_id': 'pcd.pn_c1', - 'submodel': 'PNCH012', - 'label': 'variable volume pneumatic chamber with heat exchange (preferred)', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '2'}, - {'name': 'port_2', 'type': 'pflow', 'face': '1'}, - {'name': 'port_3', 'type': 'pflow', 'face': '3'}, - {'name': 'port_4', 'type': 'pflow', 'face': '0'}], - 'parameters': [{'name': 'cvol0', 'title': 'dead volume', 'value': 'V', 'units': 'L'}, - {'name': 'kth', - 'title': 'thermal exchange coefficient', - 'value': '1.50000000000000e+03', - 'units': 'J/m**2/K/s'}, - {'name': 'sth', - 'title': 'thermal exchange area', - 'value': '7.00000000000000e-01', - 'units': 'm**2'}, - {'name': 'extemp', - 'title': 'external temperature', - 'value': '2.93150000000000e+02', - 'units': 'K'}, - {'name': 'gi', 'title': 'gas type index', 'value': '1', 'units': ''}]}, - {'index': 46, - 'alias': 'pn_plug_22', - 'component_name': 'pn_plug', - 'library_id': 'pn.pn_plug', - 'submodel': 'PNPL01', - 'label': 'zero pneumatic flow source', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}], - 'parameters': []}, - {'index': 47, - 'alias': 'pn_plug_23', - 'component_name': 'pn_plug', - 'library_id': 'pn.pn_plug', - 'submodel': 'PNPL01', - 'label': 'zero pneumatic flow source', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '0'}], - 'parameters': []}, - {'index': 48, - 'alias': 'pn_c1_12', - 'component_name': 'pn_c1', - 'library_id': 'pcd.pn_c1', - 'submodel': 'PNCH012', - 'label': 'variable volume pneumatic chamber with heat exchange (preferred)', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '2'}, - {'name': 'port_2', 'type': 'pflow', 'face': '1'}, - {'name': 'port_3', 'type': 'pflow', 'face': '3'}, - {'name': 'port_4', 'type': 'pflow', 'face': '0'}], - 'parameters': [{'name': 'cvol0', 'title': 'dead volume', 'value': 'V', 'units': 'L'}, - {'name': 'kth', - 'title': 'thermal exchange coefficient', - 'value': '1.50000000000000e+03', - 'units': 'J/m**2/K/s'}, - {'name': 'sth', - 'title': 'thermal exchange area', - 'value': '7.00000000000000e-01', - 'units': 'm**2'}, - {'name': 'extemp', - 'title': 'external temperature', - 'value': '2.93150000000000e+02', - 'units': 'K'}, - {'name': 'gi', 'title': 'gas type index', 'value': '1', 'units': ''}]}, - {'index': 49, - 'alias': 'pn_plug_24', - 'component_name': 'pn_plug', - 'library_id': 'pn.pn_plug', - 'submodel': 'PNPL01', - 'label': 'zero pneumatic flow source', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}], - 'parameters': []}, - {'index': 50, - 'alias': 'pn_plug_25', - 'component_name': 'pn_plug', - 'library_id': 'pn.pn_plug', - 'submodel': 'PNPL01', - 'label': 'zero pneumatic flow source', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '0'}], - 'parameters': []}, - {'index': 51, - 'alias': 'pn_c1_13', - 'component_name': 'pn_c1', - 'library_id': 'pcd.pn_c1', - 'submodel': 'PNCH012', - 'label': 'variable volume pneumatic chamber with heat exchange (preferred)', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '2'}, - {'name': 'port_2', 'type': 'pflow', 'face': '1'}, - {'name': 'port_3', 'type': 'pflow', 'face': '3'}, - {'name': 'port_4', 'type': 'pflow', 'face': '0'}], - 'parameters': [{'name': 'cvol0', 'title': 'dead volume', 'value': 'V', 'units': 'L'}, - {'name': 'kth', - 'title': 'thermal exchange coefficient', - 'value': '1.50000000000000e+03', - 'units': 'J/m**2/K/s'}, - {'name': 'sth', - 'title': 'thermal exchange area', - 'value': '7.00000000000000e-01', - 'units': 'm**2'}, - {'name': 'extemp', - 'title': 'external temperature', - 'value': '2.93150000000000e+02', - 'units': 'K'}, - {'name': 'gi', 'title': 'gas type index', 'value': '1', 'units': ''}]}, - {'index': 52, - 'alias': 'pn_plug_26', - 'component_name': 'pn_plug', - 'library_id': 'pn.pn_plug', - 'submodel': 'PNPL01', - 'label': 'zero pneumatic flow source', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}], - 'parameters': []}, - {'index': 53, - 'alias': 'pn_plug_27', - 'component_name': 'pn_plug', - 'library_id': 'pn.pn_plug', - 'submodel': 'PNPL01', - 'label': 'zero pneumatic flow source', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '0'}], - 'parameters': []}, - {'index': 54, - 'alias': 'pn_c1_14', - 'component_name': 'pn_c1', - 'library_id': 'pcd.pn_c1', - 'submodel': 'PNCH012', - 'label': 'variable volume pneumatic chamber with heat exchange (preferred)', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '2'}, - {'name': 'port_2', 'type': 'pflow', 'face': '1'}, - {'name': 'port_3', 'type': 'pflow', 'face': '3'}, - {'name': 'port_4', 'type': 'pflow', 'face': '0'}], - 'parameters': [{'name': 'cvol0', 'title': 'dead volume', 'value': 'V', 'units': 'L'}, - {'name': 'kth', - 'title': 'thermal exchange coefficient', - 'value': '1.50000000000000e+03', - 'units': 'J/m**2/K/s'}, - {'name': 'sth', - 'title': 'thermal exchange area', - 'value': '7.00000000000000e-01', - 'units': 'm**2'}, - {'name': 'extemp', - 'title': 'external temperature', - 'value': '2.93150000000000e+02', - 'units': 'K'}, - {'name': 'gi', 'title': 'gas type index', 'value': '1', 'units': ''}]}, - {'index': 55, - 'alias': 'pn_plug_28', - 'component_name': 'pn_plug', - 'library_id': 'pn.pn_plug', - 'submodel': 'PNPL01', - 'label': 'zero pneumatic flow source', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}], - 'parameters': []}, - {'index': 56, - 'alias': 'pn_plug_29', - 'component_name': 'pn_plug', - 'library_id': 'pn.pn_plug', - 'submodel': 'PNPL01', - 'label': 'zero pneumatic flow source', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '0'}], - 'parameters': []}, - {'index': 57, - 'alias': 'pn_c1_15', - 'component_name': 'pn_c1', - 'library_id': 'pcd.pn_c1', - 'submodel': 'PNCH012', - 'label': 'variable volume pneumatic chamber with heat exchange (preferred)', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '2'}, - {'name': 'port_2', 'type': 'pflow', 'face': '1'}, - {'name': 'port_3', 'type': 'pflow', 'face': '3'}, - {'name': 'port_4', 'type': 'pflow', 'face': '0'}], - 'parameters': [{'name': 'cvol0', 'title': 'dead volume', 'value': 'V', 'units': 'L'}, - {'name': 'kth', - 'title': 'thermal exchange coefficient', - 'value': '1.50000000000000e+03', - 'units': 'J/m**2/K/s'}, - {'name': 'sth', - 'title': 'thermal exchange area', - 'value': '7.00000000000000e-01', - 'units': 'm**2'}, - {'name': 'extemp', - 'title': 'external temperature', - 'value': '2.93150000000000e+02', - 'units': 'K'}, - {'name': 'gi', 'title': 'gas type index', 'value': '1', 'units': ''}]}, - {'index': 58, - 'alias': 'pn_plug_30', - 'component_name': 'pn_plug', - 'library_id': 'pn.pn_plug', - 'submodel': 'PNPL01', - 'label': 'zero pneumatic flow source', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}], - 'parameters': []}, - {'index': 59, - 'alias': 'pn_plug_31', - 'component_name': 'pn_plug', - 'library_id': 'pn.pn_plug', - 'submodel': 'PNPL01', - 'label': 'zero pneumatic flow source', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '0'}], - 'parameters': []}, - {'index': 60, - 'alias': 'zeroforcesource_26', - 'component_name': 'zeroforcesource', - 'library_id': 'meca.zeroforcesource', - 'submodel': 'F000', - 'label': 'zero force source', - 'ports': [{'name': 'port_1', 'type': 'lshaft', 'face': '2'}], - 'parameters': []}, - {'index': 61, - 'alias': 'zeroforcesource_27', - 'component_name': 'zeroforcesource', - 'library_id': 'meca.zeroforcesource', - 'submodel': 'F000', - 'label': 'zero force source', - 'ports': [{'name': 'port_1', 'type': 'lshaft', 'face': '2'}], - 'parameters': []}, - {'index': 62, - 'alias': 'zeroforcesource_28', - 'component_name': 'zeroforcesource', - 'library_id': 'meca.zeroforcesource', - 'submodel': 'F000', - 'label': 'zero force source', - 'ports': [{'name': 'port_1', 'type': 'lshaft', 'face': '2'}], - 'parameters': []}, - {'index': 63, - 'alias': 'zeroforcesource_29', - 'component_name': 'zeroforcesource', - 'library_id': 'meca.zeroforcesource', - 'submodel': 'F000', - 'label': 'zero force source', - 'ports': [{'name': 'port_1', 'type': 'lshaft', 'face': '2'}], - 'parameters': []}, - {'index': 64, - 'alias': 'zeroforcesource_30', - 'component_name': 'zeroforcesource', - 'library_id': 'meca.zeroforcesource', - 'submodel': 'F000', - 'label': 'zero force source', - 'ports': [{'name': 'port_1', 'type': 'lshaft', 'face': '2'}], - 'parameters': []}, - {'index': 65, - 'alias': 'zeroforcesource_31', - 'component_name': 'zeroforcesource', - 'library_id': 'meca.zeroforcesource', - 'submodel': 'F000', - 'label': 'zero force source', - 'ports': [{'name': 'port_1', 'type': 'lshaft', 'face': '2'}], - 'parameters': []}, - {'index': 66, - 'alias': 'zeroforcesource_32', - 'component_name': 'zeroforcesource', - 'library_id': 'meca.zeroforcesource', - 'submodel': 'F000', - 'label': 'zero force source', - 'ports': [{'name': 'port_1', 'type': 'lshaft', 'face': '2'}], - 'parameters': []}, - {'index': 67, - 'alias': 'zeroforcesource_33', - 'component_name': 'zeroforcesource', - 'library_id': 'meca.zeroforcesource', - 'submodel': 'F000', - 'label': 'zero force source', - 'ports': [{'name': 'port_1', 'type': 'lshaft', 'face': '2'}], - 'parameters': []}, - {'index': 68, - 'alias': 'forcecon_1', - 'component_name': 'forcecon', - 'library_id': 'meca.forcecon', - 'submodel': 'FORC', - 'label': 'conversion of signal input into a force in N', - 'ports': [{'name': 'port_1', 'type': 'signal', 'face': '2'}, - {'name': 'port_2', 'type': 'lshaft', 'face': '3'}], - 'parameters': []}, - {'index': 69, - 'alias': 'pn_general_chamber', - 'component_name': 'pn_general_chamber', - 'library_id': 'pn.pn_general_chamber', - 'submodel': 'PNCH023', - 'label': 'simple pneumatic chamber with heat exchange', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}, - {'name': 'port_2', 'type': 'pflow', 'face': '0'}], - 'parameters': [{'name': 'cvol', 'title': 'volume', 'value': '5.70000000000000e+01', 'units': 'L'}, - {'name': 'kth', - 'title': 'thermal exchange coefficient', - 'value': '0.00000000000000e+00', - 'units': 'J/m**2/K/s'}, - {'name': 'sth', - 'title': 'thermal exchange area', - 'value': '1.00000000000000e-01', - 'units': 'm**2'}, - {'name': 'extemp', - 'title': 'external temperature', - 'value': '2.93150000000000e+02', - 'units': 'K'}, - {'name': 'gi', 'title': 'gas type index', 'value': '1', 'units': ''}]}, - {'index': 70, - 'alias': 'pn_orifice_18', - 'component_name': 'pn_orifice', - 'library_id': 'pn.pn_orifice', - 'submodel': 'PNOR001', - 'label': 'pneumatic orifice (constant flow coefficient)', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}, - {'name': 'port_2', 'type': 'pflow', 'face': '0'}], - 'parameters': [{'name': 'cq', - 'title': 'flow coefficient (Cq)', - 'value': '9.00000000000000e-01', - 'units': 'null'}, - {'name': 'area', - 'title': 'orifice area', - 'value': '3.14*10^2/4', - 'units': 'mm**2'}, - {'name': 'Cv', - 'title': 'flow coefficient (Cv)', - 'value': '5.00000000000000e-01', - 'units': 'null'}, - {'name': 'Kv', - 'title': 'flow coefficient (Kv)', - 'value': '4.00000000000000e-01', - 'units': 'null'}, - {'name': 'gi', 'title': 'gas type index', 'value': '1', 'units': ''}, - {'name': 'flowset', - 'title': 'flow coefficient setting', - 'value': '1', - 'units': ''}]}, - {'index': 71, - 'alias': 'pn_orifice_19', - 'component_name': 'pn_orifice', - 'library_id': 'pn.pn_orifice', - 'submodel': 'PNOR001', - 'label': 'pneumatic orifice (constant flow coefficient)', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}, - {'name': 'port_2', 'type': 'pflow', 'face': '0'}], - 'parameters': [{'name': 'cq', - 'title': 'flow coefficient (Cq)', - 'value': '9.00000000000000e-01', - 'units': 'null'}, - {'name': 'area', - 'title': 'orifice area', - 'value': '3.14*10^2/4', - 'units': 'mm**2'}, - {'name': 'Cv', - 'title': 'flow coefficient (Cv)', - 'value': '5.00000000000000e-01', - 'units': 'null'}, - {'name': 'Kv', - 'title': 'flow coefficient (Kv)', - 'value': '4.00000000000000e-01', - 'units': 'null'}, - {'name': 'gi', 'title': 'gas type index', 'value': '1', 'units': ''}, - {'name': 'flowset', - 'title': 'flow coefficient setting', - 'value': '1', - 'units': ''}]}, - {'index': 72, - 'alias': 'pn_node3_8', - 'component_name': 'pn_node3', - 'library_id': 'pn.pn_node3', - 'submodel': 'PN3NODE2', - 'label': 'pneumatic junction 3 ports (temperature and pressure fixed by port 2)', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}, - {'name': 'port_2', 'type': 'pflow', 'face': '3'}, - {'name': 'port_3', 'type': 'pflow', 'face': '0'}], - 'parameters': []}, - {'index': 73, - 'alias': 'pn_node3_9', - 'component_name': 'pn_node3', - 'library_id': 'pn.pn_node3', - 'submodel': 'PN3NODE2', - 'label': 'pneumatic junction 3 ports (temperature and pressure fixed by port 2)', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}, - {'name': 'port_2', 'type': 'pflow', 'face': '3'}, - {'name': 'port_3', 'type': 'pflow', 'face': '0'}], - 'parameters': []}, - {'index': 74, - 'alias': 'pn_general_chamber_2', - 'component_name': 'pn_general_chamber', - 'library_id': 'pn.pn_general_chamber', - 'submodel': 'PNCH023', - 'label': 'simple pneumatic chamber with heat exchange', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}, - {'name': 'port_2', 'type': 'pflow', 'face': '0'}], - 'parameters': [{'name': 'cvol', 'title': 'volume', 'value': '5.70000000000000e+01', 'units': 'L'}, - {'name': 'kth', - 'title': 'thermal exchange coefficient', - 'value': '0.00000000000000e+00', - 'units': 'J/m**2/K/s'}, - {'name': 'sth', - 'title': 'thermal exchange area', - 'value': '1.00000000000000e-01', - 'units': 'm**2'}, - {'name': 'extemp', - 'title': 'external temperature', - 'value': '2.93150000000000e+02', - 'units': 'K'}, - {'name': 'gi', 'title': 'gas type index', 'value': '1', 'units': ''}]}, - {'index': 75, - 'alias': 'pn_orifice_20', - 'component_name': 'pn_orifice', - 'library_id': 'pn.pn_orifice', - 'submodel': 'PNOR001', - 'label': 'pneumatic orifice (constant flow coefficient)', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}, - {'name': 'port_2', 'type': 'pflow', 'face': '0'}], - 'parameters': [{'name': 'cq', - 'title': 'flow coefficient (Cq)', - 'value': '9.00000000000000e-01', - 'units': 'null'}, - {'name': 'area', - 'title': 'orifice area', - 'value': '3.14*10^2/4', - 'units': 'mm**2'}, - {'name': 'Cv', - 'title': 'flow coefficient (Cv)', - 'value': '5.00000000000000e-01', - 'units': 'null'}, - {'name': 'Kv', - 'title': 'flow coefficient (Kv)', - 'value': '4.00000000000000e-01', - 'units': 'null'}, - {'name': 'gi', 'title': 'gas type index', 'value': '1', 'units': ''}, - {'name': 'flowset', - 'title': 'flow coefficient setting', - 'value': '1', - 'units': ''}]}, - {'index': 76, - 'alias': 'pn_orifice_21', - 'component_name': 'pn_orifice', - 'library_id': 'pn.pn_orifice', - 'submodel': 'PNOR001', - 'label': 'pneumatic orifice (constant flow coefficient)', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}, - {'name': 'port_2', 'type': 'pflow', 'face': '0'}], - 'parameters': [{'name': 'cq', - 'title': 'flow coefficient (Cq)', - 'value': '9.00000000000000e-01', - 'units': 'null'}, - {'name': 'area', - 'title': 'orifice area', - 'value': '3.14*10^2/4', - 'units': 'mm**2'}, - {'name': 'Cv', - 'title': 'flow coefficient (Cv)', - 'value': '5.00000000000000e-01', - 'units': 'null'}, - {'name': 'Kv', - 'title': 'flow coefficient (Kv)', - 'value': '4.00000000000000e-01', - 'units': 'null'}, - {'name': 'gi', 'title': 'gas type index', 'value': '1', 'units': ''}, - {'name': 'flowset', - 'title': 'flow coefficient setting', - 'value': '1', - 'units': ''}]}, - {'index': 77, - 'alias': 'pn_node3_10', - 'component_name': 'pn_node3', - 'library_id': 'pn.pn_node3', - 'submodel': 'PN3NODE2', - 'label': 'pneumatic junction 3 ports (temperature and pressure fixed by port 2)', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}, - {'name': 'port_2', 'type': 'pflow', 'face': '3'}, - {'name': 'port_3', 'type': 'pflow', 'face': '0'}], - 'parameters': []}, - {'index': 78, - 'alias': 'pn_node3_11', - 'component_name': 'pn_node3', - 'library_id': 'pn.pn_node3', - 'submodel': 'PN3NODE2', - 'label': 'pneumatic junction 3 ports (temperature and pressure fixed by port 2)', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}, - {'name': 'port_2', 'type': 'pflow', 'face': '3'}, - {'name': 'port_3', 'type': 'pflow', 'face': '0'}], - 'parameters': []}, - {'index': 79, - 'alias': 'pn_general_chamber_4', - 'component_name': 'pn_general_chamber', - 'library_id': 'pn.pn_general_chamber', - 'submodel': 'PNCH023', - 'label': 'simple pneumatic chamber with heat exchange', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}, - {'name': 'port_2', 'type': 'pflow', 'face': '0'}], - 'parameters': [{'name': 'cvol', 'title': 'volume', 'value': '5.70000000000000e+01', 'units': 'L'}, - {'name': 'kth', - 'title': 'thermal exchange coefficient', - 'value': '0.00000000000000e+00', - 'units': 'J/m**2/K/s'}, - {'name': 'sth', - 'title': 'thermal exchange area', - 'value': '1.00000000000000e-01', - 'units': 'm**2'}, - {'name': 'extemp', - 'title': 'external temperature', - 'value': '2.93150000000000e+02', - 'units': 'K'}, - {'name': 'gi', 'title': 'gas type index', 'value': '1', 'units': ''}]}, - {'index': 80, - 'alias': 'pn_orifice_22', - 'component_name': 'pn_orifice', - 'library_id': 'pn.pn_orifice', - 'submodel': 'PNOR001', - 'label': 'pneumatic orifice (constant flow coefficient)', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}, - {'name': 'port_2', 'type': 'pflow', 'face': '0'}], - 'parameters': [{'name': 'cq', - 'title': 'flow coefficient (Cq)', - 'value': '9.00000000000000e-01', - 'units': 'null'}, - {'name': 'area', - 'title': 'orifice area', - 'value': '3.14*10^2/4', - 'units': 'mm**2'}, - {'name': 'Cv', - 'title': 'flow coefficient (Cv)', - 'value': '5.00000000000000e-01', - 'units': 'null'}, - {'name': 'Kv', - 'title': 'flow coefficient (Kv)', - 'value': '4.00000000000000e-01', - 'units': 'null'}, - {'name': 'gi', 'title': 'gas type index', 'value': '1', 'units': ''}, - {'name': 'flowset', - 'title': 'flow coefficient setting', - 'value': '1', - 'units': ''}]}, - {'index': 81, - 'alias': 'pn_orifice_23', - 'component_name': 'pn_orifice', - 'library_id': 'pn.pn_orifice', - 'submodel': 'PNOR001', - 'label': 'pneumatic orifice (constant flow coefficient)', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}, - {'name': 'port_2', 'type': 'pflow', 'face': '0'}], - 'parameters': [{'name': 'cq', - 'title': 'flow coefficient (Cq)', - 'value': '9.00000000000000e-01', - 'units': 'null'}, - {'name': 'area', - 'title': 'orifice area', - 'value': '3.14*10^2/4', - 'units': 'mm**2'}, - {'name': 'Cv', - 'title': 'flow coefficient (Cv)', - 'value': '5.00000000000000e-01', - 'units': 'null'}, - {'name': 'Kv', - 'title': 'flow coefficient (Kv)', - 'value': '4.00000000000000e-01', - 'units': 'null'}, - {'name': 'gi', 'title': 'gas type index', 'value': '1', 'units': ''}, - {'name': 'flowset', - 'title': 'flow coefficient setting', - 'value': '1', - 'units': ''}]}, - {'index': 82, - 'alias': 'pn_node3_12', - 'component_name': 'pn_node3', - 'library_id': 'pn.pn_node3', - 'submodel': 'PN3NODE2', - 'label': 'pneumatic junction 3 ports (temperature and pressure fixed by port 2)', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}, - {'name': 'port_2', 'type': 'pflow', 'face': '3'}, - {'name': 'port_3', 'type': 'pflow', 'face': '0'}], - 'parameters': []}, - {'index': 83, - 'alias': 'pn_node3_13', - 'component_name': 'pn_node3', - 'library_id': 'pn.pn_node3', - 'submodel': 'PN3NODE2', - 'label': 'pneumatic junction 3 ports (temperature and pressure fixed by port 2)', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}, - {'name': 'port_2', 'type': 'pflow', 'face': '3'}, - {'name': 'port_3', 'type': 'pflow', 'face': '0'}], - 'parameters': []}, - {'index': 84, - 'alias': 'pn_general_chamber_5', - 'component_name': 'pn_general_chamber', - 'library_id': 'pn.pn_general_chamber', - 'submodel': 'PNCH023', - 'label': 'simple pneumatic chamber with heat exchange', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}, - {'name': 'port_2', 'type': 'pflow', 'face': '0'}], - 'parameters': [{'name': 'cvol', 'title': 'volume', 'value': '5.70000000000000e+01', 'units': 'L'}, - {'name': 'kth', - 'title': 'thermal exchange coefficient', - 'value': '0.00000000000000e+00', - 'units': 'J/m**2/K/s'}, - {'name': 'sth', - 'title': 'thermal exchange area', - 'value': '1.00000000000000e-01', - 'units': 'm**2'}, - {'name': 'extemp', - 'title': 'external temperature', - 'value': '2.93150000000000e+02', - 'units': 'K'}, - {'name': 'gi', 'title': 'gas type index', 'value': '1', 'units': ''}]}, - {'index': 85, - 'alias': 'pn_orifice_24', - 'component_name': 'pn_orifice', - 'library_id': 'pn.pn_orifice', - 'submodel': 'PNOR001', - 'label': 'pneumatic orifice (constant flow coefficient)', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}, - {'name': 'port_2', 'type': 'pflow', 'face': '0'}], - 'parameters': [{'name': 'cq', - 'title': 'flow coefficient (Cq)', - 'value': '9.00000000000000e-01', - 'units': 'null'}, - {'name': 'area', - 'title': 'orifice area', - 'value': '3.14*10^2/4', - 'units': 'mm**2'}, - {'name': 'Cv', - 'title': 'flow coefficient (Cv)', - 'value': '5.00000000000000e-01', - 'units': 'null'}, - {'name': 'Kv', - 'title': 'flow coefficient (Kv)', - 'value': '4.00000000000000e-01', - 'units': 'null'}, - {'name': 'gi', 'title': 'gas type index', 'value': '1', 'units': ''}, - {'name': 'flowset', - 'title': 'flow coefficient setting', - 'value': '1', - 'units': ''}]}, - {'index': 86, - 'alias': 'pn_orifice_25', - 'component_name': 'pn_orifice', - 'library_id': 'pn.pn_orifice', - 'submodel': 'PNOR001', - 'label': 'pneumatic orifice (constant flow coefficient)', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}, - {'name': 'port_2', 'type': 'pflow', 'face': '0'}], - 'parameters': [{'name': 'cq', - 'title': 'flow coefficient (Cq)', - 'value': '9.00000000000000e-01', - 'units': 'null'}, - {'name': 'area', - 'title': 'orifice area', - 'value': '3.14*10^2/4', - 'units': 'mm**2'}, - {'name': 'Cv', - 'title': 'flow coefficient (Cv)', - 'value': '5.00000000000000e-01', - 'units': 'null'}, - {'name': 'Kv', - 'title': 'flow coefficient (Kv)', - 'value': '4.00000000000000e-01', - 'units': 'null'}, - {'name': 'gi', 'title': 'gas type index', 'value': '1', 'units': ''}, - {'name': 'flowset', - 'title': 'flow coefficient setting', - 'value': '1', - 'units': ''}]}, - {'index': 87, - 'alias': 'pn_node3_14', - 'component_name': 'pn_node3', - 'library_id': 'pn.pn_node3', - 'submodel': 'PN3NODE2', - 'label': 'pneumatic junction 3 ports (temperature and pressure fixed by port 2)', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}, - {'name': 'port_2', 'type': 'pflow', 'face': '3'}, - {'name': 'port_3', 'type': 'pflow', 'face': '0'}], - 'parameters': []}, - {'index': 88, - 'alias': 'pn_node3_15', - 'component_name': 'pn_node3', - 'library_id': 'pn.pn_node3', - 'submodel': 'PN3NODE2', - 'label': 'pneumatic junction 3 ports (temperature and pressure fixed by port 2)', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}, - {'name': 'port_2', 'type': 'pflow', 'face': '3'}, - {'name': 'port_3', 'type': 'pflow', 'face': '0'}], - 'parameters': []}, - {'index': 89, - 'alias': 'pnnode4_16', - 'component_name': 'pnnode4', - 'library_id': 'pn.pnnode4', - 'submodel': 'P4NODE2', - 'label': 'pneumatic junction 4 ports (temperature and pressure fixed by port 2)', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}, - {'name': 'port_2', 'type': 'pflow', 'face': '3'}, - {'name': 'port_3', 'type': 'pflow', 'face': '0'}, - {'name': 'port_4', 'type': 'pflow', 'face': '2'}], - 'parameters': []}, - {'index': 90, - 'alias': 'pnnode4_17', - 'component_name': 'pnnode4', - 'library_id': 'pn.pnnode4', - 'submodel': 'P4NODE2', - 'label': 'pneumatic junction 4 ports (temperature and pressure fixed by port 2)', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}, - {'name': 'port_2', 'type': 'pflow', 'face': '3'}, - {'name': 'port_3', 'type': 'pflow', 'face': '0'}, - {'name': 'port_4', 'type': 'pflow', 'face': '2'}], - 'parameters': []}, - {'index': 91, - 'alias': 'pnnode4_18', - 'component_name': 'pnnode4', - 'library_id': 'pn.pnnode4', - 'submodel': 'P4NODE2', - 'label': 'pneumatic junction 4 ports (temperature and pressure fixed by port 2)', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}, - {'name': 'port_2', 'type': 'pflow', 'face': '3'}, - {'name': 'port_3', 'type': 'pflow', 'face': '0'}, - {'name': 'port_4', 'type': 'pflow', 'face': '2'}], - 'parameters': []}, - {'index': 92, - 'alias': 'pnnode4_19', - 'component_name': 'pnnode4', - 'library_id': 'pn.pnnode4', - 'submodel': 'P4NODE2', - 'label': 'pneumatic junction 4 ports (temperature and pressure fixed by port 2)', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}, - {'name': 'port_2', 'type': 'pflow', 'face': '3'}, - {'name': 'port_3', 'type': 'pflow', 'face': '0'}, - {'name': 'port_4', 'type': 'pflow', 'face': '2'}], - 'parameters': []}, - {'index': 93, - 'alias': 'pnnode4_20', - 'component_name': 'pnnode4', - 'library_id': 'pn.pnnode4', - 'submodel': 'P4NODE2', - 'label': 'pneumatic junction 4 ports (temperature and pressure fixed by port 2)', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}, - {'name': 'port_2', 'type': 'pflow', 'face': '3'}, - {'name': 'port_3', 'type': 'pflow', 'face': '0'}, - {'name': 'port_4', 'type': 'pflow', 'face': '2'}], - 'parameters': []}, - {'index': 94, - 'alias': 'pnnode4_21', - 'component_name': 'pnnode4', - 'library_id': 'pn.pnnode4', - 'submodel': 'P4NODE2', - 'label': 'pneumatic junction 4 ports (temperature and pressure fixed by port 2)', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}, - {'name': 'port_2', 'type': 'pflow', 'face': '3'}, - {'name': 'port_3', 'type': 'pflow', 'face': '0'}, - {'name': 'port_4', 'type': 'pflow', 'face': '2'}], - 'parameters': []}, - {'index': 95, - 'alias': 'pnnode4_22', - 'component_name': 'pnnode4', - 'library_id': 'pn.pnnode4', - 'submodel': 'P4NODE2', - 'label': 'pneumatic junction 4 ports (temperature and pressure fixed by port 2)', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}, - {'name': 'port_2', 'type': 'pflow', 'face': '3'}, - {'name': 'port_3', 'type': 'pflow', 'face': '0'}, - {'name': 'port_4', 'type': 'pflow', 'face': '2'}], - 'parameters': []}, - {'index': 96, - 'alias': 'pnnode4_23', - 'component_name': 'pnnode4', - 'library_id': 'pn.pnnode4', - 'submodel': 'P4NODE2', - 'label': 'pneumatic junction 4 ports (temperature and pressure fixed by port 2)', - 'ports': [{'name': 'port_1', 'type': 'pflow', 'face': '1'}, - {'name': 'port_2', 'type': 'pflow', 'face': '3'}, - {'name': 'port_3', 'type': 'pflow', 'face': '0'}, - {'name': 'port_4', 'type': 'pflow', 'face': '2'}], - 'parameters': []}, - {'index': 97, - 'alias': 'pn_morifice_11', - 'component_name': 'pn_morifice', - 'library_id': 'pn.pn_morifice', - 'submodel': 'PNVO001', - 'label': 'modulated pneumatic orifice (constant flow coefficient)', - 'ports': [{'name': 'port_1', 'type': 'signal', 'face': '1'}, - {'name': 'port_2', 'type': 'pflow', 'face': '3'}, - {'name': 'port_3', 'type': 'pflow', 'face': '2'}], - 'parameters': [{'name': 'cq', 'title': 'flow coefficient (Cq)', 'value': 'cf', 'units': 'null'}, - {'name': 'area0', - 'title': 'orifice area at maximum opening', - 'value': '3.14*10^2/4', - 'units': 'mm**2'}, - {'name': 'Cv', - 'title': 'maximum flow coefficient (Cv)', - 'value': '5.00000000000000e-01', - 'units': 'null'}, - {'name': 'Kv', - 'title': 'maximum flow coefficient (Kv)', - 'value': '4.00000000000000e-01', - 'units': 'null'}, - {'name': 'gi', 'title': 'gas type index', 'value': '1', 'units': ''}, - {'name': 'flowset', - 'title': 'flow coefficient setting', - 'value': '1', - 'units': ''}]}, - {'index': 98, - 'alias': 'step_8', - 'component_name': 'step', - 'library_id': 'sig.step', - 'submodel': 'STEP0', - 'label': 'step function', - 'ports': [{'name': 'port_1', 'type': 'signal', 'face': '3'}], - 'parameters': [{'name': 'out0', - 'title': 'value before step', - 'value': '0.00000000000000e+00', - 'units': 'null'}, - {'name': 'out1', - 'title': 'value after step', - 'value': '1.00000000000000e+00', - 'units': 'null'}, - {'name': 't0', - 'title': 'step time', - 'value': '4.00000000000000e-02', - 'units': 's'}, - {'name': 'td', - 'title': 'transition duration', - 'value': '1.00000000000000e-01', - 'units': 's'}, - {'name': 'transitionType', - 'title': 'transition type', - 'value': '1', - 'units': ''}]}, - {'index': 99, - 'alias': 'pn_morifice_12', - 'component_name': 'pn_morifice', - 'library_id': 'pn.pn_morifice', - 'submodel': 'PNVO001', - 'label': 'modulated pneumatic orifice (constant flow coefficient)', - 'ports': [{'name': 'port_1', 'type': 'signal', 'face': '1'}, - {'name': 'port_2', 'type': 'pflow', 'face': '3'}, - {'name': 'port_3', 'type': 'pflow', 'face': '2'}], - 'parameters': [{'name': 'cq', 'title': 'flow coefficient (Cq)', 'value': 'cf', 'units': 'null'}, - {'name': 'area0', - 'title': 'orifice area at maximum opening', - 'value': '3.14*10^2/4', - 'units': 'mm**2'}, - {'name': 'Cv', - 'title': 'maximum flow coefficient (Cv)', - 'value': '5.00000000000000e-01', - 'units': 'null'}, - {'name': 'Kv', - 'title': 'maximum flow coefficient (Kv)', - 'value': '4.00000000000000e-01', - 'units': 'null'}, - {'name': 'gi', 'title': 'gas type index', 'value': '1', 'units': ''}, - {'name': 'flowset', - 'title': 'flow coefficient setting', - 'value': '1', - 'units': ''}]}, - {'index': 100, - 'alias': 'step_9', - 'component_name': 'step', - 'library_id': 'sig.step', - 'submodel': 'STEP0', - 'label': 'step function', - 'ports': [{'name': 'port_1', 'type': 'signal', 'face': '3'}], - 'parameters': [{'name': 'out0', - 'title': 'value before step', - 'value': '0.00000000000000e+00', - 'units': 'null'}, - {'name': 'out1', - 'title': 'value after step', - 'value': '1.00000000000000e+00', - 'units': 'null'}, - {'name': 't0', - 'title': 'step time', - 'value': '4.00000000000000e-02', - 'units': 's'}, - {'name': 'td', - 'title': 'transition duration', - 'value': '1.00000000000000e-01', - 'units': 's'}, - {'name': 'transitionType', - 'title': 'transition type', - 'value': '1', - 'units': ''}]}, - {'index': 101, - 'alias': 'pn_morifice_13', - 'component_name': 'pn_morifice', - 'library_id': 'pn.pn_morifice', - 'submodel': 'PNVO001', - 'label': 'modulated pneumatic orifice (constant flow coefficient)', - 'ports': [{'name': 'port_1', 'type': 'signal', 'face': '1'}, - {'name': 'port_2', 'type': 'pflow', 'face': '3'}, - {'name': 'port_3', 'type': 'pflow', 'face': '2'}], - 'parameters': [{'name': 'cq', 'title': 'flow coefficient (Cq)', 'value': 'cf', 'units': 'null'}, - {'name': 'area0', - 'title': 'orifice area at maximum opening', - 'value': '3.14*10^2/4', - 'units': 'mm**2'}, - {'name': 'Cv', - 'title': 'maximum flow coefficient (Cv)', - 'value': '5.00000000000000e-01', - 'units': 'null'}, - {'name': 'Kv', - 'title': 'maximum flow coefficient (Kv)', - 'value': '4.00000000000000e-01', - 'units': 'null'}, - {'name': 'gi', 'title': 'gas type index', 'value': '1', 'units': ''}, - {'name': 'flowset', - 'title': 'flow coefficient setting', - 'value': '1', - 'units': ''}]}, - {'index': 102, - 'alias': 'step_10', - 'component_name': 'step', - 'library_id': 'sig.step', - 'submodel': 'STEP0', - 'label': 'step function', - 'ports': [{'name': 'port_1', 'type': 'signal', 'face': '3'}], - 'parameters': [{'name': 'out0', - 'title': 'value before step', - 'value': '0.00000000000000e+00', - 'units': 'null'}, - {'name': 'out1', - 'title': 'value after step', - 'value': '1.00000000000000e+00', - 'units': 'null'}, - {'name': 't0', - 'title': 'step time', - 'value': '4.00000000000000e-02', - 'units': 's'}, - {'name': 'td', - 'title': 'transition duration', - 'value': '1.00000000000000e-01', - 'units': 's'}, - {'name': 'transitionType', - 'title': 'transition type', - 'value': '1', - 'units': ''}]}, - {'index': 103, - 'alias': 'pn_morifice_14', - 'component_name': 'pn_morifice', - 'library_id': 'pn.pn_morifice', - 'submodel': 'PNVO001', - 'label': 'modulated pneumatic orifice (constant flow coefficient)', - 'ports': [{'name': 'port_1', 'type': 'signal', 'face': '1'}, - {'name': 'port_2', 'type': 'pflow', 'face': '3'}, - {'name': 'port_3', 'type': 'pflow', 'face': '2'}], - 'parameters': [{'name': 'cq', 'title': 'flow coefficient (Cq)', 'value': 'cf', 'units': 'null'}, - {'name': 'area0', - 'title': 'orifice area at maximum opening', - 'value': '3.14*10^2/4', - 'units': 'mm**2'}, - {'name': 'Cv', - 'title': 'maximum flow coefficient (Cv)', - 'value': '5.00000000000000e-01', - 'units': 'null'}, - {'name': 'Kv', - 'title': 'maximum flow coefficient (Kv)', - 'value': '4.00000000000000e-01', - 'units': 'null'}, - {'name': 'gi', 'title': 'gas type index', 'value': '1', 'units': ''}, - {'name': 'flowset', - 'title': 'flow coefficient setting', - 'value': '1', - 'units': ''}]}, - {'index': 104, - 'alias': 'step_11', - 'component_name': 'step', - 'library_id': 'sig.step', - 'submodel': 'STEP0', - 'label': 'step function', - 'ports': [{'name': 'port_1', 'type': 'signal', 'face': '3'}], - 'parameters': [{'name': 'out0', - 'title': 'value before step', - 'value': '0.00000000000000e+00', - 'units': 'null'}, - {'name': 'out1', - 'title': 'value after step', - 'value': '1.00000000000000e+00', - 'units': 'null'}, - {'name': 't0', - 'title': 'step time', - 'value': '4.00000000000000e-02', - 'units': 's'}, - {'name': 'td', - 'title': 'transition duration', - 'value': '1.00000000000000e-01', - 'units': 's'}, - {'name': 'transitionType', - 'title': 'transition type', - 'value': '1', - 'units': ''}]}, - {'index': 105, - 'alias': 'forcecon_2', - 'component_name': 'forcecon', - 'library_id': 'meca.forcecon', - 'submodel': 'FORC', - 'label': 'conversion of signal input into a force in N', - 'ports': [{'name': 'port_1', 'type': 'signal', 'face': '3'}, - {'name': 'port_2', 'type': 'lshaft', 'face': '2'}], - 'parameters': []}, - {'index': 106, - 'alias': 'piecewiselinear', - 'component_name': 'piecewiselinear', - 'library_id': 'sig.piecewiselinear', - 'submodel': 'UD00', - 'label': 'piecewise linear signal source', - 'ports': [{'name': 'port_1', 'type': 'signal', 'face': '3'}], - 'parameters': [{'name': 'tstart', - 'title': 'time at which duty cycle starts', - 'value': '0.00000000000000e+00', - 'units': 's'}, - {'name': 'start1', - 'title': 'output at start of stage 1', - 'value': '1.00000000000000e+17', - 'units': 'null'}, - {'name': 'end1', - 'title': 'output at end of stage 1', - 'value': '1.00000000000000e+17', - 'units': 'null'}, - {'name': 't1', - 'title': 'duration of stage 1', - 'value': '8.00000000000000e-01', - 'units': 's'}, - {'name': 'start2', - 'title': 'output at start of stage 2', - 'value': '4.90000000000000e+04', - 'units': 'null'}, - {'name': 'end2', - 'title': 'output at end of stage 2', - 'value': '4.90000000000000e+04', - 'units': 'null'}, - {'name': 't2', - 'title': 'duration of stage 2', - 'value': '1.00000000000000e+01', - 'units': 's'}, - {'name': 'start3', - 'title': 'output at start of stage 3', - 'value': '0.00000000000000e+00', - 'units': 'null'}, - {'name': 'end3', - 'title': 'output at end of stage 3', - 'value': '0.00000000000000e+00', - 'units': 'null'}, - {'name': 't3', - 'title': 'duration of stage 3', - 'value': '3.00000000000000e+00', - 'units': 's'}, - {'name': 'start4', - 'title': 'output at start of stage 4', - 'value': '0.00000000000000e+00', - 'units': 'null'}, - {'name': 'end4', - 'title': 'output at end of stage 4', - 'value': '0.00000000000000e+00', - 'units': 'null'}, - {'name': 't4', - 'title': 'duration of stage 4', - 'value': '3.00000000000000e+00', - 'units': 's'}, - {'name': 'start5', - 'title': 'output at start of stage 5', - 'value': '0.00000000000000e+00', - 'units': 'null'}, - {'name': 'end5', - 'title': 'output at end of stage 5', - 'value': '0.00000000000000e+00', - 'units': 'null'}, - {'name': 't5', - 'title': 'duration of stage 5', - 'value': '3.00000000000000e+00', - 'units': 's'}, - {'name': 'start6', - 'title': 'output at start of stage 6', - 'value': '0.00000000000000e+00', - 'units': 'null'}, - {'name': 'end6', - 'title': 'output at end of stage 6', - 'value': '0.00000000000000e+00', - 'units': 'null'}, - {'name': 't6', - 'title': 'duration of stage 6', - 'value': '3.00000000000000e+00', - 'units': 's'}, - {'name': 'start7', - 'title': 'output at start of stage 7', - 'value': '0.00000000000000e+00', - 'units': 'null'}, - {'name': 'end7', - 'title': 'output at end of stage 7', - 'value': '0.00000000000000e+00', - 'units': 'null'}, - {'name': 't7', - 'title': 'duration of stage 7', - 'value': '3.00000000000000e+00', - 'units': 's'}, - {'name': 'start8', - 'title': 'output at start of stage 8', - 'value': '0.00000000000000e+00', - 'units': 'null'}, - {'name': 'end8', - 'title': 'output at end of stage 8', - 'value': '0.00000000000000e+00', - 'units': 'null'}, - {'name': 't8', - 'title': 'duration of stage 8', - 'value': '3.00000000000000e+00', - 'units': 's'}, - {'name': 'nstages', 'title': 'number of stages', 'value': '2', 'units': ''}, - {'name': 'iscyclic', 'title': 'cyclic', 'value': '1', 'units': ''}]}, - {'index': 107, - 'alias': 'piecewiselinear_1', - 'component_name': 'piecewiselinear', - 'library_id': 'sig.piecewiselinear', - 'submodel': 'UD00', - 'label': 'piecewise linear signal source', - 'ports': [{'name': 'port_1', 'type': 'signal', 'face': '2'}], - 'parameters': [{'name': 'tstart', - 'title': 'time at which duty cycle starts', - 'value': '0.00000000000000e+00', - 'units': 's'}, - {'name': 'start1', - 'title': 'output at start of stage 1', - 'value': '1.00000000000000e+12', - 'units': 'null'}, - {'name': 'end1', - 'title': 'output at end of stage 1', - 'value': '1.00000000000000e+12', - 'units': 'null'}, - {'name': 't1', - 'title': 'duration of stage 1', - 'value': '8.00000000000000e-01', - 'units': 's'}, - {'name': 'start2', - 'title': 'output at start of stage 2', - 'value': '0.00000000000000e+00', - 'units': 'null'}, - {'name': 'end2', - 'title': 'output at end of stage 2', - 'value': '0.00000000000000e+00', - 'units': 'null'}, - {'name': 't2', - 'title': 'duration of stage 2', - 'value': '1.00000000000000e+01', - 'units': 's'}, - {'name': 'start3', - 'title': 'output at start of stage 3', - 'value': '0.00000000000000e+00', - 'units': 'null'}, - {'name': 'end3', - 'title': 'output at end of stage 3', - 'value': '0.00000000000000e+00', - 'units': 'null'}, - {'name': 't3', - 'title': 'duration of stage 3', - 'value': '3.00000000000000e+00', - 'units': 's'}, - {'name': 'start4', - 'title': 'output at start of stage 4', - 'value': '0.00000000000000e+00', - 'units': 'null'}, - {'name': 'end4', - 'title': 'output at end of stage 4', - 'value': '0.00000000000000e+00', - 'units': 'null'}, - {'name': 't4', - 'title': 'duration of stage 4', - 'value': '3.00000000000000e+00', - 'units': 's'}, - {'name': 'start5', - 'title': 'output at start of stage 5', - 'value': '0.00000000000000e+00', - 'units': 'null'}, - {'name': 'end5', - 'title': 'output at end of stage 5', - 'value': '0.00000000000000e+00', - 'units': 'null'}, - {'name': 't5', - 'title': 'duration of stage 5', - 'value': '3.00000000000000e+00', - 'units': 's'}, - {'name': 'start6', - 'title': 'output at start of stage 6', - 'value': '0.00000000000000e+00', - 'units': 'null'}, - {'name': 'end6', - 'title': 'output at end of stage 6', - 'value': '0.00000000000000e+00', - 'units': 'null'}, - {'name': 't6', - 'title': 'duration of stage 6', - 'value': '3.00000000000000e+00', - 'units': 's'}, - {'name': 'start7', - 'title': 'output at start of stage 7', - 'value': '0.00000000000000e+00', - 'units': 'null'}, - {'name': 'end7', - 'title': 'output at end of stage 7', - 'value': '0.00000000000000e+00', - 'units': 'null'}, - {'name': 't7', - 'title': 'duration of stage 7', - 'value': '3.00000000000000e+00', - 'units': 's'}, - {'name': 'start8', - 'title': 'output at start of stage 8', - 'value': '0.00000000000000e+00', - 'units': 'null'}, - {'name': 'end8', - 'title': 'output at end of stage 8', - 'value': '0.00000000000000e+00', - 'units': 'null'}, - {'name': 't8', - 'title': 'duration of stage 8', - 'value': '3.00000000000000e+00', - 'units': 's'}, - {'name': 'nstages', 'title': 'number of stages', 'value': '2', 'units': ''}, - {'name': 'iscyclic', 'title': 'cyclic', 'value': '1', 'units': ''}]}, - {'index': 108, - 'alias': 'pn_morifice_1', - 'component_name': 'pn_morifice', - 'library_id': 'pn.pn_morifice', - 'submodel': 'PNVO001', - 'label': 'modulated pneumatic orifice (constant flow coefficient)', - 'ports': [{'name': 'port_1', 'type': 'signal', 'face': '1'}, - {'name': 'port_2', 'type': 'pflow', 'face': '3'}, - {'name': 'port_3', 'type': 'pflow', 'face': '2'}], - 'parameters': [{'name': 'cq', 'title': 'flow coefficient (Cq)', 'value': 'cf', 'units': 'null'}, - {'name': 'area0', - 'title': 'orifice area at maximum opening', - 'value': '3.14*10^2/4', - 'units': 'mm**2'}, - {'name': 'Cv', - 'title': 'maximum flow coefficient (Cv)', - 'value': '5.00000000000000e-01', - 'units': 'null'}, - {'name': 'Kv', - 'title': 'maximum flow coefficient (Kv)', - 'value': '4.00000000000000e-01', - 'units': 'null'}, - {'name': 'gi', 'title': 'gas type index', 'value': '1', 'units': ''}, - {'name': 'flowset', - 'title': 'flow coefficient setting', - 'value': '1', - 'units': ''}]}, - {'index': 109, - 'alias': 'step_12', - 'component_name': 'step', - 'library_id': 'sig.step', - 'submodel': 'STEP0', - 'label': 'step function', - 'ports': [{'name': 'port_1', 'type': 'signal', 'face': '3'}], - 'parameters': [{'name': 'out0', - 'title': 'value before step', - 'value': '0.00000000000000e+00', - 'units': 'null'}, - {'name': 'out1', - 'title': 'value after step', - 'value': '1.00000000000000e+00', - 'units': 'null'}, - {'name': 't0', - 'title': 'step time', - 'value': '4.00000000000000e-02', - 'units': 's'}, - {'name': 'td', - 'title': 'transition duration', - 'value': '1.00000000000000e-01', - 'units': 's'}, - {'name': 'transitionType', - 'title': 'transition type', - 'value': '1', - 'units': ''}]}, - {'index': 110, - 'alias': 'pn_morifice_9', - 'component_name': 'pn_morifice', - 'library_id': 'pn.pn_morifice', - 'submodel': 'PNVO001', - 'label': 'modulated pneumatic orifice (constant flow coefficient)', - 'ports': [{'name': 'port_1', 'type': 'signal', 'face': '1'}, - {'name': 'port_2', 'type': 'pflow', 'face': '3'}, - {'name': 'port_3', 'type': 'pflow', 'face': '2'}], - 'parameters': [{'name': 'cq', 'title': 'flow coefficient (Cq)', 'value': 'cf', 'units': 'null'}, - {'name': 'area0', - 'title': 'orifice area at maximum opening', - 'value': '3.14*10^2/4', - 'units': 'mm**2'}, - {'name': 'Cv', - 'title': 'maximum flow coefficient (Cv)', - 'value': '5.00000000000000e-01', - 'units': 'null'}, - {'name': 'Kv', - 'title': 'maximum flow coefficient (Kv)', - 'value': '4.00000000000000e-01', - 'units': 'null'}, - {'name': 'gi', 'title': 'gas type index', 'value': '1', 'units': ''}, - {'name': 'flowset', - 'title': 'flow coefficient setting', - 'value': '1', - 'units': ''}]}, - {'index': 111, - 'alias': 'step_13', - 'component_name': 'step', - 'library_id': 'sig.step', - 'submodel': 'STEP0', - 'label': 'step function', - 'ports': [{'name': 'port_1', 'type': 'signal', 'face': '3'}], - 'parameters': [{'name': 'out0', - 'title': 'value before step', - 'value': '0.00000000000000e+00', - 'units': 'null'}, - {'name': 'out1', - 'title': 'value after step', - 'value': '1.00000000000000e+00', - 'units': 'null'}, - {'name': 't0', - 'title': 'step time', - 'value': '4.00000000000000e-02', - 'units': 's'}, - {'name': 'td', - 'title': 'transition duration', - 'value': '1.00000000000000e-01', - 'units': 's'}, - {'name': 'transitionType', - 'title': 'transition type', - 'value': '1', - 'units': ''}]}, - {'index': 112, - 'alias': 'pn_morifice_10', - 'component_name': 'pn_morifice', - 'library_id': 'pn.pn_morifice', - 'submodel': 'PNVO001', - 'label': 'modulated pneumatic orifice (constant flow coefficient)', - 'ports': [{'name': 'port_1', 'type': 'signal', 'face': '1'}, - {'name': 'port_2', 'type': 'pflow', 'face': '3'}, - {'name': 'port_3', 'type': 'pflow', 'face': '2'}], - 'parameters': [{'name': 'cq', 'title': 'flow coefficient (Cq)', 'value': 'cf', 'units': 'null'}, - {'name': 'area0', - 'title': 'orifice area at maximum opening', - 'value': '3.14*10^2/4', - 'units': 'mm**2'}, - {'name': 'Cv', - 'title': 'maximum flow coefficient (Cv)', - 'value': '5.00000000000000e-01', - 'units': 'null'}, - {'name': 'Kv', - 'title': 'maximum flow coefficient (Kv)', - 'value': '4.00000000000000e-01', - 'units': 'null'}, - {'name': 'gi', 'title': 'gas type index', 'value': '1', 'units': ''}, - {'name': 'flowset', - 'title': 'flow coefficient setting', - 'value': '1', - 'units': ''}]}, - {'index': 113, - 'alias': 'step_14', - 'component_name': 'step', - 'library_id': 'sig.step', - 'submodel': 'STEP0', - 'label': 'step function', - 'ports': [{'name': 'port_1', 'type': 'signal', 'face': '3'}], - 'parameters': [{'name': 'out0', - 'title': 'value before step', - 'value': '0.00000000000000e+00', - 'units': 'null'}, - {'name': 'out1', - 'title': 'value after step', - 'value': '1.00000000000000e+00', - 'units': 'null'}, - {'name': 't0', - 'title': 'step time', - 'value': '4.00000000000000e-02', - 'units': 's'}, - {'name': 'td', - 'title': 'transition duration', - 'value': '1.00000000000000e-01', - 'units': 's'}, - {'name': 'transitionType', - 'title': 'transition type', - 'value': '1', - 'units': ''}]}, - {'index': 114, - 'alias': 'pn_morifice_15', - 'component_name': 'pn_morifice', - 'library_id': 'pn.pn_morifice', - 'submodel': 'PNVO001', - 'label': 'modulated pneumatic orifice (constant flow coefficient)', - 'ports': [{'name': 'port_1', 'type': 'signal', 'face': '1'}, - {'name': 'port_2', 'type': 'pflow', 'face': '3'}, - {'name': 'port_3', 'type': 'pflow', 'face': '2'}], - 'parameters': [{'name': 'cq', 'title': 'flow coefficient (Cq)', 'value': 'cf', 'units': 'null'}, - {'name': 'area0', - 'title': 'orifice area at maximum opening', - 'value': '3.14*10^2/4', - 'units': 'mm**2'}, - {'name': 'Cv', - 'title': 'maximum flow coefficient (Cv)', - 'value': '5.00000000000000e-01', - 'units': 'null'}, - {'name': 'Kv', - 'title': 'maximum flow coefficient (Kv)', - 'value': '4.00000000000000e-01', - 'units': 'null'}, - {'name': 'gi', 'title': 'gas type index', 'value': '1', 'units': ''}, - {'name': 'flowset', - 'title': 'flow coefficient setting', - 'value': '1', - 'units': ''}]}, - {'index': 115, - 'alias': 'step_15', - 'component_name': 'step', - 'library_id': 'sig.step', - 'submodel': 'STEP0', - 'label': 'step function', - 'ports': [{'name': 'port_1', 'type': 'signal', 'face': '3'}], - 'parameters': [{'name': 'out0', - 'title': 'value before step', - 'value': '0.00000000000000e+00', - 'units': 'null'}, - {'name': 'out1', - 'title': 'value after step', - 'value': '1.00000000000000e+00', - 'units': 'null'}, - {'name': 't0', - 'title': 'step time', - 'value': '4.00000000000000e-02', - 'units': 's'}, - {'name': 'td', - 'title': 'transition duration', - 'value': '1.00000000000000e-01', - 'units': 's'}, - {'name': 'transitionType', - 'title': 'transition type', - 'value': '1', - 'units': ''}]}, - {'index': 116, - 'alias': 'pn_gas_data', - 'component_name': 'pn_gas_data', - 'library_id': 'pn.pn_gas_data', - 'submodel': 'PNGD00', - 'label': 'gas definition for helium (He)', - 'ports': [], - 'parameters': [{'name': 'cp', - 'title': 'constant-pressure specific heat (Cp)', - 'value': '1.00520000000000e+03', - 'units': 'J/kg/K'}, - {'name': 'cv', - 'title': 'constant-volume specific heat (Cv)', - 'value': '7.18000000000000e+02', - 'units': 'J/kg/K'}, - {'name': 'gamma', - 'title': 'specific heat ratio (gamma)', - 'value': '1.40000000000000e+00', - 'units': 'null'}, - {'name': 'rGas', - 'title': 'specific gas constant (r)', - 'value': '2.87200000000000e+02', - 'units': 'J/kg/K'}, - {'name': 'mu', - 'title': 'absolute viscosity (mu)', - 'value': '1.82000000000000e-02', - 'units': 'cP'}, - {'name': 'lam', - 'title': 'thermal conductivity', - 'value': '2.64000000000000e-02', - 'units': 'W/m/K'}, - {'name': 'M', - 'title': 'molar mass', - 'value': '2.89651300000000e+01', - 'units': 'g/mol'}, - {'name': 'operatingTempK', - 'title': 'operating temperature', - 'value': '2.98150000000000e+02', - 'units': 'K'}, - {'name': 'enthForm', - 'title': 'enthalpy of formation', - 'value': '-1.25530000000000e+02', - 'units': 'J/mol'}, - {'name': 'gi', 'title': 'gas type index', 'value': '1', 'units': ''}, - {'name': 'fluidType', 'title': 'fluid definition', 'value': '12', 'units': ''}, - {'name': 'eosType', 'title': 'properties definition', 'value': '6', 'units': ''}, - {'name': 'gasSetting', - 'title': 'setting properties through', - 'value': '1', - 'units': ''}]}] -CONNECTION_SPECS = [{'index': 0, - 'alias': 'linear_24', - 'source_component': 'pn_brp2_8', - 'source_port': 'port_5', - 'target_component': 'elasticendstop_8', - 'target_port': 'port_1', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 1, - 'alias': 'linear_25', - 'source_component': 'elasticendstop_8', - 'source_port': 'port_2', - 'target_component': 'dynamic_mechanical_node_alternative_2', - 'target_port': 'port_1', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 2, - 'alias': 'linear_26', - 'source_component': 'pn_brp2_9', - 'source_port': 'port_5', - 'target_component': 'elasticendstop_9', - 'target_port': 'port_1', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 3, - 'alias': 'linear_27', - 'source_component': 'elasticendstop_9', - 'source_port': 'port_2', - 'target_component': 'dynamic_mechanical_node_alternative_2', - 'target_port': 'port_2', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 4, - 'alias': 'linear_28', - 'source_component': 'pn_brp2_10', - 'source_port': 'port_5', - 'target_component': 'elasticendstop_10', - 'target_port': 'port_1', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 5, - 'alias': 'linear_29', - 'source_component': 'pn_brp2_11', - 'source_port': 'port_5', - 'target_component': 'elasticendstop_11', - 'target_port': 'port_1', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 6, - 'alias': 'linear_30', - 'source_component': 'pn_brp2_12', - 'source_port': 'port_5', - 'target_component': 'elasticendstop_12', - 'target_port': 'port_1', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 7, - 'alias': 'linear_31', - 'source_component': 'pn_brp2_13', - 'source_port': 'port_5', - 'target_component': 'elasticendstop_13', - 'target_port': 'port_1', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 8, - 'alias': 'linear_32', - 'source_component': 'pn_brp2_14', - 'source_port': 'port_5', - 'target_component': 'elasticendstop_14', - 'target_port': 'port_1', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 9, - 'alias': 'linear_33', - 'source_component': 'pn_brp2_15', - 'source_port': 'port_5', - 'target_component': 'elasticendstop_15', - 'target_port': 'port_1', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 10, - 'alias': 'linear_34', - 'source_component': 'elasticendstop_10', - 'source_port': 'port_2', - 'target_component': 'dynamic_mechanical_node_alternative_2', - 'target_port': 'port_3', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 11, - 'alias': 'linear_35', - 'source_component': 'elasticendstop_11', - 'source_port': 'port_2', - 'target_component': 'dynamic_mechanical_node_alternative_2', - 'target_port': 'port_4', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 12, - 'alias': 'linear_36', - 'source_component': 'elasticendstop_12', - 'source_port': 'port_2', - 'target_component': 'dynamic_mechanical_node_alternative_2', - 'target_port': 'port_5', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 13, - 'alias': 'linear_37', - 'source_component': 'elasticendstop_13', - 'source_port': 'port_2', - 'target_component': 'dynamic_mechanical_node_alternative_2', - 'target_port': 'port_6', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 14, - 'alias': 'linear_38', - 'source_component': 'elasticendstop_14', - 'source_port': 'port_2', - 'target_component': 'dynamic_mechanical_node_alternative_2', - 'target_port': 'port_7', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 15, - 'alias': 'linear_39', - 'source_component': 'elasticendstop_15', - 'source_port': 'port_2', - 'target_component': 'dynamic_mechanical_node_alternative_2', - 'target_port': 'port_8', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 16, - 'alias': 'linear_40', - 'source_component': 'dynamic_mechanical_node_alternative_3', - 'source_port': 'port_8', - 'target_component': 'pn_brp2_8', - 'target_port': 'port_3', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 17, - 'alias': 'linear_41', - 'source_component': 'dynamic_mechanical_node_alternative_3', - 'source_port': 'port_7', - 'target_component': 'pn_brp2_9', - 'target_port': 'port_3', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 18, - 'alias': 'linear_42', - 'source_component': 'dynamic_mechanical_node_alternative_3', - 'source_port': 'port_6', - 'target_component': 'pn_brp2_10', - 'target_port': 'port_3', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 19, - 'alias': 'linear_43', - 'source_component': 'dynamic_mechanical_node_alternative_3', - 'source_port': 'port_5', - 'target_component': 'pn_brp2_11', - 'target_port': 'port_3', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 20, - 'alias': 'linear_44', - 'source_component': 'dynamic_mechanical_node_alternative_3', - 'source_port': 'port_4', - 'target_component': 'pn_brp2_12', - 'target_port': 'port_3', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 21, - 'alias': 'linear_45', - 'source_component': 'dynamic_mechanical_node_alternative_3', - 'source_port': 'port_3', - 'target_component': 'pn_brp2_13', - 'target_port': 'port_3', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 22, - 'alias': 'linear_46', - 'source_component': 'dynamic_mechanical_node_alternative_3', - 'source_port': 'port_2', - 'target_component': 'pn_brp2_14', - 'target_port': 'port_3', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 23, - 'alias': 'linear_47', - 'source_component': 'dynamic_mechanical_node_alternative_3', - 'source_port': 'port_1', - 'target_component': 'pn_brp2_15', - 'target_port': 'port_3', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 24, - 'alias': 'pneumatic_56', - 'source_component': 'pn_c1_8', - 'source_port': 'port_3', - 'target_component': 'pn_brp2_8', - 'target_port': 'port_1', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 25, - 'alias': 'pneumatic_57', - 'source_component': 'pn_c1_9', - 'source_port': 'port_3', - 'target_component': 'pn_brp2_9', - 'target_port': 'port_1', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 26, - 'alias': 'pneumatic_58', - 'source_component': 'pn_c1_10', - 'source_port': 'port_3', - 'target_component': 'pn_brp2_10', - 'target_port': 'port_1', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 27, - 'alias': 'pneumatic_59', - 'source_component': 'pn_c1_11', - 'source_port': 'port_3', - 'target_component': 'pn_brp2_11', - 'target_port': 'port_1', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 28, - 'alias': 'pneumatic_60', - 'source_component': 'pn_c1_12', - 'source_port': 'port_3', - 'target_component': 'pn_brp2_12', - 'target_port': 'port_1', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 29, - 'alias': 'pneumatic_61', - 'source_component': 'pn_c1_13', - 'source_port': 'port_3', - 'target_component': 'pn_brp2_13', - 'target_port': 'port_1', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 30, - 'alias': 'pneumatic_62', - 'source_component': 'pn_c1_14', - 'source_port': 'port_3', - 'target_component': 'pn_brp2_14', - 'target_port': 'port_1', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 31, - 'alias': 'pneumatic_63', - 'source_component': 'pn_c1_15', - 'source_port': 'port_3', - 'target_component': 'pn_brp2_15', - 'target_port': 'port_1', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 32, - 'alias': 'pneumatic_64', - 'source_component': 'pn_morifice_15', - 'source_port': 'port_2', - 'target_component': 'pnnode4_19', - 'target_port': 'port_4', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 33, - 'alias': 'pneumatic_65', - 'source_component': 'pnnode4_19', - 'source_port': 'port_2', - 'target_component': 'pn_c1_11', - 'target_port': 'port_1', - 'submodel': 'PNL0001', - 'label': 'Compressibility + friction submodel of pneumatic pipe (C-R)'}, - {'index': 34, - 'alias': 'pneumatic_66', - 'source_component': 'pnnode4_18', - 'source_port': 'port_2', - 'target_component': 'pn_c1_10', - 'target_port': 'port_1', - 'submodel': 'PNL0001', - 'label': 'Compressibility + friction submodel of pneumatic pipe (C-R)'}, - {'index': 35, - 'alias': 'pneumatic_67', - 'source_component': 'pn_morifice_10', - 'source_port': 'port_2', - 'target_component': 'pnnode4_18', - 'target_port': 'port_4', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 36, - 'alias': 'pneumatic_68', - 'source_component': 'pnnode4_17', - 'source_port': 'port_2', - 'target_component': 'pn_c1_9', - 'target_port': 'port_1', - 'submodel': 'PNL0001', - 'label': 'Compressibility + friction submodel of pneumatic pipe (C-R)'}, - {'index': 37, - 'alias': 'pneumatic_69', - 'source_component': 'pnnode4_16', - 'source_port': 'port_2', - 'target_component': 'pn_c1_8', - 'target_port': 'port_1', - 'submodel': 'PNL0001', - 'label': 'Compressibility + friction submodel of pneumatic pipe (C-R)'}, - {'index': 38, - 'alias': 'pneumatic_70', - 'source_component': 'pn_morifice_12', - 'source_port': 'port_2', - 'target_component': 'pnnode4_20', - 'target_port': 'port_4', - 'submodel': 'PNL0001', - 'label': 'Compressibility + friction submodel of pneumatic pipe (C-R)'}, - {'index': 39, - 'alias': 'pneumatic_71', - 'source_component': 'pnnode4_23', - 'source_port': 'port_2', - 'target_component': 'pn_c1_12', - 'target_port': 'port_1', - 'submodel': 'PNL0001', - 'label': 'Compressibility + friction submodel of pneumatic pipe (C-R)'}, - {'index': 40, - 'alias': 'pneumatic_72', - 'source_component': 'pnnode4_20', - 'source_port': 'port_2', - 'target_component': 'pn_c1_13', - 'target_port': 'port_1', - 'submodel': 'PNL0001', - 'label': 'Compressibility + friction submodel of pneumatic pipe (C-R)'}, - {'index': 41, - 'alias': 'pneumatic_73', - 'source_component': 'pnnode4_21', - 'source_port': 'port_2', - 'target_component': 'pn_c1_14', - 'target_port': 'port_1', - 'submodel': 'PNL0001', - 'label': 'Compressibility + friction submodel of pneumatic pipe (C-R)'}, - {'index': 42, - 'alias': 'pneumatic_74', - 'source_component': 'pnnode4_22', - 'source_port': 'port_2', - 'target_component': 'pn_c1_15', - 'target_port': 'port_1', - 'submodel': 'PNL0001', - 'label': 'Compressibility + friction submodel of pneumatic pipe (C-R)'}, - {'index': 43, - 'alias': 'pneumatic_75', - 'source_component': 'pn_node3_10', - 'source_port': 'port_2', - 'target_component': 'pn_morifice_10', - 'target_port': 'port_3', - 'submodel': 'PNL0003', - 'label': 'Compressibility + friction submodel of pneumatic pipe (C-R-C)'}, - {'index': 44, - 'alias': 'pneumatic_76', - 'source_component': 'pn_node3_11', - 'source_port': 'port_2', - 'target_component': 'pn_morifice_15', - 'target_port': 'port_3', - 'submodel': 'PNL0003', - 'label': 'Compressibility + friction submodel of pneumatic pipe (C-R-C)'}, - {'index': 45, - 'alias': 'pneumatic_77', - 'source_component': 'pn_node3_12', - 'source_port': 'port_2', - 'target_component': 'pn_morifice_11', - 'target_port': 'port_3', - 'submodel': 'PNL0003', - 'label': 'Compressibility + friction submodel of pneumatic pipe (C-R-C)'}, - {'index': 46, - 'alias': 'pneumatic_78', - 'source_component': 'pn_node3_13', - 'source_port': 'port_2', - 'target_component': 'pn_morifice_12', - 'target_port': 'port_3', - 'submodel': 'PNL0003', - 'label': 'Compressibility + friction submodel of pneumatic pipe (C-R-C)'}, - {'index': 47, - 'alias': 'pneumatic_79', - 'source_component': 'pnnode4_23', - 'source_port': 'port_4', - 'target_component': 'pn_morifice_11', - 'target_port': 'port_2', - 'submodel': 'PNL0001', - 'label': 'Compressibility + friction submodel of pneumatic pipe (C-R)'}, - {'index': 48, - 'alias': 'pneumatic_80', - 'source_component': 'pnnode4_18', - 'source_port': 'port_1', - 'target_component': 'pnnode4_19', - 'target_port': 'port_3', - 'submodel': 'PNL0002', - 'label': 'Compressibility + friction submodel of pneumatic pipe (R-C-R)'}, - {'index': 49, - 'alias': 'pneumatic_81', - 'source_component': 'pnnode4_23', - 'source_port': 'port_1', - 'target_component': 'pnnode4_20', - 'target_port': 'port_3', - 'submodel': 'PNL0002', - 'label': 'Compressibility + friction submodel of pneumatic pipe (R-C-R)'}, - {'index': 50, - 'alias': 'pneumatic_82', - 'source_component': 'pnnode4_21', - 'source_port': 'port_1', - 'target_component': 'pnnode4_22', - 'target_port': 'port_3', - 'submodel': 'PNL0002', - 'label': 'Compressibility + friction submodel of pneumatic pipe (R-C-R)'}, - {'index': 51, - 'alias': 'pneumatic_83', - 'source_component': 'pnnode4_16', - 'source_port': 'port_1', - 'target_component': 'pnnode4_17', - 'target_port': 'port_3', - 'submodel': 'PNL0002', - 'label': 'Compressibility + friction submodel of pneumatic pipe (R-C-R)'}, - {'index': 52, - 'alias': 'pneumatic_84', - 'source_component': 'pnnode4_20', - 'source_port': 'port_1', - 'target_component': 'pnnode4_21', - 'target_port': 'port_3', - 'submodel': 'PNL0002', - 'label': 'Compressibility + friction submodel of pneumatic pipe (R-C-R)'}, - {'index': 53, - 'alias': 'pneumatic_85', - 'source_component': 'pnnode4_19', - 'source_port': 'port_1', - 'target_component': 'pnnode4_23', - 'target_port': 'port_3', - 'submodel': 'PNL0002', - 'label': 'Compressibility + friction submodel of pneumatic pipe (R-C-R)'}, - {'index': 54, - 'alias': 'pneumatic_86', - 'source_component': 'pnnode4_17', - 'source_port': 'port_1', - 'target_component': 'pnnode4_18', - 'target_port': 'port_3', - 'submodel': 'PNL0002', - 'label': 'Compressibility + friction submodel of pneumatic pipe (R-C-R)'}, - {'index': 55, - 'alias': 'pneumatic_87', - 'source_component': 'pn_node3_8', - 'source_port': 'port_2', - 'target_component': 'pn_morifice_1', - 'target_port': 'port_3', - 'submodel': 'PNL0003', - 'label': 'Compressibility + friction submodel of pneumatic pipe (C-R-C)'}, - {'index': 56, - 'alias': 'pneumatic_88', - 'source_component': 'pn_node3_9', - 'source_port': 'port_2', - 'target_component': 'pn_morifice_9', - 'target_port': 'port_3', - 'submodel': 'PNL0003', - 'label': 'Compressibility + friction submodel of pneumatic pipe (C-R-C)'}, - {'index': 57, - 'alias': 'pneumatic_89', - 'source_component': 'pn_morifice_1', - 'source_port': 'port_2', - 'target_component': 'pnnode4_16', - 'target_port': 'port_4', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 58, - 'alias': 'pneumatic_90', - 'source_component': 'pn_morifice_9', - 'source_port': 'port_2', - 'target_component': 'pnnode4_17', - 'target_port': 'port_4', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 59, - 'alias': 'pneumatic_91', - 'source_component': 'pn_node3_14', - 'source_port': 'port_2', - 'target_component': 'pn_morifice_13', - 'target_port': 'port_3', - 'submodel': 'PNL0003', - 'label': 'Compressibility + friction submodel of pneumatic pipe (C-R-C)'}, - {'index': 60, - 'alias': 'pneumatic_92', - 'source_component': 'pn_node3_15', - 'source_port': 'port_2', - 'target_component': 'pn_morifice_14', - 'target_port': 'port_3', - 'submodel': 'PNL0003', - 'label': 'Compressibility + friction submodel of pneumatic pipe (C-R-C)'}, - {'index': 61, - 'alias': 'pneumatic_93', - 'source_component': 'pn_morifice_13', - 'source_port': 'port_2', - 'target_component': 'pnnode4_21', - 'target_port': 'port_4', - 'submodel': 'PNL0001', - 'label': 'Compressibility + friction submodel of pneumatic pipe (C-R)'}, - {'index': 62, - 'alias': 'pneumatic_94', - 'source_component': 'pn_morifice_14', - 'source_port': 'port_2', - 'target_component': 'pnnode4_22', - 'target_port': 'port_4', - 'submodel': 'PNL0001', - 'label': 'Compressibility + friction submodel of pneumatic pipe (C-R)'}, - {'index': 63, - 'alias': 'pneumatic_95', - 'source_component': 'pnnode4_22', - 'source_port': 'port_1', - 'target_component': 'pnnode4_16', - 'target_port': 'port_3', - 'submodel': 'PNL0002', - 'label': 'Compressibility + friction submodel of pneumatic pipe (R-C-R)'}, - {'index': 64, - 'alias': 'pneumatic_96', - 'source_component': 'pn_node3_8', - 'source_port': 'port_1', - 'target_component': 'pn_orifice_18', - 'target_port': 'port_2', - 'submodel': 'PNL0001', - 'label': 'Compressibility + friction submodel of pneumatic pipe (C-R)'}, - {'index': 65, - 'alias': 'pneumatic_97', - 'source_component': 'pn_orifice_19', - 'source_port': 'port_1', - 'target_component': 'pn_node3_9', - 'target_port': 'port_3', - 'submodel': 'PNL0001', - 'label': 'Compressibility + friction submodel of pneumatic pipe (C-R)'}, - {'index': 66, - 'alias': 'pneumatic_98', - 'source_component': 'pn_node3_10', - 'source_port': 'port_1', - 'target_component': 'pn_orifice_20', - 'target_port': 'port_2', - 'submodel': 'PNL0001', - 'label': 'Compressibility + friction submodel of pneumatic pipe (C-R)'}, - {'index': 67, - 'alias': 'pneumatic_99', - 'source_component': 'pn_orifice_21', - 'source_port': 'port_1', - 'target_component': 'pn_node3_11', - 'target_port': 'port_3', - 'submodel': 'PNL0001', - 'label': 'Compressibility + friction submodel of pneumatic pipe (C-R)'}, - {'index': 68, - 'alias': 'pneumatic_100', - 'source_component': 'pn_node3_9', - 'source_port': 'port_1', - 'target_component': 'pn_node3_10', - 'target_port': 'port_3', - 'submodel': 'PNL00R', - 'label': 'Friction submodel of pneumatic pipe (R)'}, - {'index': 69, - 'alias': 'pneumatic_101', - 'source_component': 'pn_node3_12', - 'source_port': 'port_1', - 'target_component': 'pn_orifice_22', - 'target_port': 'port_2', - 'submodel': 'PNL0001', - 'label': 'Compressibility + friction submodel of pneumatic pipe (C-R)'}, - {'index': 70, - 'alias': 'pneumatic_102', - 'source_component': 'pn_orifice_23', - 'source_port': 'port_1', - 'target_component': 'pn_node3_13', - 'target_port': 'port_3', - 'submodel': 'PNL0001', - 'label': 'Compressibility + friction submodel of pneumatic pipe (C-R)'}, - {'index': 71, - 'alias': 'pneumatic_103', - 'source_component': 'pn_node3_14', - 'source_port': 'port_1', - 'target_component': 'pn_orifice_24', - 'target_port': 'port_2', - 'submodel': 'PNL0001', - 'label': 'Compressibility + friction submodel of pneumatic pipe (C-R)'}, - {'index': 72, - 'alias': 'pneumatic_104', - 'source_component': 'pn_orifice_25', - 'source_port': 'port_1', - 'target_component': 'pn_node3_15', - 'target_port': 'port_3', - 'submodel': 'PNL0001', - 'label': 'Compressibility + friction submodel of pneumatic pipe (C-R)'}, - {'index': 73, - 'alias': 'pneumatic_105', - 'source_component': 'pn_node3_8', - 'source_port': 'port_3', - 'target_component': 'pn_node3_15', - 'target_port': 'port_1', - 'submodel': 'PNL00R', - 'label': 'Friction submodel of pneumatic pipe (R)'}, - {'index': 74, - 'alias': 'pneumatic_106', - 'source_component': 'pn_node3_13', - 'source_port': 'port_1', - 'target_component': 'pn_node3_14', - 'target_port': 'port_3', - 'submodel': 'PNL00R', - 'label': 'Friction submodel of pneumatic pipe (R)'}, - {'index': 75, - 'alias': 'pneumatic_107', - 'source_component': 'pn_node3_11', - 'source_port': 'port_1', - 'target_component': 'pn_node3_12', - 'target_port': 'port_3', - 'submodel': 'PNL00R', - 'label': 'Friction submodel of pneumatic pipe (R)'}, - {'index': 76, - 'alias': 'control_8', - 'source_component': 'step_8', - 'source_port': 'port_1', - 'target_component': 'pn_morifice_11', - 'target_port': 'port_1', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 77, - 'alias': 'control_9', - 'source_component': 'step_9', - 'source_port': 'port_1', - 'target_component': 'pn_morifice_12', - 'target_port': 'port_1', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 78, - 'alias': 'control_10', - 'source_component': 'step_10', - 'source_port': 'port_1', - 'target_component': 'pn_morifice_13', - 'target_port': 'port_1', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 79, - 'alias': 'control_11', - 'source_component': 'step_11', - 'source_port': 'port_1', - 'target_component': 'pn_morifice_14', - 'target_port': 'port_1', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 80, - 'alias': 'control_12', - 'source_component': 'step_12', - 'source_port': 'port_1', - 'target_component': 'pn_morifice_1', - 'target_port': 'port_1', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 81, - 'alias': 'control_13', - 'source_component': 'step_13', - 'source_port': 'port_1', - 'target_component': 'pn_morifice_9', - 'target_port': 'port_1', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 82, - 'alias': 'control_14', - 'source_component': 'step_14', - 'source_port': 'port_1', - 'target_component': 'pn_morifice_10', - 'target_port': 'port_1', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}, - {'index': 83, - 'alias': 'control_15', - 'source_component': 'step_15', - 'source_port': 'port_1', - 'target_component': 'pn_morifice_15', - 'target_port': 'port_1', - 'submodel': 'DIRECT', - 'label': 'Direct connection'}] - - -@dataclass(frozen=True) -class TestMqlRunConfig: - t_start: float = SIMULATION_SETTINGS["t_start"] - t_stop: float = SIMULATION_SETTINGS["t_stop"] - sample_step: float = SIMULATION_SETTINGS["sample_step"] - - def sample_times(self) -> list[float]: - if self.sample_step <= 0.0: - raise ValueError("sample_step must be positive") - point_count = int(round((self.t_stop - self.t_start) / self.sample_step)) - return [self.t_start + index * self.sample_step for index in range(point_count + 1)] - - -@dataclass(frozen=True) -class TestMqlSnapshot: - model_name: str - component_count: int - connection_count: int - continuous_state_count: int - discrete_state_count: int - global_parameters: dict[str, str] - submodel_counts: dict[str, int] - - -@dataclass(frozen=True) -class TestMqlSimulationResult: - t: list[float] - y: list[list[float]] - series: dict[str, list[float]] - - -@dataclass(frozen=True) -class TestMqlFullStateSnapshot: - pneumatic: object - mechanical: object - mechanical_node_total_force_by_alias: dict[str, float] - - @property - def state_count(self) -> int: - return self.pneumatic_state_count + self.mechanical_state_count - - @property - def pneumatic_state_count(self) -> int: - return 112 - - @property - def mechanical_state_count(self) -> int: - return self.mechanical.state_count - - -@dataclass(frozen=True) -class TestMqlVariableChamberRhsDiagnostic: - chamber_alias: str - piston_alias: str - chamber_pressure_pa: float - chamber_temperature_k: float - chamber_volume_m3: float - chamber_volume_rate_m3_s: float - port_a_mass_flow_kg_s: float - port_b_mass_flow_kg_s: float - mass_derivative_kg_s: float - port_a_energy_flow_w: float - port_b_energy_flow_w: float - boundary_work_w: float - thermal_energy_flow_w: float - energy_derivative_w: float - - -@dataclass(frozen=True) -class TestMqlPnl0001LineRhsDiagnostic: - line_alias: str - chamber_alias: str - line_pressure_pa: float - line_temperature_k: float - chamber_pressure_pa: float - chamber_temperature_k: float - chamber_to_line_flow_kg_s: float - node_to_line_flow_kg_s: float - mass_derivative_kg_s: float - port_1_energy_flow_w: float - port_2_energy_flow_w: float - thermal_energy_flow_w: float - energy_derivative_w: float - - -_PISTON_FORCE_BINDINGS = ( - ("mass_friction_endstops_10", "pn_brp2_8", "p4_port3_remote_primary_chamber"), - ("mass_friction_endstops_11", "pn_brp2_9", "p4_primary_chamber"), - ("mass_friction_endstops_12", "pn_brp2_10", "p4_port1_remote_primary_chamber"), - ("mass_friction_endstops_13", "pn_brp2_11", "p4_port1_far_primary_chamber"), - ("mass_friction_endstops_14", "pn_brp2_12", "p4_port1_next_primary_chamber"), - ("mass_friction_endstops_15", "pn_brp2_13", "p4_bridge_primary_chamber"), - ("mass_friction_endstops_16", "pn_brp2_14", "p4_port3_next_primary_chamber"), - ("mass_friction_endstops_17", "pn_brp2_15", "p4_port3_far_primary_chamber"), -) - -_ELASTIC_ENDSTOP_FORCE_BINDINGS = ( - ("mass_friction_endstops_10", "mass_friction_endstops_18", "elasticendstop_8"), - ("mass_friction_endstops_11", "mass_friction_endstops_18", "elasticendstop_9"), - ("mass_friction_endstops_12", "mass_friction_endstops_18", "elasticendstop_10"), - ("mass_friction_endstops_13", "mass_friction_endstops_18", "elasticendstop_11"), - ("mass_friction_endstops_14", "mass_friction_endstops_18", "elasticendstop_12"), - ("mass_friction_endstops_15", "mass_friction_endstops_18", "elasticendstop_13"), - ("mass_friction_endstops_16", "mass_friction_endstops_18", "elasticendstop_14"), - ("mass_friction_endstops_17", "mass_friction_endstops_18", "elasticendstop_15"), -) - - -def _default_full_state_data_paths() -> tuple[str, ...]: - chamber_paths: list[str] = [] - piston_paths: list[str] = [] - for _mass_alias, piston_alias, chamber_field in _PISTON_FORCE_BINDINGS: - chamber_alias = _PNCH012_ALIAS_BY_SNAPSHOT_FIELD[chamber_field] - chamber_paths.extend([f"press@{chamber_alias}", f"vol@{chamber_alias}"]) - piston_paths.extend([f"vol1@{piston_alias}", f"vvol1@{piston_alias}"]) - mass_paths = [ - f"{signal}@mass_friction_endstops_{index}" - for index in range(10, 20) - for signal in ("x1", "v1", "acc1") - ] - return tuple( - [ - *chamber_paths, - *piston_paths, - *mass_paths, - "dm1@pneumatic_69", - "xv@pn_morifice_1", - "dm2@pn_morifice_1", - ] - ) - - -_PNCH012_ALIAS_BY_SNAPSHOT_FIELD = { - "p4_port3_remote_primary_chamber": "pn_c1_8", - "p4_primary_chamber": "pn_c1_9", - "p4_port1_remote_primary_chamber": "pn_c1_10", - "p4_port1_far_primary_chamber": "pn_c1_11", - "p4_port1_next_primary_chamber": "pn_c1_12", - "p4_bridge_primary_chamber": "pn_c1_13", - "p4_port3_next_primary_chamber": "pn_c1_14", - "p4_port3_far_primary_chamber": "pn_c1_15", -} - - -_PRIMARY_CHAMBER_DIAGNOSTIC_FIELDS = { - "pn_c1_8": ( - "p4_port3_remote_primary_chamber", - "p4_port3_remote_primary_line", - "p4_port3_remote_chamber_to_line_flow", - ), - "pn_c1_9": ( - "p4_primary_chamber", - "p4_primary_line", - "p4_primary_chamber_to_line_flow", - ), - "pn_c1_10": ( - "p4_port1_remote_primary_chamber", - "p4_port1_remote_primary_line", - "p4_port1_remote_chamber_to_line_flow", - ), - "pn_c1_11": ( - "p4_port1_far_primary_chamber", - "p4_port1_far_primary_line", - "p4_port1_far_chamber_to_line_flow", - ), - "pn_c1_12": ( - "p4_port1_next_primary_chamber", - "p4_port1_next_primary_line", - "p4_port1_next_chamber_to_line_flow", - ), - "pn_c1_13": ( - "p4_bridge_primary_chamber", - "p4_bridge_primary_line", - "p4_bridge_chamber_to_line_flow", - ), - "pn_c1_14": ( - "p4_port3_next_primary_chamber", - "p4_port3_next_primary_line", - "p4_port3_next_chamber_to_line_flow", - ), - "pn_c1_15": ( - "p4_port3_far_primary_chamber", - "p4_port3_far_primary_line", - "p4_port3_far_chamber_to_line_flow", - ), -} - - -_PNL0001_LINE_DIAGNOSTIC_FIELDS = { - "pneumatic_69": ( - "p4_port3_remote_primary_line", - "p4_port3_remote_primary_chamber", - "p4_port3_remote_chamber_to_line_flow", - "p4_port3_remote_node_to_primary_line_flow", - ), -} - -_VARIABLE_ORIFICE_MASS_FLOW_DIAGNOSTIC_FIELDS = { - "pn_morifice_1": "p4_port3_remote_orifice_to_node_flow", -} - - -class TestMqlFullStateClosure: - 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()) - - @property - def state_count(self) -> int: - return self.pneumatic_state_count + self.mechanical_state_count - - def initial_state_vector(self) -> list[float]: - return [ - *self.pneumatic_closure.initial_state_vector(), - *self.mechanical_closure.initial_state_vector(), - ] - - def _split_state(self, state_vector: list[float]) -> tuple[list[float], list[float]]: - if len(state_vector) != self.state_count: - raise ValueError("full test_mql state vector requires 132 values") - return ( - state_vector[: self.pneumatic_state_count], - state_vector[self.pneumatic_state_count :], - ) - - 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, - mechanical=mechanical_snapshot, - mechanical_node_total_force_by_alias=( - self._mechanical_node_total_force_by_alias( - pneumatic_snapshot, - mechanical_snapshot, - ) - ), - ) - - def rhs(self, state_vector: list[float]) -> list[float]: - return self.rhs_at(0.0, state_vector) - - def rhs_at(self, time_s: float, state_vector: list[float]) -> list[float]: - 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), - *self.mechanical_closure.rhs( - mechanical_state, - force_by_mass_alias=self._force_by_mass_alias( - time_s, - pneumatic_snapshot, - mechanical_snapshot, - ), - constrained_mass_aliases={ - "mass_friction_endstops_18", - "mass_friction_endstops_19", - }, - ), - ] - - def _sync_variable_chamber_kinematics(self, mechanical_snapshot: object) -> None: - for _mass_alias, piston_alias, chamber_field in _PISTON_FORCE_BINDINGS: - piston = self.mechanical_closure.assembly.pistons[piston_alias] - kinematics = mechanical_snapshot.piston_kinematics_by_alias[piston_alias] - chamber = getattr(self.pneumatic_closure.components, chamber_field) - chamber.set_external_volume_m3( - piston.geometry().chamber_volume_m3( - kinematics.port_3_displacement_m, - kinematics.port_2_displacement_m, - ), - piston.geometry().chamber_volume_rate_m3_s( - kinematics.port_3_velocity_m_s, - kinematics.port_2_velocity_m_s, - ), - ) - - def variable_chamber_rhs_diagnostic( - self, - *, - 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) - chamber_field, line_field, flow_field = _PRIMARY_CHAMBER_DIAGNOSTIC_FIELDS[ - chamber_alias - ] - piston_alias = next( - piston_alias - for _mass_alias, piston_alias, bound_chamber_field in _PISTON_FORCE_BINDINGS - if bound_chamber_field == chamber_field - ) - 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) - chamber_to_line_flow = getattr(snapshot.pneumatic, flow_field) - port_a_mass_flow = -chamber_to_line_flow - port_b_mass_flow = 0.0 - port_a_inlet_h = chamber.connection_inlet_enthalpy( - port_m_flow=port_a_mass_flow, - connected_h=line_properties.h, - internal_h=chamber_properties.h, - ) - port_b_inlet_h = chamber.connection_inlet_enthalpy( - port_m_flow=port_b_mass_flow, - connected_h=chamber_properties.h, - internal_h=chamber_properties.h, - ) - port_a_energy_flow = port_a_mass_flow * port_a_inlet_h - port_b_energy_flow = port_b_mass_flow * port_b_inlet_h - boundary_work = -chamber_properties.p * chamber.volume_rate_m3_s() - thermal_energy_flow = chamber.thermal_energy_flow_w( - chamber_properties.T - ) - return TestMqlVariableChamberRhsDiagnostic( - chamber_alias=chamber_alias, - piston_alias=piston_alias, - chamber_pressure_pa=chamber_properties.p, - chamber_temperature_k=chamber_properties.T, - chamber_volume_m3=chamber.volume, - chamber_volume_rate_m3_s=chamber.volume_rate_m3_s(), - port_a_mass_flow_kg_s=port_a_mass_flow, - port_b_mass_flow_kg_s=port_b_mass_flow, - mass_derivative_kg_s=port_a_mass_flow + port_b_mass_flow, - port_a_energy_flow_w=port_a_energy_flow, - port_b_energy_flow_w=port_b_energy_flow, - boundary_work_w=boundary_work, - thermal_energy_flow_w=thermal_energy_flow, - energy_derivative_w=( - port_a_energy_flow - + port_b_energy_flow - + boundary_work - + thermal_energy_flow - ), - ) - - def pnl0001_line_rhs_diagnostic( - self, - *, - line_alias: str, - state_vector: list[float], - time_s: float = 0.0, - ) -> TestMqlPnl0001LineRhsDiagnostic: - if line_alias not in _PNL0001_LINE_DIAGNOSTIC_FIELDS: - raise KeyError(line_alias) - ( - line_field, - chamber_field, - chamber_flow_field, - node_flow_field, - ) = _PNL0001_LINE_DIAGNOSTIC_FIELDS[line_alias] - chamber_alias = _PNCH012_ALIAS_BY_SNAPSHOT_FIELD[chamber_field] - snapshot = self.snapshot_at(time_s, state_vector) - line = getattr(self.pneumatic_closure.components, line_field) - line_properties = getattr(snapshot.pneumatic, line_field) - chamber_properties = getattr(snapshot.pneumatic, chamber_field) - chamber_to_line_flow = getattr(snapshot.pneumatic, chamber_flow_field) - node_to_line_flow = getattr(snapshot.pneumatic, node_flow_field) - p4_balance = snapshot.pneumatic.p4_port3_remote_balance - p4_port_2_mass_flow_kg_s = p4_balance.port_2_mass_flow_g_s * 1.0e-3 - node_connected_h = ( - line_properties.h - if abs(p4_port_2_mass_flow_kg_s) <= 1.0e-12 - else p4_balance.port_2_enthalpy_flow_w / p4_port_2_mass_flow_kg_s - ) - line_derivative = line.derivatives_from_transport_enthalpy_connections( - port_1_m_flow=chamber_to_line_flow, - connected_h_1=chamber_properties.h, - port_2_m_flow=node_to_line_flow, - connected_h_2=node_connected_h, - ) - port_1_inlet_h = ( - chamber_properties.h if chamber_to_line_flow > 0.0 else line_properties.h - ) - port_2_inlet_h = ( - node_connected_h if node_to_line_flow > 0.0 else line_properties.h - ) - thermal_energy_flow = ( - line.heat_transfer_coefficient - * line.heat_transfer_area - * (line.external_temperature - line_properties.T) - ) - return TestMqlPnl0001LineRhsDiagnostic( - line_alias=line_alias, - chamber_alias=chamber_alias, - line_pressure_pa=line_properties.p, - line_temperature_k=line_properties.T, - chamber_pressure_pa=chamber_properties.p, - chamber_temperature_k=chamber_properties.T, - chamber_to_line_flow_kg_s=chamber_to_line_flow, - node_to_line_flow_kg_s=node_to_line_flow, - mass_derivative_kg_s=line_derivative.m, - port_1_energy_flow_w=chamber_to_line_flow * port_1_inlet_h, - port_2_energy_flow_w=node_to_line_flow * port_2_inlet_h, - thermal_energy_flow_w=thermal_energy_flow, - energy_derivative_w=line_derivative.U, - ) - - def data_path_values( - self, - *, - time_s: float, - state_vector: list[float], - data_paths: tuple[str, ...] | list[str] | None = None, - ) -> dict[str, float]: - 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_at(time_s, state_vector) - rhs = self.rhs_at(time_s, state_vector) - chamber_properties_by_alias = {} - chamber_by_alias = {} - for _mass_alias, _piston_alias, chamber_field in _PISTON_FORCE_BINDINGS: - chamber = getattr(self.pneumatic_closure.components, chamber_field) - chamber_by_alias[chamber.name] = chamber - chamber_properties_by_alias[chamber.name] = getattr(snapshot.pneumatic, chamber_field) - - state_by_alias = {state.alias: state for state in snapshot.mechanical.states} - acceleration_by_mass_alias = { - alias: rhs[self.pneumatic_state_count + 2 * index] - for index, alias in enumerate(self.mechanical_closure.mass_aliases) - } - values_by_data_path = {} - for data_path in selected_paths: - signal, alias = self._split_data_path(data_path) - if alias in _PNL0001_LINE_DIAGNOSTIC_FIELDS: - ( - line_field, - _chamber_field, - chamber_flow_field, - _node_flow_field, - ) = _PNL0001_LINE_DIAGNOSTIC_FIELDS[alias] - if signal == "dm1": - canonical_flow_kg_s = getattr(snapshot.pneumatic, chamber_flow_field) - values_by_data_path[data_path] = -canonical_flow_kg_s * 1.0e3 - elif signal == "p2": - line_properties = getattr(snapshot.pneumatic, line_field) - values_by_data_path[data_path] = pressure_to_amesim_gauge_pa( - line_properties.p - ) - elif signal == "t2": - line_properties = getattr(snapshot.pneumatic, line_field) - values_by_data_path[data_path] = line_properties.T - elif signal == "mgas": - line = getattr(self.pneumatic_closure.components, line_field) - values_by_data_path[data_path] = line.gas_mass_g() - elif signal in {"re", "v", "ff"}: - line = getattr(self.pneumatic_closure.components, line_field) - line_properties = getattr(snapshot.pneumatic, line_field) - canonical_flow_kg_s = getattr(snapshot.pneumatic, chamber_flow_field) - diagnostics = line.diagnostics( - mass_flow_kg_s=-canonical_flow_kg_s, - temperature_k=line_properties.T, - ) - if signal == "re": - values_by_data_path[data_path] = diagnostics.reynolds_number - elif signal == "v": - values_by_data_path[data_path] = diagnostics.gas_velocity_m_s - else: - values_by_data_path[data_path] = diagnostics.friction_factor - else: - raise KeyError(data_path) - elif alias in self.pneumatic_assembly.variable_orifices: - orifice = self.pneumatic_assembly.variable_orifices[alias] - if signal == "xv": - values_by_data_path[data_path] = orifice.opening - elif ( - signal == "dm2" - and alias in _VARIABLE_ORIFICE_MASS_FLOW_DIAGNOSTIC_FIELDS - ): - flow_kg_s = getattr( - snapshot.pneumatic, - _VARIABLE_ORIFICE_MASS_FLOW_DIAGNOSTIC_FIELDS[alias], - ) - values_by_data_path[data_path] = flow_kg_s * 1.0e3 - else: - raise KeyError(data_path) - elif alias in chamber_properties_by_alias: - chamber_properties = chamber_properties_by_alias[alias] - chamber = chamber_by_alias[alias] - if signal == "press": - values_by_data_path[data_path] = pressure_to_amesim_gauge_pa( - chamber_properties.p - ) - elif signal == "temp": - values_by_data_path[data_path] = chamber_properties.T - elif signal == "vol": - values_by_data_path[data_path] = chamber.volume_cm3() - elif signal in {"mgas", "mgas1"}: - values_by_data_path[data_path] = chamber.gas_mass_g() - else: - raise KeyError(data_path) - elif alias in self.mechanical_closure.assembly.pistons: - piston = self.mechanical_closure.assembly.pistons[alias] - kinematics = snapshot.mechanical.piston_kinematics_by_alias[alias] - geometry = piston.geometry() - if signal == "vol1": - values_by_data_path[data_path] = geometry.chamber_volume_cm3( - kinematics.port_3_displacement_m, - kinematics.port_2_displacement_m, - ) - elif signal == "vvol1": - values_by_data_path[data_path] = geometry.chamber_volume_rate_l_min( - kinematics.port_3_velocity_m_s, - kinematics.port_2_velocity_m_s, - ) - elif signal == "length": - values_by_data_path[data_path] = geometry.chamber_length_mm( - kinematics.port_3_displacement_m, - kinematics.port_2_displacement_m, - ) - elif signal == "x4": - values_by_data_path[data_path] = kinematics.port_3_displacement_m - elif signal == "x5": - values_by_data_path[data_path] = kinematics.port_2_displacement_m - elif signal == "v4": - values_by_data_path[data_path] = kinematics.port_3_velocity_m_s - elif signal == "v5": - values_by_data_path[data_path] = kinematics.port_2_velocity_m_s - else: - raise KeyError(data_path) - elif alias in state_by_alias: - mass_state = state_by_alias[alias] - if signal == "x1": - values_by_data_path[data_path] = mass_state.displacement_m - elif signal == "v1": - values_by_data_path[data_path] = mass_state.velocity_m_s - elif signal == "acc1": - values_by_data_path[data_path] = acceleration_by_mass_alias[alias] - else: - raise KeyError(data_path) - else: - raise KeyError(data_path) - return values_by_data_path - - def data_path_series( - self, - *, - times: tuple[float, ...] | list[float], - state_rows: list[list[float]], - data_paths: tuple[str, ...] | list[str] | None = None, - ) -> dict[str, list[float]]: - selected_paths = tuple(data_paths) if data_paths is not None else _default_full_state_data_paths() - series = {data_path: [] for data_path in selected_paths} - for sample_index, time_value in enumerate(times): - state_vector = [row[sample_index] for row in state_rows] - values = self.data_path_values( - time_s=float(time_value), - state_vector=state_vector, - data_paths=selected_paths, - ) - for data_path in selected_paths: - series[data_path].append(values[data_path]) - return series - - @staticmethod - def _split_data_path(data_path: str) -> tuple[str, str]: - if "@" not in data_path: - raise KeyError(data_path) - signal, alias = data_path.split("@", 1) - return signal, alias - - def _force_by_mass_alias( - self, - time_s: float, - pneumatic_snapshot: object, - mechanical_snapshot: object, - ) -> dict[str, float]: - force_by_mass_alias = self._piston_force_by_mass_alias(pneumatic_snapshot) - elastic_force_by_mass_alias = self._elastic_endstop_force_by_mass_alias( - mechanical_snapshot - ) - for mass_alias, contact_force in elastic_force_by_mass_alias.items(): - force_by_mass_alias[mass_alias] = ( - force_by_mass_alias.get(mass_alias, 0.0) - contact_force - ) - force_by_mass_alias.update( - self._large_mass_force_by_alias( - time_s, - pneumatic_snapshot, - mechanical_snapshot, - ) - ) - return force_by_mass_alias - - def _mechanical_node_total_force_by_alias( - self, - pneumatic_snapshot: object, - mechanical_snapshot: object, - ) -> dict[str, float]: - return { - "dynamic_mechanical_node_alternative_2": sum( - self._elastic_endstop_force_by_mass_alias(mechanical_snapshot).values() - ), - "dynamic_mechanical_node_alternative_3": sum( - self._piston_force_by_mass_alias(pneumatic_snapshot).values() - ), - } - - def _large_mass_force_by_alias( - self, - time_s: float, - pneumatic_snapshot: object, - mechanical_snapshot: object, - ) -> dict[str, float]: - node_force = self._mechanical_node_total_force_by_alias( - pneumatic_snapshot, - mechanical_snapshot, - ) - forcecon_1 = self.mechanical_closure.assembly.force_connectors["forcecon_1"] - forcecon_2 = self.mechanical_closure.assembly.force_connectors["forcecon_2"] - signals = self.mechanical_closure.assembly.piecewise_signals - return { - "mass_friction_endstops_18": ( - node_force["dynamic_mechanical_node_alternative_2"] - - forcecon_2.force_at(time_s, signals) - ), - "mass_friction_endstops_19": ( - forcecon_1.force_at(time_s, signals) - - node_force["dynamic_mechanical_node_alternative_3"] - ), - } - - def _piston_force_by_mass_alias(self, pneumatic_snapshot: object) -> dict[str, float]: - from PythonModels.systems.test_mql_pneumatic import pressure_to_amesim_gauge_pa - - force_by_mass_alias: dict[str, float] = {} - for mass_alias, piston_alias, chamber_field in _PISTON_FORCE_BINDINGS: - chamber = getattr(pneumatic_snapshot, chamber_field) - piston = self.mechanical_closure.assembly.pistons[piston_alias] - force_by_mass_alias[mass_alias] = ( - pressure_to_amesim_gauge_pa(chamber.p) * piston.annulus_area_m2 - ) - return force_by_mass_alias - - def _elastic_endstop_force_by_mass_alias( - self, - mechanical_snapshot: object, - ) -> dict[str, float]: - state_by_alias = {state.alias: state for state in mechanical_snapshot.states} - force_by_mass_alias: dict[str, float] = {} - for mass_alias, support_mass_alias, endstop_alias in _ELASTIC_ENDSTOP_FORCE_BINDINGS: - mass_state = state_by_alias[mass_alias] - support_state = state_by_alias[support_mass_alias] - endstop = self.mechanical_closure.assembly.elastic_endstops[ - endstop_alias - ].endstop() - gap_mm = (support_state.displacement_m - mass_state.displacement_m) * 1.0e3 - penetration_velocity_m_s = ( - mass_state.velocity_m_s - support_state.velocity_m_s - ) - force_by_mass_alias[mass_alias] = endstop.contact_force( - gap_mm, - penetration_velocity_m_s, - ) - return force_by_mass_alias - - -class AmesimStructuralComponent(Component): - def __init__(self, spec: dict[str, object]) -> None: - super().__init__(name=str(spec["alias"])) - self.spec = spec - self.submodel = str(spec["submodel"]) - self.component_name = str(spec["component_name"]) - self.parameters = { - str(parameter["name"]): parameter - for parameter in spec.get("parameters", []) - } - - @property - def port_names(self) -> tuple[str, ...]: - return tuple(str(port["name"]) for port in self.spec.get("ports", [])) - - -class TestMqlSystem: - """Structural Python port scaffold for AMESim model ``test_mql``.""" - - def __init__(self, archive_path: Path | None = None) -> None: - self.archive_path = archive_path or Path(__file__).resolve().parents[2] / AMESIM_ARCHIVE_RELATIVE_PATH - self.network = SimulationNetwork(name=MODEL_NAME) - self.pnl0001_assembly = self._build_pnl0001_assembly() - self.pnl0002_assembly = self._build_pnl0002_assembly() - self.pnl0003_assembly = self._build_pnl0003_assembly() - self.pnl00r_assembly = self._build_pnl00r_assembly() - self.node3_assembly = self._build_node3_assembly() - self.node4_assembly = self._build_node4_assembly() - self.mechanical_assembly = self._build_mechanical_assembly() - self.pneumatic_assembly = self._build_pneumatic_assembly() - pneumatic_components = self._pneumatic_components_by_alias() - for spec in COMPONENT_SPECS: - alias = str(spec["alias"]) - self.network.add_component( - pneumatic_components.get(alias, AmesimStructuralComponent(spec)) - ) - for spec in CONNECTION_SPECS: - self.network.connect( - str(spec["source_component"]), - str(spec["source_port"]), - str(spec["target_component"]), - str(spec["target_port"]), - ) - - @staticmethod - def _build_pneumatic_assembly(): - # Local import avoids a module cycle: test_mql_config reads constants from this module. - from PythonModels.systems.test_mql_pneumatic import build_test_mql_pneumatic_assembly - - return build_test_mql_pneumatic_assembly() - - @staticmethod - def _build_mechanical_assembly(): - # Local import avoids a module cycle: test_mql_config reads constants from this module. - from PythonModels.systems.test_mql_mechanical import build_test_mql_mechanical_assembly - - return build_test_mql_mechanical_assembly() - - def _build_pnl0001_assembly(self): - from PythonModels.systems.test_mql_pneumatic_lines import ( - build_test_mql_pnl0001_assembly, - ) - - return build_test_mql_pnl0001_assembly(self.archive_path) - - def _build_pnl0002_assembly(self): - from PythonModels.systems.test_mql_pneumatic_lines import ( - build_test_mql_pnl0002_assembly, - ) - - return build_test_mql_pnl0002_assembly(self.archive_path) - - def _build_pnl0003_assembly(self): - from PythonModels.systems.test_mql_pneumatic_lines import ( - build_test_mql_pnl0003_assembly, - ) - - return build_test_mql_pnl0003_assembly(self.archive_path) - - def _build_pnl00r_assembly(self): - from PythonModels.systems.test_mql_pneumatic_lines import ( - build_test_mql_pnl00r_assembly, - ) - - return build_test_mql_pnl00r_assembly(self.archive_path) - - @staticmethod - def _build_node3_assembly(): - from PythonModels.systems.test_mql_nodes import build_test_mql_node3_assembly - - return build_test_mql_node3_assembly() - - @staticmethod - def _build_node4_assembly(): - from PythonModels.systems.test_mql_nodes import build_test_mql_node4_assembly - - return build_test_mql_node4_assembly() - - def _pneumatic_components_by_alias(self) -> dict[str, Component]: - return { - **self.pneumatic_assembly.fixed_chambers, - **self.pneumatic_assembly.variable_chambers, - **self.pneumatic_assembly.fixed_orifices, - **self.pneumatic_assembly.variable_orifices, - } - - @property - def typed_pneumatic_component_count(self) -> int: - return len(self._pneumatic_components_by_alias()) - - @property - def typed_pnl0001_line_count(self) -> int: - return len(self.pnl0001_assembly.lines) - - @property - def typed_pnl0002_line_count(self) -> int: - return len(self.pnl0002_assembly.lines) - - @property - def typed_pnl0003_line_count(self) -> int: - return len(self.pnl0003_assembly.lines) - - @property - def typed_pnl00r_line_count(self) -> int: - return len(self.pnl00r_assembly.lines) - - @property - def typed_node3_count(self) -> int: - return len(self.node3_assembly) - - @property - def typed_node4_count(self) -> int: - return len(self.node4_assembly) - - def _pnl0001_spec_for_node_port(self, node_alias: str, port_name: str): - for spec in self.pnl0001_assembly.specs: - if spec.source_component == node_alias and spec.source_port == port_name: - return spec - if spec.target_component == node_alias and spec.target_port == port_name: - return spec - raise KeyError(f"No PNL0001 line attached to {node_alias}.{port_name}") - - def _pnl0001_line_for_node_port(self, node_alias: str, port_name: str): - spec = self._pnl0001_spec_for_node_port(node_alias, port_name) - return self.pnl0001_assembly.lines[spec.alias] - - def _pnl0002_connection_for_node_port(self, node_alias: str, port_name: str): - from PythonModels.systems.test_mql_nodes import TestMqlP4NodePortConnection - - spec = self._pnl0002_spec_for_node_port(node_alias, port_name) - if spec.source_component == node_alias and spec.source_port == port_name: - remote_node_alias = spec.target_component - remote_port = spec.target_port - elif spec.target_component == node_alias and spec.target_port == port_name: - remote_node_alias = spec.source_component - remote_port = spec.source_port - else: - raise KeyError(f"No PNL0002 line attached to {node_alias}.{port_name}") - if remote_node_alias not in self.node4_assembly: - raise KeyError( - f"PNL0002 line {spec.alias} remote endpoint is not a P4 node: " - f"{remote_node_alias}.{remote_port}" - ) - return TestMqlP4NodePortConnection( - line_alias=spec.alias, - local_node_alias=node_alias, - local_port=port_name, - remote_node_alias=remote_node_alias, - remote_port=remote_port, - ) - - def _pnl0002_spec_for_node_port(self, node_alias: str, port_name: str): - for spec in self.pnl0002_assembly.specs: - if spec.source_component == node_alias and spec.source_port == port_name: - return spec - if spec.target_component == node_alias and spec.target_port == port_name: - return spec - raise KeyError(f"No PNL0002 line attached to {node_alias}.{port_name}") - - def _pnl0002_line_for_node_port(self, node_alias: str, port_name: str): - spec = self._pnl0002_spec_for_node_port(node_alias, port_name) - return self.pnl0002_assembly.lines[spec.alias] - - def _p4_primary_connection_for_node(self, node_alias: str): - from PythonModels.systems.test_mql_nodes import TestMqlP4NodePrimaryConnection - - spec = self._pnl0001_spec_for_node_port(node_alias, "port_2") - if spec.source_component == node_alias and spec.source_port == "port_2": - chamber_alias = spec.target_component - chamber_port = spec.target_port - elif spec.target_component == node_alias and spec.target_port == "port_2": - chamber_alias = spec.source_component - chamber_port = spec.source_port - else: - raise KeyError(f"No PNL0001 primary line attached to {node_alias}.port_2") - return TestMqlP4NodePrimaryConnection( - line_alias=spec.alias, - node_alias=node_alias, - node_port="port_2", - chamber_alias=chamber_alias, - chamber_port=chamber_port, - ) - - def _p4_port4_orifice_connection_for_node(self, node_alias: str): - from PythonModels.systems.test_mql_nodes import TestMqlP4NodeOrificeConnection - - for spec in CONNECTION_SPECS: - if spec["submodel"] != "DIRECT": - continue - if spec["target_component"] == node_alias and spec["target_port"] == "port_4": - orifice_alias = str(spec["source_component"]) - if orifice_alias in self.pneumatic_assembly.variable_orifices: - return TestMqlP4NodeOrificeConnection( - orifice_alias=orifice_alias, - node_alias=node_alias, - node_port="port_4", - direct_line_alias=str(spec["alias"]), - ) - if spec["source_component"] == node_alias and spec["source_port"] == "port_4": - orifice_alias = str(spec["target_component"]) - if orifice_alias in self.pneumatic_assembly.variable_orifices: - return TestMqlP4NodeOrificeConnection( - orifice_alias=orifice_alias, - node_alias=node_alias, - node_port="port_4", - direct_line_alias=str(spec["alias"]), - ) - raise KeyError(f"No PNVO orifice attached to {node_alias}.port_4") - - def p4_node_neighborhood(self, node_alias: str): - from PythonModels.systems.test_mql_nodes import TestMqlP4NodeNeighborhood - - if node_alias not in self.node4_assembly: - raise KeyError(f"Unknown P4 node: {node_alias}") - return TestMqlP4NodeNeighborhood( - node_alias=node_alias, - primary=self._p4_primary_connection_for_node(node_alias), - port_1=self._pnl0002_connection_for_node_port(node_alias, "port_1"), - port_3=self._pnl0002_connection_for_node_port(node_alias, "port_3"), - port_4=self._p4_port4_orifice_connection_for_node(node_alias), - ) - - def _p4_node_alias_for_orifice_port_2(self, orifice_alias: str) -> str: - for spec in CONNECTION_SPECS: - if spec["submodel"] != "DIRECT": - continue - if ( - spec["source_component"] == orifice_alias - and spec["source_port"] == "port_2" - and str(spec["target_component"]) in self.node4_assembly - ): - return str(spec["target_component"]) - if ( - spec["target_component"] == orifice_alias - and spec["target_port"] == "port_2" - and str(spec["source_component"]) in self.node4_assembly - ): - return str(spec["source_component"]) - raise KeyError(f"No P4 node attached to {orifice_alias}.port_2") - - def _pnl0003_spec_for_node_port(self, node_alias: str, port_name: str): - for spec in self.pnl0003_assembly.specs: - if spec.source_component == node_alias and spec.source_port == port_name: - return spec - if spec.target_component == node_alias and spec.target_port == port_name: - return spec - raise KeyError(f"No PNL0003 line attached to {node_alias}.{port_name}") - - def _pnl0003_line_for_node_port(self, node_alias: str, port_name: str): - spec = self._pnl0003_spec_for_node_port(node_alias, port_name) - return self.pnl0003_assembly.lines[spec.alias] - - def _pnl00r_line_for_node_port(self, node_alias: str, port_name: str): - for spec in self.pnl00r_assembly.specs: - if spec.source_component == node_alias and spec.source_port == port_name: - return self.pnl00r_assembly.lines[spec.alias] - if spec.target_component == node_alias and spec.target_port == port_name: - return self.pnl00r_assembly.lines[spec.alias] - raise KeyError(f"No PNL00R line attached to {node_alias}.{port_name}") - - def pneumatic_state_vector(self) -> list[float]: - return self.network.initial_state_vector() - - def mechanical_mass_closure(self): - from PythonModels.systems.test_mql_mechanical import TestMqlMechanicalMassClosure - - return TestMqlMechanicalMassClosure(self.mechanical_assembly) - - def mechanical_state_vector(self) -> list[float]: - return self.mechanical_mass_closure().initial_state_vector() - - def simulate_mechanical_masses( - self, - config: SolveIVPConfig | None = None, - t_eval: list[float] | None = None, - ): - closure = self.mechanical_mass_closure() - return integrate_ode( - rhs=lambda t, state: closure.rhs(state), - initial_state=closure.initial_state_vector(), - config=config or SolveIVPConfig(), - t_eval=t_eval, - ) - - def full_state_closure_from_spec( - self, - spec, - *, - inlet_node_pressure_pa: float, - resistance_boundary_pressure_pa: float, - inlet_node_temperature_k: float = 293.15, - resistance_boundary_temperature_k: float = 293.15, - use_pneumatic_96_reference_rhs: bool = False, - use_pneumatic_69_reference_rhs: bool = False, - ) -> TestMqlFullStateClosure: - return TestMqlFullStateClosure( - pneumatic_closure=self.pn3_p4_node_chamber_segment_closure_from_spec( - spec, - inlet_node_pressure_pa=inlet_node_pressure_pa, - resistance_boundary_pressure_pa=resistance_boundary_pressure_pa, - inlet_node_temperature_k=inlet_node_temperature_k, - resistance_boundary_temperature_k=resistance_boundary_temperature_k, - use_inlet_line_reference_rhs=use_pneumatic_96_reference_rhs, - use_p4_port3_remote_primary_reference_rhs=( - use_pneumatic_69_reference_rhs - ), - ), - mechanical_closure=self.mechanical_mass_closure(), - pneumatic_assembly=self.pneumatic_assembly, - ) - - def simulate_full_state_from_spec( - self, - spec, - *, - inlet_node_pressure_pa: float, - resistance_boundary_pressure_pa: float, - inlet_node_temperature_k: float = 293.15, - resistance_boundary_temperature_k: float = 293.15, - use_pneumatic_96_reference_rhs: bool = False, - use_pneumatic_69_reference_rhs: bool = False, - config: SolveIVPConfig | None = None, - t_eval: list[float] | None = None, - ): - closure = self.full_state_closure_from_spec( - spec, - inlet_node_pressure_pa=inlet_node_pressure_pa, - resistance_boundary_pressure_pa=resistance_boundary_pressure_pa, - inlet_node_temperature_k=inlet_node_temperature_k, - resistance_boundary_temperature_k=resistance_boundary_temperature_k, - use_pneumatic_96_reference_rhs=use_pneumatic_96_reference_rhs, - use_pneumatic_69_reference_rhs=use_pneumatic_69_reference_rhs, - ) - return integrate_ode( - rhs=lambda t, state: closure.rhs_at(t, state), - initial_state=closure.initial_state_vector(), - config=config or SolveIVPConfig(t_stop=1.0e-4, max_step=1.0e-5), - t_eval=t_eval, - ) - - def simulate_full_state_series_from_spec( - self, - spec, - *, - inlet_node_pressure_pa: float, - resistance_boundary_pressure_pa: float, - inlet_node_temperature_k: float = 293.15, - resistance_boundary_temperature_k: float = 293.15, - config: SolveIVPConfig | None = None, - t_eval: list[float] | None = None, - data_paths: tuple[str, ...] | list[str] | None = None, - use_pneumatic_96_reference_rhs: bool = False, - use_pneumatic_69_reference_rhs: bool = False, - ) -> TestMqlSimulationResult: - closure = self.full_state_closure_from_spec( - spec, - inlet_node_pressure_pa=inlet_node_pressure_pa, - resistance_boundary_pressure_pa=resistance_boundary_pressure_pa, - inlet_node_temperature_k=inlet_node_temperature_k, - resistance_boundary_temperature_k=resistance_boundary_temperature_k, - use_pneumatic_96_reference_rhs=use_pneumatic_96_reference_rhs, - use_pneumatic_69_reference_rhs=use_pneumatic_69_reference_rhs, - ) - solution = integrate_ode( - rhs=lambda t, state: closure.rhs_at(t, state), - initial_state=closure.initial_state_vector(), - config=config or SolveIVPConfig(t_stop=1.0e-4, max_step=1.0e-5), - t_eval=t_eval, - ) - times = [float(value) for value in solution.t] - state_rows = [list(row) for row in solution.y] - series = { - "time": times, - **closure.data_path_series( - times=times, - state_rows=state_rows, - data_paths=data_paths, - ), - } - return TestMqlSimulationResult(t=times, y=state_rows, series=series) - - @staticmethod - def pneumatic_branch_spec( - *, - name: str, - upstream_volume_alias: str, - orifice_alias: str, - downstream_volume_alias: str, - source: str = "manual", - ): - from PythonModels.systems.test_mql_closure import TestMqlPneumaticBranchSpec - - return TestMqlPneumaticBranchSpec( - name=name, - upstream_volume_alias=upstream_volume_alias, - orifice_alias=orifice_alias, - downstream_volume_alias=downstream_volume_alias, - source=source, - ) - - def discover_pneumatic_branch_topology(self): - from PythonModels.systems.test_mql_closure import ( - TestMqlPneumaticTopologyCandidate, - TestMqlPneumaticTopologyDiscovery, - ) - from PythonModels.systems.test_mql_pneumatic_topology import ( - discover_fixed_chamber_segments, - ) - from PythonModels.systems.test_mql_topology import load_test_mql_cir_topology - - topology = load_test_mql_cir_topology(self.archive_path) - chamber_segment_specs = discover_fixed_chamber_segments( - topology, - COMPONENT_SPECS, - CONNECTION_SPECS, - ) - - typed_aliases = set(self._pneumatic_components_by_alias()) - component_submodels = { - str(component["alias"]): str(component["submodel"]) - for component in COMPONENT_SPECS - } - blocked_candidates = [] - for spec in CONNECTION_SPECS: - source_component = str(spec["source_component"]) - target_component = str(spec["target_component"]) - source_is_typed = source_component in typed_aliases - target_is_typed = target_component in typed_aliases - if not (source_is_typed or target_is_typed): - continue - - line_submodel = str(spec["submodel"]) - endpoint_submodels = { - component_submodels.get(source_component, ""), - component_submodels.get(target_component, ""), - } - if "PNRP17" in endpoint_submodels: - category = "piston-boundary" - reason = "requires pneumatic piston and mechanical coupling interpretation" - elif "STEP0" in endpoint_submodels: - category = "control-boundary" - reason = "requires modulated-orifice control signal evaluation" - elif endpoint_submodels & {"PN3NODE2", "P4NODE2"}: - category = "node-line-boundary" - reason = "requires AMESim node/line equations before full-network closure" - elif source_is_typed and target_is_typed: - category = "typed-direct" - reason = "direct typed connection is not a complete simulated subnetwork" - elif line_submodel != "DIRECT": - category = "line-boundary" - reason = "requires AMESim line equations before full-network closure" - else: - category = "component-boundary" - reason = "requires AMESim boundary component interpretation" - - blocked_candidates.append( - TestMqlPneumaticTopologyCandidate( - connection_alias=str(spec["alias"]), - line_submodel=line_submodel, - source_component=source_component, - source_port=str(spec["source_port"]), - target_component=target_component, - target_port=str(spec["target_port"]), - source_is_typed_pneumatic=source_is_typed, - target_is_typed_pneumatic=target_is_typed, - category=category, - reason=reason, - ) - ) - - return TestMqlPneumaticTopologyDiscovery( - branch_specs=(), - chamber_segment_specs=chamber_segment_specs, - blocked_candidates=tuple(blocked_candidates), - ) - - def pneumatic_chamber_segment_closure_from_spec( - self, - spec, - *, - inlet_pressure_pa: float, - outlet_pressure_pa: float, - inlet_temperature_k: float = 293.15, - outlet_temperature_k: float = 293.15, - ): - """Build a reduced segment using absolute-pressure line boundaries. - - Boundary values apply at the PNL0001/orifice interfaces, not at the nodes. - """ - from PythonModels.components.amesim_pneumatic import ( - AmesimPneumaticOrifice, - AmesimPneumaticVolume, - ) - from PythonModels.systems.test_mql_closure import ( - TestMqlPneumaticBoundaryCondition, - TestMqlPneumaticChamberSegmentClosure, - TestMqlPneumaticChamberSegmentComponents, - ) - - volume = self.network.components[spec.volume_alias] - inlet_orifice = self.network.components[spec.inlet_orifice_alias] - outlet_orifice = self.network.components[spec.outlet_orifice_alias] - if not isinstance(volume, AmesimPneumaticVolume): - raise TypeError(f"{spec.volume_alias} is not an AMESim pneumatic volume") - if not isinstance(inlet_orifice, AmesimPneumaticOrifice): - raise TypeError( - f"{spec.inlet_orifice_alias} is not an AMESim pneumatic orifice" - ) - if not isinstance(outlet_orifice, AmesimPneumaticOrifice): - raise TypeError( - f"{spec.outlet_orifice_alias} is not an AMESim pneumatic orifice" - ) - return TestMqlPneumaticChamberSegmentClosure( - components=TestMqlPneumaticChamberSegmentComponents( - volume=volume, - inlet_orifice=inlet_orifice, - outlet_orifice=outlet_orifice, - spec=spec, - ), - inlet_boundary=TestMqlPneumaticBoundaryCondition( - pressure_pa=inlet_pressure_pa, - temperature_k=inlet_temperature_k, - ), - outlet_boundary=TestMqlPneumaticBoundaryCondition( - pressure_pa=outlet_pressure_pa, - temperature_k=outlet_temperature_k, - ), - ) - - def simulate_pneumatic_chamber_segment_from_spec( - self, - spec, - *, - inlet_pressure_pa: float, - outlet_pressure_pa: float, - inlet_temperature_k: float = 293.15, - outlet_temperature_k: float = 293.15, - config: SolveIVPConfig | None = None, - t_eval: list[float] | None = None, - ): - """Integrate a reduced segment with absolute-pressure line boundaries.""" - closure = self.pneumatic_chamber_segment_closure_from_spec( - spec, - inlet_pressure_pa=inlet_pressure_pa, - outlet_pressure_pa=outlet_pressure_pa, - inlet_temperature_k=inlet_temperature_k, - outlet_temperature_k=outlet_temperature_k, - ) - run_config = config or SolveIVPConfig(t_stop=1.0e-4, max_step=1.0e-5) - return integrate_ode( - rhs=lambda t, state: closure.rhs(state), - initial_state=closure.initial_state_vector(), - config=run_config, - t_eval=t_eval, - ) - - def pnl0001_chamber_segment_closure_from_spec( - self, - spec, - *, - inlet_node_pressure_pa: float, - outlet_pressure_pa: float, - inlet_node_temperature_k: float = 293.15, - outlet_temperature_k: float = 293.15, - ): - """Insert the topology-derived inlet PNL0001 into a chamber segment.""" - from PythonModels.components.amesim_pneumatic import ( - AmesimPneumaticOrifice, - AmesimPneumaticVolume, - ) - from PythonModels.systems.test_mql_closure import ( - TestMqlPneumaticBoundaryCondition, - TestMqlPnl0001ChamberSegmentClosure, - TestMqlPnl0001ChamberSegmentComponents, - ) - - inlet_line = self.pnl0001_assembly.lines[spec.inlet_line_alias] - volume = self.network.components[spec.volume_alias] - inlet_orifice = self.network.components[spec.inlet_orifice_alias] - outlet_orifice = self.network.components[spec.outlet_orifice_alias] - if not isinstance(volume, AmesimPneumaticVolume): - raise TypeError(f"{spec.volume_alias} is not an AMESim pneumatic volume") - if not isinstance(inlet_orifice, AmesimPneumaticOrifice): - raise TypeError( - f"{spec.inlet_orifice_alias} is not an AMESim pneumatic orifice" - ) - if not isinstance(outlet_orifice, AmesimPneumaticOrifice): - raise TypeError( - f"{spec.outlet_orifice_alias} is not an AMESim pneumatic orifice" - ) - return TestMqlPnl0001ChamberSegmentClosure( - components=TestMqlPnl0001ChamberSegmentComponents( - inlet_line=inlet_line, - volume=volume, - inlet_orifice=inlet_orifice, - outlet_orifice=outlet_orifice, - spec=spec, - ), - inlet_node=TestMqlPneumaticBoundaryCondition( - pressure_pa=inlet_node_pressure_pa, - temperature_k=inlet_node_temperature_k, - ), - outlet_boundary=TestMqlPneumaticBoundaryCondition( - pressure_pa=outlet_pressure_pa, - temperature_k=outlet_temperature_k, - ), - ) - - def simulate_pnl0001_chamber_segment_from_spec( - self, - spec, - *, - inlet_node_pressure_pa: float, - outlet_pressure_pa: float, - inlet_node_temperature_k: float = 293.15, - outlet_temperature_k: float = 293.15, - config: SolveIVPConfig | None = None, - t_eval: list[float] | None = None, - ): - closure = self.pnl0001_chamber_segment_closure_from_spec( - spec, - inlet_node_pressure_pa=inlet_node_pressure_pa, - outlet_pressure_pa=outlet_pressure_pa, - inlet_node_temperature_k=inlet_node_temperature_k, - outlet_temperature_k=outlet_temperature_k, - ) - run_config = config or SolveIVPConfig(t_stop=1.0e-4, max_step=1.0e-5) - return integrate_ode( - rhs=lambda t, state: closure.rhs(state), - initial_state=closure.initial_state_vector(), - config=run_config, - t_eval=t_eval, - ) - - def pnl0001_pair_chamber_segment_closure_from_spec( - self, - spec, - *, - inlet_node_pressure_pa: float, - outlet_node_pressure_pa: float, - inlet_node_temperature_k: float = 293.15, - outlet_node_temperature_k: float = 293.15, - ): - """Insert both topology-derived PNL0001 states into the segment.""" - from PythonModels.components.amesim_pneumatic import ( - AmesimPneumaticOrifice, - AmesimPneumaticVolume, - ) - from PythonModels.systems.test_mql_closure import ( - TestMqlPneumaticBoundaryCondition, - TestMqlPnl0001PairChamberSegmentClosure, - TestMqlPnl0001PairChamberSegmentComponents, - ) - - inlet_line = self.pnl0001_assembly.lines[spec.inlet_line_alias] - outlet_line = self.pnl0001_assembly.lines[spec.outlet_line_alias] - volume = self.network.components[spec.volume_alias] - inlet_orifice = self.network.components[spec.inlet_orifice_alias] - outlet_orifice = self.network.components[spec.outlet_orifice_alias] - if not isinstance(volume, AmesimPneumaticVolume): - raise TypeError(f"{spec.volume_alias} is not an AMESim pneumatic volume") - if not isinstance(inlet_orifice, AmesimPneumaticOrifice): - raise TypeError( - f"{spec.inlet_orifice_alias} is not an AMESim pneumatic orifice" - ) - if not isinstance(outlet_orifice, AmesimPneumaticOrifice): - raise TypeError( - f"{spec.outlet_orifice_alias} is not an AMESim pneumatic orifice" - ) - return TestMqlPnl0001PairChamberSegmentClosure( - components=TestMqlPnl0001PairChamberSegmentComponents( - inlet_line=inlet_line, - outlet_line=outlet_line, - volume=volume, - inlet_orifice=inlet_orifice, - outlet_orifice=outlet_orifice, - spec=spec, - ), - inlet_node=TestMqlPneumaticBoundaryCondition( - pressure_pa=inlet_node_pressure_pa, - temperature_k=inlet_node_temperature_k, - ), - outlet_node=TestMqlPneumaticBoundaryCondition( - pressure_pa=outlet_node_pressure_pa, - temperature_k=outlet_node_temperature_k, - ), - ) - - def simulate_pnl0001_pair_chamber_segment_from_spec( - self, - spec, - *, - inlet_node_pressure_pa: float, - outlet_node_pressure_pa: float, - inlet_node_temperature_k: float = 293.15, - outlet_node_temperature_k: float = 293.15, - config: SolveIVPConfig | None = None, - t_eval: list[float] | None = None, - ): - closure = self.pnl0001_pair_chamber_segment_closure_from_spec( - spec, - inlet_node_pressure_pa=inlet_node_pressure_pa, - outlet_node_pressure_pa=outlet_node_pressure_pa, - inlet_node_temperature_k=inlet_node_temperature_k, - outlet_node_temperature_k=outlet_node_temperature_k, - ) - run_config = config or SolveIVPConfig(t_stop=1.0e-4, max_step=1.0e-5) - return integrate_ode( - rhs=lambda t, state: closure.rhs(state), - initial_state=closure.initial_state_vector(), - config=run_config, - t_eval=t_eval, - ) - - def pn3_node_chamber_segment_closure_from_spec( - self, - spec, - *, - inlet_node_pressure_pa: float, - resistance_boundary_pressure_pa: float, - inlet_node_temperature_k: float = 293.15, - resistance_boundary_temperature_k: float = 293.15, - p4_boundary_pressure_pa: float = 15.3e6, - p4_boundary_temperature_k: float = 293.15, - ): - """Close a PNL0001 chamber segment with the real outlet PN3 node. - - The PN3 primary pressure/temperature is taken from the attached PNL0003 - port-1 compliance. The PNL0003 port-2 side is connected through the - real PNVO001 orifice to a prescribed P4 pressure boundary. - """ - from PythonModels.components.amesim_pneumatic import ( - AmesimPneumaticOrifice, - AmesimPneumaticVolume, - ) - from PythonModels.systems.test_mql_closure import ( - TestMqlPneumaticBoundaryCondition, - TestMqlPn3NodeChamberSegmentClosure, - TestMqlPn3NodeChamberSegmentComponents, - ) - - inlet_line = self.pnl0001_assembly.lines[spec.inlet_line_alias] - outlet_line = self.pnl0001_assembly.lines[spec.outlet_line_alias] - node_line_spec = self._pnl0003_spec_for_node_port(spec.outlet_node_alias, "port_2") - node_line = self.pnl0003_assembly.lines[node_line_spec.alias] - node_orifice_alias = ( - node_line_spec.target_component - if node_line_spec.source_component == spec.outlet_node_alias - else node_line_spec.source_component - ) - node_orifice = self.pneumatic_assembly.variable_orifices[node_orifice_alias] - node_resistance = self._pnl00r_line_for_node_port( - spec.outlet_node_alias, "port_1" - ) - node = self.node3_assembly[spec.outlet_node_alias] - volume = self.network.components[spec.volume_alias] - inlet_orifice = self.network.components[spec.inlet_orifice_alias] - outlet_orifice = self.network.components[spec.outlet_orifice_alias] - if not isinstance(volume, AmesimPneumaticVolume): - raise TypeError(f"{spec.volume_alias} is not an AMESim pneumatic volume") - if not isinstance(inlet_orifice, AmesimPneumaticOrifice): - raise TypeError( - f"{spec.inlet_orifice_alias} is not an AMESim pneumatic orifice" - ) - if not isinstance(outlet_orifice, AmesimPneumaticOrifice): - raise TypeError( - f"{spec.outlet_orifice_alias} is not an AMESim pneumatic orifice" - ) - return TestMqlPn3NodeChamberSegmentClosure( - components=TestMqlPn3NodeChamberSegmentComponents( - inlet_line=inlet_line, - outlet_line=outlet_line, - node_line=node_line, - node_resistance=node_resistance, - node_orifice=node_orifice, - node=node, - volume=volume, - inlet_orifice=inlet_orifice, - outlet_orifice=outlet_orifice, - spec=spec, - ), - inlet_node=TestMqlPneumaticBoundaryCondition( - pressure_pa=inlet_node_pressure_pa, - temperature_k=inlet_node_temperature_k, - ), - resistance_boundary=TestMqlPneumaticBoundaryCondition( - pressure_pa=resistance_boundary_pressure_pa, - temperature_k=resistance_boundary_temperature_k, - ), - p4_boundary=TestMqlPneumaticBoundaryCondition( - pressure_pa=p4_boundary_pressure_pa, - temperature_k=p4_boundary_temperature_k, - ), - ) - - def simulate_pn3_node_chamber_segment_from_spec( - self, - spec, - *, - inlet_node_pressure_pa: float, - resistance_boundary_pressure_pa: float, - inlet_node_temperature_k: float = 293.15, - resistance_boundary_temperature_k: float = 293.15, - p4_boundary_pressure_pa: float = 15.3e6, - p4_boundary_temperature_k: float = 293.15, - config: SolveIVPConfig | None = None, - t_eval: list[float] | None = None, - ): - closure = self.pn3_node_chamber_segment_closure_from_spec( - spec, - inlet_node_pressure_pa=inlet_node_pressure_pa, - resistance_boundary_pressure_pa=resistance_boundary_pressure_pa, - inlet_node_temperature_k=inlet_node_temperature_k, - resistance_boundary_temperature_k=resistance_boundary_temperature_k, - p4_boundary_pressure_pa=p4_boundary_pressure_pa, - p4_boundary_temperature_k=p4_boundary_temperature_k, - ) - run_config = config or SolveIVPConfig(t_stop=1.0e-4, max_step=1.0e-5) - return integrate_ode( - rhs=lambda t, state: closure.rhs(state), - initial_state=closure.initial_state_vector(), - config=run_config, - t_eval=t_eval, - ) - - def pn3_p4_node_chamber_segment_closure_from_spec( - self, - spec, - *, - inlet_node_pressure_pa: float, - resistance_boundary_pressure_pa: float, - inlet_node_temperature_k: float = 293.15, - resistance_boundary_temperature_k: float = 293.15, - use_inlet_line_reference_rhs: bool = False, - use_p4_port3_remote_primary_reference_rhs: bool = False, - ): - """Close a PN3 chamber segment through the adjacent real P4 node. - - The P4 primary pressure/temperature is supplied by the PNL0001 line on - P4 port 2. Ports 1 and 3 use the adjacent PNL0002 center compliances. - The next P4 nodes contribute their primary PNL0001/chamber states, and - PNVO upstream endpoints are closed through their adjacent PN3/PNL0003 - subnets. - """ - from PythonModels.components.amesim_pneumatic import ( - AmesimPneumaticOrifice, - AmesimPneumaticVolume, - ) - from PythonModels.systems.test_mql_closure import ( - TestMqlPneumaticBoundaryCondition, - TestMqlPn3P4NodeChamberSegmentClosure, - TestMqlPn3P4NodeChamberSegmentComponents, - ) - - def pnl0003_line_and_node_for_orifice(orifice_alias: str): - for line_spec in self.pnl0003_assembly.specs: - if ( - line_spec.target_component == orifice_alias - and line_spec.target_port == "port_3" - and line_spec.source_component in self.node3_assembly - ): - return ( - self.pnl0003_assembly.lines[line_spec.alias], - self.node3_assembly[line_spec.source_component], - ) - if ( - line_spec.source_component == orifice_alias - and line_spec.source_port == "port_3" - and line_spec.target_component in self.node3_assembly - ): - return ( - self.pnl0003_assembly.lines[line_spec.alias], - self.node3_assembly[line_spec.target_component], - ) - raise KeyError(f"No PN3/PNL0003 line attached to {orifice_alias}.port_3") - - def other_node3_for_pnl00r(line_alias: str, local_node_alias: str): - for line_spec in self.pnl00r_assembly.specs: - if line_spec.alias != line_alias: - continue - if line_spec.source_component == local_node_alias: - return self.node3_assembly[line_spec.target_component] - if line_spec.target_component == local_node_alias: - return self.node3_assembly[line_spec.source_component] - raise KeyError(f"No remote PN3 node found for {line_alias}") - - def chamber_segment_for_inlet_node(node_alias: str): - chamber_specs = ( - self.discover_pneumatic_branch_topology().chamber_segment_specs - ) - for chamber_spec in chamber_specs: - if chamber_spec.inlet_node_alias == node_alias: - return chamber_spec - raise KeyError(f"No fixed chamber segment starts at {node_alias}") - - def chamber_segment_for_outlet_node(node_alias: str): - chamber_specs = ( - self.discover_pneumatic_branch_topology().chamber_segment_specs - ) - for chamber_spec in chamber_specs: - if chamber_spec.outlet_node_alias == node_alias: - return chamber_spec - raise KeyError(f"No fixed chamber segment ends at {node_alias}") - - inlet_line = self.pnl0001_assembly.lines[spec.inlet_line_alias] - outlet_line = self.pnl0001_assembly.lines[spec.outlet_line_alias] - node_line_spec = self._pnl0003_spec_for_node_port( - spec.outlet_node_alias, - "port_2", - ) - node_line = self.pnl0003_assembly.lines[node_line_spec.alias] - node_orifice_alias = ( - node_line_spec.target_component - if node_line_spec.source_component == spec.outlet_node_alias - else node_line_spec.source_component - ) - p4_node_alias = self._p4_node_alias_for_orifice_port_2(node_orifice_alias) - p4_neighborhood = self.p4_node_neighborhood(p4_node_alias) - if p4_neighborhood.port_4.orifice_alias != node_orifice_alias: - raise ValueError( - f"P4 neighborhood for {p4_node_alias} does not match " - f"{node_orifice_alias}" - ) - node_orifice = self.pneumatic_assembly.variable_orifices[ - p4_neighborhood.port_4.orifice_alias - ] - node_resistance = self._pnl00r_line_for_node_port( - spec.outlet_node_alias, - "port_1", - ) - node = self.node3_assembly[spec.outlet_node_alias] - p4_node = self.node4_assembly[p4_neighborhood.node_alias] - p4_primary_line = self.pnl0001_assembly.lines[ - p4_neighborhood.primary.line_alias - ] - p4_primary_chamber_alias = p4_neighborhood.primary.chamber_alias - p4_primary_chamber = self.network.components[p4_primary_chamber_alias] - p4_port1_line = self.pnl0002_assembly.lines[ - p4_neighborhood.port_1.line_alias - ] - p4_port3_line = self.pnl0002_assembly.lines[ - p4_neighborhood.port_3.line_alias - ] - p4_port1_remote_neighborhood = self.p4_node_neighborhood( - p4_neighborhood.port_1.remote_node_alias - ) - p4_port1_remote_node = self.node4_assembly[ - p4_port1_remote_neighborhood.node_alias - ] - p4_port1_remote_primary_line = self.pnl0001_assembly.lines[ - p4_port1_remote_neighborhood.primary.line_alias - ] - p4_port1_remote_primary_chamber_alias = ( - p4_port1_remote_neighborhood.primary.chamber_alias - ) - p4_port1_remote_primary_chamber = self.network.components[ - p4_port1_remote_primary_chamber_alias - ] - p4_port1_remote_port1_line = self.pnl0002_assembly.lines[ - p4_port1_remote_neighborhood.port_1.line_alias - ] - p4_port1_far_node_alias = p4_port1_remote_neighborhood.port_1.remote_node_alias - p4_port1_far_node = self.node4_assembly[p4_port1_far_node_alias] - p4_port1_far_primary = self._p4_primary_connection_for_node( - p4_port1_far_node_alias - ) - p4_port1_far_primary_line = self.pnl0001_assembly.lines[ - p4_port1_far_primary.line_alias - ] - p4_port1_far_primary_chamber_alias = p4_port1_far_primary.chamber_alias - p4_port1_far_primary_chamber = self.network.components[ - p4_port1_far_primary_chamber_alias - ] - p4_port1_far_port1 = self._pnl0002_connection_for_node_port( - p4_port1_far_node_alias, - "port_1", - ) - p4_port1_far_port1_line = self.pnl0002_assembly.lines[ - p4_port1_far_port1.line_alias - ] - p4_port1_next_node_alias = p4_port1_far_port1.remote_node_alias - p4_port1_next_node = self.node4_assembly[p4_port1_next_node_alias] - p4_port1_next_primary = self._p4_primary_connection_for_node( - p4_port1_next_node_alias - ) - p4_port1_next_primary_line = self.pnl0001_assembly.lines[ - p4_port1_next_primary.line_alias - ] - p4_port1_next_primary_chamber_alias = p4_port1_next_primary.chamber_alias - p4_port1_next_primary_chamber = self.network.components[ - p4_port1_next_primary_chamber_alias - ] - p4_port1_next_port1 = self._pnl0002_connection_for_node_port( - p4_port1_next_node_alias, - "port_1", - ) - p4_port1_next_port1_line = self.pnl0002_assembly.lines[ - p4_port1_next_port1.line_alias - ] - p4_port1_next_orifice_output_spec = self._pnl0001_spec_for_node_port( - p4_port1_next_node_alias, - "port_4", - ) - p4_port1_next_orifice_output_line = self.pnl0001_assembly.lines[ - p4_port1_next_orifice_output_spec.alias - ] - p4_bridge_node_alias = p4_port1_next_port1.remote_node_alias - p4_bridge_node = self.node4_assembly[p4_bridge_node_alias] - p4_bridge_primary = self._p4_primary_connection_for_node( - p4_bridge_node_alias - ) - p4_bridge_primary_line = self.pnl0001_assembly.lines[ - p4_bridge_primary.line_alias - ] - p4_bridge_primary_chamber_alias = p4_bridge_primary.chamber_alias - p4_bridge_primary_chamber = self.network.components[ - p4_bridge_primary_chamber_alias - ] - p4_port1_remote_orifice = self.pneumatic_assembly.variable_orifices[ - p4_port1_remote_neighborhood.port_4.orifice_alias - ] - ( - p4_port1_remote_orifice_line, - p4_port1_remote_orifice_node, - ) = pnl0003_line_and_node_for_orifice( - p4_port1_remote_neighborhood.port_4.orifice_alias - ) - p4_port3_remote_neighborhood = self.p4_node_neighborhood( - p4_neighborhood.port_3.remote_node_alias - ) - p4_port3_remote_node = self.node4_assembly[ - p4_port3_remote_neighborhood.node_alias - ] - p4_port3_remote_primary_line = self.pnl0001_assembly.lines[ - p4_port3_remote_neighborhood.primary.line_alias - ] - p4_port3_remote_primary_chamber_alias = ( - p4_port3_remote_neighborhood.primary.chamber_alias - ) - p4_port3_remote_primary_chamber = self.network.components[ - p4_port3_remote_primary_chamber_alias - ] - p4_port3_remote_port3_line = self.pnl0002_assembly.lines[ - p4_port3_remote_neighborhood.port_3.line_alias - ] - p4_port3_far_node_alias = p4_port3_remote_neighborhood.port_3.remote_node_alias - p4_port3_far_node = self.node4_assembly[p4_port3_far_node_alias] - p4_port3_far_primary = self._p4_primary_connection_for_node( - p4_port3_far_node_alias - ) - p4_port3_far_primary_line = self.pnl0001_assembly.lines[ - p4_port3_far_primary.line_alias - ] - p4_port3_far_primary_chamber_alias = p4_port3_far_primary.chamber_alias - p4_port3_far_primary_chamber = self.network.components[ - p4_port3_far_primary_chamber_alias - ] - p4_port3_far_port3 = self._pnl0002_connection_for_node_port( - p4_port3_far_node_alias, - "port_3", - ) - p4_port3_far_port3_line = self.pnl0002_assembly.lines[ - p4_port3_far_port3.line_alias - ] - p4_port3_next_node_alias = p4_port3_far_port3.remote_node_alias - p4_port3_next_node = self.node4_assembly[p4_port3_next_node_alias] - p4_port3_next_primary = self._p4_primary_connection_for_node( - p4_port3_next_node_alias - ) - p4_port3_next_primary_line = self.pnl0001_assembly.lines[ - p4_port3_next_primary.line_alias - ] - p4_port3_next_primary_chamber_alias = p4_port3_next_primary.chamber_alias - p4_port3_next_primary_chamber = self.network.components[ - p4_port3_next_primary_chamber_alias - ] - p4_port3_next_port3 = self._pnl0002_connection_for_node_port( - p4_port3_next_node_alias, - "port_3", - ) - if p4_port3_next_port3.remote_node_alias != p4_bridge_node_alias: - raise ValueError( - f"{p4_port3_next_port3.line_alias} does not connect back to " - f"{p4_bridge_node_alias}" - ) - p4_port3_next_port3_line = self.pnl0002_assembly.lines[ - p4_port3_next_port3.line_alias - ] - p4_port3_next_orifice_output_spec = self._pnl0001_spec_for_node_port( - p4_port3_next_node_alias, - "port_4", - ) - p4_port3_next_orifice_alias = ( - p4_port3_next_orifice_output_spec.source_component - if p4_port3_next_orifice_output_spec.target_component - == p4_port3_next_node_alias - else p4_port3_next_orifice_output_spec.target_component - ) - p4_port3_next_orifice = self.pneumatic_assembly.variable_orifices[ - p4_port3_next_orifice_alias - ] - ( - p4_port3_next_orifice_line, - p4_port3_next_orifice_node, - ) = pnl0003_line_and_node_for_orifice(p4_port3_next_orifice_alias) - p4_port3_next_orifice_output_line = self.pnl0001_assembly.lines[ - p4_port3_next_orifice_output_spec.alias - ] - p4_port3_next_chamber_spec = chamber_segment_for_inlet_node( - p4_port3_next_orifice_node.alias - ) - p4_port3_next_chamber_inlet_line = self.pnl0001_assembly.lines[ - p4_port3_next_chamber_spec.inlet_line_alias - ] - p4_port3_next_chamber = self.network.components[ - p4_port3_next_chamber_spec.volume_alias - ] - p4_port3_next_chamber_outlet_line = self.pnl0001_assembly.lines[ - p4_port3_next_chamber_spec.outlet_line_alias - ] - p4_port3_next_chamber_inlet_orifice = self.pneumatic_assembly.fixed_orifices[ - p4_port3_next_chamber_spec.inlet_orifice_alias - ] - p4_port3_next_chamber_outlet_orifice = self.pneumatic_assembly.fixed_orifices[ - p4_port3_next_chamber_spec.outlet_orifice_alias - ] - p4_port3_next_orifice_resistance = self._pnl00r_line_for_node_port( - p4_port3_next_orifice_node.alias, - "port_3", - ) - p4_port3_next_resistance_node = other_node3_for_pnl00r( - p4_port3_next_orifice_resistance.name, - p4_port3_next_orifice_node.alias, - ) - p4_port3_next_resistance_node_line_spec = self._pnl0003_spec_for_node_port( - p4_port3_next_resistance_node.alias, - "port_2", - ) - p4_port3_next_resistance_node_line = self.pnl0003_assembly.lines[ - p4_port3_next_resistance_node_line_spec.alias - ] - p4_port3_next_resistance_orifice_alias = ( - p4_port3_next_resistance_node_line_spec.target_component - if p4_port3_next_resistance_node_line_spec.source_component - == p4_port3_next_resistance_node.alias - else p4_port3_next_resistance_node_line_spec.source_component - ) - p4_port3_next_resistance_orifice = self.pneumatic_assembly.variable_orifices[ - p4_port3_next_resistance_orifice_alias - ] - p4_port3_next_resistance_output_spec = self._pnl0001_spec_for_node_port( - p4_bridge_node_alias, - "port_4", - ) - if ( - p4_port3_next_resistance_output_spec.source_component - != p4_port3_next_resistance_orifice_alias - ): - raise ValueError( - f"{p4_port3_next_resistance_output_spec.alias} is not fed by " - f"{p4_port3_next_resistance_orifice_alias}" - ) - p4_port3_next_resistance_output_line = self.pnl0001_assembly.lines[ - p4_port3_next_resistance_output_spec.alias - ] - p4_port3_next_resistance_chamber_spec = chamber_segment_for_outlet_node( - p4_port3_next_resistance_node.alias - ) - p4_port3_next_resistance_chamber_inlet_line = self.pnl0001_assembly.lines[ - p4_port3_next_resistance_chamber_spec.inlet_line_alias - ] - p4_port3_next_resistance_chamber = self.network.components[ - p4_port3_next_resistance_chamber_spec.volume_alias - ] - p4_port3_next_resistance_chamber_outlet_line = self.pnl0001_assembly.lines[ - p4_port3_next_resistance_chamber_spec.outlet_line_alias - ] - p4_port3_next_resistance_chamber_inlet_orifice = ( - self.pneumatic_assembly.fixed_orifices[ - p4_port3_next_resistance_chamber_spec.inlet_orifice_alias - ] - ) - p4_port3_next_resistance_chamber_outlet_orifice = ( - self.pneumatic_assembly.fixed_orifices[ - p4_port3_next_resistance_chamber_spec.outlet_orifice_alias - ] - ) - p4_port1_next_orifice_node = self.node3_assembly[ - p4_port3_next_resistance_chamber_spec.inlet_node_alias - ] - p4_port1_next_orifice_line_spec = self._pnl0003_spec_for_node_port( - p4_port1_next_orifice_node.alias, - "port_2", - ) - p4_port1_next_orifice_line = self.pnl0003_assembly.lines[ - p4_port1_next_orifice_line_spec.alias - ] - p4_port1_next_orifice_alias = ( - p4_port1_next_orifice_line_spec.target_component - if p4_port1_next_orifice_line_spec.source_component - == p4_port1_next_orifice_node.alias - else p4_port1_next_orifice_line_spec.source_component - ) - p4_port1_next_orifice = self.pneumatic_assembly.variable_orifices[ - p4_port1_next_orifice_alias - ] - if ( - p4_port1_next_orifice_output_spec.source_component != p4_port1_next_node_alias - or p4_port1_next_orifice_output_spec.target_component - != p4_port1_next_orifice_alias - ): - raise ValueError( - f"{p4_port1_next_orifice_output_spec.alias} does not connect " - f"{p4_port1_next_node_alias}.port_4 to {p4_port1_next_orifice_alias}" - ) - p4_port1_next_orifice_resistance = self._pnl00r_line_for_node_port( - p4_port1_next_orifice_node.alias, - "port_3", - ) - p4_port1_next_orifice_resistance_node = other_node3_for_pnl00r( - p4_port1_next_orifice_resistance.name, - p4_port1_next_orifice_node.alias, - ) - p4_port1_next_orifice_resistance_line_spec = self._pnl0003_spec_for_node_port( - p4_port1_next_orifice_resistance_node.alias, - "port_2", - ) - p4_port1_next_orifice_resistance_line = self.pnl0003_assembly.lines[ - p4_port1_next_orifice_resistance_line_spec.alias - ] - p4_port1_next_orifice_resistance_orifice_alias = ( - p4_port1_next_orifice_resistance_line_spec.target_component - if p4_port1_next_orifice_resistance_line_spec.source_component - == p4_port1_next_orifice_resistance_node.alias - else p4_port1_next_orifice_resistance_line_spec.source_component - ) - p4_port1_next_orifice_resistance_orifice = ( - self.pneumatic_assembly.variable_orifices[ - p4_port1_next_orifice_resistance_orifice_alias - ] - ) - p4_port1_far_orifice_direct = self.p4_node_neighborhood( - p4_port1_far_node_alias - ).port_4 - if ( - p4_port1_far_orifice_direct.orifice_alias - != p4_port1_next_orifice_resistance_orifice_alias - ): - raise ValueError( - f"{p4_port1_far_node_alias}.port_4 does not use " - f"{p4_port1_next_orifice_resistance_orifice_alias}" - ) - p4_port1_next_orifice_resistance_chamber_spec = ( - chamber_segment_for_outlet_node( - p4_port1_next_orifice_resistance_node.alias - ) - ) - p4_port1_next_orifice_resistance_chamber_inlet_line = ( - self.pnl0001_assembly.lines[ - p4_port1_next_orifice_resistance_chamber_spec.inlet_line_alias - ] - ) - p4_port1_next_orifice_resistance_chamber = self.network.components[ - p4_port1_next_orifice_resistance_chamber_spec.volume_alias - ] - p4_port1_next_orifice_resistance_chamber_outlet_line = ( - self.pnl0001_assembly.lines[ - p4_port1_next_orifice_resistance_chamber_spec.outlet_line_alias - ] - ) - p4_port1_next_orifice_resistance_chamber_inlet_orifice = ( - self.pneumatic_assembly.fixed_orifices[ - p4_port1_next_orifice_resistance_chamber_spec.inlet_orifice_alias - ] - ) - p4_port1_next_orifice_resistance_chamber_outlet_orifice = ( - self.pneumatic_assembly.fixed_orifices[ - p4_port1_next_orifice_resistance_chamber_spec.outlet_orifice_alias - ] - ) - p4_port3_remote_orifice = self.pneumatic_assembly.variable_orifices[ - p4_port3_remote_neighborhood.port_4.orifice_alias - ] - ( - p4_port3_remote_orifice_line, - p4_port3_remote_orifice_node, - ) = pnl0003_line_and_node_for_orifice( - p4_port3_remote_neighborhood.port_4.orifice_alias - ) - p4_port3_remote_orifice_resistance = self._pnl00r_line_for_node_port( - p4_port3_remote_orifice_node.alias, - "port_3", - ) - p4_port3_remote_resistance_node = other_node3_for_pnl00r( - p4_port3_remote_orifice_resistance.name, - p4_port3_remote_orifice_node.alias, - ) - p4_port3_remote_resistance_node_line_spec = self._pnl0003_spec_for_node_port( - p4_port3_remote_resistance_node.alias, - "port_2", - ) - p4_port3_remote_resistance_node_line = self.pnl0003_assembly.lines[ - p4_port3_remote_resistance_node_line_spec.alias - ] - p4_port3_remote_resistance_orifice_alias = ( - p4_port3_remote_resistance_node_line_spec.target_component - if p4_port3_remote_resistance_node_line_spec.source_component - == p4_port3_remote_resistance_node.alias - else p4_port3_remote_resistance_node_line_spec.source_component - ) - p4_port3_remote_resistance_orifice = self.pneumatic_assembly.variable_orifices[ - p4_port3_remote_resistance_orifice_alias - ] - p4_port3_remote_resistance_output_spec = self._pnl0001_spec_for_node_port( - p4_port3_far_node_alias, - "port_4", - ) - if ( - p4_port3_remote_resistance_output_spec.source_component - != p4_port3_remote_resistance_orifice_alias - ): - raise ValueError( - f"{p4_port3_remote_resistance_output_spec.alias} is not fed by " - f"{p4_port3_remote_resistance_orifice_alias}" - ) - p4_port3_remote_resistance_output_line = self.pnl0001_assembly.lines[ - p4_port3_remote_resistance_output_spec.alias - ] - volume = self.network.components[spec.volume_alias] - inlet_orifice = self.network.components[spec.inlet_orifice_alias] - outlet_orifice = self.network.components[spec.outlet_orifice_alias] - if not isinstance(volume, AmesimPneumaticVolume): - raise TypeError(f"{spec.volume_alias} is not an AMESim pneumatic volume") - if not isinstance(p4_primary_chamber, AmesimPneumaticVolume): - raise TypeError( - f"{p4_primary_chamber_alias} is not an AMESim pneumatic volume" - ) - if not isinstance(p4_port1_remote_primary_chamber, AmesimPneumaticVolume): - raise TypeError( - f"{p4_port1_remote_primary_chamber_alias} is not an AMESim pneumatic volume" - ) - if not isinstance(p4_port3_remote_primary_chamber, AmesimPneumaticVolume): - raise TypeError( - f"{p4_port3_remote_primary_chamber_alias} is not an AMESim pneumatic volume" - ) - if not isinstance(p4_port1_far_primary_chamber, AmesimPneumaticVolume): - raise TypeError( - f"{p4_port1_far_primary_chamber_alias} is not an AMESim pneumatic volume" - ) - if not isinstance(p4_port3_far_primary_chamber, AmesimPneumaticVolume): - raise TypeError( - f"{p4_port3_far_primary_chamber_alias} is not an AMESim pneumatic volume" - ) - if not isinstance(p4_port1_next_primary_chamber, AmesimPneumaticVolume): - raise TypeError( - f"{p4_port1_next_primary_chamber_alias} is not an AMESim pneumatic volume" - ) - if not isinstance(p4_port3_next_primary_chamber, AmesimPneumaticVolume): - raise TypeError( - f"{p4_port3_next_primary_chamber_alias} is not an AMESim pneumatic volume" - ) - if not isinstance(p4_port3_next_orifice, AmesimPneumaticOrifice): - raise TypeError( - f"{p4_port3_next_orifice_alias} is not an AMESim pneumatic orifice" - ) - if not isinstance(p4_port3_next_chamber, AmesimPneumaticVolume): - raise TypeError( - f"{p4_port3_next_chamber_spec.volume_alias} is not an AMESim pneumatic volume" - ) - if not isinstance(p4_port3_next_chamber_inlet_orifice, AmesimPneumaticOrifice): - raise TypeError( - f"{p4_port3_next_chamber_spec.inlet_orifice_alias} is not an AMESim pneumatic orifice" - ) - if not isinstance(p4_port3_next_chamber_outlet_orifice, AmesimPneumaticOrifice): - raise TypeError( - f"{p4_port3_next_chamber_spec.outlet_orifice_alias} is not an AMESim pneumatic orifice" - ) - if not isinstance(p4_port3_next_resistance_orifice, AmesimPneumaticOrifice): - raise TypeError( - f"{p4_port3_next_resistance_orifice_alias} is not an AMESim pneumatic orifice" - ) - if not isinstance(p4_port3_next_resistance_chamber, AmesimPneumaticVolume): - raise TypeError( - f"{p4_port3_next_resistance_chamber_spec.volume_alias} is not an AMESim pneumatic volume" - ) - if not isinstance( - p4_port3_next_resistance_chamber_inlet_orifice, - AmesimPneumaticOrifice, - ): - raise TypeError( - f"{p4_port3_next_resistance_chamber_spec.inlet_orifice_alias} is not an AMESim pneumatic orifice" - ) - if not isinstance( - p4_port3_next_resistance_chamber_outlet_orifice, - AmesimPneumaticOrifice, - ): - raise TypeError( - f"{p4_port3_next_resistance_chamber_spec.outlet_orifice_alias} is not an AMESim pneumatic orifice" - ) - if not isinstance(p4_port1_next_orifice, AmesimPneumaticOrifice): - raise TypeError( - f"{p4_port1_next_orifice_alias} is not an AMESim pneumatic orifice" - ) - if not isinstance( - p4_port1_next_orifice_resistance_orifice, - AmesimPneumaticOrifice, - ): - raise TypeError( - f"{p4_port1_next_orifice_resistance_orifice_alias} is not an AMESim pneumatic orifice" - ) - if not isinstance( - p4_port1_next_orifice_resistance_chamber, - AmesimPneumaticVolume, - ): - raise TypeError( - f"{p4_port1_next_orifice_resistance_chamber_spec.volume_alias} is not an AMESim pneumatic volume" - ) - if not isinstance( - p4_port1_next_orifice_resistance_chamber_inlet_orifice, - AmesimPneumaticOrifice, - ): - raise TypeError( - f"{p4_port1_next_orifice_resistance_chamber_spec.inlet_orifice_alias} is not an AMESim pneumatic orifice" - ) - if not isinstance( - p4_port1_next_orifice_resistance_chamber_outlet_orifice, - AmesimPneumaticOrifice, - ): - raise TypeError( - f"{p4_port1_next_orifice_resistance_chamber_spec.outlet_orifice_alias} is not an AMESim pneumatic orifice" - ) - if not isinstance(p4_bridge_primary_chamber, AmesimPneumaticVolume): - raise TypeError( - f"{p4_bridge_primary_chamber_alias} is not an AMESim pneumatic volume" - ) - if not isinstance(inlet_orifice, AmesimPneumaticOrifice): - raise TypeError( - f"{spec.inlet_orifice_alias} is not an AMESim pneumatic orifice" - ) - if not isinstance(outlet_orifice, AmesimPneumaticOrifice): - raise TypeError( - f"{spec.outlet_orifice_alias} is not an AMESim pneumatic orifice" - ) - if not isinstance(p4_port3_remote_resistance_orifice, AmesimPneumaticOrifice): - raise TypeError( - f"{p4_port3_remote_resistance_orifice_alias} is not an AMESim pneumatic orifice" - ) - return TestMqlPn3P4NodeChamberSegmentClosure( - components=TestMqlPn3P4NodeChamberSegmentComponents( - inlet_line=inlet_line, - outlet_line=outlet_line, - node_line=node_line, - node_resistance=node_resistance, - node_orifice=node_orifice, - node=node, - p4_node=p4_node, - p4_primary_line=p4_primary_line, - p4_primary_chamber=p4_primary_chamber, - p4_port1_line=p4_port1_line, - p4_port1_remote_node=p4_port1_remote_node, - p4_port1_remote_primary_line=p4_port1_remote_primary_line, - p4_port1_remote_primary_chamber=p4_port1_remote_primary_chamber, - p4_port1_remote_port1_line=p4_port1_remote_port1_line, - p4_port1_far_node=p4_port1_far_node, - p4_port1_far_primary_line=p4_port1_far_primary_line, - p4_port1_far_primary_chamber=p4_port1_far_primary_chamber, - p4_port1_far_port1_line=p4_port1_far_port1_line, - p4_port1_next_node=p4_port1_next_node, - p4_port1_next_primary_line=p4_port1_next_primary_line, - p4_port1_next_primary_chamber=p4_port1_next_primary_chamber, - p4_port1_next_port1_line=p4_port1_next_port1_line, - p4_port1_next_orifice=p4_port1_next_orifice, - p4_port1_next_orifice_line=p4_port1_next_orifice_line, - p4_port1_next_orifice_node=p4_port1_next_orifice_node, - p4_port1_next_orifice_output_line=p4_port1_next_orifice_output_line, - p4_port1_next_orifice_resistance=p4_port1_next_orifice_resistance, - p4_port1_next_orifice_resistance_node=( - p4_port1_next_orifice_resistance_node - ), - p4_port1_next_orifice_resistance_line=( - p4_port1_next_orifice_resistance_line - ), - p4_port1_next_orifice_resistance_orifice=( - p4_port1_next_orifice_resistance_orifice - ), - p4_port1_next_orifice_resistance_chamber_inlet_line=( - p4_port1_next_orifice_resistance_chamber_inlet_line - ), - p4_port1_next_orifice_resistance_chamber=( - p4_port1_next_orifice_resistance_chamber - ), - p4_port1_next_orifice_resistance_chamber_outlet_line=( - p4_port1_next_orifice_resistance_chamber_outlet_line - ), - p4_port1_next_orifice_resistance_chamber_inlet_orifice=( - p4_port1_next_orifice_resistance_chamber_inlet_orifice - ), - p4_port1_next_orifice_resistance_chamber_outlet_orifice=( - p4_port1_next_orifice_resistance_chamber_outlet_orifice - ), - p4_port1_next_orifice_resistance_chamber_spec=( - p4_port1_next_orifice_resistance_chamber_spec - ), - p4_bridge_node=p4_bridge_node, - p4_bridge_primary_line=p4_bridge_primary_line, - p4_bridge_primary_chamber=p4_bridge_primary_chamber, - p4_port1_remote_orifice=p4_port1_remote_orifice, - p4_port1_remote_orifice_line=p4_port1_remote_orifice_line, - p4_port1_remote_orifice_node=p4_port1_remote_orifice_node, - p4_port3_line=p4_port3_line, - p4_port3_remote_node=p4_port3_remote_node, - p4_port3_remote_primary_line=p4_port3_remote_primary_line, - p4_port3_remote_primary_chamber=p4_port3_remote_primary_chamber, - p4_port3_remote_port3_line=p4_port3_remote_port3_line, - p4_port3_far_node=p4_port3_far_node, - p4_port3_far_primary_line=p4_port3_far_primary_line, - p4_port3_far_primary_chamber=p4_port3_far_primary_chamber, - p4_port3_far_port3_line=p4_port3_far_port3_line, - p4_port3_next_node=p4_port3_next_node, - p4_port3_next_primary_line=p4_port3_next_primary_line, - p4_port3_next_primary_chamber=p4_port3_next_primary_chamber, - p4_port3_next_port3_line=p4_port3_next_port3_line, - p4_port3_next_orifice=p4_port3_next_orifice, - p4_port3_next_orifice_line=p4_port3_next_orifice_line, - p4_port3_next_orifice_node=p4_port3_next_orifice_node, - p4_port3_next_orifice_output_line=p4_port3_next_orifice_output_line, - p4_port3_next_chamber_inlet_line=p4_port3_next_chamber_inlet_line, - p4_port3_next_chamber=p4_port3_next_chamber, - p4_port3_next_chamber_outlet_line=p4_port3_next_chamber_outlet_line, - p4_port3_next_chamber_inlet_orifice=p4_port3_next_chamber_inlet_orifice, - p4_port3_next_chamber_outlet_orifice=p4_port3_next_chamber_outlet_orifice, - p4_port3_next_chamber_spec=p4_port3_next_chamber_spec, - p4_port3_next_orifice_resistance=p4_port3_next_orifice_resistance, - p4_port3_next_resistance_node=p4_port3_next_resistance_node, - p4_port3_next_resistance_node_line=p4_port3_next_resistance_node_line, - p4_port3_next_resistance_orifice=p4_port3_next_resistance_orifice, - p4_port3_next_resistance_output_line=p4_port3_next_resistance_output_line, - p4_port3_next_resistance_chamber_inlet_line=( - p4_port3_next_resistance_chamber_inlet_line - ), - p4_port3_next_resistance_chamber=p4_port3_next_resistance_chamber, - p4_port3_next_resistance_chamber_outlet_line=( - p4_port3_next_resistance_chamber_outlet_line - ), - p4_port3_next_resistance_chamber_inlet_orifice=( - p4_port3_next_resistance_chamber_inlet_orifice - ), - p4_port3_next_resistance_chamber_outlet_orifice=( - p4_port3_next_resistance_chamber_outlet_orifice - ), - p4_port3_next_resistance_chamber_spec=( - p4_port3_next_resistance_chamber_spec - ), - p4_port3_remote_orifice=p4_port3_remote_orifice, - p4_port3_remote_orifice_line=p4_port3_remote_orifice_line, - p4_port3_remote_orifice_node=p4_port3_remote_orifice_node, - p4_port3_remote_orifice_resistance=p4_port3_remote_orifice_resistance, - p4_port3_remote_resistance_node=p4_port3_remote_resistance_node, - p4_port3_remote_resistance_node_line=( - p4_port3_remote_resistance_node_line - ), - p4_port3_remote_resistance_orifice=( - p4_port3_remote_resistance_orifice - ), - p4_port3_remote_resistance_output_line=( - p4_port3_remote_resistance_output_line - ), - volume=volume, - inlet_orifice=inlet_orifice, - outlet_orifice=outlet_orifice, - spec=spec, - ), - inlet_node=TestMqlPneumaticBoundaryCondition( - pressure_pa=inlet_node_pressure_pa, - temperature_k=inlet_node_temperature_k, - ), - resistance_boundary=TestMqlPneumaticBoundaryCondition( - pressure_pa=resistance_boundary_pressure_pa, - temperature_k=resistance_boundary_temperature_k, - ), - use_inlet_line_reference_rhs=use_inlet_line_reference_rhs, - use_p4_port3_remote_primary_reference_rhs=( - use_p4_port3_remote_primary_reference_rhs - ), - ) - - def simulate_pn3_p4_node_chamber_segment_from_spec( - self, - spec, - *, - inlet_node_pressure_pa: float, - resistance_boundary_pressure_pa: float, - inlet_node_temperature_k: float = 293.15, - resistance_boundary_temperature_k: float = 293.15, - config: SolveIVPConfig | None = None, - t_eval: list[float] | None = None, - ): - closure = self.pn3_p4_node_chamber_segment_closure_from_spec( - spec, - inlet_node_pressure_pa=inlet_node_pressure_pa, - resistance_boundary_pressure_pa=resistance_boundary_pressure_pa, - inlet_node_temperature_k=inlet_node_temperature_k, - resistance_boundary_temperature_k=resistance_boundary_temperature_k, - ) - run_config = config or SolveIVPConfig(t_stop=1.0e-4, max_step=1.0e-5) - return integrate_ode( - rhs=lambda t, state: closure.rhs(state), - initial_state=closure.initial_state_vector(), - config=run_config, - t_eval=t_eval, - ) - - def pneumatic_branch_closure_from_spec(self, spec): - return self.pneumatic_branch_closure( - name=spec.name, - upstream_volume_alias=spec.upstream_volume_alias, - orifice_alias=spec.orifice_alias, - downstream_volume_alias=spec.downstream_volume_alias, - spec=spec, - ) - - def pneumatic_branch_closure( - self, - *, - name: str, - upstream_volume_alias: str, - orifice_alias: str, - downstream_volume_alias: str, - spec=None, - ): - from PythonModels.components.amesim_pneumatic import ( - AmesimPneumaticOrifice, - AmesimPneumaticVolume, - ) - from PythonModels.systems.test_mql_closure import ( - TestMqlPneumaticBranchComponents, - TestMqlPneumaticBranchSpec, - TestMqlPneumaticClosure, - ) - - upstream_volume = self.network.components[upstream_volume_alias] - orifice = self.network.components[orifice_alias] - downstream_volume = self.network.components[downstream_volume_alias] - if not isinstance(upstream_volume, AmesimPneumaticVolume): - raise TypeError(f"{upstream_volume_alias} is not an AMESim pneumatic volume") - if not isinstance(downstream_volume, AmesimPneumaticVolume): - raise TypeError(f"{downstream_volume_alias} is not an AMESim pneumatic volume") - if not isinstance(orifice, AmesimPneumaticOrifice): - raise TypeError(f"{orifice_alias} is not an AMESim pneumatic orifice") - - def initial_state_vector() -> list[float]: - return [ - *upstream_volume.get_state_vector(), - *downstream_volume.get_state_vector(), - ] - - def apply_state_vector(values: list[float]) -> None: - if len(values) != 4: - raise ValueError("two-volume pneumatic branch state vector requires four values") - upstream_volume.set_state_vector(values[:2]) - downstream_volume.set_state_vector(values[2:]) - - branch_spec = spec or TestMqlPneumaticBranchSpec( - name=name, - upstream_volume_alias=upstream_volume_alias, - orifice_alias=orifice_alias, - downstream_volume_alias=downstream_volume_alias, - ) - return TestMqlPneumaticClosure( - components=TestMqlPneumaticBranchComponents( - name=name, - upstream_volume=upstream_volume, - orifice=orifice, - downstream_volume=downstream_volume, - spec=branch_spec, - ), - initial_state_vector=initial_state_vector, - apply_state_vector=apply_state_vector, - ) - - def simulate_pneumatic_branch( - self, - *, - name: str, - upstream_volume_alias: str, - orifice_alias: str, - downstream_volume_alias: str, - config: SolveIVPConfig | None = None, - t_eval: list[float] | None = None, - ): - closure = self.pneumatic_branch_closure( - name=name, - upstream_volume_alias=upstream_volume_alias, - orifice_alias=orifice_alias, - downstream_volume_alias=downstream_volume_alias, - ) - run_config = config or SolveIVPConfig(t_stop=1.0e-4, max_step=1.0e-5) - return integrate_ode( - rhs=lambda t, state: closure.rhs(state), - initial_state=closure.initial_state_vector(), - config=run_config, - t_eval=t_eval, - ) - - def simulate_pneumatic_branch_from_spec( - self, - spec, - *, - config: SolveIVPConfig | None = None, - t_eval: list[float] | None = None, - ): - return self.simulate_pneumatic_branch( - name=spec.name, - upstream_volume_alias=spec.upstream_volume_alias, - orifice_alias=spec.orifice_alias, - downstream_volume_alias=spec.downstream_volume_alias, - config=config, - t_eval=t_eval, - ) - - def evaluate_pneumatic_branch_solution_from_spec( - self, - solution: object, - spec, - ) -> dict[str, list[float]]: - return self.evaluate_pneumatic_branch_solution( - solution, - name=spec.name, - upstream_volume_alias=spec.upstream_volume_alias, - orifice_alias=spec.orifice_alias, - downstream_volume_alias=spec.downstream_volume_alias, - ) - - def evaluate_pneumatic_branch_solution( - self, - solution: object, - *, - name: str, - upstream_volume_alias: str, - orifice_alias: str, - downstream_volume_alias: str, - ) -> dict[str, list[float]]: - closure = self.pneumatic_branch_closure( - name=name, - upstream_volume_alias=upstream_volume_alias, - orifice_alias=orifice_alias, - downstream_volume_alias=downstream_volume_alias, - ) - series = { - "time": [], - f"{name}.flow": [], - f"{upstream_volume_alias}.p": [], - f"{upstream_volume_alias}.T": [], - f"{upstream_volume_alias}.m_flow": [], - f"{downstream_volume_alias}.p": [], - f"{downstream_volume_alias}.T": [], - f"{downstream_volume_alias}.m_flow": [], - } - for index, time_value in enumerate(solution.t): - state_vector = [row[index] for row in solution.y] - snapshot = closure.snapshot(state_vector) - series["time"].append(float(time_value)) - series[f"{name}.flow"].append(snapshot.flow) - series[f"{upstream_volume_alias}.p"].append(snapshot.upstream.p) - series[f"{upstream_volume_alias}.T"].append(snapshot.upstream.T) - series[f"{upstream_volume_alias}.m_flow"].append( - closure.components.upstream_volume.port_a.m_flow - ) - series[f"{downstream_volume_alias}.p"].append(snapshot.downstream.p) - series[f"{downstream_volume_alias}.T"].append(snapshot.downstream.T) - series[f"{downstream_volume_alias}.m_flow"].append( - closure.components.downstream_volume.port_a.m_flow - ) - return series - - def snapshot(self) -> TestMqlSnapshot: - return TestMqlSnapshot( - model_name=MODEL_NAME, - component_count=len(COMPONENT_SPECS), - connection_count=len(CONNECTION_SPECS), - continuous_state_count=int(MODEL_INFO.get("num_states", 0)), - discrete_state_count=int(MODEL_INFO.get("num_discrete_states", 0)), - global_parameters=dict(GLOBAL_PARAMETERS), - submodel_counts=dict(SUBMODEL_COUNTS), - ) - - def initial_state_vector(self) -> list[float]: - return [0.0] * int(MODEL_INFO.get("num_states", 0)) - - def simulate(self, config: TestMqlRunConfig | None = None) -> TestMqlSimulationResult: - run_config = config or TestMqlRunConfig() - times = run_config.sample_times() - state_count = int(MODEL_INFO.get("num_states", 0)) - y = [[0.0 for _ in times] for _ in range(state_count)] - series = self.evaluate_solution(SimpleNamespace(t=times, y=y)) - return TestMqlSimulationResult(t=times, y=y, series=series) - - def evaluate_solution(self, solution: object) -> dict[str, list[float]]: - times = [float(value) for value in solution.t] - return { - "time": times, - "component_count": [float(len(COMPONENT_SPECS)) for _ in times], - "connection_count": [float(len(CONNECTION_SPECS)) for _ in times], - "continuous_state_count": [float(MODEL_INFO.get("num_states", 0)) for _ in times], - "discrete_state_count": [float(MODEL_INFO.get("num_discrete_states", 0)) for _ in times], - } - - def component_specs_by_submodel(self) -> dict[str, list[dict[str, object]]]: - grouped: dict[str, list[dict[str, object]]] = {} - for spec in COMPONENT_SPECS: - grouped.setdefault(str(spec["submodel"]), []).append(spec) - return grouped - - def connection_specs_by_submodel(self) -> dict[str, list[dict[str, object]]]: - grouped: dict[str, list[dict[str, object]]] = {} - for spec in CONNECTION_SPECS: - grouped.setdefault(str(spec["submodel"]), []).append(spec) - return grouped - - -def build_test_mql() -> SimulationNetwork: - return TestMqlSystem().network diff --git a/PythonModels/systems/test_mql_closure.py b/PythonModels/systems/test_mql_closure.py deleted file mode 100644 index 984d9f7..0000000 --- a/PythonModels/systems/test_mql_closure.py +++ /dev/null @@ -1,4213 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass -from typing import Callable - -from PythonModels.components.amesim_pneumatic import ( - AmesimPneumaticGas, - AmesimPneumaticOrifice, - AmesimPneumaticVolume, -) -from PythonModels.components.amesim_pneumatic_line import ( - AmesimPnl0001Pipe, - AmesimPnl0002Pipe, - AmesimPnl0003Pipe, - AmesimPnl00rPipe, -) -from PythonModels.core.medium import ThermodynamicProperties -from PythonModels.core.ports import PortState -from PythonModels.core.state import VolumeState -from PythonModels.systems.test_mql_nodes import ( - TestMqlPneumaticNode3, - TestMqlPneumaticNode3Balance, - TestMqlPneumaticNode4, - TestMqlPneumaticNode4Balance, -) - - -@dataclass(frozen=True) -class TestMqlPneumaticBranchSpec: - name: str - upstream_volume_alias: str - orifice_alias: str - downstream_volume_alias: str - source: str = "manual" - - -@dataclass(frozen=True) -class TestMqlPneumaticTopologyCandidate: - connection_alias: str - line_submodel: str - source_component: str - source_port: str - target_component: str - target_port: str - source_is_typed_pneumatic: bool - target_is_typed_pneumatic: bool - category: str - reason: str - - -@dataclass(frozen=True) -class TestMqlPneumaticChamberSegmentSpec: - name: str - inlet_node_alias: str - inlet_line_alias: str - inlet_orifice_alias: str - inlet_orifice_boundary_port: str - inlet_orifice_volume_port: str - volume_alias: str - volume_inlet_port: str - volume_outlet_port: str - outlet_orifice_alias: str - outlet_orifice_volume_port: str - outlet_orifice_boundary_port: str - outlet_line_alias: str - outlet_node_alias: str - source: str = "amesim-cir-topology" - - -@dataclass(frozen=True) -class TestMqlPneumaticTopologyDiscovery: - branch_specs: tuple[TestMqlPneumaticBranchSpec, ...] - chamber_segment_specs: tuple[TestMqlPneumaticChamberSegmentSpec, ...] - blocked_candidates: tuple[TestMqlPneumaticTopologyCandidate, ...] - - -@dataclass(frozen=True) -class TestMqlPneumaticBoundaryCondition: - pressure_pa: float - temperature_k: float - - def properties(self, gas: AmesimPneumaticGas) -> ThermodynamicProperties: - if self.pressure_pa <= 0.0: - raise ValueError("boundary pressure must be positive") - if self.temperature_k <= 0.0: - raise ValueError("boundary temperature must be positive") - return ThermodynamicProperties( - p=self.pressure_pa, - T=self.temperature_k, - rho=gas.density(self.pressure_pa, self.temperature_k), - u=gas.specific_internal_energy(self.temperature_k), - h=gas.specific_enthalpy(self.temperature_k), - ) - - -@dataclass(frozen=True) -class TestMqlPneumaticChamberSegmentComponents: - volume: AmesimPneumaticVolume - inlet_orifice: AmesimPneumaticOrifice - outlet_orifice: AmesimPneumaticOrifice - spec: TestMqlPneumaticChamberSegmentSpec - - -@dataclass(frozen=True) -class TestMqlPneumaticChamberSegmentSnapshot: - chamber: ThermodynamicProperties - inlet_boundary: ThermodynamicProperties - outlet_boundary: ThermodynamicProperties - inlet_flow: float - outlet_flow: float - - -@dataclass(frozen=True) -class TestMqlPnl0001ChamberSegmentComponents: - inlet_line: AmesimPnl0001Pipe - volume: AmesimPneumaticVolume - inlet_orifice: AmesimPneumaticOrifice - outlet_orifice: AmesimPneumaticOrifice - spec: TestMqlPneumaticChamberSegmentSpec - - -@dataclass(frozen=True) -class TestMqlPnl0001ChamberSegmentSnapshot: - inlet_line: ThermodynamicProperties - chamber: ThermodynamicProperties - inlet_node: ThermodynamicProperties - outlet_boundary: ThermodynamicProperties - node_to_line_flow: float - line_to_chamber_flow: float - outlet_flow: float - - -@dataclass(frozen=True) -class TestMqlPnl0001PairChamberSegmentComponents: - inlet_line: AmesimPnl0001Pipe - outlet_line: AmesimPnl0001Pipe - volume: AmesimPneumaticVolume - inlet_orifice: AmesimPneumaticOrifice - outlet_orifice: AmesimPneumaticOrifice - spec: TestMqlPneumaticChamberSegmentSpec - - -@dataclass(frozen=True) -class TestMqlPnl0001PairChamberSegmentSnapshot: - inlet_line: ThermodynamicProperties - chamber: ThermodynamicProperties - outlet_line: ThermodynamicProperties - inlet_node: ThermodynamicProperties - outlet_node: ThermodynamicProperties - inlet_node_to_line_flow: float - inlet_line_to_chamber_flow: float - outlet_node_to_line_flow: float - outlet_line_to_chamber_flow: float - - -@dataclass(frozen=True) -class TestMqlPn3NodeChamberSegmentComponents: - inlet_line: AmesimPnl0001Pipe - outlet_line: AmesimPnl0001Pipe - node_line: AmesimPnl0003Pipe - node_resistance: AmesimPnl00rPipe - node_orifice: AmesimPneumaticOrifice - node: TestMqlPneumaticNode3 - volume: AmesimPneumaticVolume - inlet_orifice: AmesimPneumaticOrifice - outlet_orifice: AmesimPneumaticOrifice - spec: TestMqlPneumaticChamberSegmentSpec - - -@dataclass(frozen=True) -class TestMqlPn3NodeChamberSegmentSnapshot: - inlet_line: ThermodynamicProperties - chamber: ThermodynamicProperties - outlet_line: ThermodynamicProperties - node_line_port_1: ThermodynamicProperties - node_line_port_2: ThermodynamicProperties - inlet_node: ThermodynamicProperties - resistance_boundary: ThermodynamicProperties - p4_boundary: ThermodynamicProperties - node_balance: TestMqlPneumaticNode3Balance - inlet_node_to_line_flow: float - inlet_line_to_chamber_flow: float - outlet_node_to_line_flow: float - outlet_line_to_chamber_flow: float - node_to_resistance_flow: float - node_to_pnl0003_flow: float - pnl0003_center_flow: float - pnl0003_to_p4_flow: float - - -@dataclass(frozen=True) -class TestMqlPn3P4NodeChamberSegmentComponents: - inlet_line: AmesimPnl0001Pipe - outlet_line: AmesimPnl0001Pipe - node_line: AmesimPnl0003Pipe - node_resistance: AmesimPnl00rPipe - node_orifice: AmesimPneumaticOrifice - node: TestMqlPneumaticNode3 - p4_node: TestMqlPneumaticNode4 - p4_primary_line: AmesimPnl0001Pipe - p4_primary_chamber: AmesimPneumaticVolume - p4_port1_line: AmesimPnl0002Pipe - p4_port1_remote_node: TestMqlPneumaticNode4 - p4_port1_remote_primary_line: AmesimPnl0001Pipe - p4_port1_remote_primary_chamber: AmesimPneumaticVolume - p4_port1_remote_port1_line: AmesimPnl0002Pipe - p4_port1_far_node: TestMqlPneumaticNode4 - p4_port1_far_primary_line: AmesimPnl0001Pipe - p4_port1_far_primary_chamber: AmesimPneumaticVolume - p4_port1_far_port1_line: AmesimPnl0002Pipe - p4_port1_next_node: TestMqlPneumaticNode4 - p4_port1_next_primary_line: AmesimPnl0001Pipe - p4_port1_next_primary_chamber: AmesimPneumaticVolume - p4_port1_next_port1_line: AmesimPnl0002Pipe - p4_port1_next_orifice: AmesimPneumaticOrifice - p4_port1_next_orifice_line: AmesimPnl0003Pipe - p4_port1_next_orifice_node: TestMqlPneumaticNode3 - p4_port1_next_orifice_output_line: AmesimPnl0001Pipe - p4_port1_next_orifice_resistance: AmesimPnl00rPipe - p4_port1_next_orifice_resistance_node: TestMqlPneumaticNode3 - p4_port1_next_orifice_resistance_line: AmesimPnl0003Pipe - p4_port1_next_orifice_resistance_orifice: AmesimPneumaticOrifice - p4_port1_next_orifice_resistance_chamber_inlet_line: AmesimPnl0001Pipe - p4_port1_next_orifice_resistance_chamber: AmesimPneumaticVolume - p4_port1_next_orifice_resistance_chamber_outlet_line: AmesimPnl0001Pipe - p4_port1_next_orifice_resistance_chamber_inlet_orifice: AmesimPneumaticOrifice - p4_port1_next_orifice_resistance_chamber_outlet_orifice: AmesimPneumaticOrifice - p4_port1_next_orifice_resistance_chamber_spec: TestMqlPneumaticChamberSegmentSpec - p4_bridge_node: TestMqlPneumaticNode4 - p4_bridge_primary_line: AmesimPnl0001Pipe - p4_bridge_primary_chamber: AmesimPneumaticVolume - p4_port1_remote_orifice: AmesimPneumaticOrifice - p4_port1_remote_orifice_line: AmesimPnl0003Pipe - p4_port1_remote_orifice_node: TestMqlPneumaticNode3 - p4_port3_line: AmesimPnl0002Pipe - p4_port3_remote_node: TestMqlPneumaticNode4 - p4_port3_remote_primary_line: AmesimPnl0001Pipe - p4_port3_remote_primary_chamber: AmesimPneumaticVolume - p4_port3_remote_port3_line: AmesimPnl0002Pipe - p4_port3_far_node: TestMqlPneumaticNode4 - p4_port3_far_primary_line: AmesimPnl0001Pipe - p4_port3_far_primary_chamber: AmesimPneumaticVolume - p4_port3_far_port3_line: AmesimPnl0002Pipe - p4_port3_next_node: TestMqlPneumaticNode4 - p4_port3_next_primary_line: AmesimPnl0001Pipe - p4_port3_next_primary_chamber: AmesimPneumaticVolume - p4_port3_next_port3_line: AmesimPnl0002Pipe - p4_port3_next_orifice: AmesimPneumaticOrifice - p4_port3_next_orifice_line: AmesimPnl0003Pipe - p4_port3_next_orifice_node: TestMqlPneumaticNode3 - p4_port3_next_orifice_output_line: AmesimPnl0001Pipe - p4_port3_next_chamber_inlet_line: AmesimPnl0001Pipe - p4_port3_next_chamber: AmesimPneumaticVolume - p4_port3_next_chamber_outlet_line: AmesimPnl0001Pipe - p4_port3_next_chamber_inlet_orifice: AmesimPneumaticOrifice - p4_port3_next_chamber_outlet_orifice: AmesimPneumaticOrifice - p4_port3_next_chamber_spec: TestMqlPneumaticChamberSegmentSpec - p4_port3_next_orifice_resistance: AmesimPnl00rPipe - p4_port3_next_resistance_node: TestMqlPneumaticNode3 - p4_port3_next_resistance_node_line: AmesimPnl0003Pipe - p4_port3_next_resistance_orifice: AmesimPneumaticOrifice - p4_port3_next_resistance_output_line: AmesimPnl0001Pipe - p4_port3_next_resistance_chamber_inlet_line: AmesimPnl0001Pipe - p4_port3_next_resistance_chamber: AmesimPneumaticVolume - p4_port3_next_resistance_chamber_outlet_line: AmesimPnl0001Pipe - p4_port3_next_resistance_chamber_inlet_orifice: AmesimPneumaticOrifice - p4_port3_next_resistance_chamber_outlet_orifice: AmesimPneumaticOrifice - p4_port3_next_resistance_chamber_spec: TestMqlPneumaticChamberSegmentSpec - p4_port3_remote_orifice: AmesimPneumaticOrifice - p4_port3_remote_orifice_line: AmesimPnl0003Pipe - p4_port3_remote_orifice_node: TestMqlPneumaticNode3 - p4_port3_remote_orifice_resistance: AmesimPnl00rPipe - p4_port3_remote_resistance_node: TestMqlPneumaticNode3 - p4_port3_remote_resistance_node_line: AmesimPnl0003Pipe - p4_port3_remote_resistance_orifice: AmesimPneumaticOrifice - p4_port3_remote_resistance_output_line: AmesimPnl0001Pipe - volume: AmesimPneumaticVolume - inlet_orifice: AmesimPneumaticOrifice - outlet_orifice: AmesimPneumaticOrifice - spec: TestMqlPneumaticChamberSegmentSpec - - -@dataclass(frozen=True) -class TestMqlTwoStateOrificeLineFlows: - line_center_flow: float - line_to_boundary_flow: float - - -@dataclass(frozen=True) -class TestMqlP4NeighborhoodLineFlows: - primary_chamber_to_line_flow: float - port_1_line_flow: float - port_3_line_flow: float - - -@dataclass(frozen=True) -class TestMqlFixedChamberSegmentFlows: - inlet_node_to_line_flow: float - inlet_line_to_chamber_flow: float - outlet_node_to_line_flow: float - outlet_line_to_chamber_flow: float - - -@dataclass(frozen=True) -class TestMqlPn3NodeBalancePort: - flow_kg_s: float - node_h: float - connected_h: float - port_mass_flow_kg_s: float - - -@dataclass(frozen=True) -class TestMqlP4NodeBalancePort: - flow_kg_s: float - node_h: float - connected_h: float - port_mass_flow_kg_s: float - - -@dataclass(frozen=True) -class TestMqlPn3P4NodeChamberSegmentSnapshot: - inlet_line: ThermodynamicProperties - chamber: ThermodynamicProperties - outlet_line: ThermodynamicProperties - node_line_port_1: ThermodynamicProperties - node_line_port_2: ThermodynamicProperties - p4_primary_line: ThermodynamicProperties - p4_boundary: ThermodynamicProperties - p4_primary_chamber: ThermodynamicProperties - p4_port1_line: ThermodynamicProperties - p4_port1_remote_primary_line: ThermodynamicProperties - p4_port1_remote_primary_chamber: ThermodynamicProperties - p4_port1_remote_port1_line: ThermodynamicProperties - p4_port1_far_primary_line: ThermodynamicProperties - p4_port1_far_primary_chamber: ThermodynamicProperties - p4_port1_far_port1_line: ThermodynamicProperties - p4_port1_next_primary_line: ThermodynamicProperties - p4_port1_next_primary_chamber: ThermodynamicProperties - p4_port1_next_port1_line: ThermodynamicProperties - p4_port1_next_orifice_line_port_1: ThermodynamicProperties - p4_port1_next_orifice_line_port_2: ThermodynamicProperties - p4_port1_next_orifice_output_line: ThermodynamicProperties - p4_port1_next_orifice_resistance_line_port_1: ThermodynamicProperties - p4_port1_next_orifice_resistance_line_port_2: ThermodynamicProperties - p4_port1_next_orifice_resistance_chamber_inlet_line: ThermodynamicProperties - p4_port1_next_orifice_resistance_chamber: ThermodynamicProperties - p4_port1_next_orifice_resistance_chamber_outlet_line: ThermodynamicProperties - p4_bridge_primary_line: ThermodynamicProperties - p4_bridge_primary_chamber: ThermodynamicProperties - p4_port1_remote_orifice_line_port_1: ThermodynamicProperties - p4_port1_remote_orifice_line_port_2: ThermodynamicProperties - p4_port3_line: ThermodynamicProperties - p4_port3_remote_primary_line: ThermodynamicProperties - p4_port3_remote_primary_chamber: ThermodynamicProperties - p4_port3_remote_port3_line: ThermodynamicProperties - p4_port3_far_primary_line: ThermodynamicProperties - p4_port3_far_primary_chamber: ThermodynamicProperties - p4_port3_far_port3_line: ThermodynamicProperties - p4_port3_next_primary_line: ThermodynamicProperties - p4_port3_next_primary_chamber: ThermodynamicProperties - p4_port3_next_port3_line: ThermodynamicProperties - p4_port3_next_orifice_line_port_1: ThermodynamicProperties - p4_port3_next_orifice_line_port_2: ThermodynamicProperties - p4_port3_next_orifice_output_line: ThermodynamicProperties - p4_port3_next_chamber_inlet_line: ThermodynamicProperties - p4_port3_next_chamber: ThermodynamicProperties - p4_port3_next_chamber_outlet_line: ThermodynamicProperties - p4_port3_next_resistance_node_line_port_1: ThermodynamicProperties - p4_port3_next_resistance_node_line_port_2: ThermodynamicProperties - p4_port3_next_resistance_output_line: ThermodynamicProperties - p4_port3_next_resistance_chamber_inlet_line: ThermodynamicProperties - p4_port3_next_resistance_chamber: ThermodynamicProperties - p4_port3_next_resistance_chamber_outlet_line: ThermodynamicProperties - p4_port3_remote_orifice_line_port_1: ThermodynamicProperties - p4_port3_remote_orifice_line_port_2: ThermodynamicProperties - p4_port3_remote_resistance_node_line_port_1: ThermodynamicProperties - p4_port3_remote_resistance_node_line_port_2: ThermodynamicProperties - p4_port3_remote_resistance_output_line: ThermodynamicProperties - inlet_node: ThermodynamicProperties - resistance_boundary: ThermodynamicProperties - node_balance: TestMqlPneumaticNode3Balance - p4_balance: TestMqlPneumaticNode4Balance - p4_port1_remote_balance: TestMqlPneumaticNode4Balance - p4_port1_far_balance: TestMqlPneumaticNode4Balance - p4_port1_next_balance: TestMqlPneumaticNode4Balance - p4_bridge_balance: TestMqlPneumaticNode4Balance - p4_port1_remote_orifice_node_balance: TestMqlPneumaticNode3Balance - p4_port1_next_orifice_node_balance: TestMqlPneumaticNode3Balance - p4_port1_next_orifice_resistance_node_balance: TestMqlPneumaticNode3Balance - p4_port3_remote_balance: TestMqlPneumaticNode4Balance - p4_port3_far_balance: TestMqlPneumaticNode4Balance - p4_port3_next_balance: TestMqlPneumaticNode4Balance - p4_port3_next_orifice_node_balance: TestMqlPneumaticNode3Balance - p4_port3_next_resistance_node_balance: TestMqlPneumaticNode3Balance - p4_port3_remote_orifice_node_balance: TestMqlPneumaticNode3Balance - p4_port3_remote_resistance_node_balance: TestMqlPneumaticNode3Balance - inlet_node_to_line_flow: float - inlet_line_to_chamber_flow: float - outlet_node_to_line_flow: float - outlet_line_to_chamber_flow: float - node_to_resistance_flow: float - node_to_pnl0003_flow: float - pnl0003_center_flow: float - pnl0003_to_p4_flow: float - p4_primary_chamber_to_line_flow: float - p4_node_to_primary_line_flow: float - p4_to_port1_line_flow: float - p4_port1_remote_node_to_line_flow: float - p4_port1_remote_chamber_to_line_flow: float - p4_port1_remote_node_to_primary_line_flow: float - p4_port1_remote_to_port1_line_flow: float - p4_port1_far_node_to_line_flow: float - p4_port1_far_chamber_to_line_flow: float - p4_port1_far_node_to_primary_line_flow: float - p4_port1_far_to_next_line_flow: float - p4_port1_next_node_to_line_flow: float - p4_port1_next_chamber_to_line_flow: float - p4_port1_next_node_to_primary_line_flow: float - p4_port1_next_to_bridge_line_flow: float - p4_port1_next_orifice_p4_to_output_line_flow: float - p4_port1_next_orifice_node_to_line_flow: float - p4_port1_next_orifice_line_center_flow: float - p4_port1_next_orifice_to_output_line_flow: float - p4_port1_next_orifice_resistance_node_to_next_flow: float - p4_port1_next_orifice_resistance_node_to_line_flow: float - p4_port1_next_orifice_resistance_line_center_flow: float - p4_port1_next_orifice_resistance_to_p4_flow: float - p4_port1_next_orifice_resistance_chamber_inlet_node_to_line_flow: float - p4_port1_next_orifice_resistance_chamber_inlet_line_to_chamber_flow: float - p4_port1_next_orifice_resistance_chamber_outlet_node_to_line_flow: float - p4_port1_next_orifice_resistance_chamber_outlet_line_to_chamber_flow: float - p4_bridge_to_port1_next_line_flow: float - p4_bridge_chamber_to_line_flow: float - p4_bridge_node_to_primary_line_flow: float - p4_port1_remote_node_to_orifice_line_flow: float - p4_port1_remote_orifice_line_center_flow: float - p4_port1_remote_orifice_to_node_flow: float - p4_to_port3_line_flow: float - p4_port3_remote_node_to_line_flow: float - p4_port3_remote_chamber_to_line_flow: float - p4_port3_remote_node_to_primary_line_flow: float - p4_port3_remote_to_port3_line_flow: float - p4_port3_far_node_to_line_flow: float - p4_port3_far_chamber_to_line_flow: float - p4_port3_far_node_to_primary_line_flow: float - p4_port3_far_to_next_line_flow: float - p4_port3_next_node_to_line_flow: float - p4_port3_next_chamber_to_line_flow: float - p4_port3_next_node_to_primary_line_flow: float - p4_bridge_to_port3_next_line_flow: float - p4_port3_next_to_bridge_line_flow: float - p4_port3_next_orifice_node_to_line_flow: float - p4_port3_next_orifice_line_center_flow: float - p4_port3_next_orifice_to_output_line_flow: float - p4_port3_next_orifice_output_to_p4_flow: float - p4_port3_next_chamber_inlet_node_to_line_flow: float - p4_port3_next_chamber_inlet_line_to_chamber_flow: float - p4_port3_next_chamber_outlet_node_to_line_flow: float - p4_port3_next_chamber_outlet_line_to_chamber_flow: float - p4_port3_next_resistance_node_to_next_flow: float - p4_port3_next_resistance_node_to_line_flow: float - p4_port3_next_resistance_line_center_flow: float - p4_port3_next_resistance_to_output_line_flow: float - p4_port3_next_resistance_output_to_p4_flow: float - p4_port3_next_resistance_chamber_inlet_node_to_line_flow: float - p4_port3_next_resistance_chamber_inlet_line_to_chamber_flow: float - p4_port3_next_resistance_chamber_outlet_node_to_line_flow: float - p4_port3_next_resistance_chamber_outlet_line_to_chamber_flow: float - p4_port3_remote_node_to_orifice_line_flow: float - p4_port3_remote_node_to_resistance_flow: float - p4_port3_remote_resistance_node_to_line_flow: float - p4_port3_remote_resistance_line_center_flow: float - p4_port3_remote_resistance_to_output_line_flow: float - p4_port3_remote_resistance_output_to_p4_flow: float - p4_port3_remote_orifice_line_center_flow: float - p4_port3_remote_orifice_to_node_flow: float - - -class TestMqlPn3P4NodeChamberSegmentClosure: - """PN3 chamber segment closed through the adjacent real P4 node. - - The P4 primary pressure/temperature comes from the PNL0001 storage on - ``port_2``. P4 ports 1 and 3 are represented by the adjacent PNL0002 center - compliances; their far nodes remain prescribed pressure boundaries. - """ - - def __init__( - self, - *, - components: TestMqlPn3P4NodeChamberSegmentComponents, - inlet_node: TestMqlPneumaticBoundaryCondition, - resistance_boundary: TestMqlPneumaticBoundaryCondition, - use_inlet_line_reference_rhs: bool = False, - use_p4_port3_remote_primary_reference_rhs: bool = False, - ) -> None: - self.components = components - self.inlet_node = inlet_node - self.resistance_boundary = resistance_boundary - self.use_inlet_line_reference_rhs = use_inlet_line_reference_rhs - self.use_p4_port3_remote_primary_reference_rhs = ( - use_p4_port3_remote_primary_reference_rhs - ) - - def initial_state_vector(self) -> list[float]: - return [ - *self.components.inlet_line.get_state_vector(), - *self.components.volume.get_state_vector(), - *self.components.outlet_line.get_state_vector(), - *self.components.node_line.get_state_vector(), - *self.components.p4_primary_line.get_state_vector(), - *self.components.p4_primary_chamber.get_state_vector(), - *self.components.p4_port1_line.get_state_vector(), - *self.components.p4_port1_remote_primary_line.get_state_vector(), - *self.components.p4_port1_remote_primary_chamber.get_state_vector(), - *self.components.p4_port1_remote_port1_line.get_state_vector(), - *self.components.p4_port1_far_primary_line.get_state_vector(), - *self.components.p4_port1_far_primary_chamber.get_state_vector(), - *self.components.p4_port1_far_port1_line.get_state_vector(), - *self.components.p4_port1_next_primary_line.get_state_vector(), - *self.components.p4_port1_next_primary_chamber.get_state_vector(), - *self.components.p4_port1_next_port1_line.get_state_vector(), - *self.components.p4_bridge_primary_line.get_state_vector(), - *self.components.p4_bridge_primary_chamber.get_state_vector(), - *self.components.p4_port1_remote_orifice_line.get_state_vector(), - *self.components.p4_port3_line.get_state_vector(), - *self.components.p4_port3_remote_primary_line.get_state_vector(), - *self.components.p4_port3_remote_primary_chamber.get_state_vector(), - *self.components.p4_port3_remote_port3_line.get_state_vector(), - *self.components.p4_port3_far_primary_line.get_state_vector(), - *self.components.p4_port3_far_primary_chamber.get_state_vector(), - *self.components.p4_port3_far_port3_line.get_state_vector(), - *self.components.p4_port3_next_primary_line.get_state_vector(), - *self.components.p4_port3_next_primary_chamber.get_state_vector(), - *self.components.p4_port3_next_port3_line.get_state_vector(), - *self.components.p4_port3_remote_resistance_node_line.get_state_vector(), - *self.components.p4_port3_remote_resistance_output_line.get_state_vector(), - *self.components.p4_port3_remote_orifice_line.get_state_vector(), - *self.components.p4_port3_next_orifice_line.get_state_vector(), - *self.components.p4_port3_next_orifice_output_line.get_state_vector(), - *self.components.p4_port3_next_chamber_inlet_line.get_state_vector(), - *self.components.p4_port3_next_chamber.get_state_vector(), - *self.components.p4_port3_next_chamber_outlet_line.get_state_vector(), - *self.components.p4_port3_next_resistance_node_line.get_state_vector(), - *self.components.p4_port3_next_resistance_output_line.get_state_vector(), - *self.components.p4_port3_next_resistance_chamber_inlet_line.get_state_vector(), - *self.components.p4_port3_next_resistance_chamber.get_state_vector(), - *self.components.p4_port3_next_resistance_chamber_outlet_line.get_state_vector(), - *self.components.p4_port1_next_orifice_line.get_state_vector(), - *self.components.p4_port1_next_orifice_output_line.get_state_vector(), - *self.components.p4_port1_next_orifice_resistance_line.get_state_vector(), - *self.components.p4_port1_next_orifice_resistance_chamber_inlet_line.get_state_vector(), - *self.components.p4_port1_next_orifice_resistance_chamber.get_state_vector(), - *self.components.p4_port1_next_orifice_resistance_chamber_outlet_line.get_state_vector(), - ] - - def apply_state_vector(self, values: list[float]) -> None: - if len(values) != 112: - raise ValueError("PN3/P4 node chamber segment state vector requires one hundred twelve values") - self.components.inlet_line.set_state_vector(values[:2]) - self.components.volume.set_state_vector(values[2:4]) - self.components.outlet_line.set_state_vector(values[4:6]) - self.components.node_line.set_state_vector(values[6:10]) - self.components.p4_primary_line.set_state_vector(values[10:12]) - self.components.p4_primary_chamber.set_state_vector(values[12:14]) - self.components.p4_port1_line.set_state_vector(values[14:16]) - self.components.p4_port1_remote_primary_line.set_state_vector(values[16:18]) - self.components.p4_port1_remote_primary_chamber.set_state_vector(values[18:20]) - self.components.p4_port1_remote_port1_line.set_state_vector(values[20:22]) - self.components.p4_port1_far_primary_line.set_state_vector(values[22:24]) - self.components.p4_port1_far_primary_chamber.set_state_vector(values[24:26]) - self.components.p4_port1_far_port1_line.set_state_vector(values[26:28]) - self.components.p4_port1_next_primary_line.set_state_vector(values[28:30]) - self.components.p4_port1_next_primary_chamber.set_state_vector(values[30:32]) - self.components.p4_port1_next_port1_line.set_state_vector(values[32:34]) - self.components.p4_bridge_primary_line.set_state_vector(values[34:36]) - self.components.p4_bridge_primary_chamber.set_state_vector(values[36:38]) - self.components.p4_port1_remote_orifice_line.set_state_vector(values[38:42]) - self.components.p4_port3_line.set_state_vector(values[42:44]) - self.components.p4_port3_remote_primary_line.set_state_vector(values[44:46]) - self.components.p4_port3_remote_primary_chamber.set_state_vector(values[46:48]) - self.components.p4_port3_remote_port3_line.set_state_vector(values[48:50]) - self.components.p4_port3_far_primary_line.set_state_vector(values[50:52]) - self.components.p4_port3_far_primary_chamber.set_state_vector(values[52:54]) - self.components.p4_port3_far_port3_line.set_state_vector(values[54:56]) - self.components.p4_port3_next_primary_line.set_state_vector(values[56:58]) - self.components.p4_port3_next_primary_chamber.set_state_vector(values[58:60]) - self.components.p4_port3_next_port3_line.set_state_vector(values[60:62]) - self.components.p4_port3_remote_resistance_node_line.set_state_vector(values[62:66]) - self.components.p4_port3_remote_resistance_output_line.set_state_vector(values[66:68]) - self.components.p4_port3_remote_orifice_line.set_state_vector(values[68:72]) - self.components.p4_port3_next_orifice_line.set_state_vector(values[72:76]) - self.components.p4_port3_next_orifice_output_line.set_state_vector(values[76:78]) - self.components.p4_port3_next_chamber_inlet_line.set_state_vector(values[78:80]) - self.components.p4_port3_next_chamber.set_state_vector(values[80:82]) - self.components.p4_port3_next_chamber_outlet_line.set_state_vector(values[82:84]) - self.components.p4_port3_next_resistance_node_line.set_state_vector(values[84:88]) - self.components.p4_port3_next_resistance_output_line.set_state_vector(values[88:90]) - self.components.p4_port3_next_resistance_chamber_inlet_line.set_state_vector(values[90:92]) - self.components.p4_port3_next_resistance_chamber.set_state_vector(values[92:94]) - self.components.p4_port3_next_resistance_chamber_outlet_line.set_state_vector(values[94:96]) - self.components.p4_port1_next_orifice_line.set_state_vector(values[96:100]) - self.components.p4_port1_next_orifice_output_line.set_state_vector(values[100:102]) - self.components.p4_port1_next_orifice_resistance_line.set_state_vector(values[102:106]) - self.components.p4_port1_next_orifice_resistance_chamber_inlet_line.set_state_vector(values[106:108]) - self.components.p4_port1_next_orifice_resistance_chamber.set_state_vector(values[108:110]) - self.components.p4_port1_next_orifice_resistance_chamber_outlet_line.set_state_vector(values[110:]) - - def snapshot( - self, - state_vector: list[float] | None = None, - ) -> TestMqlPn3P4NodeChamberSegmentSnapshot: - if state_vector is not None: - self.apply_state_vector(state_vector) - inlet_line = self.components.inlet_line.properties() - chamber = self.components.volume.properties() - outlet_line = self.components.outlet_line.properties() - node_line_port_1 = self.components.node_line.properties_1() - node_line_port_2 = self.components.node_line.properties_2() - p4_primary_line = self.components.p4_primary_line.properties() - p4_primary_chamber = self.components.p4_primary_chamber.properties() - p4_port1_line = self.components.p4_port1_line.properties() - p4_port1_remote_primary_line = ( - self.components.p4_port1_remote_primary_line.properties() - ) - p4_port1_remote_primary_chamber = ( - self.components.p4_port1_remote_primary_chamber.properties() - ) - p4_port1_remote_port1_line = ( - self.components.p4_port1_remote_port1_line.properties() - ) - p4_port1_far_primary_line = self.components.p4_port1_far_primary_line.properties() - p4_port1_far_primary_chamber = ( - self.components.p4_port1_far_primary_chamber.properties() - ) - p4_port1_far_port1_line = self.components.p4_port1_far_port1_line.properties() - p4_port1_next_primary_line = self.components.p4_port1_next_primary_line.properties() - p4_port1_next_primary_chamber = ( - self.components.p4_port1_next_primary_chamber.properties() - ) - p4_port1_next_port1_line = self.components.p4_port1_next_port1_line.properties() - p4_port1_next_orifice_line_port_1 = ( - self.components.p4_port1_next_orifice_line.properties_1() - ) - p4_port1_next_orifice_line_port_2 = ( - self.components.p4_port1_next_orifice_line.properties_2() - ) - p4_port1_next_orifice_output_line = ( - self.components.p4_port1_next_orifice_output_line.properties() - ) - p4_port1_next_orifice_resistance_line_port_1 = ( - self.components.p4_port1_next_orifice_resistance_line.properties_1() - ) - p4_port1_next_orifice_resistance_line_port_2 = ( - self.components.p4_port1_next_orifice_resistance_line.properties_2() - ) - p4_port1_next_orifice_resistance_chamber_inlet_line = ( - self.components.p4_port1_next_orifice_resistance_chamber_inlet_line.properties() - ) - p4_port1_next_orifice_resistance_chamber = ( - self.components.p4_port1_next_orifice_resistance_chamber.properties() - ) - p4_port1_next_orifice_resistance_chamber_outlet_line = ( - self.components.p4_port1_next_orifice_resistance_chamber_outlet_line.properties() - ) - p4_bridge_primary_line = self.components.p4_bridge_primary_line.properties() - p4_bridge_primary_chamber = self.components.p4_bridge_primary_chamber.properties() - p4_port1_remote_orifice_line_port_1 = ( - self.components.p4_port1_remote_orifice_line.properties_1() - ) - p4_port1_remote_orifice_line_port_2 = ( - self.components.p4_port1_remote_orifice_line.properties_2() - ) - p4_port3_line = self.components.p4_port3_line.properties() - p4_port3_remote_primary_line = ( - self.components.p4_port3_remote_primary_line.properties() - ) - p4_port3_remote_primary_chamber = ( - self.components.p4_port3_remote_primary_chamber.properties() - ) - p4_port3_remote_port3_line = ( - self.components.p4_port3_remote_port3_line.properties() - ) - p4_port3_far_primary_line = self.components.p4_port3_far_primary_line.properties() - p4_port3_far_primary_chamber = ( - self.components.p4_port3_far_primary_chamber.properties() - ) - p4_port3_far_port3_line = self.components.p4_port3_far_port3_line.properties() - p4_port3_next_primary_line = self.components.p4_port3_next_primary_line.properties() - p4_port3_next_primary_chamber = ( - self.components.p4_port3_next_primary_chamber.properties() - ) - p4_port3_next_port3_line = self.components.p4_port3_next_port3_line.properties() - p4_port3_next_orifice_line_port_1 = ( - self.components.p4_port3_next_orifice_line.properties_1() - ) - p4_port3_next_orifice_line_port_2 = ( - self.components.p4_port3_next_orifice_line.properties_2() - ) - p4_port3_next_orifice_output_line = ( - self.components.p4_port3_next_orifice_output_line.properties() - ) - p4_port3_next_chamber_inlet_line = ( - self.components.p4_port3_next_chamber_inlet_line.properties() - ) - p4_port3_next_chamber = self.components.p4_port3_next_chamber.properties() - p4_port3_next_chamber_outlet_line = ( - self.components.p4_port3_next_chamber_outlet_line.properties() - ) - p4_port3_next_resistance_node_line_port_1 = ( - self.components.p4_port3_next_resistance_node_line.properties_1() - ) - p4_port3_next_resistance_node_line_port_2 = ( - self.components.p4_port3_next_resistance_node_line.properties_2() - ) - p4_port3_next_resistance_output_line = ( - self.components.p4_port3_next_resistance_output_line.properties() - ) - p4_port3_next_resistance_chamber_inlet_line = ( - self.components.p4_port3_next_resistance_chamber_inlet_line.properties() - ) - p4_port3_next_resistance_chamber = ( - self.components.p4_port3_next_resistance_chamber.properties() - ) - p4_port3_next_resistance_chamber_outlet_line = ( - self.components.p4_port3_next_resistance_chamber_outlet_line.properties() - ) - p4_port3_remote_orifice_line_port_1 = ( - self.components.p4_port3_remote_orifice_line.properties_1() - ) - p4_port3_remote_orifice_line_port_2 = ( - self.components.p4_port3_remote_orifice_line.properties_2() - ) - p4_port3_remote_resistance_node_line_port_1 = ( - self.components.p4_port3_remote_resistance_node_line.properties_1() - ) - p4_port3_remote_resistance_node_line_port_2 = ( - self.components.p4_port3_remote_resistance_node_line.properties_2() - ) - p4_port3_remote_resistance_output_line = ( - self.components.p4_port3_remote_resistance_output_line.properties() - ) - inlet_node = self.inlet_node.properties(self.components.inlet_line.gas) - resistance_boundary = self.resistance_boundary.properties( - self.components.node_resistance.gas - ) - chamber_segment_flows = self._fixed_chamber_segment_flows( - chamber_properties=chamber, - inlet_line=self.components.inlet_line, - inlet_line_properties=inlet_line, - inlet_node_properties=p4_port3_remote_orifice_line_port_1, - inlet_orifice=self.components.inlet_orifice, - outlet_line=self.components.outlet_line, - outlet_line_properties=outlet_line, - outlet_node_properties=node_line_port_1, - outlet_orifice=self.components.outlet_orifice, - ) - inlet_node_to_line_flow = chamber_segment_flows.inlet_node_to_line_flow - inlet_line_to_chamber_flow = chamber_segment_flows.inlet_line_to_chamber_flow - outlet_node_to_line_flow = chamber_segment_flows.outlet_node_to_line_flow - outlet_line_to_chamber_flow = chamber_segment_flows.outlet_line_to_chamber_flow - node_to_resistance_flow = self._resistance_flow( - resistance=self.components.node_resistance, - port_1_properties=node_line_port_1, - port_2_properties=p4_port1_remote_orifice_line_port_1, - ) - node_balance = self._pn3_node_balance( - node=self.components.node, - primary_properties=node_line_port_1, - port_1=TestMqlPn3NodeBalancePort( - flow_kg_s=node_to_resistance_flow, - node_h=node_line_port_1.h, - connected_h=p4_port1_remote_orifice_line_port_1.h, - port_mass_flow_kg_s=node_to_resistance_flow, - ), - port_3=TestMqlPn3NodeBalancePort( - flow_kg_s=outlet_node_to_line_flow, - node_h=node_line_port_1.h, - connected_h=outlet_line.h, - port_mass_flow_kg_s=outlet_node_to_line_flow, - ), - ) - node_to_pnl0003_flow = -node_balance.port_2_mass_flow_g_s * 1.0e-3 - pnl0003_center_flow = self.components.node_line.resistance_mass_flow() - pnl0003_to_p4_flow = self._line_to_p4_flow( - line=node_line_port_2, - p4_boundary=p4_primary_line, - orifice=self.components.node_orifice, - ) - - p4_node_line_flows = self._p4_neighborhood_line_flows( - primary_line=self.components.p4_primary_line, - primary_chamber_properties=p4_primary_chamber, - port_1_line=self.components.p4_port1_line, - port_1_properties=p4_primary_line, - port_3_line=self.components.p4_port3_line, - port_3_properties=p4_primary_line, - ) - p4_primary_chamber_to_line_flow = ( - p4_node_line_flows.primary_chamber_to_line_flow - ) - p4_to_port1_line_flow = p4_node_line_flows.port_1_line_flow - p4_to_port3_line_flow = p4_node_line_flows.port_3_line_flow - p4_port1_remote_line_flows = self._p4_neighborhood_line_flows( - primary_line=self.components.p4_port1_remote_primary_line, - primary_chamber_properties=p4_port1_remote_primary_chamber, - port_1_line=self.components.p4_port1_remote_port1_line, - port_1_properties=p4_port1_remote_primary_line, - port_3_line=self.components.p4_port1_line, - port_3_properties=p4_port1_remote_primary_line, - ) - p4_port1_remote_chamber_to_line_flow = ( - p4_port1_remote_line_flows.primary_chamber_to_line_flow - ) - p4_port1_remote_to_port1_line_flow = ( - p4_port1_remote_line_flows.port_1_line_flow - ) - p4_port1_remote_node_to_line_flow = ( - p4_port1_remote_line_flows.port_3_line_flow - ) - p4_port1_far_line_flows = self._p4_neighborhood_line_flows( - primary_line=self.components.p4_port1_far_primary_line, - primary_chamber_properties=p4_port1_far_primary_chamber, - port_1_line=self.components.p4_port1_far_port1_line, - port_1_properties=p4_port1_far_primary_line, - port_3_line=self.components.p4_port1_remote_port1_line, - port_3_properties=p4_port1_far_primary_line, - ) - p4_port1_far_chamber_to_line_flow = ( - p4_port1_far_line_flows.primary_chamber_to_line_flow - ) - p4_port1_far_to_next_line_flow = p4_port1_far_line_flows.port_1_line_flow - p4_port1_far_node_to_line_flow = p4_port1_far_line_flows.port_3_line_flow - p4_port1_next_line_flows = self._p4_neighborhood_line_flows( - primary_line=self.components.p4_port1_next_primary_line, - primary_chamber_properties=p4_port1_next_primary_chamber, - port_1_line=self.components.p4_port1_next_port1_line, - port_1_properties=p4_port1_next_primary_line, - port_3_line=self.components.p4_port1_far_port1_line, - port_3_properties=p4_port1_next_primary_line, - ) - p4_port1_next_chamber_to_line_flow = ( - p4_port1_next_line_flows.primary_chamber_to_line_flow - ) - p4_port1_next_to_bridge_line_flow = ( - p4_port1_next_line_flows.port_1_line_flow - ) - p4_port1_next_node_to_line_flow = p4_port1_next_line_flows.port_3_line_flow - p4_port1_next_orifice_p4_to_output_line_flow = ( - self.components.p4_port1_next_orifice_output_line.resistance_mass_flow( - port_1_pressure_pa=p4_port1_next_primary_line.p, - port_1_temperature_k=p4_port1_next_primary_line.T, - ) - ) - p4_bridge_line_flows = self._p4_neighborhood_line_flows( - primary_line=self.components.p4_bridge_primary_line, - primary_chamber_properties=p4_bridge_primary_chamber, - port_1_line=self.components.p4_port3_next_port3_line, - port_1_properties=p4_bridge_primary_line, - port_3_line=self.components.p4_port1_next_port1_line, - port_3_properties=p4_bridge_primary_line, - ) - p4_bridge_chamber_to_line_flow = ( - p4_bridge_line_flows.primary_chamber_to_line_flow - ) - p4_bridge_to_port3_next_line_flow = p4_bridge_line_flows.port_1_line_flow - p4_bridge_to_port1_next_line_flow = p4_bridge_line_flows.port_3_line_flow - p4_port1_next_orifice_resistance_chamber_flows = ( - self._fixed_chamber_segment_flows( - chamber_properties=p4_port1_next_orifice_resistance_chamber, - inlet_line=( - self.components.p4_port1_next_orifice_resistance_chamber_inlet_line - ), - inlet_line_properties=( - p4_port1_next_orifice_resistance_chamber_inlet_line - ), - inlet_node_properties=p4_port1_remote_orifice_line_port_1, - inlet_orifice=( - self.components.p4_port1_next_orifice_resistance_chamber_inlet_orifice - ), - outlet_line=( - self.components.p4_port1_next_orifice_resistance_chamber_outlet_line - ), - outlet_line_properties=( - p4_port1_next_orifice_resistance_chamber_outlet_line - ), - outlet_node_properties=p4_port1_next_orifice_resistance_line_port_1, - outlet_orifice=( - self.components.p4_port1_next_orifice_resistance_chamber_outlet_orifice - ), - ) - ) - p4_port1_next_orifice_resistance_chamber_inlet_node_to_line_flow = ( - p4_port1_next_orifice_resistance_chamber_flows.inlet_node_to_line_flow - ) - p4_port1_next_orifice_resistance_chamber_inlet_line_to_chamber_flow = ( - p4_port1_next_orifice_resistance_chamber_flows.inlet_line_to_chamber_flow - ) - p4_port1_next_orifice_resistance_chamber_outlet_node_to_line_flow = ( - p4_port1_next_orifice_resistance_chamber_flows.outlet_node_to_line_flow - ) - p4_port1_next_orifice_resistance_chamber_outlet_line_to_chamber_flow = ( - p4_port1_next_orifice_resistance_chamber_flows.outlet_line_to_chamber_flow - ) - p4_port1_remote_orifice_node_balance = self._pn3_node_balance( - node=self.components.p4_port1_remote_orifice_node, - primary_properties=p4_port1_remote_orifice_line_port_1, - port_1=TestMqlPn3NodeBalancePort( - flow_kg_s=( - p4_port1_next_orifice_resistance_chamber_inlet_node_to_line_flow - ), - node_h=p4_port1_remote_orifice_line_port_1.h, - connected_h=p4_port1_next_orifice_resistance_chamber_inlet_line.h, - port_mass_flow_kg_s=( - p4_port1_next_orifice_resistance_chamber_inlet_node_to_line_flow - ), - ), - port_3=TestMqlPn3NodeBalancePort( - flow_kg_s=-node_to_resistance_flow, - node_h=p4_port1_remote_orifice_line_port_1.h, - connected_h=node_line_port_1.h, - port_mass_flow_kg_s=-node_to_resistance_flow, - ), - ) - p4_port1_remote_node_to_orifice_line_flow = ( - -p4_port1_remote_orifice_node_balance.port_2_mass_flow_g_s * 1.0e-3 - ) - p4_port1_remote_orifice_line_flows = self._two_state_orifice_line_flows( - line=self.components.p4_port1_remote_orifice_line, - line_port_2_properties=p4_port1_remote_orifice_line_port_2, - boundary_properties=p4_port1_remote_primary_line, - orifice=self.components.p4_port1_remote_orifice, - ) - p4_port1_remote_orifice_line_center_flow = ( - p4_port1_remote_orifice_line_flows.line_center_flow - ) - p4_port1_remote_orifice_to_node_flow = ( - p4_port1_remote_orifice_line_flows.line_to_boundary_flow - ) - p4_port3_remote_line_flows = self._p4_neighborhood_line_flows( - primary_line=self.components.p4_port3_remote_primary_line, - primary_chamber_properties=p4_port3_remote_primary_chamber, - port_1_line=self.components.p4_port3_line, - port_1_properties=p4_port3_remote_primary_line, - port_3_line=self.components.p4_port3_remote_port3_line, - port_3_properties=p4_port3_remote_primary_line, - ) - p4_port3_remote_chamber_to_line_flow = ( - p4_port3_remote_line_flows.primary_chamber_to_line_flow - ) - p4_port3_remote_node_to_line_flow = ( - p4_port3_remote_line_flows.port_1_line_flow - ) - p4_port3_remote_to_port3_line_flow = ( - p4_port3_remote_line_flows.port_3_line_flow - ) - p4_port3_far_line_flows = self._p4_neighborhood_line_flows( - primary_line=self.components.p4_port3_far_primary_line, - primary_chamber_properties=p4_port3_far_primary_chamber, - port_1_line=self.components.p4_port3_remote_port3_line, - port_1_properties=p4_port3_far_primary_line, - port_3_line=self.components.p4_port3_far_port3_line, - port_3_properties=p4_port3_far_primary_line, - ) - p4_port3_far_chamber_to_line_flow = ( - p4_port3_far_line_flows.primary_chamber_to_line_flow - ) - p4_port3_far_node_to_line_flow = p4_port3_far_line_flows.port_1_line_flow - p4_port3_far_to_next_line_flow = p4_port3_far_line_flows.port_3_line_flow - p4_port3_next_line_flows = self._p4_neighborhood_line_flows( - primary_line=self.components.p4_port3_next_primary_line, - primary_chamber_properties=p4_port3_next_primary_chamber, - port_1_line=self.components.p4_port3_far_port3_line, - port_1_properties=p4_port3_next_primary_line, - port_3_line=self.components.p4_port3_next_port3_line, - port_3_properties=p4_port3_next_primary_line, - ) - p4_port3_next_chamber_to_line_flow = ( - p4_port3_next_line_flows.primary_chamber_to_line_flow - ) - p4_port3_next_node_to_line_flow = p4_port3_next_line_flows.port_1_line_flow - p4_port3_next_to_bridge_line_flow = ( - p4_port3_next_line_flows.port_3_line_flow - ) - p4_port3_next_chamber_flows = self._fixed_chamber_segment_flows( - chamber_properties=p4_port3_next_chamber, - inlet_line=self.components.p4_port3_next_chamber_inlet_line, - inlet_line_properties=p4_port3_next_chamber_inlet_line, - inlet_node_properties=p4_port3_next_orifice_line_port_1, - inlet_orifice=self.components.p4_port3_next_chamber_inlet_orifice, - outlet_line=self.components.p4_port3_next_chamber_outlet_line, - outlet_line_properties=p4_port3_next_chamber_outlet_line, - outlet_node_properties=p4_port3_remote_resistance_node_line_port_1, - outlet_orifice=self.components.p4_port3_next_chamber_outlet_orifice, - ) - p4_port3_next_chamber_inlet_node_to_line_flow = ( - p4_port3_next_chamber_flows.inlet_node_to_line_flow - ) - p4_port3_next_chamber_inlet_line_to_chamber_flow = ( - p4_port3_next_chamber_flows.inlet_line_to_chamber_flow - ) - p4_port3_next_chamber_outlet_node_to_line_flow = ( - p4_port3_next_chamber_flows.outlet_node_to_line_flow - ) - p4_port3_next_chamber_outlet_line_to_chamber_flow = ( - p4_port3_next_chamber_flows.outlet_line_to_chamber_flow - ) - p4_port3_next_resistance_node_to_next_flow = self._resistance_flow( - resistance=self.components.p4_port3_next_orifice_resistance, - port_1_properties=p4_port3_next_resistance_node_line_port_1, - port_2_properties=p4_port3_next_orifice_line_port_1, - ) - p4_port3_next_orifice_node_balance = self._pn3_node_balance( - node=self.components.p4_port3_next_orifice_node, - primary_properties=p4_port3_next_orifice_line_port_1, - port_1=TestMqlPn3NodeBalancePort( - flow_kg_s=p4_port3_next_chamber_inlet_node_to_line_flow, - node_h=p4_port3_next_orifice_line_port_1.h, - connected_h=p4_port3_next_chamber_inlet_line.h, - port_mass_flow_kg_s=p4_port3_next_chamber_inlet_node_to_line_flow, - ), - port_3=TestMqlPn3NodeBalancePort( - flow_kg_s=-p4_port3_next_resistance_node_to_next_flow, - node_h=p4_port3_next_orifice_line_port_1.h, - connected_h=p4_port3_next_resistance_node_line_port_1.h, - port_mass_flow_kg_s=-p4_port3_next_resistance_node_to_next_flow, - ), - ) - p4_port3_next_orifice_node_to_line_flow = ( - -p4_port3_next_orifice_node_balance.port_2_mass_flow_g_s * 1.0e-3 - ) - p4_port3_next_orifice_line_flows = self._two_state_orifice_line_flows( - line=self.components.p4_port3_next_orifice_line, - line_port_2_properties=p4_port3_next_orifice_line_port_2, - boundary_properties=p4_port3_next_orifice_output_line, - orifice=self.components.p4_port3_next_orifice, - ) - p4_port3_next_orifice_line_center_flow = ( - p4_port3_next_orifice_line_flows.line_center_flow - ) - p4_port3_next_orifice_to_output_line_flow = ( - p4_port3_next_orifice_line_flows.line_to_boundary_flow - ) - p4_port3_next_orifice_output_to_p4_flow = ( - p4_port3_next_orifice_to_output_line_flow - ) - p4_port3_next_resistance_chamber_flows = ( - self._fixed_chamber_segment_flows( - chamber_properties=p4_port3_next_resistance_chamber, - inlet_line=self.components.p4_port3_next_resistance_chamber_inlet_line, - inlet_line_properties=p4_port3_next_resistance_chamber_inlet_line, - inlet_node_properties=p4_port1_next_orifice_line_port_1, - inlet_orifice=( - self.components.p4_port3_next_resistance_chamber_inlet_orifice - ), - outlet_line=( - self.components.p4_port3_next_resistance_chamber_outlet_line - ), - outlet_line_properties=p4_port3_next_resistance_chamber_outlet_line, - outlet_node_properties=p4_port3_next_resistance_node_line_port_1, - outlet_orifice=( - self.components.p4_port3_next_resistance_chamber_outlet_orifice - ), - ) - ) - p4_port3_next_resistance_chamber_inlet_node_to_line_flow = ( - p4_port3_next_resistance_chamber_flows.inlet_node_to_line_flow - ) - p4_port3_next_resistance_chamber_inlet_line_to_chamber_flow = ( - p4_port3_next_resistance_chamber_flows.inlet_line_to_chamber_flow - ) - p4_port3_next_resistance_chamber_outlet_node_to_line_flow = ( - p4_port3_next_resistance_chamber_flows.outlet_node_to_line_flow - ) - p4_port3_next_resistance_chamber_outlet_line_to_chamber_flow = ( - p4_port3_next_resistance_chamber_flows.outlet_line_to_chamber_flow - ) - p4_port1_next_orifice_resistance_node_to_next_flow = self._resistance_flow( - resistance=self.components.p4_port1_next_orifice_resistance, - port_1_properties=p4_port1_next_orifice_resistance_line_port_1, - port_2_properties=p4_port1_next_orifice_line_port_1, - ) - p4_port1_next_orifice_node_balance = self._pn3_node_balance( - node=self.components.p4_port1_next_orifice_node, - primary_properties=p4_port1_next_orifice_line_port_1, - port_1=TestMqlPn3NodeBalancePort( - flow_kg_s=p4_port3_next_resistance_chamber_inlet_node_to_line_flow, - node_h=p4_port1_next_orifice_line_port_1.h, - connected_h=p4_port3_next_resistance_chamber_inlet_line.h, - port_mass_flow_kg_s=( - p4_port3_next_resistance_chamber_inlet_node_to_line_flow - ), - ), - port_3=TestMqlPn3NodeBalancePort( - flow_kg_s=-p4_port1_next_orifice_resistance_node_to_next_flow, - node_h=p4_port1_next_orifice_line_port_1.h, - connected_h=p4_port1_next_orifice_resistance_line_port_1.h, - port_mass_flow_kg_s=( - -p4_port1_next_orifice_resistance_node_to_next_flow - ), - ), - ) - p4_port1_next_orifice_node_to_line_flow = ( - -p4_port1_next_orifice_node_balance.port_2_mass_flow_g_s * 1.0e-3 - ) - p4_port1_next_orifice_line_flows = self._two_state_orifice_line_flows( - line=self.components.p4_port1_next_orifice_line, - line_port_2_properties=p4_port1_next_orifice_line_port_2, - boundary_properties=p4_port1_next_orifice_output_line, - orifice=self.components.p4_port1_next_orifice, - ) - p4_port1_next_orifice_line_center_flow = ( - p4_port1_next_orifice_line_flows.line_center_flow - ) - p4_port1_next_orifice_to_output_line_flow = ( - p4_port1_next_orifice_line_flows.line_to_boundary_flow - ) - p4_port1_next_orifice_resistance_node_balance = self._pn3_node_balance( - node=self.components.p4_port1_next_orifice_resistance_node, - primary_properties=p4_port1_next_orifice_resistance_line_port_1, - port_1=TestMqlPn3NodeBalancePort( - flow_kg_s=p4_port1_next_orifice_resistance_node_to_next_flow, - node_h=p4_port1_next_orifice_resistance_line_port_1.h, - connected_h=p4_port1_next_orifice_line_port_1.h, - port_mass_flow_kg_s=p4_port1_next_orifice_resistance_node_to_next_flow, - ), - port_3=TestMqlPn3NodeBalancePort( - flow_kg_s=( - p4_port1_next_orifice_resistance_chamber_outlet_node_to_line_flow - ), - node_h=p4_port1_next_orifice_resistance_line_port_1.h, - connected_h=p4_port1_next_orifice_resistance_chamber_outlet_line.h, - port_mass_flow_kg_s=( - p4_port1_next_orifice_resistance_chamber_outlet_node_to_line_flow - ), - ), - ) - p4_port1_next_orifice_resistance_node_to_line_flow = ( - -p4_port1_next_orifice_resistance_node_balance.port_2_mass_flow_g_s - * 1.0e-3 - ) - p4_port1_next_orifice_resistance_line_flows = ( - self._two_state_orifice_line_flows( - line=self.components.p4_port1_next_orifice_resistance_line, - line_port_2_properties=( - p4_port1_next_orifice_resistance_line_port_2 - ), - boundary_properties=p4_port1_far_primary_line, - orifice=self.components.p4_port1_next_orifice_resistance_orifice, - ) - ) - p4_port1_next_orifice_resistance_line_center_flow = ( - p4_port1_next_orifice_resistance_line_flows.line_center_flow - ) - p4_port1_next_orifice_resistance_to_p4_flow = ( - p4_port1_next_orifice_resistance_line_flows.line_to_boundary_flow - ) - p4_port3_next_resistance_node_balance = self._pn3_node_balance( - node=self.components.p4_port3_next_resistance_node, - primary_properties=p4_port3_next_resistance_node_line_port_1, - port_1=TestMqlPn3NodeBalancePort( - flow_kg_s=p4_port3_next_resistance_node_to_next_flow, - node_h=p4_port3_next_resistance_node_line_port_1.h, - connected_h=p4_port3_next_orifice_line_port_1.h, - port_mass_flow_kg_s=p4_port3_next_resistance_node_to_next_flow, - ), - port_3=TestMqlPn3NodeBalancePort( - flow_kg_s=p4_port3_next_resistance_chamber_outlet_node_to_line_flow, - node_h=p4_port3_next_resistance_node_line_port_1.h, - connected_h=p4_port3_next_resistance_chamber_outlet_line.h, - port_mass_flow_kg_s=( - p4_port3_next_resistance_chamber_outlet_node_to_line_flow - ), - ), - ) - p4_port3_next_resistance_node_to_line_flow = ( - -p4_port3_next_resistance_node_balance.port_2_mass_flow_g_s * 1.0e-3 - ) - p4_port3_next_resistance_line_flows = self._two_state_orifice_line_flows( - line=self.components.p4_port3_next_resistance_node_line, - line_port_2_properties=p4_port3_next_resistance_node_line_port_2, - boundary_properties=p4_port3_next_resistance_output_line, - orifice=self.components.p4_port3_next_resistance_orifice, - ) - p4_port3_next_resistance_line_center_flow = ( - p4_port3_next_resistance_line_flows.line_center_flow - ) - p4_port3_next_resistance_to_output_line_flow = ( - p4_port3_next_resistance_line_flows.line_to_boundary_flow - ) - p4_port3_next_resistance_output_to_p4_flow = ( - p4_port3_next_resistance_to_output_line_flow - ) - p4_port3_remote_node_to_resistance_flow = self._resistance_flow( - resistance=self.components.p4_port3_remote_orifice_resistance, - port_1_properties=p4_port3_remote_orifice_line_port_1, - port_2_properties=p4_port3_remote_resistance_node_line_port_1, - ) - p4_port3_remote_orifice_node_balance = self._pn3_node_balance( - node=self.components.p4_port3_remote_orifice_node, - primary_properties=p4_port3_remote_orifice_line_port_1, - port_1=TestMqlPn3NodeBalancePort( - flow_kg_s=inlet_node_to_line_flow, - node_h=p4_port3_remote_orifice_line_port_1.h, - connected_h=inlet_line.h, - port_mass_flow_kg_s=inlet_node_to_line_flow, - ), - port_3=TestMqlPn3NodeBalancePort( - flow_kg_s=p4_port3_remote_node_to_resistance_flow, - node_h=p4_port3_remote_orifice_line_port_1.h, - connected_h=p4_port3_remote_resistance_node_line_port_1.h, - port_mass_flow_kg_s=p4_port3_remote_node_to_resistance_flow, - ), - ) - p4_port3_remote_node_to_orifice_line_flow = ( - -p4_port3_remote_orifice_node_balance.port_2_mass_flow_g_s * 1.0e-3 - ) - p4_port3_remote_resistance_node_balance = self._pn3_node_balance( - node=self.components.p4_port3_remote_resistance_node, - primary_properties=p4_port3_remote_resistance_node_line_port_1, - port_1=TestMqlPn3NodeBalancePort( - flow_kg_s=-p4_port3_remote_node_to_resistance_flow, - node_h=p4_port3_remote_resistance_node_line_port_1.h, - connected_h=p4_port3_remote_orifice_line_port_1.h, - port_mass_flow_kg_s=-p4_port3_remote_node_to_resistance_flow, - ), - port_3=TestMqlPn3NodeBalancePort( - flow_kg_s=p4_port3_next_chamber_outlet_node_to_line_flow, - node_h=p4_port3_remote_resistance_node_line_port_1.h, - connected_h=p4_port3_next_chamber_outlet_line.h, - port_mass_flow_kg_s=p4_port3_next_chamber_outlet_node_to_line_flow, - ), - ) - p4_port3_remote_resistance_node_to_line_flow = ( - -p4_port3_remote_resistance_node_balance.port_2_mass_flow_g_s * 1.0e-3 - ) - p4_port3_remote_resistance_line_flows = self._two_state_orifice_line_flows( - line=self.components.p4_port3_remote_resistance_node_line, - line_port_2_properties=p4_port3_remote_resistance_node_line_port_2, - boundary_properties=p4_port3_remote_resistance_output_line, - orifice=self.components.p4_port3_remote_resistance_orifice, - ) - p4_port3_remote_resistance_line_center_flow = ( - p4_port3_remote_resistance_line_flows.line_center_flow - ) - p4_port3_remote_resistance_to_output_line_flow = ( - p4_port3_remote_resistance_line_flows.line_to_boundary_flow - ) - p4_port3_remote_resistance_output_to_p4_flow = ( - p4_port3_remote_resistance_to_output_line_flow - ) - p4_port3_remote_orifice_line_flows = self._two_state_orifice_line_flows( - line=self.components.p4_port3_remote_orifice_line, - line_port_2_properties=p4_port3_remote_orifice_line_port_2, - boundary_properties=p4_port3_remote_primary_line, - orifice=self.components.p4_port3_remote_orifice, - ) - p4_port3_remote_orifice_line_center_flow = ( - p4_port3_remote_orifice_line_flows.line_center_flow - ) - p4_port3_remote_orifice_to_node_flow = ( - p4_port3_remote_orifice_line_flows.line_to_boundary_flow - ) - p4_balance = self._p4_node_balance( - node=self.components.p4_node, - primary_properties=p4_primary_line, - port_1=TestMqlP4NodeBalancePort( - flow_kg_s=p4_to_port1_line_flow, - node_h=p4_primary_line.h, - connected_h=p4_port1_line.h, - port_mass_flow_kg_s=-p4_to_port1_line_flow, - ), - port_3=TestMqlP4NodeBalancePort( - flow_kg_s=p4_to_port3_line_flow, - node_h=p4_primary_line.h, - connected_h=p4_port3_line.h, - port_mass_flow_kg_s=-p4_to_port3_line_flow, - ), - port_4=TestMqlP4NodeBalancePort( - flow_kg_s=pnl0003_to_p4_flow, - node_h=node_line_port_2.h, - connected_h=p4_primary_line.h, - port_mass_flow_kg_s=pnl0003_to_p4_flow, - ), - ) - p4_node_to_primary_line_flow = p4_balance.port_2_mass_flow_g_s * 1.0e-3 - p4_port1_remote_balance = self._p4_node_balance( - node=self.components.p4_port1_remote_node, - primary_properties=p4_port1_remote_primary_line, - port_1=TestMqlP4NodeBalancePort( - flow_kg_s=p4_port1_remote_to_port1_line_flow, - node_h=p4_port1_remote_primary_line.h, - connected_h=p4_port1_remote_port1_line.h, - port_mass_flow_kg_s=-p4_port1_remote_to_port1_line_flow, - ), - port_3=TestMqlP4NodeBalancePort( - flow_kg_s=p4_port1_remote_node_to_line_flow, - node_h=p4_port1_remote_primary_line.h, - connected_h=p4_port1_line.h, - port_mass_flow_kg_s=-p4_port1_remote_node_to_line_flow, - ), - port_4=TestMqlP4NodeBalancePort( - flow_kg_s=p4_port1_remote_orifice_to_node_flow, - node_h=p4_port1_remote_orifice_line_port_2.h, - connected_h=p4_port1_remote_primary_line.h, - port_mass_flow_kg_s=p4_port1_remote_orifice_to_node_flow, - ), - ) - p4_port1_remote_node_to_primary_line_flow = ( - p4_port1_remote_balance.port_2_mass_flow_g_s * 1.0e-3 - ) - p4_port1_far_balance = self._p4_node_balance( - node=self.components.p4_port1_far_node, - primary_properties=p4_port1_far_primary_line, - port_1=TestMqlP4NodeBalancePort( - flow_kg_s=p4_port1_far_to_next_line_flow, - node_h=p4_port1_far_primary_line.h, - connected_h=p4_port1_far_port1_line.h, - port_mass_flow_kg_s=-p4_port1_far_to_next_line_flow, - ), - port_3=TestMqlP4NodeBalancePort( - flow_kg_s=p4_port1_far_node_to_line_flow, - node_h=p4_port1_far_primary_line.h, - connected_h=p4_port1_remote_port1_line.h, - port_mass_flow_kg_s=-p4_port1_far_node_to_line_flow, - ), - port_4=TestMqlP4NodeBalancePort( - flow_kg_s=p4_port1_next_orifice_resistance_to_p4_flow, - node_h=p4_port1_next_orifice_resistance_line_port_2.h, - connected_h=p4_port1_far_primary_line.h, - port_mass_flow_kg_s=p4_port1_next_orifice_resistance_to_p4_flow, - ), - ) - p4_port1_far_node_to_primary_line_flow = ( - p4_port1_far_balance.port_2_mass_flow_g_s * 1.0e-3 - ) - p4_port1_next_balance = self._p4_node_balance( - node=self.components.p4_port1_next_node, - primary_properties=p4_port1_next_primary_line, - port_1=TestMqlP4NodeBalancePort( - flow_kg_s=p4_port1_next_to_bridge_line_flow, - node_h=p4_port1_next_primary_line.h, - connected_h=p4_port1_next_port1_line.h, - port_mass_flow_kg_s=-p4_port1_next_to_bridge_line_flow, - ), - port_3=TestMqlP4NodeBalancePort( - flow_kg_s=p4_port1_next_node_to_line_flow, - node_h=p4_port1_next_primary_line.h, - connected_h=p4_port1_far_port1_line.h, - port_mass_flow_kg_s=-p4_port1_next_node_to_line_flow, - ), - port_4=TestMqlP4NodeBalancePort( - flow_kg_s=p4_port1_next_orifice_p4_to_output_line_flow, - node_h=p4_port1_next_primary_line.h, - connected_h=p4_port1_next_orifice_output_line.h, - port_mass_flow_kg_s=-p4_port1_next_orifice_p4_to_output_line_flow, - ), - ) - p4_port1_next_node_to_primary_line_flow = ( - p4_port1_next_balance.port_2_mass_flow_g_s * 1.0e-3 - ) - p4_port3_remote_balance = self._p4_node_balance( - node=self.components.p4_port3_remote_node, - primary_properties=p4_port3_remote_primary_line, - port_1=TestMqlP4NodeBalancePort( - flow_kg_s=p4_port3_remote_node_to_line_flow, - node_h=p4_port3_remote_primary_line.h, - connected_h=p4_port3_line.h, - port_mass_flow_kg_s=-p4_port3_remote_node_to_line_flow, - ), - port_3=TestMqlP4NodeBalancePort( - flow_kg_s=p4_port3_remote_to_port3_line_flow, - node_h=p4_port3_remote_primary_line.h, - connected_h=p4_port3_remote_port3_line.h, - port_mass_flow_kg_s=-p4_port3_remote_to_port3_line_flow, - ), - port_4=TestMqlP4NodeBalancePort( - flow_kg_s=p4_port3_remote_orifice_to_node_flow, - node_h=p4_port3_remote_orifice_line_port_2.h, - connected_h=p4_port3_remote_primary_line.h, - port_mass_flow_kg_s=p4_port3_remote_orifice_to_node_flow, - ), - ) - p4_port3_remote_node_to_primary_line_flow = ( - p4_port3_remote_balance.port_2_mass_flow_g_s * 1.0e-3 - ) - p4_port3_far_balance = self._p4_node_balance( - node=self.components.p4_port3_far_node, - primary_properties=p4_port3_far_primary_line, - port_1=TestMqlP4NodeBalancePort( - flow_kg_s=p4_port3_far_node_to_line_flow, - node_h=p4_port3_far_primary_line.h, - connected_h=p4_port3_remote_port3_line.h, - port_mass_flow_kg_s=-p4_port3_far_node_to_line_flow, - ), - port_3=TestMqlP4NodeBalancePort( - flow_kg_s=p4_port3_far_to_next_line_flow, - node_h=p4_port3_far_primary_line.h, - connected_h=p4_port3_far_port3_line.h, - port_mass_flow_kg_s=-p4_port3_far_to_next_line_flow, - ), - port_4=TestMqlP4NodeBalancePort( - flow_kg_s=p4_port3_remote_resistance_output_to_p4_flow, - node_h=p4_port3_remote_resistance_output_line.h, - connected_h=p4_port3_far_primary_line.h, - port_mass_flow_kg_s=p4_port3_remote_resistance_output_to_p4_flow, - ), - ) - p4_port3_far_node_to_primary_line_flow = ( - p4_port3_far_balance.port_2_mass_flow_g_s * 1.0e-3 - ) - p4_port3_next_balance = self._p4_node_balance( - node=self.components.p4_port3_next_node, - primary_properties=p4_port3_next_primary_line, - port_1=TestMqlP4NodeBalancePort( - flow_kg_s=p4_port3_next_node_to_line_flow, - node_h=p4_port3_next_primary_line.h, - connected_h=p4_port3_far_port3_line.h, - port_mass_flow_kg_s=-p4_port3_next_node_to_line_flow, - ), - port_3=TestMqlP4NodeBalancePort( - flow_kg_s=p4_port3_next_to_bridge_line_flow, - node_h=p4_port3_next_primary_line.h, - connected_h=p4_port3_next_port3_line.h, - port_mass_flow_kg_s=-p4_port3_next_to_bridge_line_flow, - ), - port_4=TestMqlP4NodeBalancePort( - flow_kg_s=p4_port3_next_orifice_output_to_p4_flow, - node_h=p4_port3_next_orifice_output_line.h, - connected_h=p4_port3_next_primary_line.h, - port_mass_flow_kg_s=p4_port3_next_orifice_output_to_p4_flow, - ), - ) - p4_port3_next_node_to_primary_line_flow = ( - p4_port3_next_balance.port_2_mass_flow_g_s * 1.0e-3 - ) - p4_bridge_balance = self._p4_node_balance( - node=self.components.p4_bridge_node, - primary_properties=p4_bridge_primary_line, - port_1=TestMqlP4NodeBalancePort( - flow_kg_s=p4_bridge_to_port3_next_line_flow, - node_h=p4_bridge_primary_line.h, - connected_h=p4_port3_next_port3_line.h, - port_mass_flow_kg_s=-p4_bridge_to_port3_next_line_flow, - ), - port_3=TestMqlP4NodeBalancePort( - flow_kg_s=p4_bridge_to_port1_next_line_flow, - node_h=p4_bridge_primary_line.h, - connected_h=p4_port1_next_port1_line.h, - port_mass_flow_kg_s=-p4_bridge_to_port1_next_line_flow, - ), - port_4=TestMqlP4NodeBalancePort( - flow_kg_s=p4_port3_next_resistance_output_to_p4_flow, - node_h=p4_port3_next_resistance_output_line.h, - connected_h=p4_bridge_primary_line.h, - port_mass_flow_kg_s=p4_port3_next_resistance_output_to_p4_flow, - ), - ) - p4_bridge_node_to_primary_line_flow = ( - p4_bridge_balance.port_2_mass_flow_g_s * 1.0e-3 - ) - - snapshot = TestMqlPn3P4NodeChamberSegmentSnapshot( - inlet_line=inlet_line, - chamber=chamber, - outlet_line=outlet_line, - node_line_port_1=node_line_port_1, - node_line_port_2=node_line_port_2, - p4_primary_line=p4_primary_line, - p4_boundary=p4_primary_line, - p4_primary_chamber=p4_primary_chamber, - p4_port1_line=p4_port1_line, - p4_port1_remote_primary_line=p4_port1_remote_primary_line, - p4_port1_remote_primary_chamber=p4_port1_remote_primary_chamber, - p4_port1_remote_port1_line=p4_port1_remote_port1_line, - p4_port1_far_primary_line=p4_port1_far_primary_line, - p4_port1_far_primary_chamber=p4_port1_far_primary_chamber, - p4_port1_far_port1_line=p4_port1_far_port1_line, - p4_port1_next_primary_line=p4_port1_next_primary_line, - p4_port1_next_primary_chamber=p4_port1_next_primary_chamber, - p4_port1_next_port1_line=p4_port1_next_port1_line, - p4_port1_next_orifice_line_port_1=p4_port1_next_orifice_line_port_1, - p4_port1_next_orifice_line_port_2=p4_port1_next_orifice_line_port_2, - p4_port1_next_orifice_output_line=p4_port1_next_orifice_output_line, - p4_port1_next_orifice_resistance_line_port_1=( - p4_port1_next_orifice_resistance_line_port_1 - ), - p4_port1_next_orifice_resistance_line_port_2=( - p4_port1_next_orifice_resistance_line_port_2 - ), - p4_port1_next_orifice_resistance_chamber_inlet_line=( - p4_port1_next_orifice_resistance_chamber_inlet_line - ), - p4_port1_next_orifice_resistance_chamber=( - p4_port1_next_orifice_resistance_chamber - ), - p4_port1_next_orifice_resistance_chamber_outlet_line=( - p4_port1_next_orifice_resistance_chamber_outlet_line - ), - p4_bridge_primary_line=p4_bridge_primary_line, - p4_bridge_primary_chamber=p4_bridge_primary_chamber, - p4_port1_remote_orifice_line_port_1=p4_port1_remote_orifice_line_port_1, - p4_port1_remote_orifice_line_port_2=p4_port1_remote_orifice_line_port_2, - p4_port3_line=p4_port3_line, - p4_port3_remote_primary_line=p4_port3_remote_primary_line, - p4_port3_remote_primary_chamber=p4_port3_remote_primary_chamber, - p4_port3_remote_port3_line=p4_port3_remote_port3_line, - p4_port3_far_primary_line=p4_port3_far_primary_line, - p4_port3_far_primary_chamber=p4_port3_far_primary_chamber, - p4_port3_far_port3_line=p4_port3_far_port3_line, - p4_port3_next_primary_line=p4_port3_next_primary_line, - p4_port3_next_primary_chamber=p4_port3_next_primary_chamber, - p4_port3_next_port3_line=p4_port3_next_port3_line, - p4_port3_next_orifice_line_port_1=p4_port3_next_orifice_line_port_1, - p4_port3_next_orifice_line_port_2=p4_port3_next_orifice_line_port_2, - p4_port3_next_orifice_output_line=p4_port3_next_orifice_output_line, - p4_port3_next_chamber_inlet_line=p4_port3_next_chamber_inlet_line, - p4_port3_next_chamber=p4_port3_next_chamber, - p4_port3_next_chamber_outlet_line=p4_port3_next_chamber_outlet_line, - p4_port3_next_resistance_node_line_port_1=( - p4_port3_next_resistance_node_line_port_1 - ), - p4_port3_next_resistance_node_line_port_2=( - p4_port3_next_resistance_node_line_port_2 - ), - p4_port3_next_resistance_output_line=p4_port3_next_resistance_output_line, - p4_port3_next_resistance_chamber_inlet_line=( - p4_port3_next_resistance_chamber_inlet_line - ), - p4_port3_next_resistance_chamber=p4_port3_next_resistance_chamber, - p4_port3_next_resistance_chamber_outlet_line=( - p4_port3_next_resistance_chamber_outlet_line - ), - p4_port3_remote_orifice_line_port_1=p4_port3_remote_orifice_line_port_1, - p4_port3_remote_orifice_line_port_2=p4_port3_remote_orifice_line_port_2, - p4_port3_remote_resistance_node_line_port_1=( - p4_port3_remote_resistance_node_line_port_1 - ), - p4_port3_remote_resistance_node_line_port_2=( - p4_port3_remote_resistance_node_line_port_2 - ), - p4_port3_remote_resistance_output_line=( - p4_port3_remote_resistance_output_line - ), - inlet_node=inlet_node, - resistance_boundary=resistance_boundary, - node_balance=node_balance, - p4_balance=p4_balance, - p4_port1_remote_balance=p4_port1_remote_balance, - p4_port1_far_balance=p4_port1_far_balance, - p4_port1_next_balance=p4_port1_next_balance, - p4_bridge_balance=p4_bridge_balance, - p4_port1_remote_orifice_node_balance=p4_port1_remote_orifice_node_balance, - p4_port1_next_orifice_node_balance=p4_port1_next_orifice_node_balance, - p4_port1_next_orifice_resistance_node_balance=( - p4_port1_next_orifice_resistance_node_balance - ), - p4_port3_remote_balance=p4_port3_remote_balance, - p4_port3_far_balance=p4_port3_far_balance, - p4_port3_next_balance=p4_port3_next_balance, - p4_port3_next_orifice_node_balance=p4_port3_next_orifice_node_balance, - p4_port3_next_resistance_node_balance=p4_port3_next_resistance_node_balance, - p4_port3_remote_orifice_node_balance=p4_port3_remote_orifice_node_balance, - p4_port3_remote_resistance_node_balance=( - p4_port3_remote_resistance_node_balance - ), - inlet_node_to_line_flow=inlet_node_to_line_flow, - inlet_line_to_chamber_flow=inlet_line_to_chamber_flow, - outlet_node_to_line_flow=outlet_node_to_line_flow, - outlet_line_to_chamber_flow=outlet_line_to_chamber_flow, - node_to_resistance_flow=node_to_resistance_flow, - node_to_pnl0003_flow=node_to_pnl0003_flow, - pnl0003_center_flow=pnl0003_center_flow, - pnl0003_to_p4_flow=pnl0003_to_p4_flow, - p4_primary_chamber_to_line_flow=p4_primary_chamber_to_line_flow, - p4_node_to_primary_line_flow=p4_node_to_primary_line_flow, - p4_to_port1_line_flow=p4_to_port1_line_flow, - p4_port1_remote_node_to_line_flow=p4_port1_remote_node_to_line_flow, - p4_port1_remote_chamber_to_line_flow=p4_port1_remote_chamber_to_line_flow, - p4_port1_remote_node_to_primary_line_flow=( - p4_port1_remote_node_to_primary_line_flow - ), - p4_port1_remote_to_port1_line_flow=p4_port1_remote_to_port1_line_flow, - p4_port1_far_node_to_line_flow=p4_port1_far_node_to_line_flow, - p4_port1_far_chamber_to_line_flow=p4_port1_far_chamber_to_line_flow, - p4_port1_far_node_to_primary_line_flow=( - p4_port1_far_node_to_primary_line_flow - ), - p4_port1_far_to_next_line_flow=p4_port1_far_to_next_line_flow, - p4_port1_next_node_to_line_flow=p4_port1_next_node_to_line_flow, - p4_port1_next_chamber_to_line_flow=p4_port1_next_chamber_to_line_flow, - p4_port1_next_node_to_primary_line_flow=( - p4_port1_next_node_to_primary_line_flow - ), - p4_port1_next_to_bridge_line_flow=p4_port1_next_to_bridge_line_flow, - p4_port1_next_orifice_p4_to_output_line_flow=( - p4_port1_next_orifice_p4_to_output_line_flow - ), - p4_port1_next_orifice_node_to_line_flow=( - p4_port1_next_orifice_node_to_line_flow - ), - p4_port1_next_orifice_line_center_flow=( - p4_port1_next_orifice_line_center_flow - ), - p4_port1_next_orifice_to_output_line_flow=( - p4_port1_next_orifice_to_output_line_flow - ), - p4_port1_next_orifice_resistance_node_to_next_flow=( - p4_port1_next_orifice_resistance_node_to_next_flow - ), - p4_port1_next_orifice_resistance_node_to_line_flow=( - p4_port1_next_orifice_resistance_node_to_line_flow - ), - p4_port1_next_orifice_resistance_line_center_flow=( - p4_port1_next_orifice_resistance_line_center_flow - ), - p4_port1_next_orifice_resistance_to_p4_flow=( - p4_port1_next_orifice_resistance_to_p4_flow - ), - p4_port1_next_orifice_resistance_chamber_inlet_node_to_line_flow=( - p4_port1_next_orifice_resistance_chamber_inlet_node_to_line_flow - ), - p4_port1_next_orifice_resistance_chamber_inlet_line_to_chamber_flow=( - p4_port1_next_orifice_resistance_chamber_inlet_line_to_chamber_flow - ), - p4_port1_next_orifice_resistance_chamber_outlet_node_to_line_flow=( - p4_port1_next_orifice_resistance_chamber_outlet_node_to_line_flow - ), - p4_port1_next_orifice_resistance_chamber_outlet_line_to_chamber_flow=( - p4_port1_next_orifice_resistance_chamber_outlet_line_to_chamber_flow - ), - p4_bridge_to_port1_next_line_flow=p4_bridge_to_port1_next_line_flow, - p4_bridge_chamber_to_line_flow=p4_bridge_chamber_to_line_flow, - p4_bridge_node_to_primary_line_flow=p4_bridge_node_to_primary_line_flow, - p4_port1_remote_node_to_orifice_line_flow=( - p4_port1_remote_node_to_orifice_line_flow - ), - p4_port1_remote_orifice_line_center_flow=( - p4_port1_remote_orifice_line_center_flow - ), - p4_port1_remote_orifice_to_node_flow=p4_port1_remote_orifice_to_node_flow, - p4_to_port3_line_flow=p4_to_port3_line_flow, - p4_port3_remote_node_to_line_flow=p4_port3_remote_node_to_line_flow, - p4_port3_remote_chamber_to_line_flow=p4_port3_remote_chamber_to_line_flow, - p4_port3_remote_node_to_primary_line_flow=( - p4_port3_remote_node_to_primary_line_flow - ), - p4_port3_remote_to_port3_line_flow=p4_port3_remote_to_port3_line_flow, - p4_port3_far_node_to_line_flow=p4_port3_far_node_to_line_flow, - p4_port3_far_chamber_to_line_flow=p4_port3_far_chamber_to_line_flow, - p4_port3_far_node_to_primary_line_flow=( - p4_port3_far_node_to_primary_line_flow - ), - p4_port3_far_to_next_line_flow=p4_port3_far_to_next_line_flow, - p4_port3_next_node_to_line_flow=p4_port3_next_node_to_line_flow, - p4_port3_next_chamber_to_line_flow=p4_port3_next_chamber_to_line_flow, - p4_port3_next_node_to_primary_line_flow=( - p4_port3_next_node_to_primary_line_flow - ), - p4_bridge_to_port3_next_line_flow=p4_bridge_to_port3_next_line_flow, - p4_port3_next_to_bridge_line_flow=p4_port3_next_to_bridge_line_flow, - p4_port3_next_orifice_node_to_line_flow=( - p4_port3_next_orifice_node_to_line_flow - ), - p4_port3_next_orifice_line_center_flow=( - p4_port3_next_orifice_line_center_flow - ), - p4_port3_next_orifice_to_output_line_flow=( - p4_port3_next_orifice_to_output_line_flow - ), - p4_port3_next_orifice_output_to_p4_flow=( - p4_port3_next_orifice_output_to_p4_flow - ), - p4_port3_next_chamber_inlet_node_to_line_flow=( - p4_port3_next_chamber_inlet_node_to_line_flow - ), - p4_port3_next_chamber_inlet_line_to_chamber_flow=( - p4_port3_next_chamber_inlet_line_to_chamber_flow - ), - p4_port3_next_chamber_outlet_node_to_line_flow=( - p4_port3_next_chamber_outlet_node_to_line_flow - ), - p4_port3_next_chamber_outlet_line_to_chamber_flow=( - p4_port3_next_chamber_outlet_line_to_chamber_flow - ), - p4_port3_next_resistance_node_to_next_flow=( - p4_port3_next_resistance_node_to_next_flow - ), - p4_port3_next_resistance_node_to_line_flow=( - p4_port3_next_resistance_node_to_line_flow - ), - p4_port3_next_resistance_line_center_flow=( - p4_port3_next_resistance_line_center_flow - ), - p4_port3_next_resistance_to_output_line_flow=( - p4_port3_next_resistance_to_output_line_flow - ), - p4_port3_next_resistance_output_to_p4_flow=( - p4_port3_next_resistance_output_to_p4_flow - ), - p4_port3_next_resistance_chamber_inlet_node_to_line_flow=( - p4_port3_next_resistance_chamber_inlet_node_to_line_flow - ), - p4_port3_next_resistance_chamber_inlet_line_to_chamber_flow=( - p4_port3_next_resistance_chamber_inlet_line_to_chamber_flow - ), - p4_port3_next_resistance_chamber_outlet_node_to_line_flow=( - p4_port3_next_resistance_chamber_outlet_node_to_line_flow - ), - p4_port3_next_resistance_chamber_outlet_line_to_chamber_flow=( - p4_port3_next_resistance_chamber_outlet_line_to_chamber_flow - ), - p4_port3_remote_node_to_orifice_line_flow=( - p4_port3_remote_node_to_orifice_line_flow - ), - p4_port3_remote_node_to_resistance_flow=( - p4_port3_remote_node_to_resistance_flow - ), - p4_port3_remote_resistance_node_to_line_flow=( - p4_port3_remote_resistance_node_to_line_flow - ), - p4_port3_remote_resistance_line_center_flow=( - p4_port3_remote_resistance_line_center_flow - ), - p4_port3_remote_resistance_to_output_line_flow=( - p4_port3_remote_resistance_to_output_line_flow - ), - p4_port3_remote_resistance_output_to_p4_flow=( - p4_port3_remote_resistance_output_to_p4_flow - ), - p4_port3_remote_orifice_line_center_flow=( - p4_port3_remote_orifice_line_center_flow - ), - p4_port3_remote_orifice_to_node_flow=p4_port3_remote_orifice_to_node_flow, - ) - self._write_port_states(snapshot) - return snapshot - - @staticmethod - def _line_to_chamber_flow( - *, - line: ThermodynamicProperties, - chamber: ThermodynamicProperties, - orifice: AmesimPneumaticOrifice, - ) -> float: - return TestMqlPn3NodeChamberSegmentClosure._line_to_chamber_flow( - line=line, - chamber=chamber, - orifice=orifice, - ) - - @staticmethod - def _line_to_p4_flow( - *, - line: ThermodynamicProperties, - p4_boundary: ThermodynamicProperties, - orifice: AmesimPneumaticOrifice, - ) -> float: - return TestMqlPn3NodeChamberSegmentClosure._line_to_p4_flow( - line=line, - p4_boundary=p4_boundary, - orifice=orifice, - ) - - @staticmethod - def _boundary_to_p4_flow( - *, - boundary: ThermodynamicProperties, - p4_boundary: ThermodynamicProperties, - orifice: AmesimPneumaticOrifice, - ) -> float: - upstream_temperature = boundary.T if boundary.p >= p4_boundary.p else p4_boundary.T - return orifice.mass_flow(boundary.p, p4_boundary.p, upstream_temperature) - - @staticmethod - def _port( - component: AmesimPneumaticVolume | AmesimPneumaticOrifice, - port_name: str, - ) -> PortState: - return TestMqlPn3NodeChamberSegmentClosure._port(component, port_name) - - @staticmethod - def _enthalpy_flow_from_node( - *, - flow_kg_s: float, - node_h: float, - connected_h: float, - ) -> float: - return TestMqlPn3NodeChamberSegmentClosure._enthalpy_flow_from_node( - flow_kg_s=flow_kg_s, - node_h=node_h, - connected_h=connected_h, - ) - - @staticmethod - def _enthalpy_flow_for_node_port( - *, - flow_kg_s: float, - node_h: float, - connected_h: float, - port_mass_flow_kg_s: float, - ) -> float: - upstream_h = node_h if flow_kg_s >= 0.0 else connected_h - return port_mass_flow_kg_s * upstream_h - - def _two_state_orifice_line_flows( - self, - *, - line: AmesimPnl0003Pipe, - line_port_2_properties: ThermodynamicProperties, - boundary_properties: ThermodynamicProperties, - orifice: AmesimPneumaticOrifice, - ) -> TestMqlTwoStateOrificeLineFlows: - return TestMqlTwoStateOrificeLineFlows( - line_center_flow=line.resistance_mass_flow(), - line_to_boundary_flow=self._line_to_p4_flow( - line=line_port_2_properties, - p4_boundary=boundary_properties, - orifice=orifice, - ), - ) - - @staticmethod - def _resistance_flow( - *, - resistance: AmesimPnl00rPipe, - port_1_properties: ThermodynamicProperties, - port_2_properties: ThermodynamicProperties, - ) -> float: - return resistance.mass_flow( - port_1_pressure_pa=port_1_properties.p, - port_1_temperature_k=port_1_properties.T, - port_2_pressure_pa=port_2_properties.p, - port_2_temperature_k=port_2_properties.T, - ) - - @staticmethod - def _p4_neighborhood_line_flows( - *, - primary_line: AmesimPnl0001Pipe, - primary_chamber_properties: ThermodynamicProperties, - port_1_line: AmesimPnl0002Pipe, - port_1_properties: ThermodynamicProperties, - port_3_line: AmesimPnl0002Pipe, - port_3_properties: ThermodynamicProperties, - ) -> TestMqlP4NeighborhoodLineFlows: - return TestMqlP4NeighborhoodLineFlows( - primary_chamber_to_line_flow=primary_line.resistance_mass_flow( - port_1_pressure_pa=primary_chamber_properties.p, - port_1_temperature_k=primary_chamber_properties.T, - ), - port_1_line_flow=port_1_line.port_mass_flow( - port_pressure_pa=port_1_properties.p, - port_temperature_k=port_1_properties.T, - ), - port_3_line_flow=port_3_line.port_mass_flow( - port_pressure_pa=port_3_properties.p, - port_temperature_k=port_3_properties.T, - ), - ) - - def _fixed_chamber_segment_flows( - self, - *, - chamber_properties: ThermodynamicProperties, - inlet_line: AmesimPnl0001Pipe, - inlet_line_properties: ThermodynamicProperties, - inlet_node_properties: ThermodynamicProperties, - inlet_orifice: AmesimPneumaticOrifice, - outlet_line: AmesimPnl0001Pipe, - outlet_line_properties: ThermodynamicProperties, - outlet_node_properties: ThermodynamicProperties, - outlet_orifice: AmesimPneumaticOrifice, - ) -> TestMqlFixedChamberSegmentFlows: - return TestMqlFixedChamberSegmentFlows( - inlet_node_to_line_flow=inlet_line.resistance_mass_flow( - port_1_pressure_pa=inlet_node_properties.p, - port_1_temperature_k=inlet_node_properties.T, - ), - inlet_line_to_chamber_flow=self._line_to_chamber_flow( - line=inlet_line_properties, - chamber=chamber_properties, - orifice=inlet_orifice, - ), - outlet_node_to_line_flow=outlet_line.resistance_mass_flow( - port_1_pressure_pa=outlet_node_properties.p, - port_1_temperature_k=outlet_node_properties.T, - ), - outlet_line_to_chamber_flow=self._line_to_chamber_flow( - line=outlet_line_properties, - chamber=chamber_properties, - orifice=outlet_orifice, - ), - ) - - def _pn3_node_balance( - self, - *, - node: TestMqlPneumaticNode3, - primary_properties: ThermodynamicProperties, - port_1: TestMqlPn3NodeBalancePort, - port_3: TestMqlPn3NodeBalancePort, - ) -> TestMqlPneumaticNode3Balance: - return node.balance( - port_2_temperature_k=primary_properties.T, - port_2_pressure_pa=primary_properties.p, - port_1_enthalpy_flow_w=self._enthalpy_flow_for_node_port( - flow_kg_s=port_1.flow_kg_s, - node_h=port_1.node_h, - connected_h=port_1.connected_h, - port_mass_flow_kg_s=port_1.port_mass_flow_kg_s, - ), - port_1_mass_flow_g_s=port_1.port_mass_flow_kg_s * 1.0e3, - port_3_enthalpy_flow_w=self._enthalpy_flow_for_node_port( - flow_kg_s=port_3.flow_kg_s, - node_h=port_3.node_h, - connected_h=port_3.connected_h, - port_mass_flow_kg_s=port_3.port_mass_flow_kg_s, - ), - port_3_mass_flow_g_s=port_3.port_mass_flow_kg_s * 1.0e3, - ) - - @staticmethod - def _pn3_node_port_2_connected_h( - *, - balance: TestMqlPneumaticNode3Balance, - fallback_h: float, - ) -> float: - mass_flow_kg_s = balance.port_2_mass_flow_g_s * 1.0e-3 - if abs(mass_flow_kg_s) <= 1.0e-12: - return fallback_h - return balance.port_2_enthalpy_flow_w / mass_flow_kg_s - - @staticmethod - def _p4_node_port_2_connected_h( - *, - balance: TestMqlPneumaticNode4Balance, - fallback_h: float, - ) -> float: - mass_flow_kg_s = balance.port_2_mass_flow_g_s * 1.0e-3 - if abs(mass_flow_kg_s) <= 1.0e-12: - return fallback_h - return balance.port_2_enthalpy_flow_w / mass_flow_kg_s - - @staticmethod - def _reference_enthalpy_flow_for_node_port( - *, - gas, - node_properties: ThermodynamicProperties, - connected_properties: ThermodynamicProperties, - flow_kg_s: float, - port_mass_flow_kg_s: float, - reference_temperature_k: float = 298.15, - ) -> float: - stream_properties = ( - node_properties if flow_kg_s >= 0.0 else connected_properties - ) - return port_mass_flow_kg_s * gas.pressure_reference_enthalpy( - stream_properties.p, - stream_properties.T, - reference_temperature=reference_temperature_k, - ) - - def _p4_port3_remote_port_2_reference_h( - self, - *, - snapshot: TestMqlPn3P4NodeChamberSegmentSnapshot, - reference_temperature_k: float = 298.15, - ) -> float: - gas = self.components.p4_port3_remote_primary_line.gas - port_2_mass_flow = snapshot.p4_port3_remote_balance.port_2_mass_flow_g_s * 1.0e-3 - if abs(port_2_mass_flow) <= 1.0e-12: - return gas.pressure_reference_enthalpy( - snapshot.p4_port3_remote_primary_line.p, - snapshot.p4_port3_remote_primary_line.T, - reference_temperature=reference_temperature_k, - ) - port_2_enthalpy_flow = sum( - ( - self._reference_enthalpy_flow_for_node_port( - gas=gas, - node_properties=snapshot.p4_port3_remote_primary_line, - connected_properties=snapshot.p4_port3_line, - flow_kg_s=snapshot.p4_port3_remote_node_to_line_flow, - port_mass_flow_kg_s=-snapshot.p4_port3_remote_node_to_line_flow, - reference_temperature_k=reference_temperature_k, - ), - self._reference_enthalpy_flow_for_node_port( - gas=gas, - node_properties=snapshot.p4_port3_remote_primary_line, - connected_properties=snapshot.p4_port3_remote_port3_line, - flow_kg_s=snapshot.p4_port3_remote_to_port3_line_flow, - port_mass_flow_kg_s=-snapshot.p4_port3_remote_to_port3_line_flow, - reference_temperature_k=reference_temperature_k, - ), - self._reference_enthalpy_flow_for_node_port( - gas=gas, - node_properties=snapshot.p4_port3_remote_orifice_line_port_2, - connected_properties=snapshot.p4_port3_remote_primary_line, - flow_kg_s=snapshot.p4_port3_remote_orifice_to_node_flow, - port_mass_flow_kg_s=snapshot.p4_port3_remote_orifice_to_node_flow, - reference_temperature_k=reference_temperature_k, - ), - ) - ) - return port_2_enthalpy_flow / port_2_mass_flow - - def _p4_node_balance( - self, - *, - node: TestMqlPneumaticNode4, - primary_properties: ThermodynamicProperties, - port_1: TestMqlP4NodeBalancePort, - port_3: TestMqlP4NodeBalancePort, - port_4: TestMqlP4NodeBalancePort, - ) -> TestMqlPneumaticNode4Balance: - return node.balance( - port_2_temperature_k=primary_properties.T, - port_2_pressure_pa=primary_properties.p, - port_1_enthalpy_flow_w=self._enthalpy_flow_for_node_port( - flow_kg_s=port_1.flow_kg_s, - node_h=port_1.node_h, - connected_h=port_1.connected_h, - port_mass_flow_kg_s=port_1.port_mass_flow_kg_s, - ), - port_1_mass_flow_g_s=port_1.port_mass_flow_kg_s * 1.0e3, - port_3_enthalpy_flow_w=self._enthalpy_flow_for_node_port( - flow_kg_s=port_3.flow_kg_s, - node_h=port_3.node_h, - connected_h=port_3.connected_h, - port_mass_flow_kg_s=port_3.port_mass_flow_kg_s, - ), - port_3_mass_flow_g_s=port_3.port_mass_flow_kg_s * 1.0e3, - port_4_enthalpy_flow_w=self._enthalpy_flow_for_node_port( - flow_kg_s=port_4.flow_kg_s, - node_h=port_4.node_h, - connected_h=port_4.connected_h, - port_mass_flow_kg_s=port_4.port_mass_flow_kg_s, - ), - port_4_mass_flow_g_s=port_4.port_mass_flow_kg_s * 1.0e3, - ) - - def _write_fixed_chamber_segment( - self, - *, - spec: TestMqlPneumaticChamberSegmentSpec, - chamber: AmesimPneumaticVolume, - chamber_properties: ThermodynamicProperties, - inlet_line: AmesimPnl0001Pipe, - inlet_line_properties: ThermodynamicProperties, - inlet_node_properties: ThermodynamicProperties, - inlet_node_to_line_flow: float, - inlet_line_to_chamber_flow: float, - inlet_orifice: AmesimPneumaticOrifice, - outlet_line: AmesimPnl0001Pipe, - outlet_line_properties: ThermodynamicProperties, - outlet_node_properties: ThermodynamicProperties, - outlet_node_to_line_flow: float, - outlet_line_to_chamber_flow: float, - outlet_orifice: AmesimPneumaticOrifice, - ) -> None: - chamber_inlet_port = self._port(chamber, spec.volume_inlet_port) - chamber_outlet_port = self._port(chamber, spec.volume_outlet_port) - - TestMqlPnl0001PairChamberSegmentClosure._write_line_side( - self, - line=inlet_line, - line_properties=inlet_line_properties, - node_properties=inlet_node_properties, - node_to_line_flow=inlet_node_to_line_flow, - line_to_chamber_flow=inlet_line_to_chamber_flow, - orifice=inlet_orifice, - orifice_boundary_port=spec.inlet_orifice_boundary_port, - orifice_volume_port=spec.inlet_orifice_volume_port, - chamber_port=chamber_inlet_port, - chamber_properties=chamber_properties, - ) - TestMqlPnl0001PairChamberSegmentClosure._write_line_side( - self, - line=outlet_line, - line_properties=outlet_line_properties, - node_properties=outlet_node_properties, - node_to_line_flow=outlet_node_to_line_flow, - line_to_chamber_flow=outlet_line_to_chamber_flow, - orifice=outlet_orifice, - orifice_boundary_port=spec.outlet_orifice_boundary_port, - orifice_volume_port=spec.outlet_orifice_volume_port, - chamber_port=chamber_outlet_port, - chamber_properties=chamber_properties, - ) - - def _fixed_chamber_segment_derivatives( - self, - *, - spec: TestMqlPneumaticChamberSegmentSpec, - chamber: AmesimPneumaticVolume, - chamber_properties: ThermodynamicProperties, - inlet_line: AmesimPnl0001Pipe, - inlet_line_properties: ThermodynamicProperties, - inlet_connected_properties: ThermodynamicProperties, - inlet_node_to_line_flow: float, - inlet_line_to_chamber_flow: float, - outlet_line: AmesimPnl0001Pipe, - outlet_line_properties: ThermodynamicProperties, - outlet_connected_properties: ThermodynamicProperties, - outlet_node_to_line_flow: float, - outlet_line_to_chamber_flow: float, - ) -> tuple[VolumeState, VolumeState, VolumeState]: - inlet_derivative = inlet_line.derivatives_from_connections( - port_1_m_flow=inlet_node_to_line_flow, - connected_h_1=inlet_connected_properties.h, - port_2_m_flow=-inlet_line_to_chamber_flow, - connected_h_2=chamber_properties.h, - ) - chamber_derivative = chamber.derivatives_from_two_connections( - port_a_m_flow=self._port(chamber, "port_1").m_flow, - connected_h_a=( - inlet_line_properties.h - if spec.volume_inlet_port == "port_1" - else outlet_line_properties.h - ), - port_b_m_flow=self._port(chamber, "port_2").m_flow, - connected_h_b=( - inlet_line_properties.h - if spec.volume_inlet_port == "port_2" - else outlet_line_properties.h - ), - internal_h=chamber_properties.h, - ) - outlet_derivative = outlet_line.derivatives_from_connections( - port_1_m_flow=outlet_node_to_line_flow, - connected_h_1=outlet_connected_properties.h, - port_2_m_flow=-outlet_line_to_chamber_flow, - connected_h_2=chamber_properties.h, - ) - return inlet_derivative, chamber_derivative, outlet_derivative - - @staticmethod - def _write_primary_chamber_line( - *, - line: AmesimPnl0001Pipe, - line_properties: ThermodynamicProperties, - chamber: AmesimPneumaticVolume, - chamber_properties: ThermodynamicProperties, - chamber_to_line_flow: float, - node_to_line_flow: float, - ) -> None: - line.port_1.p = chamber_properties.p - line.port_1.m_flow = chamber_to_line_flow - line.port_1.h_outflow = line_properties.h - line.port_2.p = line_properties.p - line.port_2.m_flow = node_to_line_flow - line.port_2.h_outflow = line_properties.h - - chamber.port_a.p = chamber_properties.p - chamber.port_a.m_flow = -chamber_to_line_flow - chamber.port_a.h_outflow = chamber_properties.h - - @staticmethod - def _pnl0001_reference_enthalpy_derivative( - *, - line: AmesimPnl0001Pipe, - line_properties: ThermodynamicProperties, - port_1_properties: ThermodynamicProperties, - port_1_flow: float, - port_2_properties: ThermodynamicProperties, - port_2_flow: float, - port_1_connected_reference_h: float | None = None, - port_2_connected_reference_h: float | None = None, - reference_temperature_k: float = 298.15, - ) -> VolumeState: - def specific_reference_h( - *, - port_flow: float, - connected: ThermodynamicProperties, - connected_reference_h: float | None, - ) -> float: - if port_flow > 0.0 and connected_reference_h is not None: - return connected_reference_h - if port_flow > 0.0: - pressure = connected.p - temperature = connected.T - else: - pressure = line_properties.p - temperature = line_properties.T - return line.gas.pressure_reference_enthalpy( - pressure, - temperature, - reference_temperature=reference_temperature_k, - ) - - def reference_enthalpy_flow( - *, - port_flow: float, - connected: ThermodynamicProperties, - connected_reference_h: float | None, - ) -> float: - return port_flow * specific_reference_h( - port_flow=port_flow, - connected=connected, - connected_reference_h=connected_reference_h, - ) - - mass_derivative = port_1_flow + port_2_flow - heat_flow = ( - line.heat_transfer_coefficient - * line.heat_transfer_area - * (line.external_temperature - line_properties.T) - ) - enthalpy_flow = reference_enthalpy_flow( - port_flow=port_1_flow, - connected=port_1_properties, - connected_reference_h=port_1_connected_reference_h, - ) + reference_enthalpy_flow( - port_flow=port_2_flow, - connected=port_2_properties, - connected_reference_h=port_2_connected_reference_h, - ) - reference_offset_flow = ( - line.gas.cp * reference_temperature_k - line.gas.cv * line_properties.T - ) * mass_derivative - temperature_derivative = ( - enthalpy_flow + reference_offset_flow + heat_flow - ) / (line.state.m * line.gas.cv) - return VolumeState( - m=mass_derivative, - U=line.gas.cv - * (line.state.m * temperature_derivative + line_properties.T * mass_derivative), - ) - - @staticmethod - def _primary_chamber_line_derivatives( - *, - line: AmesimPnl0001Pipe, - line_properties: ThermodynamicProperties, - chamber: AmesimPneumaticVolume, - chamber_properties: ThermodynamicProperties, - chamber_to_line_flow: float, - node_to_line_flow: float, - node_connected_h: float | None = None, - ) -> tuple[VolumeState, VolumeState]: - derivative_method = ( - line.derivatives_from_connections - if node_connected_h is None - else line.derivatives_from_transport_enthalpy_connections - ) - line_derivative = derivative_method( - port_1_m_flow=chamber_to_line_flow, - connected_h_1=chamber_properties.h, - port_2_m_flow=node_to_line_flow, - connected_h_2=( - line_properties.h if node_connected_h is None else node_connected_h - ), - ) - chamber_derivative = chamber.derivatives_from_two_connections( - port_a_m_flow=-chamber_to_line_flow, - connected_h_a=line_properties.h, - port_b_m_flow=0.0, - connected_h_b=chamber_properties.h, - internal_h=chamber_properties.h, - ) - return line_derivative, chamber_derivative - - @staticmethod - def _write_connection_line( - *, - line: AmesimPnl0001Pipe, - line_properties: ThermodynamicProperties, - port_1_properties: ThermodynamicProperties, - port_1_flow: float, - port_2_properties: ThermodynamicProperties, - port_2_flow: float, - ) -> None: - line.port_1.p = port_1_properties.p - line.port_1.m_flow = port_1_flow - line.port_1.h_outflow = line_properties.h - line.port_2.p = port_2_properties.p - line.port_2.m_flow = port_2_flow - line.port_2.h_outflow = line_properties.h - - @staticmethod - def _connection_line_derivative( - *, - line: AmesimPnl0001Pipe, - port_1_properties: ThermodynamicProperties, - port_1_flow: float, - port_2_properties: ThermodynamicProperties, - port_2_flow: float, - ) -> VolumeState: - return line.derivatives_from_connections( - port_1_m_flow=port_1_flow, - connected_h_1=port_1_properties.h, - port_2_m_flow=port_2_flow, - connected_h_2=port_2_properties.h, - ) - - @staticmethod - def _write_two_state_connection_line( - *, - line: AmesimPnl0003Pipe, - port_1_properties: ThermodynamicProperties, - port_1_flow: float, - port_2_properties: ThermodynamicProperties, - port_2_flow: float, - ) -> None: - line.port_1.p = port_1_properties.p - line.port_1.m_flow = port_1_flow - line.port_1.h_outflow = port_1_properties.h - line.port_2.p = port_2_properties.p - line.port_2.m_flow = port_2_flow - line.port_2.h_outflow = port_2_properties.h - - @staticmethod - def _write_orifice_ports( - *, - orifice: AmesimPneumaticOrifice, - port_a_properties: ThermodynamicProperties, - port_a_flow: float, - port_b_properties: ThermodynamicProperties, - port_b_flow: float, - ) -> None: - orifice.port_a.p = port_a_properties.p - orifice.port_a.m_flow = port_a_flow - orifice.port_a.h_outflow = port_a_properties.h - orifice.port_b.p = port_b_properties.p - orifice.port_b.m_flow = port_b_flow - orifice.port_b.h_outflow = port_b_properties.h - - @staticmethod - def _write_resistance_ports( - *, - resistance: AmesimPnl00rPipe, - port_1_properties: ThermodynamicProperties, - port_1_flow: float, - port_2_properties: ThermodynamicProperties, - port_2_flow: float, - ) -> None: - resistance.port_1.p = port_1_properties.p - resistance.port_1.m_flow = port_1_flow - resistance.port_1.h_outflow = port_1_properties.h - resistance.port_2.p = port_2_properties.p - resistance.port_2.m_flow = port_2_flow - resistance.port_2.h_outflow = port_2_properties.h - - @staticmethod - def _two_state_connection_line_derivatives( - *, - line: AmesimPnl0003Pipe, - port_1_properties: ThermodynamicProperties, - port_1_flow: float, - port_2_connected_properties: ThermodynamicProperties, - port_2_flow: float, - port_1_connected_h: float | None = None, - ) -> tuple[VolumeState, VolumeState]: - return line.derivatives_from_connections( - port_1_m_flow=port_1_flow, - connected_h_1=( - port_1_properties.h - if port_1_connected_h is None - else port_1_connected_h - ), - port_2_m_flow=port_2_flow, - connected_h_2=port_2_connected_properties.h, - ) - - def _write_port_states( - self, - snapshot: TestMqlPn3P4NodeChamberSegmentSnapshot, - ) -> None: - self._write_fixed_chamber_segment( - spec=self.components.spec, - chamber=self.components.volume, - chamber_properties=snapshot.chamber, - inlet_line=self.components.inlet_line, - inlet_line_properties=snapshot.inlet_line, - inlet_node_properties=snapshot.p4_port3_remote_orifice_line_port_1, - inlet_node_to_line_flow=snapshot.inlet_node_to_line_flow, - inlet_line_to_chamber_flow=snapshot.inlet_line_to_chamber_flow, - inlet_orifice=self.components.inlet_orifice, - outlet_line=self.components.outlet_line, - outlet_line_properties=snapshot.outlet_line, - outlet_node_properties=snapshot.node_line_port_1, - outlet_node_to_line_flow=snapshot.outlet_node_to_line_flow, - outlet_line_to_chamber_flow=snapshot.outlet_line_to_chamber_flow, - outlet_orifice=self.components.outlet_orifice, - ) - - self._write_two_state_connection_line( - line=self.components.node_line, - port_1_properties=snapshot.node_line_port_1, - port_1_flow=snapshot.node_to_pnl0003_flow, - port_2_properties=snapshot.node_line_port_2, - port_2_flow=-snapshot.pnl0003_to_p4_flow, - ) - - self._write_orifice_ports( - orifice=self.components.node_orifice, - port_a_properties=snapshot.node_line_port_2, - port_a_flow=snapshot.pnl0003_to_p4_flow, - port_b_properties=snapshot.p4_primary_line, - port_b_flow=-snapshot.pnl0003_to_p4_flow, - ) - - self._write_resistance_ports( - resistance=self.components.node_resistance, - port_1_properties=snapshot.node_line_port_1, - port_1_flow=snapshot.node_to_resistance_flow, - port_2_properties=snapshot.p4_port1_remote_orifice_line_port_1, - port_2_flow=-snapshot.node_to_resistance_flow, - ) - - self._write_primary_chamber_line( - line=self.components.p4_primary_line, - line_properties=snapshot.p4_primary_line, - chamber=self.components.p4_primary_chamber, - chamber_properties=snapshot.p4_primary_chamber, - chamber_to_line_flow=snapshot.p4_primary_chamber_to_line_flow, - node_to_line_flow=snapshot.p4_node_to_primary_line_flow, - ) - - self._write_connection_line( - line=self.components.p4_port1_line, - line_properties=snapshot.p4_port1_line, - port_1_properties=snapshot.p4_primary_line, - port_1_flow=snapshot.p4_to_port1_line_flow, - port_2_properties=snapshot.p4_port1_remote_primary_line, - port_2_flow=snapshot.p4_port1_remote_node_to_line_flow, - ) - - self._write_primary_chamber_line( - line=self.components.p4_port1_remote_primary_line, - line_properties=snapshot.p4_port1_remote_primary_line, - chamber=self.components.p4_port1_remote_primary_chamber, - chamber_properties=snapshot.p4_port1_remote_primary_chamber, - chamber_to_line_flow=snapshot.p4_port1_remote_chamber_to_line_flow, - node_to_line_flow=snapshot.p4_port1_remote_node_to_primary_line_flow, - ) - - self._write_connection_line( - line=self.components.p4_port1_remote_port1_line, - line_properties=snapshot.p4_port1_remote_port1_line, - port_1_properties=snapshot.p4_port1_remote_primary_line, - port_1_flow=snapshot.p4_port1_remote_to_port1_line_flow, - port_2_properties=snapshot.p4_port1_far_primary_line, - port_2_flow=snapshot.p4_port1_far_node_to_line_flow, - ) - - self._write_primary_chamber_line( - line=self.components.p4_port1_far_primary_line, - line_properties=snapshot.p4_port1_far_primary_line, - chamber=self.components.p4_port1_far_primary_chamber, - chamber_properties=snapshot.p4_port1_far_primary_chamber, - chamber_to_line_flow=snapshot.p4_port1_far_chamber_to_line_flow, - node_to_line_flow=snapshot.p4_port1_far_node_to_primary_line_flow, - ) - - self._write_connection_line( - line=self.components.p4_port1_far_port1_line, - line_properties=snapshot.p4_port1_far_port1_line, - port_1_properties=snapshot.p4_port1_far_primary_line, - port_1_flow=snapshot.p4_port1_far_to_next_line_flow, - port_2_properties=snapshot.p4_port1_next_primary_line, - port_2_flow=snapshot.p4_port1_next_node_to_line_flow, - ) - - self._write_primary_chamber_line( - line=self.components.p4_port1_next_primary_line, - line_properties=snapshot.p4_port1_next_primary_line, - chamber=self.components.p4_port1_next_primary_chamber, - chamber_properties=snapshot.p4_port1_next_primary_chamber, - chamber_to_line_flow=snapshot.p4_port1_next_chamber_to_line_flow, - node_to_line_flow=snapshot.p4_port1_next_node_to_primary_line_flow, - ) - - self._write_connection_line( - line=self.components.p4_port1_next_port1_line, - line_properties=snapshot.p4_port1_next_port1_line, - port_1_properties=snapshot.p4_port1_next_primary_line, - port_1_flow=snapshot.p4_port1_next_to_bridge_line_flow, - port_2_properties=snapshot.p4_bridge_primary_line, - port_2_flow=snapshot.p4_bridge_to_port1_next_line_flow, - ) - - self._write_two_state_connection_line( - line=self.components.p4_port1_next_orifice_line, - port_1_properties=snapshot.p4_port1_next_orifice_line_port_1, - port_1_flow=snapshot.p4_port1_next_orifice_node_to_line_flow, - port_2_properties=snapshot.p4_port1_next_orifice_line_port_2, - port_2_flow=-snapshot.p4_port1_next_orifice_to_output_line_flow, - ) - - self._write_orifice_ports( - orifice=self.components.p4_port1_next_orifice, - port_a_properties=snapshot.p4_port1_next_orifice_line_port_2, - port_a_flow=snapshot.p4_port1_next_orifice_to_output_line_flow, - port_b_properties=snapshot.p4_port1_next_orifice_output_line, - port_b_flow=-snapshot.p4_port1_next_orifice_to_output_line_flow, - ) - - self._write_connection_line( - line=self.components.p4_port1_next_orifice_output_line, - line_properties=snapshot.p4_port1_next_orifice_output_line, - port_1_properties=snapshot.p4_port1_next_primary_line, - port_1_flow=snapshot.p4_port1_next_orifice_p4_to_output_line_flow, - port_2_properties=snapshot.p4_port1_next_orifice_output_line, - port_2_flow=snapshot.p4_port1_next_orifice_to_output_line_flow, - ) - - self._write_resistance_ports( - resistance=self.components.p4_port1_next_orifice_resistance, - port_1_properties=snapshot.p4_port1_next_orifice_resistance_line_port_1, - port_1_flow=( - snapshot.p4_port1_next_orifice_resistance_node_to_next_flow - ), - port_2_properties=snapshot.p4_port1_next_orifice_line_port_1, - port_2_flow=( - -snapshot.p4_port1_next_orifice_resistance_node_to_next_flow - ), - ) - - self._write_two_state_connection_line( - line=self.components.p4_port1_next_orifice_resistance_line, - port_1_properties=snapshot.p4_port1_next_orifice_resistance_line_port_1, - port_1_flow=snapshot.p4_port1_next_orifice_resistance_node_to_line_flow, - port_2_properties=snapshot.p4_port1_next_orifice_resistance_line_port_2, - port_2_flow=-snapshot.p4_port1_next_orifice_resistance_to_p4_flow, - ) - - self._write_orifice_ports( - orifice=self.components.p4_port1_next_orifice_resistance_orifice, - port_a_properties=snapshot.p4_port1_next_orifice_resistance_line_port_2, - port_a_flow=snapshot.p4_port1_next_orifice_resistance_to_p4_flow, - port_b_properties=snapshot.p4_port1_far_primary_line, - port_b_flow=-snapshot.p4_port1_next_orifice_resistance_to_p4_flow, - ) - - self._write_fixed_chamber_segment( - spec=self.components.p4_port1_next_orifice_resistance_chamber_spec, - chamber=self.components.p4_port1_next_orifice_resistance_chamber, - chamber_properties=snapshot.p4_port1_next_orifice_resistance_chamber, - inlet_line=self.components.p4_port1_next_orifice_resistance_chamber_inlet_line, - inlet_line_properties=( - snapshot.p4_port1_next_orifice_resistance_chamber_inlet_line - ), - inlet_node_properties=snapshot.p4_port1_remote_orifice_line_port_1, - inlet_node_to_line_flow=( - snapshot.p4_port1_next_orifice_resistance_chamber_inlet_node_to_line_flow - ), - inlet_line_to_chamber_flow=( - snapshot.p4_port1_next_orifice_resistance_chamber_inlet_line_to_chamber_flow - ), - inlet_orifice=( - self.components.p4_port1_next_orifice_resistance_chamber_inlet_orifice - ), - outlet_line=( - self.components.p4_port1_next_orifice_resistance_chamber_outlet_line - ), - outlet_line_properties=( - snapshot.p4_port1_next_orifice_resistance_chamber_outlet_line - ), - outlet_node_properties=( - snapshot.p4_port1_next_orifice_resistance_line_port_1 - ), - outlet_node_to_line_flow=( - snapshot.p4_port1_next_orifice_resistance_chamber_outlet_node_to_line_flow - ), - outlet_line_to_chamber_flow=( - snapshot.p4_port1_next_orifice_resistance_chamber_outlet_line_to_chamber_flow - ), - outlet_orifice=( - self.components.p4_port1_next_orifice_resistance_chamber_outlet_orifice - ), - ) - - self._write_primary_chamber_line( - line=self.components.p4_bridge_primary_line, - line_properties=snapshot.p4_bridge_primary_line, - chamber=self.components.p4_bridge_primary_chamber, - chamber_properties=snapshot.p4_bridge_primary_chamber, - chamber_to_line_flow=snapshot.p4_bridge_chamber_to_line_flow, - node_to_line_flow=snapshot.p4_bridge_node_to_primary_line_flow, - ) - - self._write_two_state_connection_line( - line=self.components.p4_port1_remote_orifice_line, - port_1_properties=snapshot.p4_port1_remote_orifice_line_port_1, - port_1_flow=snapshot.p4_port1_remote_node_to_orifice_line_flow, - port_2_properties=snapshot.p4_port1_remote_orifice_line_port_2, - port_2_flow=-snapshot.p4_port1_remote_orifice_to_node_flow, - ) - - self._write_orifice_ports( - orifice=self.components.p4_port1_remote_orifice, - port_a_properties=snapshot.p4_port1_remote_orifice_line_port_2, - port_a_flow=snapshot.p4_port1_remote_orifice_to_node_flow, - port_b_properties=snapshot.p4_port1_remote_primary_line, - port_b_flow=-snapshot.p4_port1_remote_orifice_to_node_flow, - ) - - self._write_connection_line( - line=self.components.p4_port3_line, - line_properties=snapshot.p4_port3_line, - port_1_properties=snapshot.p4_port3_remote_primary_line, - port_1_flow=snapshot.p4_port3_remote_node_to_line_flow, - port_2_properties=snapshot.p4_primary_line, - port_2_flow=snapshot.p4_to_port3_line_flow, - ) - - self._write_primary_chamber_line( - line=self.components.p4_port3_remote_primary_line, - line_properties=snapshot.p4_port3_remote_primary_line, - chamber=self.components.p4_port3_remote_primary_chamber, - chamber_properties=snapshot.p4_port3_remote_primary_chamber, - chamber_to_line_flow=snapshot.p4_port3_remote_chamber_to_line_flow, - node_to_line_flow=snapshot.p4_port3_remote_node_to_primary_line_flow, - ) - - self._write_connection_line( - line=self.components.p4_port3_remote_port3_line, - line_properties=snapshot.p4_port3_remote_port3_line, - port_1_properties=snapshot.p4_port3_far_primary_line, - port_1_flow=snapshot.p4_port3_far_node_to_line_flow, - port_2_properties=snapshot.p4_port3_remote_primary_line, - port_2_flow=snapshot.p4_port3_remote_to_port3_line_flow, - ) - - self._write_primary_chamber_line( - line=self.components.p4_port3_far_primary_line, - line_properties=snapshot.p4_port3_far_primary_line, - chamber=self.components.p4_port3_far_primary_chamber, - chamber_properties=snapshot.p4_port3_far_primary_chamber, - chamber_to_line_flow=snapshot.p4_port3_far_chamber_to_line_flow, - node_to_line_flow=snapshot.p4_port3_far_node_to_primary_line_flow, - ) - - self._write_connection_line( - line=self.components.p4_port3_far_port3_line, - line_properties=snapshot.p4_port3_far_port3_line, - port_1_properties=snapshot.p4_port3_next_primary_line, - port_1_flow=snapshot.p4_port3_next_node_to_line_flow, - port_2_properties=snapshot.p4_port3_far_primary_line, - port_2_flow=snapshot.p4_port3_far_to_next_line_flow, - ) - - self._write_primary_chamber_line( - line=self.components.p4_port3_next_primary_line, - line_properties=snapshot.p4_port3_next_primary_line, - chamber=self.components.p4_port3_next_primary_chamber, - chamber_properties=snapshot.p4_port3_next_primary_chamber, - chamber_to_line_flow=snapshot.p4_port3_next_chamber_to_line_flow, - node_to_line_flow=snapshot.p4_port3_next_node_to_primary_line_flow, - ) - - self._write_connection_line( - line=self.components.p4_port3_next_port3_line, - line_properties=snapshot.p4_port3_next_port3_line, - port_1_properties=snapshot.p4_bridge_primary_line, - port_1_flow=snapshot.p4_bridge_to_port3_next_line_flow, - port_2_properties=snapshot.p4_port3_next_primary_line, - port_2_flow=snapshot.p4_port3_next_to_bridge_line_flow, - ) - - self._write_two_state_connection_line( - line=self.components.p4_port3_next_orifice_line, - port_1_properties=snapshot.p4_port3_next_orifice_line_port_1, - port_1_flow=snapshot.p4_port3_next_orifice_node_to_line_flow, - port_2_properties=snapshot.p4_port3_next_orifice_line_port_2, - port_2_flow=-snapshot.p4_port3_next_orifice_to_output_line_flow, - ) - - self._write_orifice_ports( - orifice=self.components.p4_port3_next_orifice, - port_a_properties=snapshot.p4_port3_next_orifice_line_port_2, - port_a_flow=snapshot.p4_port3_next_orifice_to_output_line_flow, - port_b_properties=snapshot.p4_port3_next_orifice_output_line, - port_b_flow=-snapshot.p4_port3_next_orifice_to_output_line_flow, - ) - - self._write_connection_line( - line=self.components.p4_port3_next_orifice_output_line, - line_properties=snapshot.p4_port3_next_orifice_output_line, - port_1_properties=snapshot.p4_port3_next_orifice_line_port_2, - port_1_flow=snapshot.p4_port3_next_orifice_to_output_line_flow, - port_2_properties=snapshot.p4_port3_next_orifice_output_line, - port_2_flow=-snapshot.p4_port3_next_orifice_output_to_p4_flow, - ) - - self._write_fixed_chamber_segment( - spec=self.components.p4_port3_next_chamber_spec, - chamber=self.components.p4_port3_next_chamber, - chamber_properties=snapshot.p4_port3_next_chamber, - inlet_line=self.components.p4_port3_next_chamber_inlet_line, - inlet_line_properties=snapshot.p4_port3_next_chamber_inlet_line, - inlet_node_properties=snapshot.p4_port3_next_orifice_line_port_1, - inlet_node_to_line_flow=( - snapshot.p4_port3_next_chamber_inlet_node_to_line_flow - ), - inlet_line_to_chamber_flow=( - snapshot.p4_port3_next_chamber_inlet_line_to_chamber_flow - ), - inlet_orifice=self.components.p4_port3_next_chamber_inlet_orifice, - outlet_line=self.components.p4_port3_next_chamber_outlet_line, - outlet_line_properties=snapshot.p4_port3_next_chamber_outlet_line, - outlet_node_properties=snapshot.p4_port3_remote_resistance_node_line_port_1, - outlet_node_to_line_flow=( - snapshot.p4_port3_next_chamber_outlet_node_to_line_flow - ), - outlet_line_to_chamber_flow=( - snapshot.p4_port3_next_chamber_outlet_line_to_chamber_flow - ), - outlet_orifice=self.components.p4_port3_next_chamber_outlet_orifice, - ) - - self._write_resistance_ports( - resistance=self.components.p4_port3_next_orifice_resistance, - port_1_properties=snapshot.p4_port3_next_resistance_node_line_port_1, - port_1_flow=snapshot.p4_port3_next_resistance_node_to_next_flow, - port_2_properties=snapshot.p4_port3_next_orifice_line_port_1, - port_2_flow=-snapshot.p4_port3_next_resistance_node_to_next_flow, - ) - - self._write_two_state_connection_line( - line=self.components.p4_port3_next_resistance_node_line, - port_1_properties=snapshot.p4_port3_next_resistance_node_line_port_1, - port_1_flow=snapshot.p4_port3_next_resistance_node_to_line_flow, - port_2_properties=snapshot.p4_port3_next_resistance_node_line_port_2, - port_2_flow=-snapshot.p4_port3_next_resistance_to_output_line_flow, - ) - - self._write_orifice_ports( - orifice=self.components.p4_port3_next_resistance_orifice, - port_a_properties=snapshot.p4_port3_next_resistance_node_line_port_2, - port_a_flow=snapshot.p4_port3_next_resistance_to_output_line_flow, - port_b_properties=snapshot.p4_port3_next_resistance_output_line, - port_b_flow=-snapshot.p4_port3_next_resistance_to_output_line_flow, - ) - - self._write_connection_line( - line=self.components.p4_port3_next_resistance_output_line, - line_properties=snapshot.p4_port3_next_resistance_output_line, - port_1_properties=snapshot.p4_port3_next_resistance_node_line_port_2, - port_1_flow=snapshot.p4_port3_next_resistance_to_output_line_flow, - port_2_properties=snapshot.p4_port3_next_resistance_output_line, - port_2_flow=-snapshot.p4_port3_next_resistance_output_to_p4_flow, - ) - - self._write_fixed_chamber_segment( - spec=self.components.p4_port3_next_resistance_chamber_spec, - chamber=self.components.p4_port3_next_resistance_chamber, - chamber_properties=snapshot.p4_port3_next_resistance_chamber, - inlet_line=self.components.p4_port3_next_resistance_chamber_inlet_line, - inlet_line_properties=( - snapshot.p4_port3_next_resistance_chamber_inlet_line - ), - inlet_node_properties=snapshot.p4_port1_next_orifice_line_port_1, - inlet_node_to_line_flow=( - snapshot.p4_port3_next_resistance_chamber_inlet_node_to_line_flow - ), - inlet_line_to_chamber_flow=( - snapshot.p4_port3_next_resistance_chamber_inlet_line_to_chamber_flow - ), - inlet_orifice=( - self.components.p4_port3_next_resistance_chamber_inlet_orifice - ), - outlet_line=self.components.p4_port3_next_resistance_chamber_outlet_line, - outlet_line_properties=( - snapshot.p4_port3_next_resistance_chamber_outlet_line - ), - outlet_node_properties=snapshot.p4_port3_next_resistance_node_line_port_1, - outlet_node_to_line_flow=( - snapshot.p4_port3_next_resistance_chamber_outlet_node_to_line_flow - ), - outlet_line_to_chamber_flow=( - snapshot.p4_port3_next_resistance_chamber_outlet_line_to_chamber_flow - ), - outlet_orifice=( - self.components.p4_port3_next_resistance_chamber_outlet_orifice - ), - ) - - self._write_two_state_connection_line( - line=self.components.p4_port3_remote_orifice_line, - port_1_properties=snapshot.p4_port3_remote_orifice_line_port_1, - port_1_flow=snapshot.p4_port3_remote_node_to_orifice_line_flow, - port_2_properties=snapshot.p4_port3_remote_orifice_line_port_2, - port_2_flow=-snapshot.p4_port3_remote_orifice_to_node_flow, - ) - - self._write_resistance_ports( - resistance=self.components.p4_port3_remote_orifice_resistance, - port_1_properties=snapshot.p4_port3_remote_orifice_line_port_1, - port_1_flow=snapshot.p4_port3_remote_node_to_resistance_flow, - port_2_properties=snapshot.p4_port3_remote_resistance_node_line_port_1, - port_2_flow=-snapshot.p4_port3_remote_node_to_resistance_flow, - ) - - self._write_two_state_connection_line( - line=self.components.p4_port3_remote_resistance_node_line, - port_1_properties=snapshot.p4_port3_remote_resistance_node_line_port_1, - port_1_flow=snapshot.p4_port3_remote_resistance_node_to_line_flow, - port_2_properties=snapshot.p4_port3_remote_resistance_node_line_port_2, - port_2_flow=-snapshot.p4_port3_remote_resistance_to_output_line_flow, - ) - - self._write_orifice_ports( - orifice=self.components.p4_port3_remote_resistance_orifice, - port_a_properties=snapshot.p4_port3_remote_resistance_node_line_port_2, - port_a_flow=snapshot.p4_port3_remote_resistance_to_output_line_flow, - port_b_properties=snapshot.p4_port3_remote_resistance_output_line, - port_b_flow=-snapshot.p4_port3_remote_resistance_to_output_line_flow, - ) - - self._write_connection_line( - line=self.components.p4_port3_remote_resistance_output_line, - line_properties=snapshot.p4_port3_remote_resistance_output_line, - port_1_properties=snapshot.p4_port3_remote_resistance_node_line_port_2, - port_1_flow=snapshot.p4_port3_remote_resistance_to_output_line_flow, - port_2_properties=snapshot.p4_port3_remote_resistance_output_line, - port_2_flow=-snapshot.p4_port3_remote_resistance_output_to_p4_flow, - ) - - self._write_orifice_ports( - orifice=self.components.p4_port3_remote_orifice, - port_a_properties=snapshot.p4_port3_remote_orifice_line_port_2, - port_a_flow=snapshot.p4_port3_remote_orifice_to_node_flow, - port_b_properties=snapshot.p4_port3_remote_primary_line, - port_b_flow=-snapshot.p4_port3_remote_orifice_to_node_flow, - ) - - def rhs(self, state_vector: list[float]) -> list[float]: - snapshot = self.snapshot(state_vector) - ( - inlet_derivative, - chamber_derivative, - outlet_derivative, - ) = self._fixed_chamber_segment_derivatives( - spec=self.components.spec, - chamber=self.components.volume, - chamber_properties=snapshot.chamber, - inlet_line=self.components.inlet_line, - inlet_line_properties=snapshot.inlet_line, - inlet_connected_properties=snapshot.p4_port3_remote_orifice_line_port_1, - inlet_node_to_line_flow=snapshot.inlet_node_to_line_flow, - inlet_line_to_chamber_flow=snapshot.inlet_line_to_chamber_flow, - outlet_line=self.components.outlet_line, - outlet_line_properties=snapshot.outlet_line, - outlet_connected_properties=snapshot.node_line_port_1, - outlet_node_to_line_flow=snapshot.outlet_node_to_line_flow, - outlet_line_to_chamber_flow=snapshot.outlet_line_to_chamber_flow, - ) - if self.use_inlet_line_reference_rhs: - inlet_derivative = self._pnl0001_reference_enthalpy_derivative( - line=self.components.inlet_line, - line_properties=snapshot.inlet_line, - port_1_properties=snapshot.p4_port3_remote_orifice_line_port_1, - port_1_flow=snapshot.inlet_node_to_line_flow, - port_2_properties=snapshot.chamber, - port_2_flow=-snapshot.inlet_line_to_chamber_flow, - ) - ( - node_line_derivative_1, - node_line_derivative_2, - ) = self._two_state_connection_line_derivatives( - line=self.components.node_line, - port_1_properties=snapshot.node_line_port_1, - port_1_flow=snapshot.node_to_pnl0003_flow, - port_2_connected_properties=snapshot.p4_primary_line, - port_2_flow=-snapshot.pnl0003_to_p4_flow, - ) - ( - p4_primary_derivative, - p4_primary_chamber_derivative, - ) = self._primary_chamber_line_derivatives( - line=self.components.p4_primary_line, - line_properties=snapshot.p4_primary_line, - chamber=self.components.p4_primary_chamber, - chamber_properties=snapshot.p4_primary_chamber, - chamber_to_line_flow=snapshot.p4_primary_chamber_to_line_flow, - node_to_line_flow=snapshot.p4_node_to_primary_line_flow, - ) - p4_port1_derivative = self._connection_line_derivative( - line=self.components.p4_port1_line, - port_1_properties=snapshot.p4_primary_line, - port_1_flow=snapshot.p4_to_port1_line_flow, - port_2_properties=snapshot.p4_port1_remote_primary_line, - port_2_flow=snapshot.p4_port1_remote_node_to_line_flow, - ) - ( - p4_port1_remote_primary_derivative, - p4_port1_remote_chamber_derivative, - ) = self._primary_chamber_line_derivatives( - line=self.components.p4_port1_remote_primary_line, - line_properties=snapshot.p4_port1_remote_primary_line, - chamber=self.components.p4_port1_remote_primary_chamber, - chamber_properties=snapshot.p4_port1_remote_primary_chamber, - chamber_to_line_flow=snapshot.p4_port1_remote_chamber_to_line_flow, - node_to_line_flow=snapshot.p4_port1_remote_node_to_primary_line_flow, - ) - p4_port1_remote_port1_derivative = self._connection_line_derivative( - line=self.components.p4_port1_remote_port1_line, - port_1_properties=snapshot.p4_port1_remote_primary_line, - port_1_flow=snapshot.p4_port1_remote_to_port1_line_flow, - port_2_properties=snapshot.p4_port1_far_primary_line, - port_2_flow=snapshot.p4_port1_far_node_to_line_flow, - ) - ( - p4_port1_far_primary_derivative, - p4_port1_far_chamber_derivative, - ) = self._primary_chamber_line_derivatives( - line=self.components.p4_port1_far_primary_line, - line_properties=snapshot.p4_port1_far_primary_line, - chamber=self.components.p4_port1_far_primary_chamber, - chamber_properties=snapshot.p4_port1_far_primary_chamber, - chamber_to_line_flow=snapshot.p4_port1_far_chamber_to_line_flow, - node_to_line_flow=snapshot.p4_port1_far_node_to_primary_line_flow, - ) - p4_port1_far_port1_derivative = self._connection_line_derivative( - line=self.components.p4_port1_far_port1_line, - port_1_properties=snapshot.p4_port1_far_primary_line, - port_1_flow=snapshot.p4_port1_far_to_next_line_flow, - port_2_properties=snapshot.p4_port1_next_primary_line, - port_2_flow=snapshot.p4_port1_next_node_to_line_flow, - ) - ( - p4_port1_next_primary_derivative, - p4_port1_next_chamber_derivative, - ) = self._primary_chamber_line_derivatives( - line=self.components.p4_port1_next_primary_line, - line_properties=snapshot.p4_port1_next_primary_line, - chamber=self.components.p4_port1_next_primary_chamber, - chamber_properties=snapshot.p4_port1_next_primary_chamber, - chamber_to_line_flow=snapshot.p4_port1_next_chamber_to_line_flow, - node_to_line_flow=snapshot.p4_port1_next_node_to_primary_line_flow, - ) - p4_port1_next_port1_derivative = self._connection_line_derivative( - line=self.components.p4_port1_next_port1_line, - port_1_properties=snapshot.p4_port1_next_primary_line, - port_1_flow=snapshot.p4_port1_next_to_bridge_line_flow, - port_2_properties=snapshot.p4_bridge_primary_line, - port_2_flow=snapshot.p4_bridge_to_port1_next_line_flow, - ) - ( - p4_bridge_primary_derivative, - p4_bridge_chamber_derivative, - ) = self._primary_chamber_line_derivatives( - line=self.components.p4_bridge_primary_line, - line_properties=snapshot.p4_bridge_primary_line, - chamber=self.components.p4_bridge_primary_chamber, - chamber_properties=snapshot.p4_bridge_primary_chamber, - chamber_to_line_flow=snapshot.p4_bridge_chamber_to_line_flow, - node_to_line_flow=snapshot.p4_bridge_node_to_primary_line_flow, - ) - ( - p4_port1_remote_orifice_derivative_1, - p4_port1_remote_orifice_derivative_2, - ) = self._two_state_connection_line_derivatives( - line=self.components.p4_port1_remote_orifice_line, - port_1_properties=snapshot.p4_port1_remote_orifice_line_port_1, - port_1_flow=snapshot.p4_port1_remote_node_to_orifice_line_flow, - port_2_connected_properties=snapshot.p4_port1_remote_primary_line, - port_2_flow=-snapshot.p4_port1_remote_orifice_to_node_flow, - ) - p4_port3_derivative = self._connection_line_derivative( - line=self.components.p4_port3_line, - port_1_properties=snapshot.p4_port3_remote_primary_line, - port_1_flow=snapshot.p4_port3_remote_node_to_line_flow, - port_2_properties=snapshot.p4_primary_line, - port_2_flow=snapshot.p4_to_port3_line_flow, - ) - p4_port3_remote_node_connected_h = self._p4_node_port_2_connected_h( - balance=snapshot.p4_port3_remote_balance, - fallback_h=snapshot.p4_port3_remote_primary_line.h, - ) - ( - p4_port3_remote_primary_derivative, - p4_port3_remote_chamber_derivative, - ) = self._primary_chamber_line_derivatives( - line=self.components.p4_port3_remote_primary_line, - line_properties=snapshot.p4_port3_remote_primary_line, - chamber=self.components.p4_port3_remote_primary_chamber, - chamber_properties=snapshot.p4_port3_remote_primary_chamber, - chamber_to_line_flow=snapshot.p4_port3_remote_chamber_to_line_flow, - node_to_line_flow=snapshot.p4_port3_remote_node_to_primary_line_flow, - node_connected_h=p4_port3_remote_node_connected_h, - ) - if self.use_p4_port3_remote_primary_reference_rhs: - p4_port3_remote_primary_derivative = ( - self._pnl0001_reference_enthalpy_derivative( - line=self.components.p4_port3_remote_primary_line, - line_properties=snapshot.p4_port3_remote_primary_line, - port_1_properties=snapshot.p4_port3_remote_primary_chamber, - port_1_flow=snapshot.p4_port3_remote_chamber_to_line_flow, - port_2_properties=snapshot.p4_port3_remote_primary_line, - port_2_flow=snapshot.p4_port3_remote_node_to_primary_line_flow, - port_2_connected_reference_h=( - self._p4_port3_remote_port_2_reference_h( - snapshot=snapshot - ) - ), - ) - ) - p4_port3_remote_port3_derivative = self._connection_line_derivative( - line=self.components.p4_port3_remote_port3_line, - port_1_properties=snapshot.p4_port3_far_primary_line, - port_1_flow=snapshot.p4_port3_far_node_to_line_flow, - port_2_properties=snapshot.p4_port3_remote_primary_line, - port_2_flow=snapshot.p4_port3_remote_to_port3_line_flow, - ) - ( - p4_port3_far_primary_derivative, - p4_port3_far_chamber_derivative, - ) = self._primary_chamber_line_derivatives( - line=self.components.p4_port3_far_primary_line, - line_properties=snapshot.p4_port3_far_primary_line, - chamber=self.components.p4_port3_far_primary_chamber, - chamber_properties=snapshot.p4_port3_far_primary_chamber, - chamber_to_line_flow=snapshot.p4_port3_far_chamber_to_line_flow, - node_to_line_flow=snapshot.p4_port3_far_node_to_primary_line_flow, - ) - p4_port3_far_port3_derivative = self._connection_line_derivative( - line=self.components.p4_port3_far_port3_line, - port_1_properties=snapshot.p4_port3_next_primary_line, - port_1_flow=snapshot.p4_port3_next_node_to_line_flow, - port_2_properties=snapshot.p4_port3_far_primary_line, - port_2_flow=snapshot.p4_port3_far_to_next_line_flow, - ) - ( - p4_port3_next_primary_derivative, - p4_port3_next_chamber_derivative, - ) = self._primary_chamber_line_derivatives( - line=self.components.p4_port3_next_primary_line, - line_properties=snapshot.p4_port3_next_primary_line, - chamber=self.components.p4_port3_next_primary_chamber, - chamber_properties=snapshot.p4_port3_next_primary_chamber, - chamber_to_line_flow=snapshot.p4_port3_next_chamber_to_line_flow, - node_to_line_flow=snapshot.p4_port3_next_node_to_primary_line_flow, - ) - p4_port3_next_port3_derivative = self._connection_line_derivative( - line=self.components.p4_port3_next_port3_line, - port_1_properties=snapshot.p4_bridge_primary_line, - port_1_flow=snapshot.p4_bridge_to_port3_next_line_flow, - port_2_properties=snapshot.p4_port3_next_primary_line, - port_2_flow=snapshot.p4_port3_next_to_bridge_line_flow, - ) - ( - p4_port3_remote_resistance_derivative_1, - p4_port3_remote_resistance_derivative_2, - ) = self._two_state_connection_line_derivatives( - line=self.components.p4_port3_remote_resistance_node_line, - port_1_properties=snapshot.p4_port3_remote_resistance_node_line_port_1, - port_1_flow=snapshot.p4_port3_remote_resistance_node_to_line_flow, - port_2_connected_properties=snapshot.p4_port3_remote_resistance_output_line, - port_2_flow=-snapshot.p4_port3_remote_resistance_to_output_line_flow, - ) - p4_port3_remote_resistance_output_derivative = self._connection_line_derivative( - line=self.components.p4_port3_remote_resistance_output_line, - port_1_properties=snapshot.p4_port3_remote_resistance_node_line_port_2, - port_1_flow=snapshot.p4_port3_remote_resistance_to_output_line_flow, - port_2_properties=snapshot.p4_port3_far_primary_line, - port_2_flow=-snapshot.p4_port3_remote_resistance_output_to_p4_flow, - ) - ( - p4_port3_remote_orifice_derivative_1, - p4_port3_remote_orifice_derivative_2, - ) = self._two_state_connection_line_derivatives( - line=self.components.p4_port3_remote_orifice_line, - port_1_properties=snapshot.p4_port3_remote_orifice_line_port_1, - port_1_flow=snapshot.p4_port3_remote_node_to_orifice_line_flow, - port_2_connected_properties=snapshot.p4_port3_remote_primary_line, - port_2_flow=-snapshot.p4_port3_remote_orifice_to_node_flow, - port_1_connected_h=self._pn3_node_port_2_connected_h( - balance=snapshot.p4_port3_remote_orifice_node_balance, - fallback_h=snapshot.p4_port3_remote_orifice_line_port_1.h, - ), - ) - ( - p4_port3_next_orifice_derivative_1, - p4_port3_next_orifice_derivative_2, - ) = self._two_state_connection_line_derivatives( - line=self.components.p4_port3_next_orifice_line, - port_1_properties=snapshot.p4_port3_next_orifice_line_port_1, - port_1_flow=snapshot.p4_port3_next_orifice_node_to_line_flow, - port_2_connected_properties=snapshot.p4_port3_next_orifice_output_line, - port_2_flow=-snapshot.p4_port3_next_orifice_to_output_line_flow, - ) - p4_port3_next_orifice_output_derivative = self._connection_line_derivative( - line=self.components.p4_port3_next_orifice_output_line, - port_1_properties=snapshot.p4_port3_next_orifice_line_port_2, - port_1_flow=snapshot.p4_port3_next_orifice_to_output_line_flow, - port_2_properties=snapshot.p4_port3_next_primary_line, - port_2_flow=-snapshot.p4_port3_next_orifice_output_to_p4_flow, - ) - ( - p4_port3_next_chamber_inlet_derivative, - p4_port3_next_fixed_chamber_derivative, - p4_port3_next_chamber_outlet_derivative, - ) = self._fixed_chamber_segment_derivatives( - spec=self.components.p4_port3_next_chamber_spec, - chamber=self.components.p4_port3_next_chamber, - chamber_properties=snapshot.p4_port3_next_chamber, - inlet_line=self.components.p4_port3_next_chamber_inlet_line, - inlet_line_properties=snapshot.p4_port3_next_chamber_inlet_line, - inlet_connected_properties=snapshot.p4_port3_next_orifice_line_port_1, - inlet_node_to_line_flow=( - snapshot.p4_port3_next_chamber_inlet_node_to_line_flow - ), - inlet_line_to_chamber_flow=( - snapshot.p4_port3_next_chamber_inlet_line_to_chamber_flow - ), - outlet_line=self.components.p4_port3_next_chamber_outlet_line, - outlet_line_properties=snapshot.p4_port3_next_chamber_outlet_line, - outlet_connected_properties=( - snapshot.p4_port3_remote_resistance_node_line_port_1 - ), - outlet_node_to_line_flow=( - snapshot.p4_port3_next_chamber_outlet_node_to_line_flow - ), - outlet_line_to_chamber_flow=( - snapshot.p4_port3_next_chamber_outlet_line_to_chamber_flow - ), - ) - ( - p4_port3_next_resistance_derivative_1, - p4_port3_next_resistance_derivative_2, - ) = self._two_state_connection_line_derivatives( - line=self.components.p4_port3_next_resistance_node_line, - port_1_properties=snapshot.p4_port3_next_resistance_node_line_port_1, - port_1_flow=snapshot.p4_port3_next_resistance_node_to_line_flow, - port_2_connected_properties=snapshot.p4_port3_next_resistance_output_line, - port_2_flow=-snapshot.p4_port3_next_resistance_to_output_line_flow, - ) - p4_port3_next_resistance_output_derivative = self._connection_line_derivative( - line=self.components.p4_port3_next_resistance_output_line, - port_1_properties=snapshot.p4_port3_next_resistance_node_line_port_2, - port_1_flow=snapshot.p4_port3_next_resistance_to_output_line_flow, - port_2_properties=snapshot.p4_bridge_primary_line, - port_2_flow=-snapshot.p4_port3_next_resistance_output_to_p4_flow, - ) - ( - p4_port3_next_resistance_chamber_inlet_derivative, - p4_port3_next_resistance_fixed_chamber_derivative, - p4_port3_next_resistance_chamber_outlet_derivative, - ) = self._fixed_chamber_segment_derivatives( - spec=self.components.p4_port3_next_resistance_chamber_spec, - chamber=self.components.p4_port3_next_resistance_chamber, - chamber_properties=snapshot.p4_port3_next_resistance_chamber, - inlet_line=self.components.p4_port3_next_resistance_chamber_inlet_line, - inlet_line_properties=( - snapshot.p4_port3_next_resistance_chamber_inlet_line - ), - inlet_connected_properties=snapshot.p4_port1_next_orifice_line_port_1, - inlet_node_to_line_flow=( - snapshot.p4_port3_next_resistance_chamber_inlet_node_to_line_flow - ), - inlet_line_to_chamber_flow=( - snapshot.p4_port3_next_resistance_chamber_inlet_line_to_chamber_flow - ), - outlet_line=self.components.p4_port3_next_resistance_chamber_outlet_line, - outlet_line_properties=( - snapshot.p4_port3_next_resistance_chamber_outlet_line - ), - outlet_connected_properties=snapshot.p4_port3_next_resistance_node_line_port_1, - outlet_node_to_line_flow=( - snapshot.p4_port3_next_resistance_chamber_outlet_node_to_line_flow - ), - outlet_line_to_chamber_flow=( - snapshot.p4_port3_next_resistance_chamber_outlet_line_to_chamber_flow - ), - ) - ( - p4_port1_next_orifice_derivative_1, - p4_port1_next_orifice_derivative_2, - ) = self._two_state_connection_line_derivatives( - line=self.components.p4_port1_next_orifice_line, - port_1_properties=snapshot.p4_port1_next_orifice_line_port_1, - port_1_flow=snapshot.p4_port1_next_orifice_node_to_line_flow, - port_2_connected_properties=snapshot.p4_port1_next_orifice_output_line, - port_2_flow=-snapshot.p4_port1_next_orifice_to_output_line_flow, - ) - p4_port1_next_orifice_output_derivative = self._connection_line_derivative( - line=self.components.p4_port1_next_orifice_output_line, - port_1_properties=snapshot.p4_port1_next_primary_line, - port_1_flow=snapshot.p4_port1_next_orifice_p4_to_output_line_flow, - port_2_properties=snapshot.p4_port1_next_orifice_line_port_2, - port_2_flow=snapshot.p4_port1_next_orifice_to_output_line_flow, - ) - ( - p4_port1_next_orifice_resistance_derivative_1, - p4_port1_next_orifice_resistance_derivative_2, - ) = self._two_state_connection_line_derivatives( - line=self.components.p4_port1_next_orifice_resistance_line, - port_1_properties=snapshot.p4_port1_next_orifice_resistance_line_port_1, - port_1_flow=snapshot.p4_port1_next_orifice_resistance_node_to_line_flow, - port_2_connected_properties=snapshot.p4_port1_far_primary_line, - port_2_flow=-snapshot.p4_port1_next_orifice_resistance_to_p4_flow, - ) - ( - p4_port1_next_orifice_resistance_chamber_inlet_derivative, - p4_port1_next_orifice_resistance_fixed_chamber_derivative, - p4_port1_next_orifice_resistance_chamber_outlet_derivative, - ) = self._fixed_chamber_segment_derivatives( - spec=self.components.p4_port1_next_orifice_resistance_chamber_spec, - chamber=self.components.p4_port1_next_orifice_resistance_chamber, - chamber_properties=snapshot.p4_port1_next_orifice_resistance_chamber, - inlet_line=( - self.components.p4_port1_next_orifice_resistance_chamber_inlet_line - ), - inlet_line_properties=( - snapshot.p4_port1_next_orifice_resistance_chamber_inlet_line - ), - inlet_connected_properties=snapshot.p4_port1_remote_orifice_line_port_1, - inlet_node_to_line_flow=( - snapshot.p4_port1_next_orifice_resistance_chamber_inlet_node_to_line_flow - ), - inlet_line_to_chamber_flow=( - snapshot.p4_port1_next_orifice_resistance_chamber_inlet_line_to_chamber_flow - ), - outlet_line=( - self.components.p4_port1_next_orifice_resistance_chamber_outlet_line - ), - outlet_line_properties=( - snapshot.p4_port1_next_orifice_resistance_chamber_outlet_line - ), - outlet_connected_properties=( - snapshot.p4_port1_next_orifice_resistance_line_port_1 - ), - outlet_node_to_line_flow=( - snapshot.p4_port1_next_orifice_resistance_chamber_outlet_node_to_line_flow - ), - outlet_line_to_chamber_flow=( - snapshot.p4_port1_next_orifice_resistance_chamber_outlet_line_to_chamber_flow - ), - ) - return [ - *inlet_derivative.as_vector(), - *chamber_derivative.as_vector(), - *outlet_derivative.as_vector(), - *node_line_derivative_1.as_vector(), - *node_line_derivative_2.as_vector(), - *p4_primary_derivative.as_vector(), - *p4_primary_chamber_derivative.as_vector(), - *p4_port1_derivative.as_vector(), - *p4_port1_remote_primary_derivative.as_vector(), - *p4_port1_remote_chamber_derivative.as_vector(), - *p4_port1_remote_port1_derivative.as_vector(), - *p4_port1_far_primary_derivative.as_vector(), - *p4_port1_far_chamber_derivative.as_vector(), - *p4_port1_far_port1_derivative.as_vector(), - *p4_port1_next_primary_derivative.as_vector(), - *p4_port1_next_chamber_derivative.as_vector(), - *p4_port1_next_port1_derivative.as_vector(), - *p4_bridge_primary_derivative.as_vector(), - *p4_bridge_chamber_derivative.as_vector(), - *p4_port1_remote_orifice_derivative_1.as_vector(), - *p4_port1_remote_orifice_derivative_2.as_vector(), - *p4_port3_derivative.as_vector(), - *p4_port3_remote_primary_derivative.as_vector(), - *p4_port3_remote_chamber_derivative.as_vector(), - *p4_port3_remote_port3_derivative.as_vector(), - *p4_port3_far_primary_derivative.as_vector(), - *p4_port3_far_chamber_derivative.as_vector(), - *p4_port3_far_port3_derivative.as_vector(), - *p4_port3_next_primary_derivative.as_vector(), - *p4_port3_next_chamber_derivative.as_vector(), - *p4_port3_next_port3_derivative.as_vector(), - *p4_port3_remote_resistance_derivative_1.as_vector(), - *p4_port3_remote_resistance_derivative_2.as_vector(), - *p4_port3_remote_resistance_output_derivative.as_vector(), - *p4_port3_remote_orifice_derivative_1.as_vector(), - *p4_port3_remote_orifice_derivative_2.as_vector(), - *p4_port3_next_orifice_derivative_1.as_vector(), - *p4_port3_next_orifice_derivative_2.as_vector(), - *p4_port3_next_orifice_output_derivative.as_vector(), - *p4_port3_next_chamber_inlet_derivative.as_vector(), - *p4_port3_next_fixed_chamber_derivative.as_vector(), - *p4_port3_next_chamber_outlet_derivative.as_vector(), - *p4_port3_next_resistance_derivative_1.as_vector(), - *p4_port3_next_resistance_derivative_2.as_vector(), - *p4_port3_next_resistance_output_derivative.as_vector(), - *p4_port3_next_resistance_chamber_inlet_derivative.as_vector(), - *p4_port3_next_resistance_fixed_chamber_derivative.as_vector(), - *p4_port3_next_resistance_chamber_outlet_derivative.as_vector(), - *p4_port1_next_orifice_derivative_1.as_vector(), - *p4_port1_next_orifice_derivative_2.as_vector(), - *p4_port1_next_orifice_output_derivative.as_vector(), - *p4_port1_next_orifice_resistance_derivative_1.as_vector(), - *p4_port1_next_orifice_resistance_derivative_2.as_vector(), - *p4_port1_next_orifice_resistance_chamber_inlet_derivative.as_vector(), - *p4_port1_next_orifice_resistance_fixed_chamber_derivative.as_vector(), - *p4_port1_next_orifice_resistance_chamber_outlet_derivative.as_vector(), - ] - - -@dataclass(frozen=True) -class TestMqlPneumaticBranchComponents: - name: str - upstream_volume: AmesimPneumaticVolume - orifice: AmesimPneumaticOrifice - downstream_volume: AmesimPneumaticVolume - spec: TestMqlPneumaticBranchSpec | None = None - - -@dataclass(frozen=True) -class TestMqlPneumaticBranchState: - name: str - upstream: ThermodynamicProperties - downstream: ThermodynamicProperties - flow: float - upstream_inlet_h: float - downstream_inlet_h: float - - -@dataclass(frozen=True) -class TestMqlPneumaticSnapshot: - branch: TestMqlPneumaticBranchState - - @property - def flow(self) -> float: - return self.branch.flow - - @property - def upstream(self) -> ThermodynamicProperties: - return self.branch.upstream - - @property - def downstream(self) -> ThermodynamicProperties: - return self.branch.downstream - - -class TestMqlPneumaticClosure: - """Minimal test_mql pneumatic closure following the existing Testmodel pattern. - - The canonical branch flow is positive from ``upstream_volume`` through the - orifice port_a/port_b into ``downstream_volume``. Port ``m_flow`` values are - written with Modelica-style signs: positive means flow into that component. - """ - - def __init__( - self, - *, - components: TestMqlPneumaticBranchComponents, - initial_state_vector: Callable[[], list[float]], - apply_state_vector: Callable[[list[float]], None], - ) -> None: - self.components = components - self._initial_state_vector = initial_state_vector - self._apply_state_vector = apply_state_vector - - def initial_state_vector(self) -> list[float]: - return self._initial_state_vector() - - def apply_state_vector(self, values: list[float]) -> None: - self._apply_state_vector(values) - - def snapshot( - self, - state_vector: list[float] | None = None, - ) -> TestMqlPneumaticSnapshot: - if state_vector is not None: - self._apply_state_vector(state_vector) - upstream = self.components.upstream_volume.properties() - downstream = self.components.downstream_volume.properties() - flow = self._solve_branch_flow(upstream, downstream) - upstream_inlet_h = self.components.upstream_volume.connection_inlet_enthalpy( - port_m_flow=-flow, - connected_h=downstream.h, - internal_h=upstream.h, - ) - downstream_inlet_h = self.components.downstream_volume.connection_inlet_enthalpy( - port_m_flow=flow, - connected_h=upstream.h, - internal_h=downstream.h, - ) - branch = TestMqlPneumaticBranchState( - name=self.components.name, - upstream=upstream, - downstream=downstream, - flow=flow, - upstream_inlet_h=upstream_inlet_h, - downstream_inlet_h=downstream_inlet_h, - ) - self._write_port_states(branch) - return TestMqlPneumaticSnapshot(branch=branch) - - def _solve_branch_flow( - self, - upstream: ThermodynamicProperties, - downstream: ThermodynamicProperties, - ) -> float: - upstream_temperature = upstream.T if upstream.p >= downstream.p else downstream.T - return self.components.orifice.mass_flow( - upstream.p, - downstream.p, - upstream_temperature, - ) - - def _write_port_states(self, branch: TestMqlPneumaticBranchState) -> None: - upstream_volume = self.components.upstream_volume - downstream_volume = self.components.downstream_volume - orifice = self.components.orifice - - upstream_volume.port_a.p = branch.upstream.p - upstream_volume.port_a.m_flow = -branch.flow - upstream_volume.port_a.h_outflow = branch.upstream.h - - orifice.port_a.p = branch.upstream.p - orifice.port_a.m_flow = branch.flow - orifice.port_a.h_outflow = branch.upstream.h - orifice.port_b.p = branch.downstream.p - orifice.port_b.m_flow = -branch.flow - orifice.port_b.h_outflow = branch.downstream.h - - downstream_volume.port_a.p = branch.downstream.p - downstream_volume.port_a.m_flow = branch.flow - downstream_volume.port_a.h_outflow = branch.downstream.h - - def branch_derivatives( - self, - snapshot: TestMqlPneumaticSnapshot, - ) -> tuple[VolumeState, VolumeState]: - flow = snapshot.flow - upstream_derivative = self.components.upstream_volume.derivatives( - inlet_h=snapshot.branch.upstream_inlet_h, - m_flow=-flow, - ) - downstream_derivative = self.components.downstream_volume.derivatives( - inlet_h=snapshot.branch.downstream_inlet_h, - m_flow=flow, - ) - return upstream_derivative, downstream_derivative - - def rhs(self, state_vector: list[float]) -> list[float]: - snapshot = self.snapshot(state_vector) - upstream_derivative, downstream_derivative = self.branch_derivatives(snapshot) - return [ - *upstream_derivative.as_vector(), - *downstream_derivative.as_vector(), - ] - - -class TestMqlPneumaticChamberSegmentClosure: - """Boundary-reduced fixed chamber segment discovered from AMESim topology. - - The two PNL0001 lines remain prescribed boundary interfaces in this first - closure. Their aliases are retained in the spec so line dynamics can be - inserted later without rediscovering the component path. - """ - - def __init__( - self, - *, - components: TestMqlPneumaticChamberSegmentComponents, - inlet_boundary: TestMqlPneumaticBoundaryCondition, - outlet_boundary: TestMqlPneumaticBoundaryCondition, - ) -> None: - self.components = components - self.inlet_boundary = inlet_boundary - self.outlet_boundary = outlet_boundary - - def initial_state_vector(self) -> list[float]: - return self.components.volume.get_state_vector() - - def apply_state_vector(self, values: list[float]) -> None: - if len(values) != 2: - raise ValueError("single-chamber segment state vector requires two values") - self.components.volume.set_state_vector(values) - - def snapshot( - self, - state_vector: list[float] | None = None, - ) -> TestMqlPneumaticChamberSegmentSnapshot: - if state_vector is not None: - self.apply_state_vector(state_vector) - chamber = self.components.volume.properties() - inlet_boundary = self.inlet_boundary.properties(self.components.volume.gas) - outlet_boundary = self.outlet_boundary.properties(self.components.volume.gas) - inlet_temperature = ( - inlet_boundary.T if inlet_boundary.p >= chamber.p else chamber.T - ) - outlet_temperature = ( - chamber.T if chamber.p >= outlet_boundary.p else outlet_boundary.T - ) - inlet_flow = self.components.inlet_orifice.mass_flow( - inlet_boundary.p, - chamber.p, - inlet_temperature, - ) - outlet_flow = self.components.outlet_orifice.mass_flow( - chamber.p, - outlet_boundary.p, - outlet_temperature, - ) - snapshot = TestMqlPneumaticChamberSegmentSnapshot( - chamber=chamber, - inlet_boundary=inlet_boundary, - outlet_boundary=outlet_boundary, - inlet_flow=inlet_flow, - outlet_flow=outlet_flow, - ) - self._write_port_states(snapshot) - return snapshot - - @staticmethod - def _port( - component: AmesimPneumaticVolume | AmesimPneumaticOrifice, - port_name: str, - ) -> PortState: - if port_name == "port_1": - return component.port_a - if port_name == "port_2": - return component.port_b - raise ValueError(f"unsupported pneumatic port: {port_name}") - - def _write_port_states( - self, - snapshot: TestMqlPneumaticChamberSegmentSnapshot, - ) -> None: - spec = self.components.spec - chamber = self.components.volume - inlet_orifice = self.components.inlet_orifice - outlet_orifice = self.components.outlet_orifice - - inlet_boundary_port = self._port( - inlet_orifice, - spec.inlet_orifice_boundary_port, - ) - inlet_volume_port = self._port(inlet_orifice, spec.inlet_orifice_volume_port) - inlet_boundary_port.p = snapshot.inlet_boundary.p - inlet_boundary_port.m_flow = snapshot.inlet_flow - inlet_boundary_port.h_outflow = snapshot.inlet_boundary.h - inlet_volume_port.p = snapshot.chamber.p - inlet_volume_port.m_flow = -snapshot.inlet_flow - inlet_volume_port.h_outflow = snapshot.chamber.h - - chamber_inlet_port = self._port(chamber, spec.volume_inlet_port) - chamber_outlet_port = self._port(chamber, spec.volume_outlet_port) - chamber_inlet_port.m_flow = snapshot.inlet_flow - chamber_outlet_port.m_flow = -snapshot.outlet_flow - - outlet_volume_port = self._port( - outlet_orifice, - spec.outlet_orifice_volume_port, - ) - outlet_boundary_port = self._port( - outlet_orifice, - spec.outlet_orifice_boundary_port, - ) - outlet_volume_port.p = snapshot.chamber.p - outlet_volume_port.m_flow = snapshot.outlet_flow - outlet_volume_port.h_outflow = snapshot.chamber.h - outlet_boundary_port.p = snapshot.outlet_boundary.p - outlet_boundary_port.m_flow = -snapshot.outlet_flow - outlet_boundary_port.h_outflow = snapshot.outlet_boundary.h - - def rhs(self, state_vector: list[float]) -> list[float]: - snapshot = self.snapshot(state_vector) - spec = self.components.spec - chamber = self.components.volume - derivative = chamber.derivatives_from_two_connections( - port_a_m_flow=self._port(chamber, "port_1").m_flow, - connected_h_a=( - snapshot.inlet_boundary.h - if spec.volume_inlet_port == "port_1" - else snapshot.outlet_boundary.h - ), - port_b_m_flow=self._port(chamber, "port_2").m_flow, - connected_h_b=( - snapshot.inlet_boundary.h - if spec.volume_inlet_port == "port_2" - else snapshot.outlet_boundary.h - ), - internal_h=snapshot.chamber.h, - ) - return derivative.as_vector() - - -class TestMqlPnl0001ChamberSegmentClosure: - """Fixed chamber segment with the topology-derived inlet PNL0001 state. - - The inlet line has a closed causal boundary here: port-1 pressure and - temperature come from the PN3 node boundary, while port-2 flow comes from - the fixed orifice. The outlet line remains a boundary until its three-line - PN3 node balance is assembled. - """ - - def __init__( - self, - *, - components: TestMqlPnl0001ChamberSegmentComponents, - inlet_node: TestMqlPneumaticBoundaryCondition, - outlet_boundary: TestMqlPneumaticBoundaryCondition, - ) -> None: - self.components = components - self.inlet_node = inlet_node - self.outlet_boundary = outlet_boundary - - def initial_state_vector(self) -> list[float]: - return [ - *self.components.inlet_line.get_state_vector(), - *self.components.volume.get_state_vector(), - ] - - def apply_state_vector(self, values: list[float]) -> None: - if len(values) != 4: - raise ValueError("PNL0001/chamber segment state vector requires four values") - self.components.inlet_line.set_state_vector(values[:2]) - self.components.volume.set_state_vector(values[2:]) - - def snapshot( - self, - state_vector: list[float] | None = None, - ) -> TestMqlPnl0001ChamberSegmentSnapshot: - if state_vector is not None: - self.apply_state_vector(state_vector) - line = self.components.inlet_line.properties() - chamber = self.components.volume.properties() - inlet_node = self.inlet_node.properties(self.components.inlet_line.gas) - outlet_boundary = self.outlet_boundary.properties(self.components.volume.gas) - node_to_line_flow = self.components.inlet_line.resistance_mass_flow( - port_1_pressure_pa=inlet_node.p, - port_1_temperature_k=inlet_node.T, - ) - inlet_temperature = line.T if line.p >= chamber.p else chamber.T - line_to_chamber_flow = self.components.inlet_orifice.mass_flow( - line.p, - chamber.p, - inlet_temperature, - ) - outlet_temperature = ( - chamber.T if chamber.p >= outlet_boundary.p else outlet_boundary.T - ) - outlet_flow = self.components.outlet_orifice.mass_flow( - chamber.p, - outlet_boundary.p, - outlet_temperature, - ) - snapshot = TestMqlPnl0001ChamberSegmentSnapshot( - inlet_line=line, - chamber=chamber, - inlet_node=inlet_node, - outlet_boundary=outlet_boundary, - node_to_line_flow=node_to_line_flow, - line_to_chamber_flow=line_to_chamber_flow, - outlet_flow=outlet_flow, - ) - self._write_port_states(snapshot) - return snapshot - - @staticmethod - def _line_to_p4_flow( - *, - line: ThermodynamicProperties, - p4_boundary: ThermodynamicProperties, - orifice: AmesimPneumaticOrifice, - ) -> float: - upstream_temperature = line.T if line.p >= p4_boundary.p else p4_boundary.T - return orifice.mass_flow(line.p, p4_boundary.p, upstream_temperature) - - @staticmethod - def _port( - component: AmesimPneumaticVolume | AmesimPneumaticOrifice, - port_name: str, - ) -> PortState: - return TestMqlPneumaticChamberSegmentClosure._port(component, port_name) - - def _write_port_states( - self, - snapshot: TestMqlPnl0001ChamberSegmentSnapshot, - ) -> None: - spec = self.components.spec - line = self.components.inlet_line - chamber = self.components.volume - inlet_orifice = self.components.inlet_orifice - outlet_orifice = self.components.outlet_orifice - - line.port_1.p = snapshot.inlet_node.p - line.port_1.m_flow = snapshot.node_to_line_flow - line.port_1.h_outflow = snapshot.inlet_line.h - line.port_2.p = snapshot.inlet_line.p - line.port_2.m_flow = -snapshot.line_to_chamber_flow - - inlet_boundary_port = self._port( - inlet_orifice, - spec.inlet_orifice_boundary_port, - ) - inlet_volume_port = self._port(inlet_orifice, spec.inlet_orifice_volume_port) - inlet_boundary_port.p = snapshot.inlet_line.p - inlet_boundary_port.m_flow = snapshot.line_to_chamber_flow - inlet_boundary_port.h_outflow = snapshot.inlet_line.h - inlet_volume_port.p = snapshot.chamber.p - inlet_volume_port.m_flow = -snapshot.line_to_chamber_flow - inlet_volume_port.h_outflow = snapshot.chamber.h - - chamber_inlet_port = self._port(chamber, spec.volume_inlet_port) - chamber_outlet_port = self._port(chamber, spec.volume_outlet_port) - chamber_inlet_port.m_flow = snapshot.line_to_chamber_flow - chamber_outlet_port.m_flow = -snapshot.outlet_flow - - outlet_volume_port = self._port( - outlet_orifice, - spec.outlet_orifice_volume_port, - ) - outlet_boundary_port = self._port( - outlet_orifice, - spec.outlet_orifice_boundary_port, - ) - outlet_volume_port.p = snapshot.chamber.p - outlet_volume_port.m_flow = snapshot.outlet_flow - outlet_volume_port.h_outflow = snapshot.chamber.h - outlet_boundary_port.p = snapshot.outlet_boundary.p - outlet_boundary_port.m_flow = -snapshot.outlet_flow - outlet_boundary_port.h_outflow = snapshot.outlet_boundary.h - - def rhs(self, state_vector: list[float]) -> list[float]: - snapshot = self.snapshot(state_vector) - line_derivative = self.components.inlet_line.derivatives_from_connections( - port_1_m_flow=snapshot.node_to_line_flow, - connected_h_1=snapshot.inlet_node.h, - port_2_m_flow=-snapshot.line_to_chamber_flow, - connected_h_2=snapshot.chamber.h, - ) - chamber = self.components.volume - spec = self.components.spec - chamber_derivative = chamber.derivatives_from_two_connections( - port_a_m_flow=self._port(chamber, "port_1").m_flow, - connected_h_a=( - snapshot.inlet_line.h - if spec.volume_inlet_port == "port_1" - else snapshot.outlet_boundary.h - ), - port_b_m_flow=self._port(chamber, "port_2").m_flow, - connected_h_b=( - snapshot.inlet_line.h - if spec.volume_inlet_port == "port_2" - else snapshot.outlet_boundary.h - ), - internal_h=snapshot.chamber.h, - ) - return [*line_derivative.as_vector(), *chamber_derivative.as_vector()] - - -class TestMqlPnl0001PairChamberSegmentClosure: - """Six-state fixed chamber segment with both PNL0001 compliance states. - - Generated C shows that both line instances receive ``t1/p1`` from their - PN3 nodes and send their explicit ``t2/p2`` states to the fixed orifices. - The reduced closure therefore prescribes node pressure/temperature at both - resistive port-1 boundaries while keeping both line storage states native. - """ - - def __init__( - self, - *, - components: TestMqlPnl0001PairChamberSegmentComponents, - inlet_node: TestMqlPneumaticBoundaryCondition, - outlet_node: TestMqlPneumaticBoundaryCondition, - ) -> None: - self.components = components - self.inlet_node = inlet_node - self.outlet_node = outlet_node - - def initial_state_vector(self) -> list[float]: - return [ - *self.components.inlet_line.get_state_vector(), - *self.components.volume.get_state_vector(), - *self.components.outlet_line.get_state_vector(), - ] - - def apply_state_vector(self, values: list[float]) -> None: - if len(values) != 6: - raise ValueError("PNL0001 pair/chamber state vector requires six values") - self.components.inlet_line.set_state_vector(values[:2]) - self.components.volume.set_state_vector(values[2:4]) - self.components.outlet_line.set_state_vector(values[4:]) - - def snapshot( - self, - state_vector: list[float] | None = None, - ) -> TestMqlPnl0001PairChamberSegmentSnapshot: - if state_vector is not None: - self.apply_state_vector(state_vector) - inlet_line = self.components.inlet_line.properties() - chamber = self.components.volume.properties() - outlet_line = self.components.outlet_line.properties() - inlet_node = self.inlet_node.properties(self.components.inlet_line.gas) - outlet_node = self.outlet_node.properties(self.components.outlet_line.gas) - inlet_node_to_line_flow = self.components.inlet_line.resistance_mass_flow( - port_1_pressure_pa=inlet_node.p, - port_1_temperature_k=inlet_node.T, - ) - outlet_node_to_line_flow = self.components.outlet_line.resistance_mass_flow( - port_1_pressure_pa=outlet_node.p, - port_1_temperature_k=outlet_node.T, - ) - inlet_line_to_chamber_flow = self._line_to_chamber_flow( - line=inlet_line, - chamber=chamber, - orifice=self.components.inlet_orifice, - ) - outlet_line_to_chamber_flow = self._line_to_chamber_flow( - line=outlet_line, - chamber=chamber, - orifice=self.components.outlet_orifice, - ) - snapshot = TestMqlPnl0001PairChamberSegmentSnapshot( - inlet_line=inlet_line, - chamber=chamber, - outlet_line=outlet_line, - inlet_node=inlet_node, - outlet_node=outlet_node, - inlet_node_to_line_flow=inlet_node_to_line_flow, - inlet_line_to_chamber_flow=inlet_line_to_chamber_flow, - outlet_node_to_line_flow=outlet_node_to_line_flow, - outlet_line_to_chamber_flow=outlet_line_to_chamber_flow, - ) - self._write_port_states(snapshot) - return snapshot - - @staticmethod - def _line_to_chamber_flow( - *, - line: ThermodynamicProperties, - chamber: ThermodynamicProperties, - orifice: AmesimPneumaticOrifice, - ) -> float: - upstream_temperature = line.T if line.p >= chamber.p else chamber.T - return orifice.mass_flow(line.p, chamber.p, upstream_temperature) - - @staticmethod - def _line_to_p4_flow( - *, - line: ThermodynamicProperties, - p4_boundary: ThermodynamicProperties, - orifice: AmesimPneumaticOrifice, - ) -> float: - upstream_temperature = line.T if line.p >= p4_boundary.p else p4_boundary.T - return orifice.mass_flow(line.p, p4_boundary.p, upstream_temperature) - - @staticmethod - def _port( - component: AmesimPneumaticVolume | AmesimPneumaticOrifice, - port_name: str, - ) -> PortState: - return TestMqlPneumaticChamberSegmentClosure._port(component, port_name) - - def _write_port_states( - self, - snapshot: TestMqlPnl0001PairChamberSegmentSnapshot, - ) -> None: - spec = self.components.spec - chamber = self.components.volume - chamber_inlet_port = self._port(chamber, spec.volume_inlet_port) - chamber_outlet_port = self._port(chamber, spec.volume_outlet_port) - - self._write_line_side( - line=self.components.inlet_line, - line_properties=snapshot.inlet_line, - node_properties=snapshot.inlet_node, - node_to_line_flow=snapshot.inlet_node_to_line_flow, - line_to_chamber_flow=snapshot.inlet_line_to_chamber_flow, - orifice=self.components.inlet_orifice, - orifice_boundary_port=spec.inlet_orifice_boundary_port, - orifice_volume_port=spec.inlet_orifice_volume_port, - chamber_port=chamber_inlet_port, - chamber_properties=snapshot.chamber, - ) - self._write_line_side( - line=self.components.outlet_line, - line_properties=snapshot.outlet_line, - node_properties=snapshot.outlet_node, - node_to_line_flow=snapshot.outlet_node_to_line_flow, - line_to_chamber_flow=snapshot.outlet_line_to_chamber_flow, - orifice=self.components.outlet_orifice, - orifice_boundary_port=spec.outlet_orifice_boundary_port, - orifice_volume_port=spec.outlet_orifice_volume_port, - chamber_port=chamber_outlet_port, - chamber_properties=snapshot.chamber, - ) - - def _write_line_side( - self, - *, - line: AmesimPnl0001Pipe, - line_properties: ThermodynamicProperties, - node_properties: ThermodynamicProperties, - node_to_line_flow: float, - line_to_chamber_flow: float, - orifice: AmesimPneumaticOrifice, - orifice_boundary_port: str, - orifice_volume_port: str, - chamber_port: PortState, - chamber_properties: ThermodynamicProperties, - ) -> None: - line.port_1.p = node_properties.p - line.port_1.m_flow = node_to_line_flow - line.port_1.h_outflow = line_properties.h - line.port_2.p = line_properties.p - line.port_2.m_flow = -line_to_chamber_flow - - boundary_port = self._port(orifice, orifice_boundary_port) - volume_port = self._port(orifice, orifice_volume_port) - boundary_port.p = line_properties.p - boundary_port.m_flow = line_to_chamber_flow - boundary_port.h_outflow = line_properties.h - volume_port.p = chamber_properties.p - volume_port.m_flow = -line_to_chamber_flow - volume_port.h_outflow = chamber_properties.h - chamber_port.m_flow = line_to_chamber_flow - - def rhs(self, state_vector: list[float]) -> list[float]: - snapshot = self.snapshot(state_vector) - inlet_derivative = self.components.inlet_line.derivatives_from_connections( - port_1_m_flow=snapshot.inlet_node_to_line_flow, - connected_h_1=snapshot.inlet_node.h, - port_2_m_flow=-snapshot.inlet_line_to_chamber_flow, - connected_h_2=snapshot.chamber.h, - ) - outlet_derivative = self.components.outlet_line.derivatives_from_connections( - port_1_m_flow=snapshot.outlet_node_to_line_flow, - connected_h_1=snapshot.outlet_node.h, - port_2_m_flow=-snapshot.outlet_line_to_chamber_flow, - connected_h_2=snapshot.chamber.h, - ) - chamber = self.components.volume - spec = self.components.spec - chamber_derivative = chamber.derivatives_from_two_connections( - port_a_m_flow=self._port(chamber, "port_1").m_flow, - connected_h_a=( - snapshot.inlet_line.h - if spec.volume_inlet_port == "port_1" - else snapshot.outlet_line.h - ), - port_b_m_flow=self._port(chamber, "port_2").m_flow, - connected_h_b=( - snapshot.inlet_line.h - if spec.volume_inlet_port == "port_2" - else snapshot.outlet_line.h - ), - internal_h=snapshot.chamber.h, - ) - return [ - *inlet_derivative.as_vector(), - *chamber_derivative.as_vector(), - *outlet_derivative.as_vector(), - ] - - -class TestMqlPn3NodeChamberSegmentClosure: - """Boundary-reduced chamber segment closed by one real PN3 node. - - ``pneumatic_88`` supplies the PN3 primary pressure/temperature from its - port-1 compliance. The node has no storage, so the sum of the port-1 - PNL00R flow and port-3 PNL0001 flow is written as the opposite port-1 - flow into the PNL0003 state. - """ - - def __init__( - self, - *, - components: TestMqlPn3NodeChamberSegmentComponents, - inlet_node: TestMqlPneumaticBoundaryCondition, - resistance_boundary: TestMqlPneumaticBoundaryCondition, - p4_boundary: TestMqlPneumaticBoundaryCondition, - ) -> None: - self.components = components - self.inlet_node = inlet_node - self.resistance_boundary = resistance_boundary - self.p4_boundary = p4_boundary - - def initial_state_vector(self) -> list[float]: - return [ - *self.components.inlet_line.get_state_vector(), - *self.components.volume.get_state_vector(), - *self.components.outlet_line.get_state_vector(), - *self.components.node_line.get_state_vector(), - ] - - def apply_state_vector(self, values: list[float]) -> None: - if len(values) != 10: - raise ValueError("PN3 node/chamber segment state vector requires ten values") - self.components.inlet_line.set_state_vector(values[:2]) - self.components.volume.set_state_vector(values[2:4]) - self.components.outlet_line.set_state_vector(values[4:6]) - self.components.node_line.set_state_vector(values[6:]) - - def snapshot( - self, - state_vector: list[float] | None = None, - ) -> TestMqlPn3NodeChamberSegmentSnapshot: - if state_vector is not None: - self.apply_state_vector(state_vector) - inlet_line = self.components.inlet_line.properties() - chamber = self.components.volume.properties() - outlet_line = self.components.outlet_line.properties() - node_line_port_1 = self.components.node_line.properties_1() - node_line_port_2 = self.components.node_line.properties_2() - inlet_node = self.inlet_node.properties(self.components.inlet_line.gas) - resistance_boundary = self.resistance_boundary.properties( - self.components.node_resistance.gas - ) - p4_boundary = self.p4_boundary.properties(self.components.node_line.gas) - inlet_node_to_line_flow = self.components.inlet_line.resistance_mass_flow( - port_1_pressure_pa=inlet_node.p, - port_1_temperature_k=inlet_node.T, - ) - outlet_node_to_line_flow = self.components.outlet_line.resistance_mass_flow( - port_1_pressure_pa=node_line_port_1.p, - port_1_temperature_k=node_line_port_1.T, - ) - inlet_line_to_chamber_flow = self._line_to_chamber_flow( - line=inlet_line, - chamber=chamber, - orifice=self.components.inlet_orifice, - ) - outlet_line_to_chamber_flow = self._line_to_chamber_flow( - line=outlet_line, - chamber=chamber, - orifice=self.components.outlet_orifice, - ) - node_to_resistance_flow = self.components.node_resistance.mass_flow( - port_1_pressure_pa=node_line_port_1.p, - port_1_temperature_k=node_line_port_1.T, - port_2_pressure_pa=resistance_boundary.p, - port_2_temperature_k=resistance_boundary.T, - ) - node_balance = self.components.node.balance( - port_2_temperature_k=node_line_port_1.T, - port_2_pressure_pa=node_line_port_1.p, - port_1_enthalpy_flow_w=self._enthalpy_flow_from_node( - flow_kg_s=node_to_resistance_flow, - node_h=node_line_port_1.h, - connected_h=resistance_boundary.h, - ), - port_1_mass_flow_g_s=node_to_resistance_flow * 1.0e3, - port_3_enthalpy_flow_w=self._enthalpy_flow_from_node( - flow_kg_s=outlet_node_to_line_flow, - node_h=node_line_port_1.h, - connected_h=outlet_line.h, - ), - port_3_mass_flow_g_s=outlet_node_to_line_flow * 1.0e3, - ) - node_to_pnl0003_flow = -node_balance.port_2_mass_flow_g_s * 1.0e-3 - pnl0003_center_flow = self.components.node_line.resistance_mass_flow() - pnl0003_to_p4_flow = self._line_to_p4_flow( - line=node_line_port_2, - p4_boundary=p4_boundary, - orifice=self.components.node_orifice, - ) - snapshot = TestMqlPn3NodeChamberSegmentSnapshot( - inlet_line=inlet_line, - chamber=chamber, - outlet_line=outlet_line, - node_line_port_1=node_line_port_1, - node_line_port_2=node_line_port_2, - inlet_node=inlet_node, - resistance_boundary=resistance_boundary, - p4_boundary=p4_boundary, - node_balance=node_balance, - inlet_node_to_line_flow=inlet_node_to_line_flow, - inlet_line_to_chamber_flow=inlet_line_to_chamber_flow, - outlet_node_to_line_flow=outlet_node_to_line_flow, - outlet_line_to_chamber_flow=outlet_line_to_chamber_flow, - node_to_resistance_flow=node_to_resistance_flow, - node_to_pnl0003_flow=node_to_pnl0003_flow, - pnl0003_center_flow=pnl0003_center_flow, - pnl0003_to_p4_flow=pnl0003_to_p4_flow, - ) - self._write_port_states(snapshot) - return snapshot - - @staticmethod - def _line_to_chamber_flow( - *, - line: ThermodynamicProperties, - chamber: ThermodynamicProperties, - orifice: AmesimPneumaticOrifice, - ) -> float: - return TestMqlPnl0001PairChamberSegmentClosure._line_to_chamber_flow( - line=line, - chamber=chamber, - orifice=orifice, - ) - - @staticmethod - def _line_to_p4_flow( - *, - line: ThermodynamicProperties, - p4_boundary: ThermodynamicProperties, - orifice: AmesimPneumaticOrifice, - ) -> float: - upstream_temperature = line.T if line.p >= p4_boundary.p else p4_boundary.T - return orifice.mass_flow(line.p, p4_boundary.p, upstream_temperature) - - @staticmethod - def _port( - component: AmesimPneumaticVolume | AmesimPneumaticOrifice, - port_name: str, - ) -> PortState: - return TestMqlPneumaticChamberSegmentClosure._port(component, port_name) - - @staticmethod - def _enthalpy_flow_from_node( - *, - flow_kg_s: float, - node_h: float, - connected_h: float, - ) -> float: - return flow_kg_s * (node_h if flow_kg_s >= 0.0 else connected_h) - - def _write_port_states( - self, - snapshot: TestMqlPn3NodeChamberSegmentSnapshot, - ) -> None: - spec = self.components.spec - chamber = self.components.volume - chamber_inlet_port = self._port(chamber, spec.volume_inlet_port) - chamber_outlet_port = self._port(chamber, spec.volume_outlet_port) - - TestMqlPnl0001PairChamberSegmentClosure._write_line_side( - self, - line=self.components.inlet_line, - line_properties=snapshot.inlet_line, - node_properties=snapshot.inlet_node, - node_to_line_flow=snapshot.inlet_node_to_line_flow, - line_to_chamber_flow=snapshot.inlet_line_to_chamber_flow, - orifice=self.components.inlet_orifice, - orifice_boundary_port=spec.inlet_orifice_boundary_port, - orifice_volume_port=spec.inlet_orifice_volume_port, - chamber_port=chamber_inlet_port, - chamber_properties=snapshot.chamber, - ) - TestMqlPnl0001PairChamberSegmentClosure._write_line_side( - self, - line=self.components.outlet_line, - line_properties=snapshot.outlet_line, - node_properties=snapshot.node_line_port_1, - node_to_line_flow=snapshot.outlet_node_to_line_flow, - line_to_chamber_flow=snapshot.outlet_line_to_chamber_flow, - orifice=self.components.outlet_orifice, - orifice_boundary_port=spec.outlet_orifice_boundary_port, - orifice_volume_port=spec.outlet_orifice_volume_port, - chamber_port=chamber_outlet_port, - chamber_properties=snapshot.chamber, - ) - node_line = self.components.node_line - node_line.port_1.p = snapshot.node_line_port_1.p - node_line.port_1.m_flow = snapshot.node_to_pnl0003_flow - node_line.port_1.h_outflow = snapshot.node_line_port_1.h - node_line.port_2.p = snapshot.node_line_port_2.p - node_line.port_2.m_flow = -snapshot.pnl0003_to_p4_flow - node_line.port_2.h_outflow = snapshot.node_line_port_2.h - - node_orifice = self.components.node_orifice - node_orifice.port_a.p = snapshot.node_line_port_2.p - node_orifice.port_a.m_flow = snapshot.pnl0003_to_p4_flow - node_orifice.port_a.h_outflow = snapshot.node_line_port_2.h - node_orifice.port_b.p = snapshot.p4_boundary.p - node_orifice.port_b.m_flow = -snapshot.pnl0003_to_p4_flow - node_orifice.port_b.h_outflow = snapshot.p4_boundary.h - - resistance = self.components.node_resistance - resistance.port_1.p = snapshot.node_line_port_1.p - resistance.port_1.m_flow = snapshot.node_to_resistance_flow - resistance.port_1.h_outflow = snapshot.node_line_port_1.h - resistance.port_2.p = snapshot.resistance_boundary.p - resistance.port_2.m_flow = -snapshot.node_to_resistance_flow - resistance.port_2.h_outflow = snapshot.resistance_boundary.h - - def rhs(self, state_vector: list[float]) -> list[float]: - snapshot = self.snapshot(state_vector) - inlet_derivative = self.components.inlet_line.derivatives_from_connections( - port_1_m_flow=snapshot.inlet_node_to_line_flow, - connected_h_1=snapshot.inlet_node.h, - port_2_m_flow=-snapshot.inlet_line_to_chamber_flow, - connected_h_2=snapshot.chamber.h, - ) - outlet_derivative = self.components.outlet_line.derivatives_from_connections( - port_1_m_flow=snapshot.outlet_node_to_line_flow, - connected_h_1=snapshot.node_line_port_1.h, - port_2_m_flow=-snapshot.outlet_line_to_chamber_flow, - connected_h_2=snapshot.chamber.h, - ) - chamber = self.components.volume - spec = self.components.spec - chamber_derivative = chamber.derivatives_from_two_connections( - port_a_m_flow=self._port(chamber, "port_1").m_flow, - connected_h_a=( - snapshot.inlet_line.h - if spec.volume_inlet_port == "port_1" - else snapshot.outlet_line.h - ), - port_b_m_flow=self._port(chamber, "port_2").m_flow, - connected_h_b=( - snapshot.inlet_line.h - if spec.volume_inlet_port == "port_2" - else snapshot.outlet_line.h - ), - internal_h=snapshot.chamber.h, - ) - node_line_derivative_1, node_line_derivative_2 = ( - self.components.node_line.derivatives_from_connections( - port_1_m_flow=snapshot.node_to_pnl0003_flow, - connected_h_1=snapshot.node_line_port_1.h, - port_2_m_flow=-snapshot.pnl0003_to_p4_flow, - connected_h_2=snapshot.p4_boundary.h, - ) - ) - return [ - *inlet_derivative.as_vector(), - *chamber_derivative.as_vector(), - *outlet_derivative.as_vector(), - *node_line_derivative_1.as_vector(), - *node_line_derivative_2.as_vector(), - ] diff --git a/PythonModels/systems/test_mql_config.py b/PythonModels/systems/test_mql_config.py deleted file mode 100644 index 487936c..0000000 --- a/PythonModels/systems/test_mql_config.py +++ /dev/null @@ -1,151 +0,0 @@ -from __future__ import annotations - -import ast -import operator -from dataclasses import dataclass -from math import isfinite -from typing import Any - -from PythonModels.core.peng_robinson import HELIUM_PR, PengRobinsonFluid -from PythonModels.systems.test_mql import COMPONENT_SPECS, GLOBAL_PARAMETERS - - -_BINARY_OPERATORS = { - ast.Add: operator.add, - ast.Sub: operator.sub, - ast.Mult: operator.mul, - ast.Div: operator.truediv, - ast.Pow: operator.pow, -} -_UNARY_OPERATORS = { - ast.UAdd: operator.pos, - ast.USub: operator.neg, -} - - -class TestMqlExpressionError(ValueError): - """Raised when an AMESim parameter expression cannot be resolved safely.""" - - -@dataclass(frozen=True) -class TestMqlResolvedParameter: - name: str - title: str - raw_value: str - units: str - value: float | None - - @property - def is_numeric(self) -> bool: - return self.value is not None - - -@dataclass(frozen=True) -class TestMqlResolvedComponent: - alias: str - component_name: str - submodel: str - label: str - parameters: dict[str, TestMqlResolvedParameter] - - def parameter_value(self, name: str) -> float: - parameter = self.parameters[name] - if parameter.value is None: - raise KeyError(f"Parameter {name!r} on {self.alias!r} is not numeric") - return parameter.value - - -@dataclass(frozen=True) -class TestMqlConfig: - raw_global_parameters: dict[str, str] - global_parameters: dict[str, float] - fluid: PengRobinsonFluid - components: tuple[TestMqlResolvedComponent, ...] - - @classmethod - def from_amesim_specs(cls) -> "TestMqlConfig": - raw_globals = dict(GLOBAL_PARAMETERS) - numeric_globals = { - name: value - for name, raw in raw_globals.items() - if (value := resolve_numeric_expression(raw, {})) is not None - } - components = tuple( - _resolve_component(spec, numeric_globals) - for spec in COMPONENT_SPECS - ) - return cls( - raw_global_parameters=raw_globals, - global_parameters=numeric_globals, - fluid=HELIUM_PR, - components=components, - ) - - def component(self, alias: str) -> TestMqlResolvedComponent: - for component in self.components: - if component.alias == alias: - return component - raise KeyError(alias) - - def components_by_submodel(self, submodel: str) -> tuple[TestMqlResolvedComponent, ...]: - return tuple(component for component in self.components if component.submodel == submodel) - - -def _resolve_component( - spec: dict[str, Any], - variables: dict[str, float], -) -> TestMqlResolvedComponent: - parameters = {} - for parameter in spec.get("parameters", []): - name = str(parameter["name"]) - raw_value = str(parameter["value"]) - parameters[name] = TestMqlResolvedParameter( - name=name, - title=str(parameter["title"]), - raw_value=raw_value, - units=str(parameter["units"]), - value=resolve_numeric_expression(raw_value, variables), - ) - return TestMqlResolvedComponent( - alias=str(spec["alias"]), - component_name=str(spec["component_name"]), - submodel=str(spec["submodel"]), - label=str(spec["label"]), - parameters=parameters, - ) - - -def resolve_numeric_expression( - expression: str, - variables: dict[str, float], -) -> float | None: - expression = expression.strip() - if not expression: - return None - normalized = expression.replace("^", "**") - try: - parsed = ast.parse(normalized, mode="eval") - value = float(_eval_node(parsed.body, variables)) - except (SyntaxError, TestMqlExpressionError, ValueError, TypeError, ZeroDivisionError): - return None - return value if isfinite(value) else None - - -def _eval_node(node: ast.AST, variables: dict[str, float]) -> float: - if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)): - return float(node.value) - if isinstance(node, ast.Name): - if node.id not in variables: - raise TestMqlExpressionError(f"Unknown variable: {node.id}") - return float(variables[node.id]) - if isinstance(node, ast.BinOp): - operator_type = type(node.op) - if operator_type not in _BINARY_OPERATORS: - raise TestMqlExpressionError(f"Unsupported binary operator: {operator_type}") - return float(_BINARY_OPERATORS[operator_type](_eval_node(node.left, variables), _eval_node(node.right, variables))) - if isinstance(node, ast.UnaryOp): - operator_type = type(node.op) - if operator_type not in _UNARY_OPERATORS: - raise TestMqlExpressionError(f"Unsupported unary operator: {operator_type}") - return float(_UNARY_OPERATORS[operator_type](_eval_node(node.operand, variables))) - raise TestMqlExpressionError(f"Unsupported expression node: {type(node)}") diff --git a/PythonModels/systems/test_mql_line_parameters.py b/PythonModels/systems/test_mql_line_parameters.py deleted file mode 100644 index e79298f..0000000 --- a/PythonModels/systems/test_mql_line_parameters.py +++ /dev/null @@ -1,451 +0,0 @@ -from __future__ import annotations - -import re -import tarfile -from dataclasses import dataclass -from pathlib import Path - -from PythonModels.systems.test_mql import CONNECTION_SPECS, GLOBAL_PARAMETERS -from PythonModels.systems.test_mql_config import resolve_numeric_expression - - -AMESIM_REFERENCE_PRESSURE_PA = 101_300.0 - - -@dataclass(frozen=True) -class TestMqlPnl0001Spec: - alias: str - source_component: str - source_port: str - target_component: str - target_port: str - diameter_mm: float - length_m: float - relative_roughness: float - polytropic_constant: float - heat_transfer_coefficient: float - external_temperature_k: float - gas_type_index: int - mode: int - initial_temperature_k: float - initial_gauge_pressure_pa: float - - @property - def initial_absolute_pressure_pa(self) -> float: - return self.initial_gauge_pressure_pa + AMESIM_REFERENCE_PRESSURE_PA - - -@dataclass(frozen=True) -class TestMqlPnl0002Spec: - alias: str - source_component: str - source_port: str - target_component: str - target_port: str - diameter_mm: float - length_m: float - relative_roughness: float - polytropic_constant: float - heat_transfer_coefficient: float - external_temperature_k: float - gas_type_index: int - mode: int - initial_center_temperature_k: float - initial_center_gauge_pressure_pa: float - - @property - def initial_center_absolute_pressure_pa(self) -> float: - return self.initial_center_gauge_pressure_pa + AMESIM_REFERENCE_PRESSURE_PA - - -@dataclass(frozen=True) -class TestMqlPnl0003Spec: - alias: str - source_component: str - source_port: str - target_component: str - target_port: str - diameter_mm: float - length_m: float - relative_roughness: float - polytropic_constant: float - heat_transfer_coefficient: float - external_temperature_k: float - gas_type_index: int - mode: int - initial_temperature_1_k: float - initial_gauge_pressure_1_pa: float - initial_temperature_2_k: float - initial_gauge_pressure_2_pa: float - - @property - def initial_absolute_pressure_1_pa(self) -> float: - return self.initial_gauge_pressure_1_pa + AMESIM_REFERENCE_PRESSURE_PA - - @property - def initial_absolute_pressure_2_pa(self) -> float: - return self.initial_gauge_pressure_2_pa + AMESIM_REFERENCE_PRESSURE_PA - - -@dataclass(frozen=True) -class TestMqlPnl00rSpec: - alias: str - source_component: str - source_port: str - target_component: str - target_port: str - diameter_mm: float - length_m: float - relative_roughness: float - gas_type_index: int - - -def load_test_mql_pnl0001_specs( - archive_path: str | Path, - *, - cir_member: str = "test_mql_.cir", -) -> tuple[TestMqlPnl0001Spec, ...]: - """Load resolved PNL0001 geometry and initial states from the AMESim source.""" - with tarfile.open(archive_path) as archive: - cir_file = archive.extractfile(cir_member) - if cir_file is None: - raise ValueError(f"Missing AMESim circuit member: {cir_member}") - cir_text = cir_file.read().decode("latin1") - - numeric_globals = { - name: value - for name, expression in GLOBAL_PARAMETERS.items() - if (value := resolve_numeric_expression(expression, {})) is not None - } - connections = { - str(connection["alias"]): connection - for connection in CONNECTION_SPECS - if connection["submodel"] == "PNL0001" - } - specs = [] - for block in re.findall(r".*?", cir_text, flags=re.DOTALL): - if _optional_text(block, "SUB_NAME") != "PNL0001": - continue - alias = _required_text(block, "ALIAS") - connection = connections.get(alias) - if connection is None: - raise ValueError(f"PNL0001 line {alias!r} is absent from CONNECTION_SPECS") - real_parameters = _parameter_expressions(block, "RPARAM") - integer_parameters = _parameter_expressions(block, "IPARAM") - state_values = _evar_values(block) - specs.append( - TestMqlPnl0001Spec( - alias=alias, - source_component=str(connection["source_component"]), - source_port=str(connection["source_port"]), - target_component=str(connection["target_component"]), - target_port=str(connection["target_port"]), - diameter_mm=_required_numeric( - alias, "diam", real_parameters, numeric_globals - ), - length_m=_required_numeric(alias, "le", real_parameters, numeric_globals), - relative_roughness=_required_numeric( - alias, "rr", real_parameters, numeric_globals - ), - polytropic_constant=_required_numeric( - alias, "k", real_parameters, numeric_globals - ), - heat_transfer_coefficient=_required_numeric( - alias, "kth", real_parameters, numeric_globals - ), - external_temperature_k=_required_numeric( - alias, "extemp", real_parameters, numeric_globals - ), - gas_type_index=int( - _required_numeric(alias, "gi", integer_parameters, numeric_globals) - ), - mode=int( - _required_numeric(alias, "mode", integer_parameters, numeric_globals) - ), - initial_temperature_k=_required_numeric( - alias, "t2", state_values, numeric_globals - ), - initial_gauge_pressure_pa=_required_numeric( - alias, "p2", state_values, numeric_globals - ), - ) - ) - if set(connections) != {spec.alias for spec in specs}: - missing = sorted(set(connections) - {spec.alias for spec in specs}) - raise ValueError(f"Missing PNL0001 parameter blocks: {missing}") - return tuple(specs) - - -def load_test_mql_pnl0002_specs( - archive_path: str | Path, - *, - cir_member: str = "test_mql_.cir", -) -> tuple[TestMqlPnl0002Spec, ...]: - """Load resolved PNL0002 geometry and center compliance initial state.""" - with tarfile.open(archive_path) as archive: - cir_file = archive.extractfile(cir_member) - if cir_file is None: - raise ValueError(f"Missing AMESim circuit member: {cir_member}") - cir_text = cir_file.read().decode("latin1") - - numeric_globals = { - name: value - for name, expression in GLOBAL_PARAMETERS.items() - if (value := resolve_numeric_expression(expression, {})) is not None - } - connections = { - str(connection["alias"]): connection - for connection in CONNECTION_SPECS - if connection["submodel"] == "PNL0002" - } - specs = [] - for block in re.findall(r".*?", cir_text, flags=re.DOTALL): - if _optional_text(block, "SUB_NAME") != "PNL0002": - continue - alias = _required_text(block, "ALIAS") - connection = connections.get(alias) - if connection is None: - raise ValueError(f"PNL0002 line {alias!r} is absent from CONNECTION_SPECS") - real_parameters = _parameter_expressions(block, "RPARAM") - integer_parameters = _parameter_expressions(block, "IPARAM") - state_values = _ivar_values(block) - specs.append( - TestMqlPnl0002Spec( - alias=alias, - source_component=str(connection["source_component"]), - source_port=str(connection["source_port"]), - target_component=str(connection["target_component"]), - target_port=str(connection["target_port"]), - diameter_mm=_required_numeric( - alias, "diam", real_parameters, numeric_globals - ), - length_m=_required_numeric(alias, "le", real_parameters, numeric_globals), - relative_roughness=_required_numeric( - alias, "rr", real_parameters, numeric_globals - ), - polytropic_constant=_required_numeric( - alias, "k", real_parameters, numeric_globals - ), - heat_transfer_coefficient=_required_numeric( - alias, "kth", real_parameters, numeric_globals - ), - external_temperature_k=_required_numeric( - alias, "extemp", real_parameters, numeric_globals - ), - gas_type_index=int( - _required_numeric(alias, "gi", integer_parameters, numeric_globals) - ), - mode=int( - _required_numeric(alias, "mode", integer_parameters, numeric_globals) - ), - initial_center_temperature_k=_required_numeric( - alias, "tctr", state_values, numeric_globals - ), - initial_center_gauge_pressure_pa=_required_numeric( - alias, "pctr", state_values, numeric_globals - ), - ) - ) - if set(connections) != {spec.alias for spec in specs}: - missing = sorted(set(connections) - {spec.alias for spec in specs}) - raise ValueError(f"Missing PNL0002 parameter blocks: {missing}") - return tuple(specs) - - -def load_test_mql_pnl0003_specs( - archive_path: str | Path, - *, - cir_member: str = "test_mql_.cir", -) -> tuple[TestMqlPnl0003Spec, ...]: - """Load resolved PNL0003 geometry and both compliance initial states.""" - with tarfile.open(archive_path) as archive: - cir_file = archive.extractfile(cir_member) - if cir_file is None: - raise ValueError(f"Missing AMESim circuit member: {cir_member}") - cir_text = cir_file.read().decode("latin1") - - numeric_globals = { - name: value - for name, expression in GLOBAL_PARAMETERS.items() - if (value := resolve_numeric_expression(expression, {})) is not None - } - connections = { - str(connection["alias"]): connection - for connection in CONNECTION_SPECS - if connection["submodel"] == "PNL0003" - } - specs = [] - for block in re.findall(r".*?", cir_text, flags=re.DOTALL): - if _optional_text(block, "SUB_NAME") != "PNL0003": - continue - alias = _required_text(block, "ALIAS") - connection = connections.get(alias) - if connection is None: - raise ValueError(f"PNL0003 line {alias!r} is absent from CONNECTION_SPECS") - real_parameters = _parameter_expressions(block, "RPARAM") - integer_parameters = _parameter_expressions(block, "IPARAM") - state_values = _evar_values(block) - specs.append( - TestMqlPnl0003Spec( - alias=alias, - source_component=str(connection["source_component"]), - source_port=str(connection["source_port"]), - target_component=str(connection["target_component"]), - target_port=str(connection["target_port"]), - diameter_mm=_required_numeric( - alias, "diam", real_parameters, numeric_globals - ), - length_m=_required_numeric(alias, "le", real_parameters, numeric_globals), - relative_roughness=_required_numeric( - alias, "rr", real_parameters, numeric_globals - ), - polytropic_constant=_required_numeric( - alias, "k", real_parameters, numeric_globals - ), - heat_transfer_coefficient=_required_numeric( - alias, "kth", real_parameters, numeric_globals - ), - external_temperature_k=_required_numeric( - alias, "extemp", real_parameters, numeric_globals - ), - gas_type_index=int( - _required_numeric(alias, "gi", integer_parameters, numeric_globals) - ), - mode=int( - _required_numeric(alias, "mode", integer_parameters, numeric_globals) - ), - initial_temperature_1_k=_required_numeric( - alias, "t1", state_values, numeric_globals - ), - initial_gauge_pressure_1_pa=_required_numeric( - alias, "p1", state_values, numeric_globals - ), - initial_temperature_2_k=_required_numeric( - alias, "t2", state_values, numeric_globals - ), - initial_gauge_pressure_2_pa=_required_numeric( - alias, "p2", state_values, numeric_globals - ), - ) - ) - if set(connections) != {spec.alias for spec in specs}: - missing = sorted(set(connections) - {spec.alias for spec in specs}) - raise ValueError(f"Missing PNL0003 parameter blocks: {missing}") - return tuple(specs) - - -def load_test_mql_pnl00r_specs( - archive_path: str | Path, - *, - cir_member: str = "test_mql_.cir", -) -> tuple[TestMqlPnl00rSpec, ...]: - """Load resolved PNL00R geometry from the AMESim source.""" - with tarfile.open(archive_path) as archive: - cir_file = archive.extractfile(cir_member) - if cir_file is None: - raise ValueError(f"Missing AMESim circuit member: {cir_member}") - cir_text = cir_file.read().decode("latin1") - - numeric_globals = { - name: value - for name, expression in GLOBAL_PARAMETERS.items() - if (value := resolve_numeric_expression(expression, {})) is not None - } - connections = { - str(connection["alias"]): connection - for connection in CONNECTION_SPECS - if connection["submodel"] == "PNL00R" - } - specs = [] - for block in re.findall(r".*?", cir_text, flags=re.DOTALL): - if _optional_text(block, "SUB_NAME") != "PNL00R": - continue - alias = _required_text(block, "ALIAS") - connection = connections.get(alias) - if connection is None: - raise ValueError(f"PNL00R line {alias!r} is absent from CONNECTION_SPECS") - real_parameters = _parameter_expressions(block, "RPARAM") - integer_parameters = _parameter_expressions(block, "IPARAM") - specs.append( - TestMqlPnl00rSpec( - alias=alias, - source_component=str(connection["source_component"]), - source_port=str(connection["source_port"]), - target_component=str(connection["target_component"]), - target_port=str(connection["target_port"]), - diameter_mm=_required_numeric( - alias, "diam", real_parameters, numeric_globals - ), - length_m=_required_numeric(alias, "le", real_parameters, numeric_globals), - relative_roughness=_required_numeric( - alias, "rr", real_parameters, numeric_globals - ), - gas_type_index=int( - _required_numeric(alias, "gi", integer_parameters, numeric_globals) - ), - ) - ) - if set(connections) != {spec.alias for spec in specs}: - missing = sorted(set(connections) - {spec.alias for spec in specs}) - raise ValueError(f"Missing PNL00R parameter blocks: {missing}") - return tuple(specs) - - -def _parameter_expressions(block: str, tag_name: str) -> dict[str, str]: - parameters = {} - for parameter_block in re.findall( - rf"<{tag_name}>.*?", - block, - flags=re.DOTALL, - ): - parameters[_required_text(parameter_block, "VARNAME")] = _required_text( - parameter_block, - "VALUE", - ) - return parameters - - -def _ivar_values(block: str) -> dict[str, str]: - values = {} - for variable_block in re.findall(r".*?", block, flags=re.DOTALL): - value = _optional_text(variable_block, "VALUE") - if value: - values[_required_text(variable_block, "VARNAME")] = value - return values - - -def _evar_values(block: str) -> dict[str, str]: - values = {} - for variable_block in re.findall(r".*?", block, flags=re.DOTALL): - value = _optional_text(variable_block, "VALUE") - if value: - values[_required_text(variable_block, "VARNAME")] = value - return values - - -def _required_numeric( - alias: str, - name: str, - expressions: dict[str, str], - variables: dict[str, float], -) -> float: - if name not in expressions: - raise ValueError(f"Missing {name!r} on line {alias!r}") - value = resolve_numeric_expression(expressions[name], variables) - if value is None: - raise ValueError( - f"Cannot resolve {name!r}={expressions[name]!r} on line {alias!r}" - ) - return value - - -def _required_text(block: str, tag_name: str) -> str: - value = _optional_text(block, tag_name) - if value is None: - raise ValueError(f"Missing AMESim circuit element: {tag_name}") - return value - - -def _optional_text(block: str, tag_name: str) -> str | None: - match = re.search(rf"<{tag_name}>(.*?)", block, flags=re.DOTALL) - return match.group(1).strip() if match is not None else None diff --git a/PythonModels/systems/test_mql_mechanical.py b/PythonModels/systems/test_mql_mechanical.py deleted file mode 100644 index 1b8481f..0000000 --- a/PythonModels/systems/test_mql_mechanical.py +++ /dev/null @@ -1,544 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass - -from PythonModels.components.amesim_mechanical import ( - AmesimElasticEndstop, - AmesimMassFrictionEndstops, - AmesimPistonGeometry, - circular_area, - mm_to_m, -) -from PythonModels.reporting.amesim_results import AmesimResults -from PythonModels.reporting.test_mql_variables import ( - TestMqlVariableCatalog, - build_test_mql_variable_catalog, -) -from PythonModels.systems.test_mql_config import TestMqlConfig, TestMqlResolvedComponent - - -MM_TO_M = 1.0e-3 -N_PER_MM_TO_N_PER_M = 1.0e3 -N_PER_MM_PER_S_TO_N_PER_M_PER_S = 1.0e3 - - -@dataclass(frozen=True) -class TestMqlPistonSpec: - alias: str - piston_diameter_m: float - rod_diameter_m: float - zero_displacement_m: float - piston_area_m2: float - rod_area_m2: float - annulus_area_m2: float - data_paths: tuple[str, ...] - - def geometry(self) -> AmesimPistonGeometry: - return AmesimPistonGeometry( - piston_diameter_m=self.piston_diameter_m, - rod_diameter_m=self.rod_diameter_m, - zero_length_m=self.zero_displacement_m, - ) - - -@dataclass(frozen=True) -class TestMqlMassEndstopSpec: - alias: str - mass_kg: float - xmin_m: float - xmax_m: float - min_stiffness_n_per_m: float - max_stiffness_n_per_m: float - min_damping_n_per_m_per_s: float - max_damping_n_per_m_per_s: float - min_penetration_m: float - max_penetration_m: float - stiction_force_n: float - coulomb_friction_n: float - viscous_friction_n_per_m_per_s: float - windage_n_per_m2_per_s2: float - stick_velocity_threshold_m_s: float - reset_velocity_threshold_m_s: float - rest_coeff: float - stribeck_constant_m_s: float - use_friction: bool - stop_type: int - initial_velocity_m_s: float - initial_displacement_m: float - data_paths: tuple[str, ...] - - def endstop(self) -> AmesimMassFrictionEndstops: - return AmesimMassFrictionEndstops( - mass_kg=self.mass_kg, - lower_limit_m=self.xmin_m, - upper_limit_m=self.xmax_m, - lower_stiffness_n_per_m=self.min_stiffness_n_per_m, - upper_stiffness_n_per_m=self.max_stiffness_n_per_m, - lower_damping_n_per_m_per_s=self.min_damping_n_per_m_per_s, - upper_damping_n_per_m_per_s=self.max_damping_n_per_m_per_s, - viscous_friction_n_per_m_per_s=self.viscous_friction_n_per_m_per_s, - coulomb_friction_n=self.coulomb_friction_n, - stiction_force_n=self.stiction_force_n, - windage_n_per_m2_per_s2=self.windage_n_per_m2_per_s2, - ) - - -@dataclass(frozen=True) -class TestMqlElasticEndstopSpec: - alias: str - gap_m: float - contact_stiffness_n_per_m: float - contact_damping_n_per_m_per_s: float - spring_diameter_m: float - wire_diameter_m: float - data_paths: tuple[str, ...] - - def endstop(self) -> AmesimElasticEndstop: - return AmesimElasticEndstop( - contact_stiffness_n_per_m=self.contact_stiffness_n_per_m, - contact_damping_n_per_m_per_s=self.contact_damping_n_per_m_per_s, - gap0_m=self.gap_m, - ) - - -@dataclass(frozen=True) -class TestMqlMechanicalNodeSpec: - alias: str - port_count: int - sum_mode: int - data_paths: tuple[str, ...] - - -@dataclass(frozen=True) -class TestMqlPiecewiseLinearSignalSpec: - alias: str - t_start_s: float - starts: tuple[float, ...] - ends: tuple[float, ...] - durations_s: tuple[float, ...] - stage_count: int - is_cyclic: bool - data_paths: tuple[str, ...] - - def output_at(self, time_s: float) -> float: - if self.stage_count <= 0: - return 0.0 - elapsed = max(time_s - self.t_start_s, 0.0) - active_durations = self.durations_s[: self.stage_count] - total_duration = sum(active_durations) - if self.is_cyclic and total_duration > 0.0: - elapsed = elapsed % total_duration - - stage_start_time = 0.0 - for index, duration in enumerate(active_durations): - stage_end_time = stage_start_time + duration - if elapsed < stage_end_time or index == self.stage_count - 1: - if duration <= 0.0: - return self.ends[index] - fraction = (elapsed - stage_start_time) / duration - return self.starts[index] + fraction * (self.ends[index] - self.starts[index]) - stage_start_time = stage_end_time - return self.ends[self.stage_count - 1] - - -@dataclass(frozen=True) -class TestMqlForceConnectorSpec: - alias: str - signal_alias: str - target_mass_alias: str - data_paths: tuple[str, ...] - - def force_at( - self, - time_s: float, - signals: dict[str, TestMqlPiecewiseLinearSignalSpec], - ) -> float: - return signals[self.signal_alias].output_at(time_s) - - -@dataclass(frozen=True) -class TestMqlMechanicalAssembly: - pistons: dict[str, TestMqlPistonSpec] - masses: dict[str, TestMqlMassEndstopSpec] - elastic_endstops: dict[str, TestMqlElasticEndstopSpec] - mechanical_nodes: dict[str, TestMqlMechanicalNodeSpec] - piecewise_signals: dict[str, TestMqlPiecewiseLinearSignalSpec] - force_connectors: dict[str, TestMqlForceConnectorSpec] - zero_force_sources: tuple[str, ...] - - @property - def component_count(self) -> int: - return ( - len(self.pistons) - + len(self.masses) - + len(self.elastic_endstops) - + len(self.mechanical_nodes) - + len(self.piecewise_signals) - + len(self.force_connectors) - + len(self.zero_force_sources) - ) - - @property - def aliases(self) -> tuple[str, ...]: - return tuple( - [ - *self.pistons, - *self.masses, - *self.elastic_endstops, - *self.mechanical_nodes, - *self.piecewise_signals, - *self.force_connectors, - *self.zero_force_sources, - ] - ) - - -@dataclass(frozen=True) -class TestMqlMechanicalMassState: - alias: str - velocity_m_s: float - displacement_m: float - - def as_vector(self) -> list[float]: - return [self.velocity_m_s, self.displacement_m] - - -@dataclass(frozen=True) -class TestMqlMechanicalNodeKinematics: - alias: str - velocities_m_s: dict[int, float] - displacements_m: dict[int, float] - - -@dataclass(frozen=True) -class TestMqlPistonKinematics: - alias: str - port_2_velocity_m_s: float - port_2_displacement_m: float - port_3_velocity_m_s: float - port_3_displacement_m: float - - -@dataclass(frozen=True) -class TestMqlMechanicalMassSnapshot: - states: tuple[TestMqlMechanicalMassState, ...] - node_kinematics_by_alias: dict[str, TestMqlMechanicalNodeKinematics] - piston_kinematics_by_alias: dict[str, TestMqlPistonKinematics] - - @property - def state_count(self) -> int: - return 2 * len(self.states) - - -class TestMqlMechanicalMassClosure: - def __init__(self, assembly: TestMqlMechanicalAssembly) -> None: - self.assembly = assembly - self.mass_aliases = tuple(assembly.masses) - - def initial_state_vector(self) -> list[float]: - state: list[float] = [] - for alias in self.mass_aliases: - spec = self.assembly.masses[alias] - state.extend([spec.initial_velocity_m_s, spec.initial_displacement_m]) - return state - - def snapshot(self, state_vector: list[float] | None = None) -> TestMqlMechanicalMassSnapshot: - values = self.initial_state_vector() if state_vector is None else list(state_vector) - if len(values) != 2 * len(self.mass_aliases): - raise ValueError("mechanical mass state vector requires two values per mass") - states = tuple( - TestMqlMechanicalMassState( - alias=alias, - velocity_m_s=values[2 * index], - displacement_m=values[2 * index + 1], - ) - for index, alias in enumerate(self.mass_aliases) - ) - node_kinematics = self._node_kinematics_by_alias(states) - return TestMqlMechanicalMassSnapshot( - states=states, - node_kinematics_by_alias=node_kinematics, - piston_kinematics_by_alias=self._piston_kinematics_by_alias( - states, - node_kinematics, - ), - ) - - def _node_kinematics_by_alias( - self, - states: tuple[TestMqlMechanicalMassState, ...], - ) -> dict[str, TestMqlMechanicalNodeKinematics]: - state_by_alias = {state.alias: state for state in states} - front = state_by_alias["mass_friction_endstops_18"] - rear = state_by_alias["mass_friction_endstops_19"] - return { - "dynamic_mechanical_node_alternative_2": TestMqlMechanicalNodeKinematics( - alias="dynamic_mechanical_node_alternative_2", - velocities_m_s={port: -front.velocity_m_s for port in range(1, 9)}, - displacements_m={port: -front.displacement_m for port in range(1, 9)}, - ), - "dynamic_mechanical_node_alternative_3": TestMqlMechanicalNodeKinematics( - alias="dynamic_mechanical_node_alternative_3", - velocities_m_s={port: rear.velocity_m_s for port in range(1, 9)}, - displacements_m={port: rear.displacement_m for port in range(1, 9)}, - ), - } - - def _piston_kinematics_by_alias( - self, - states: tuple[TestMqlMechanicalMassState, ...], - node_kinematics_by_alias: dict[str, TestMqlMechanicalNodeKinematics], - ) -> dict[str, TestMqlPistonKinematics]: - state_by_alias = {state.alias: state for state in states} - rear_node = node_kinematics_by_alias["dynamic_mechanical_node_alternative_3"] - piston_bindings = ( - ("pn_brp2_8", "mass_friction_endstops_10", 8), - ("pn_brp2_9", "mass_friction_endstops_11", 7), - ("pn_brp2_10", "mass_friction_endstops_12", 6), - ("pn_brp2_11", "mass_friction_endstops_13", 5), - ("pn_brp2_12", "mass_friction_endstops_14", 4), - ("pn_brp2_13", "mass_friction_endstops_15", 3), - ("pn_brp2_14", "mass_friction_endstops_16", 2), - ("pn_brp2_15", "mass_friction_endstops_17", 1), - ) - return { - piston_alias: TestMqlPistonKinematics( - alias=piston_alias, - port_2_velocity_m_s=state_by_alias[mass_alias].velocity_m_s, - port_2_displacement_m=state_by_alias[mass_alias].displacement_m, - port_3_velocity_m_s=rear_node.velocities_m_s[rear_node_port], - port_3_displacement_m=rear_node.displacements_m[rear_node_port], - ) - for piston_alias, mass_alias, rear_node_port in piston_bindings - } - - def rhs( - self, - state_vector: list[float], - *, - force_by_mass_alias: dict[str, float] | None = None, - constrained_mass_aliases: set[str] | None = None, - ) -> list[float]: - snapshot = self.snapshot(state_vector) - force_by_mass_alias = force_by_mass_alias or {} - constrained_mass_aliases = constrained_mass_aliases or set() - derivatives: list[float] = [] - for state in snapshot.states: - spec = self.assembly.masses[state.alias] - mass = spec.endstop() - applied_force = force_by_mass_alias.get(state.alias, 0.0) - acceleration, velocity = mass.derivatives( - velocity_m_s=state.velocity_m_s, - displacement_m=state.displacement_m, - port_1_force_n=applied_force, - ) - if state.alias in constrained_mass_aliases and _limit_constraint_holds( - spec, - state, - applied_force, - ): - acceleration = 0.0 - velocity = 0.0 - derivatives.extend([acceleration, velocity]) - return derivatives - - -def build_test_mql_mechanical_assembly( - config: TestMqlConfig | None = None, - amesim_results: AmesimResults | None = None, - variable_catalog: TestMqlVariableCatalog | None = None, -) -> TestMqlMechanicalAssembly: - config = config or TestMqlConfig.from_amesim_specs() - if variable_catalog is None and amesim_results is not None: - variable_catalog = build_test_mql_variable_catalog(amesim_results) - - pistons = { - component.alias: _build_piston(component, variable_catalog) - for component in config.components_by_submodel("PNRP17") - } - masses = { - component.alias: _build_mass(component, variable_catalog, amesim_results) - for component in config.components_by_submodel("MECMAS21") - } - elastic_endstops = { - component.alias: _build_elastic_endstop(component, variable_catalog) - for component in config.components_by_submodel("LSTP00A") - } - mechanical_nodes = { - component.alias: _build_mechanical_node(component, variable_catalog) - for component in config.components_by_submodel("LMECHN1") - } - piecewise_signals = { - component.alias: _build_piecewise_signal(component, variable_catalog) - for component in config.components_by_submodel("UD00") - } - force_connectors = { - component.alias: _build_force_connector(component, variable_catalog) - for component in config.components_by_submodel("FORC") - } - zero_force_sources = tuple(component.alias for component in config.components_by_submodel("F000")) - return TestMqlMechanicalAssembly( - pistons=pistons, - masses=masses, - elastic_endstops=elastic_endstops, - mechanical_nodes=mechanical_nodes, - piecewise_signals=piecewise_signals, - force_connectors=force_connectors, - zero_force_sources=zero_force_sources, - ) - - -def _build_piston( - component: TestMqlResolvedComponent, - variable_catalog: TestMqlVariableCatalog | None, -) -> TestMqlPistonSpec: - geometry = AmesimPistonGeometry( - piston_diameter_m=mm_to_m(component.parameter_value("dp")), - rod_diameter_m=mm_to_m(component.parameter_value("dr")), - zero_length_m=mm_to_m(component.parameter_value("x0")), - ) - return TestMqlPistonSpec( - alias=component.alias, - piston_diameter_m=geometry.piston_diameter_m, - rod_diameter_m=geometry.rod_diameter_m, - zero_displacement_m=geometry.zero_length_m, - piston_area_m2=geometry.piston_area_m2, - rod_area_m2=geometry.rod_area_m2, - annulus_area_m2=geometry.annulus_area_m2, - data_paths=_data_paths(variable_catalog, component.alias), - ) - - -def _build_mass( - component: TestMqlResolvedComponent, - variable_catalog: TestMqlVariableCatalog | None, - amesim_results: AmesimResults | None, -) -> TestMqlMassEndstopSpec: - return TestMqlMassEndstopSpec( - alias=component.alias, - mass_kg=component.parameter_value("mass"), - xmin_m=component.parameter_value("xmin"), - xmax_m=component.parameter_value("xmax"), - min_stiffness_n_per_m=n_per_mm_to_n_per_m(component.parameter_value("Kbmin")), - max_stiffness_n_per_m=n_per_mm_to_n_per_m(component.parameter_value("Kbmax")), - min_damping_n_per_m_per_s=n_per_mm_per_s_to_n_per_m_per_s(component.parameter_value("Dbmin")), - max_damping_n_per_m_per_s=n_per_mm_per_s_to_n_per_m_per_s(component.parameter_value("Dbmax")), - min_penetration_m=mm_to_m(component.parameter_value("Pdmin")), - max_penetration_m=mm_to_m(component.parameter_value("Pdmax")), - stiction_force_n=component.parameter_value("fstick"), - coulomb_friction_n=component.parameter_value("fcoul"), - viscous_friction_n_per_m_per_s=component.parameter_value("rvisc"), - windage_n_per_m2_per_s2=component.parameter_value("wind"), - stick_velocity_threshold_m_s=component.parameter_value("dvel"), - reset_velocity_threshold_m_s=component.parameter_value("restdvel"), - rest_coeff=component.parameter_value("restcoeff"), - stribeck_constant_m_s=component.parameter_value("astrib"), - use_friction=bool(int(component.parameter_value("useFriction"))), - stop_type=int(component.parameter_value("stoptype")), - initial_velocity_m_s=_initial_value(amesim_results, f"v1@{component.alias}"), - initial_displacement_m=_initial_value(amesim_results, f"x1@{component.alias}"), - data_paths=_data_paths(variable_catalog, component.alias), - ) - - -def _build_elastic_endstop( - component: TestMqlResolvedComponent, - variable_catalog: TestMqlVariableCatalog | None, -) -> TestMqlElasticEndstopSpec: - return TestMqlElasticEndstopSpec( - alias=component.alias, - gap_m=mm_to_m(component.parameter_value("gap0")), - contact_stiffness_n_per_m=component.parameter_value("kcont"), - contact_damping_n_per_m_per_s=component.parameter_value("rcont"), - spring_diameter_m=mm_to_m(component.parameter_value("sdiam")), - wire_diameter_m=mm_to_m(component.parameter_value("wdiam")), - data_paths=_data_paths(variable_catalog, component.alias), - ) - - -def _build_mechanical_node( - component: TestMqlResolvedComponent, - variable_catalog: TestMqlVariableCatalog | None, -) -> TestMqlMechanicalNodeSpec: - return TestMqlMechanicalNodeSpec( - alias=component.alias, - port_count=int(component.parameter_value("v1")), - sum_mode=int(component.parameter_value("sum")), - data_paths=_data_paths(variable_catalog, component.alias), - ) - - -def _limit_constraint_holds( - spec: TestMqlMassEndstopSpec, - state: TestMqlMechanicalMassState, - applied_force_n: float, -) -> bool: - if abs(state.velocity_m_s) > spec.stick_velocity_threshold_m_s: - return False - at_lower_limit = state.displacement_m <= spec.xmin_m + spec.min_penetration_m - at_upper_limit = state.displacement_m >= spec.xmax_m - spec.max_penetration_m - return (at_lower_limit and applied_force_n <= 0.0) or ( - at_upper_limit and applied_force_n >= 0.0 - ) - - -def _build_piecewise_signal( - component: TestMqlResolvedComponent, - variable_catalog: TestMqlVariableCatalog | None, -) -> TestMqlPiecewiseLinearSignalSpec: - starts = tuple(component.parameter_value(f"start{index}") for index in range(1, 9)) - ends = tuple(component.parameter_value(f"end{index}") for index in range(1, 9)) - durations = tuple(component.parameter_value(f"t{index}") for index in range(1, 9)) - return TestMqlPiecewiseLinearSignalSpec( - alias=component.alias, - t_start_s=component.parameter_value("tstart"), - starts=starts, - ends=ends, - durations_s=durations, - stage_count=int(component.parameter_value("nstages")), - is_cyclic=bool(int(component.parameter_value("iscyclic"))), - data_paths=_data_paths(variable_catalog, component.alias), - ) - - -def _build_force_connector( - component: TestMqlResolvedComponent, - variable_catalog: TestMqlVariableCatalog | None, -) -> TestMqlForceConnectorSpec: - signal_alias_by_force_connector = { - "forcecon_1": "piecewiselinear", - "forcecon_2": "piecewiselinear_1", - } - target_mass_by_force_connector = { - "forcecon_1": "mass_friction_endstops_19", - "forcecon_2": "mass_friction_endstops_18", - } - return TestMqlForceConnectorSpec( - alias=component.alias, - signal_alias=signal_alias_by_force_connector[component.alias], - target_mass_alias=target_mass_by_force_connector[component.alias], - data_paths=_data_paths(variable_catalog, component.alias), - ) - - -def n_per_mm_to_n_per_m(value: float) -> float: - return value * N_PER_MM_TO_N_PER_M - - -def n_per_mm_per_s_to_n_per_m_per_s(value: float) -> float: - return value * N_PER_MM_PER_S_TO_N_PER_M_PER_S - - -def _initial_value(amesim_results: AmesimResults | None, data_path: str) -> float: - if amesim_results is None: - return 0.0 - return float(amesim_results.series(data_path)[0]) - - -def _data_paths( - variable_catalog: TestMqlVariableCatalog | None, - alias: str, -) -> tuple[str, ...]: - if variable_catalog is None: - return () - return variable_catalog.data_paths_for_owner(alias) diff --git a/PythonModels/systems/test_mql_nodes.py b/PythonModels/systems/test_mql_nodes.py deleted file mode 100644 index 1227abc..0000000 --- a/PythonModels/systems/test_mql_nodes.py +++ /dev/null @@ -1,215 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass - -from PythonModels.systems.test_mql import COMPONENT_SPECS - - -@dataclass(frozen=True) -class TestMqlPneumaticNode3Balance: - temperature_k: float - pressure_pa: float - port_1_enthalpy_flow_w: float - port_1_mass_flow_g_s: float - port_1_volume_derivative_l_min: float - port_1_volume_cm3: float - port_2_enthalpy_flow_w: float - port_2_mass_flow_g_s: float - port_2_volume_derivative_l_min: float - port_2_volume_cm3: float - port_3_enthalpy_flow_w: float - port_3_mass_flow_g_s: float - port_3_volume_derivative_l_min: float - port_3_volume_cm3: float - - -@dataclass(frozen=True) -class TestMqlPneumaticNode3: - """Exact algebraic contract of AMESim ``PN3NODE2``. - - Pressure and temperature are fixed by port 2 and duplicated to ports 1 and - 3. Flow and volume signals at port 2 are the sums of ports 1 and 3, matching - the ``EXPRESS2`` equations stored in ``test_mql_.cir``. - """ - - alias: str - - def balance( - self, - *, - port_2_temperature_k: float, - port_2_pressure_pa: float, - port_1_enthalpy_flow_w: float, - port_1_mass_flow_g_s: float, - port_3_enthalpy_flow_w: float, - port_3_mass_flow_g_s: float, - port_1_volume_derivative_l_min: float = 0.0, - port_1_volume_cm3: float = 0.0, - port_3_volume_derivative_l_min: float = 0.0, - port_3_volume_cm3: float = 0.0, - ) -> TestMqlPneumaticNode3Balance: - if port_2_temperature_k <= 0.0: - raise ValueError("port_2_temperature_k must be positive") - if port_2_pressure_pa <= 0.0: - raise ValueError("port_2_pressure_pa must be positive") - return TestMqlPneumaticNode3Balance( - temperature_k=port_2_temperature_k, - pressure_pa=port_2_pressure_pa, - port_1_enthalpy_flow_w=port_1_enthalpy_flow_w, - port_1_mass_flow_g_s=port_1_mass_flow_g_s, - port_1_volume_derivative_l_min=port_1_volume_derivative_l_min, - port_1_volume_cm3=port_1_volume_cm3, - port_2_enthalpy_flow_w=( - port_1_enthalpy_flow_w + port_3_enthalpy_flow_w - ), - port_2_mass_flow_g_s=port_1_mass_flow_g_s + port_3_mass_flow_g_s, - port_2_volume_derivative_l_min=( - port_1_volume_derivative_l_min + port_3_volume_derivative_l_min - ), - port_2_volume_cm3=port_1_volume_cm3 + port_3_volume_cm3, - port_3_enthalpy_flow_w=port_3_enthalpy_flow_w, - port_3_mass_flow_g_s=port_3_mass_flow_g_s, - port_3_volume_derivative_l_min=port_3_volume_derivative_l_min, - port_3_volume_cm3=port_3_volume_cm3, - ) - - -@dataclass(frozen=True) -class TestMqlPneumaticNode4Balance: - temperature_k: float - pressure_pa: float - port_1_enthalpy_flow_w: float - port_1_mass_flow_g_s: float - port_1_volume_derivative_l_min: float - port_1_volume_cm3: float - port_2_enthalpy_flow_w: float - port_2_mass_flow_g_s: float - port_2_volume_derivative_l_min: float - port_2_volume_cm3: float - port_3_enthalpy_flow_w: float - port_3_mass_flow_g_s: float - port_3_volume_derivative_l_min: float - port_3_volume_cm3: float - port_4_enthalpy_flow_w: float - port_4_mass_flow_g_s: float - port_4_volume_derivative_l_min: float - port_4_volume_cm3: float - - -@dataclass(frozen=True) -class TestMqlPneumaticNode4: - """Exact algebraic contract of AMESim ``P4NODE2``. - - Pressure and temperature are fixed by port 2 and duplicated to ports 1, 3, - and 4. Flow and volume signals at port 2 are the sums of ports 1, 3, and - 4, matching the saved AMESim variables for ``pnnode4_*`` instances. - """ - - alias: str - - def balance( - self, - *, - port_2_temperature_k: float, - port_2_pressure_pa: float, - port_1_enthalpy_flow_w: float, - port_1_mass_flow_g_s: float, - port_3_enthalpy_flow_w: float, - port_3_mass_flow_g_s: float, - port_4_enthalpy_flow_w: float, - port_4_mass_flow_g_s: float, - port_1_volume_derivative_l_min: float = 0.0, - port_1_volume_cm3: float = 0.0, - port_3_volume_derivative_l_min: float = 0.0, - port_3_volume_cm3: float = 0.0, - port_4_volume_derivative_l_min: float = 0.0, - port_4_volume_cm3: float = 0.0, - ) -> TestMqlPneumaticNode4Balance: - if port_2_temperature_k <= 0.0: - raise ValueError("port_2_temperature_k must be positive") - if port_2_pressure_pa <= 0.0: - raise ValueError("port_2_pressure_pa must be positive") - return TestMqlPneumaticNode4Balance( - temperature_k=port_2_temperature_k, - pressure_pa=port_2_pressure_pa, - port_1_enthalpy_flow_w=port_1_enthalpy_flow_w, - port_1_mass_flow_g_s=port_1_mass_flow_g_s, - port_1_volume_derivative_l_min=port_1_volume_derivative_l_min, - port_1_volume_cm3=port_1_volume_cm3, - port_2_enthalpy_flow_w=( - port_1_enthalpy_flow_w - + port_3_enthalpy_flow_w - + port_4_enthalpy_flow_w - ), - port_2_mass_flow_g_s=( - port_1_mass_flow_g_s - + port_3_mass_flow_g_s - + port_4_mass_flow_g_s - ), - port_2_volume_derivative_l_min=( - port_1_volume_derivative_l_min - + port_3_volume_derivative_l_min - + port_4_volume_derivative_l_min - ), - port_2_volume_cm3=( - port_1_volume_cm3 + port_3_volume_cm3 + port_4_volume_cm3 - ), - port_3_enthalpy_flow_w=port_3_enthalpy_flow_w, - port_3_mass_flow_g_s=port_3_mass_flow_g_s, - port_3_volume_derivative_l_min=port_3_volume_derivative_l_min, - port_3_volume_cm3=port_3_volume_cm3, - port_4_enthalpy_flow_w=port_4_enthalpy_flow_w, - port_4_mass_flow_g_s=port_4_mass_flow_g_s, - port_4_volume_derivative_l_min=port_4_volume_derivative_l_min, - port_4_volume_cm3=port_4_volume_cm3, - ) - - -@dataclass(frozen=True) -class TestMqlP4NodePortConnection: - line_alias: str - local_node_alias: str - local_port: str - remote_node_alias: str - remote_port: str - - -@dataclass(frozen=True) -class TestMqlP4NodePrimaryConnection: - line_alias: str - node_alias: str - node_port: str - chamber_alias: str - chamber_port: str - - -@dataclass(frozen=True) -class TestMqlP4NodeOrificeConnection: - orifice_alias: str - node_alias: str - node_port: str - direct_line_alias: str - - -@dataclass(frozen=True) -class TestMqlP4NodeNeighborhood: - node_alias: str - primary: TestMqlP4NodePrimaryConnection - port_1: TestMqlP4NodePortConnection - port_3: TestMqlP4NodePortConnection - port_4: TestMqlP4NodeOrificeConnection - - -def build_test_mql_node3_assembly() -> dict[str, TestMqlPneumaticNode3]: - return { - str(spec["alias"]): TestMqlPneumaticNode3(alias=str(spec["alias"])) - for spec in COMPONENT_SPECS - if spec["submodel"] == "PN3NODE2" - } - -def build_test_mql_node4_assembly() -> dict[str, TestMqlPneumaticNode4]: - return { - str(spec["alias"]): TestMqlPneumaticNode4(alias=str(spec["alias"])) - for spec in COMPONENT_SPECS - if spec["submodel"] == "P4NODE2" - } diff --git a/PythonModels/systems/test_mql_pneumatic.py b/PythonModels/systems/test_mql_pneumatic.py deleted file mode 100644 index 208507c..0000000 --- a/PythonModels/systems/test_mql_pneumatic.py +++ /dev/null @@ -1,273 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass - -from PythonModels.components.amesim_pneumatic import ( - HELIUM_PNEUMATIC_GAS, - AmesimPneumaticGas, - AmesimPneumaticOrifice, - AmesimPneumaticVolume, - AmesimVariablePneumaticVolume, -) -from PythonModels.systems.test_mql_config import TestMqlConfig, TestMqlResolvedComponent - - -AMESIM_REFERENCE_PRESSURE_PA = 101_300.0 -BAR_TO_PA = 1.0e5 -DEFAULT_TEST_MQL_TEMPERATURE_K = 293.15 -DEFAULT_VARIABLE_CHAMBER_PRESSURE_BAR = 1.0 -# Matched to PNVO001 event-window mass flow near the 0.04 s opening event. -TEST_MQL_PNVO001_FLOW_COEFFICIENT_MULTIPLIER = 0.99805 - - -@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 - - @property - def initial_pressure_pa(self) -> float: - return pressure_to_amesim_gauge_pa(self.fixed_initial_absolute_pressure_pa) - - @property - def fixed_initial_gauge_pressure_pa(self) -> float: - return pressure_to_amesim_gauge_pa(self.fixed_initial_absolute_pressure_pa) - - @property - def variable_initial_gauge_pressure_pa(self) -> float: - return pressure_to_amesim_gauge_pa(self.variable_initial_absolute_pressure_pa) - - @property - def chamber_count(self) -> int: - return len(self.fixed_chambers) + len(self.variable_chambers) - - @property - def orifice_count(self) -> int: - return len(self.fixed_orifices) + len(self.variable_orifices) - - @property - 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( - [ - *self.fixed_chambers, - *self.variable_chambers, - *self.fixed_orifices, - *self.variable_orifices, - ] - ) - - -def build_test_mql_pneumatic_assembly( - config: TestMqlConfig | None = None, - gas: AmesimPneumaticGas = HELIUM_PNEUMATIC_GAS, -) -> TestMqlPneumaticAssembly: - config = config or TestMqlConfig.from_amesim_specs() - fixed_initial_absolute_pressure_pa = absolute_pressure_from_amesim_bar_parameter( - config.global_parameters["P0"] - ) - variable_initial_absolute_pressure_pa = absolute_pressure_from_amesim_bar_parameter( - DEFAULT_VARIABLE_CHAMBER_PRESSURE_BAR - ) - fixed_chambers = { - component.alias: _build_chamber( - component, - volume_parameter="cvol", - gas=gas, - initial_pressure_pa=fixed_initial_absolute_pressure_pa, - ) - for component in config.components_by_submodel("PNCH023") - } - variable_chambers = { - component.alias: _build_chamber( - component, - volume_parameter="cvol0", - gas=gas, - initial_pressure_pa=variable_initial_absolute_pressure_pa, - ) - for component in config.components_by_submodel("PNCH012") - } - fixed_orifices = { - component.alias: _build_orifice( - component, - area_parameter="area", - gas=gas, - opening=1.0, - ) - 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=variable_orifice_controls[component.alias].opening_at(0.0), - ) - for component in config.components_by_submodel("PNVO001") - } - return TestMqlPneumaticAssembly( - fixed_chambers=fixed_chambers, - 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 - - -def pressure_to_amesim_gauge_pa(absolute_pressure_pa: float) -> float: - return absolute_pressure_pa - AMESIM_REFERENCE_PRESSURE_PA - - -def pressure_from_amesim_bar_parameter(pressure_bar: float) -> float: - return pressure_to_amesim_gauge_pa(absolute_pressure_from_amesim_bar_parameter(pressure_bar)) - - -def _build_chamber( - component: TestMqlResolvedComponent, - *, - volume_parameter: str, - gas: AmesimPneumaticGas, - initial_pressure_pa: float, -) -> AmesimPneumaticVolume: - if volume_parameter == "cvol0": - return AmesimVariablePneumaticVolume.from_liters( - name=component.alias, - dead_volume_liters=component.parameter_value(volume_parameter), - gas=gas, - p0=initial_pressure_pa, - T0=_component_temperature(component), - heat_transfer_coefficient=component.parameter_value("kth"), - heat_transfer_area=component.parameter_value("sth"), - external_temperature_k=_component_temperature(component), - ) - return AmesimPneumaticVolume.from_liters( - name=component.alias, - volume_liters=component.parameter_value(volume_parameter), - gas=gas, - p0=initial_pressure_pa, - T0=_component_temperature(component), - heat_transfer_coefficient=component.parameter_value("kth"), - heat_transfer_area=component.parameter_value("sth"), - external_temperature_k=_component_temperature(component), - ) - - -def _build_orifice( - component: TestMqlResolvedComponent, - *, - area_parameter: str, - gas: AmesimPneumaticGas, - opening: float, -) -> AmesimPneumaticOrifice: - flow_coefficient = component.parameter_value("cq") - if component.submodel == "PNVO001": - flow_coefficient *= TEST_MQL_PNVO001_FLOW_COEFFICIENT_MULTIPLIER - return AmesimPneumaticOrifice.from_mm2( - name=component.alias, - area_mm2=component.parameter_value(area_parameter), - flow_coefficient=flow_coefficient, - gas=gas, - opening=opening, - ) - - -def _component_temperature(component: TestMqlResolvedComponent) -> float: - parameter = component.parameters.get("extemp") - if parameter is None or parameter.value is None: - return DEFAULT_TEST_MQL_TEMPERATURE_K - return parameter.value diff --git a/PythonModels/systems/test_mql_pneumatic_lines.py b/PythonModels/systems/test_mql_pneumatic_lines.py deleted file mode 100644 index 850991b..0000000 --- a/PythonModels/systems/test_mql_pneumatic_lines.py +++ /dev/null @@ -1,194 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass -from pathlib import Path - -from PythonModels.components.amesim_pneumatic import ( - HELIUM_PNEUMATIC_GAS, - AmesimPneumaticGas, -) -from PythonModels.components.amesim_pneumatic_line import ( - AmesimPnl0001Pipe, - AmesimPnl0002Pipe, - AmesimPnl0003Pipe, - AmesimPnl00rPipe, -) -from PythonModels.systems.test_mql_line_parameters import ( - TestMqlPnl0001Spec, - TestMqlPnl0002Spec, - TestMqlPnl0003Spec, - TestMqlPnl00rSpec, - load_test_mql_pnl0001_specs, - load_test_mql_pnl0002_specs, - load_test_mql_pnl0003_specs, - load_test_mql_pnl00r_specs, -) - - -TEST_MQL_PNL0001_D20_L1_LINEAR_CONDUCTANCE = 5.5636e-6 - - -@dataclass(frozen=True) -class TestMqlPnl0001Assembly: - specs: tuple[TestMqlPnl0001Spec, ...] - lines: dict[str, AmesimPnl0001Pipe] - - def spec(self, alias: str) -> TestMqlPnl0001Spec: - for spec in self.specs: - if spec.alias == alias: - return spec - raise KeyError(alias) - - -@dataclass(frozen=True) -class TestMqlPnl0002Assembly: - specs: tuple[TestMqlPnl0002Spec, ...] - lines: dict[str, AmesimPnl0002Pipe] - - def spec(self, alias: str) -> TestMqlPnl0002Spec: - for spec in self.specs: - if spec.alias == alias: - return spec - raise KeyError(alias) - - -@dataclass(frozen=True) -class TestMqlPnl0003Assembly: - specs: tuple[TestMqlPnl0003Spec, ...] - lines: dict[str, AmesimPnl0003Pipe] - - def spec(self, alias: str) -> TestMqlPnl0003Spec: - for spec in self.specs: - if spec.alias == alias: - return spec - raise KeyError(alias) - - -@dataclass(frozen=True) -class TestMqlPnl00rAssembly: - specs: tuple[TestMqlPnl00rSpec, ...] - lines: dict[str, AmesimPnl00rPipe] - - def spec(self, alias: str) -> TestMqlPnl00rSpec: - for spec in self.specs: - if spec.alias == alias: - return spec - raise KeyError(alias) - - -def build_test_mql_pnl0001_assembly( - archive_path: str | Path, - *, - gas: AmesimPneumaticGas = HELIUM_PNEUMATIC_GAS, -) -> TestMqlPnl0001Assembly: - specs = load_test_mql_pnl0001_specs(archive_path) - lines = { - spec.alias: AmesimPnl0001Pipe( - name=spec.alias, - diameter_mm=spec.diameter_mm, - length_m=spec.length_m, - relative_roughness=spec.relative_roughness, - polytropic_constant=spec.polytropic_constant, - heat_transfer_coefficient=spec.heat_transfer_coefficient, - external_temperature_k=spec.external_temperature_k, - calibrated_linear_conductance=( - _test_mql_pnl0001_calibrated_linear_conductance(spec) - ), - gas=gas, - p0=spec.initial_absolute_pressure_pa, - T0=spec.initial_temperature_k, - ) - for spec in specs - } - return TestMqlPnl0001Assembly(specs=specs, lines=lines) - - -def _test_mql_pnl0001_calibrated_linear_conductance( - spec: TestMqlPnl0001Spec, -) -> float | None: - if spec.target_component.startswith("pn_c1_") and _matches_geometry( - spec, diameter_mm=20.0, length_m=1.0 - ): - return TEST_MQL_PNL0001_D20_L1_LINEAR_CONDUCTANCE - return None - - -def _matches_geometry( - spec: TestMqlPnl0001Spec, - *, - diameter_mm: float, - length_m: float, -) -> bool: - return ( - abs(spec.diameter_mm - diameter_mm) < 1.0e-12 - and abs(spec.length_m - length_m) < 1.0e-12 - ) - - -def build_test_mql_pnl0002_assembly( - archive_path: str | Path, - *, - gas: AmesimPneumaticGas = HELIUM_PNEUMATIC_GAS, -) -> TestMqlPnl0002Assembly: - specs = load_test_mql_pnl0002_specs(archive_path) - lines = { - spec.alias: AmesimPnl0002Pipe( - name=spec.alias, - diameter_mm=spec.diameter_mm, - length_m=spec.length_m, - relative_roughness=spec.relative_roughness, - polytropic_constant=spec.polytropic_constant, - heat_transfer_coefficient=spec.heat_transfer_coefficient, - external_temperature_k=spec.external_temperature_k, - gas=gas, - pctr_0=spec.initial_center_absolute_pressure_pa, - Tctr_0=spec.initial_center_temperature_k, - ) - for spec in specs - } - return TestMqlPnl0002Assembly(specs=specs, lines=lines) - - -def build_test_mql_pnl0003_assembly( - archive_path: str | Path, - *, - gas: AmesimPneumaticGas = HELIUM_PNEUMATIC_GAS, -) -> TestMqlPnl0003Assembly: - specs = load_test_mql_pnl0003_specs(archive_path) - lines = { - spec.alias: AmesimPnl0003Pipe( - name=spec.alias, - diameter_mm=spec.diameter_mm, - length_m=spec.length_m, - relative_roughness=spec.relative_roughness, - polytropic_constant=spec.polytropic_constant, - heat_transfer_coefficient=spec.heat_transfer_coefficient, - external_temperature_k=spec.external_temperature_k, - gas=gas, - p1_0=spec.initial_absolute_pressure_1_pa, - T1_0=spec.initial_temperature_1_k, - p2_0=spec.initial_absolute_pressure_2_pa, - T2_0=spec.initial_temperature_2_k, - ) - for spec in specs - } - return TestMqlPnl0003Assembly(specs=specs, lines=lines) - - -def build_test_mql_pnl00r_assembly( - archive_path: str | Path, - *, - gas: AmesimPneumaticGas = HELIUM_PNEUMATIC_GAS, -) -> TestMqlPnl00rAssembly: - specs = load_test_mql_pnl00r_specs(archive_path) - lines = { - spec.alias: AmesimPnl00rPipe( - name=spec.alias, - diameter_mm=spec.diameter_mm, - length_m=spec.length_m, - relative_roughness=spec.relative_roughness, - gas=gas, - ) - for spec in specs - } - return TestMqlPnl00rAssembly(specs=specs, lines=lines) diff --git a/PythonModels/systems/testmodel.py b/PythonModels/systems/testmodel.py deleted file mode 100644 index 2652292..0000000 --- a/PythonModels/systems/testmodel.py +++ /dev/null @@ -1,303 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass, field -from typing import Any - -from PythonModels.components.cylinder import Cylinder -from PythonModels.components.orifice import Orifice -from PythonModels.components.pipe import Pipe -from PythonModels.components.tank import Tank -from PythonModels.components.tee import Tee -from PythonModels.core.medium import IdealGasMedium -from PythonModels.core.network import SimulationNetwork -from PythonModels.core.solver import SolveIVPConfig, integrate_ode -from PythonModels.systems.testmodel_closure import ( - BranchClosureComponents, - InitializationDiagnostics, - TestModelClosure, - TestModelClosureComponents, - TestModelSnapshot, -) - - -@dataclass(frozen=True) -class CylinderConfig: - volume: float = 0.01 - p0: float = 35e6 - T0: float = 300.0 - - -@dataclass(frozen=True) -class OrificeConfig: - K: float = 1e-5 - - -@dataclass(frozen=True) -class TankConfig: - volume: float = 0.1 - p0: float = 1e5 - T0: float = 300.0 - - -@dataclass(frozen=True) -class PipeConfig: - length: float = 5.0 - diameter: float = 0.02 - lambda_darcy: float = 0.02 - p0: float = 1e5 - T0: float = 300.0 - - -@dataclass(frozen=True) -class BranchConfig: - orifice: OrificeConfig = field(default_factory=OrificeConfig) - pipe: PipeConfig = field(default_factory=PipeConfig) - - -@dataclass(frozen=True) -class TestModelConfig: - cylinder: CylinderConfig = field(default_factory=CylinderConfig) - upper_branch: BranchConfig = field(default_factory=BranchConfig) - lower_branch: BranchConfig = field(default_factory=BranchConfig) - tank: TankConfig = field(default_factory=TankConfig) - - -class TestModelSystem: - """Runnable first-pass Python system for the current Testmodel topology. - - This version keeps the component split from the Modelica model while keeping - the downstream tee-tank pressure coupling in the ODE framework. The original - Modelica system is a tighter DAE because both pipe outlets discharge into an - ideal lossless junction directly connected to the tank. Here the branch - outlet flows are solved from a pressure-consistent energy balance so the - outlet is no longer driven by an arbitrary conductance parameter. - """ - - def __init__( - self, - medium: IdealGasMedium | None = None, - config: TestModelConfig | None = None, - ) -> None: - self.medium = medium or IdealGasMedium() - self.config = config or TestModelConfig() - - self.mycylinder = Cylinder( - name="mycylinder", - medium=self.medium, - V=self.config.cylinder.volume, - p0=self.config.cylinder.p0, - T0=self.config.cylinder.T0, - ) - self.mytee = Tee(name="mytee") - self.myorifice = Orifice(name="myorifice", K=self.config.upper_branch.orifice.K) - self.mypipe = Pipe( - name="mypipe", - medium=self.medium, - L=self.config.upper_branch.pipe.length, - D=self.config.upper_branch.pipe.diameter, - lambda_darcy=self.config.upper_branch.pipe.lambda_darcy, - p0=self.config.upper_branch.pipe.p0, - T0=self.config.upper_branch.pipe.T0, - ) - self.myorifice1 = Orifice(name="myorifice1", K=self.config.lower_branch.orifice.K) - self.mypipe1 = Pipe( - name="mypipe1", - medium=self.medium, - L=self.config.lower_branch.pipe.length, - D=self.config.lower_branch.pipe.diameter, - lambda_darcy=self.config.lower_branch.pipe.lambda_darcy, - p0=self.config.lower_branch.pipe.p0, - T0=self.config.lower_branch.pipe.T0, - ) - self.mytee1 = Tee(name="mytee1") - self.mytank = Tank( - name="mytank", - medium=self.medium, - V=self.config.tank.volume, - p0=self.config.tank.p0, - T0=self.config.tank.T0, - ) - - self.network = SimulationNetwork(name="Testmodel") - for component in ( - self.mycylinder, - self.mytee, - self.myorifice, - self.mypipe, - self.myorifice1, - self.mypipe1, - self.mytee1, - self.mytank, - ): - self.network.add_component(component) - - self.network.connect("mycylinder", "port_b", "mytee", "port_in") - self.network.connect("mytee", "port_out1", "myorifice", "port_a") - self.network.connect("myorifice", "port_b", "mypipe", "port_a") - self.network.connect("mypipe", "port_b", "mytee1", "port_out2") - self.network.connect("mytee", "port_out2", "myorifice1", "port_a") - self.network.connect("myorifice1", "port_b", "mypipe1", "port_a") - self.network.connect("mypipe1", "port_b", "mytee1", "port_out1") - self.network.connect("mytee1", "port_in", "mytank", "port_a") - - self.closure = TestModelClosure( - medium=self.medium, - components=TestModelClosureComponents( - cylinder=self.mycylinder, - upstream_tee=self.mytee, - upper_branch=BranchClosureComponents( - name="upper_branch", - orifice=self.myorifice, - pipe=self.mypipe, - ), - lower_branch=BranchClosureComponents( - name="lower_branch", - orifice=self.myorifice1, - pipe=self.mypipe1, - ), - downstream_tee=self.mytee1, - tank=self.mytank, - ), - initial_state_vector=self.initial_state_vector, - apply_state_vector=self.apply_state_vector, - ) - - def initial_state_vector(self) -> list[float]: - return self.network.initial_state_vector() - - def apply_state_vector(self, values: list[float]) -> None: - self.network.apply_state_vector(values) - - def consistent_initial_state_vector(self) -> list[float]: - return self.closure.consistent_initial_state_vector() - - @property - def last_solve_diagnostics(self): - return self.closure.last_solve_diagnostics - - def initialize_consistent_state( - self, - max_iterations: int = 12, - state_tolerance: float = 1e-9, - flow_tolerance: float = 1e-9, - enthalpy_tolerance: float = 1e-6, - pressure_tolerance: float = 1e-6, - strict_internal_solvers: bool = False, - ) -> InitializationDiagnostics: - return self.closure.initialize_consistent_state( - max_iterations=max_iterations, - state_tolerance=state_tolerance, - flow_tolerance=flow_tolerance, - enthalpy_tolerance=enthalpy_tolerance, - pressure_tolerance=pressure_tolerance, - strict_internal_solvers=strict_internal_solvers, - ) - - def project_downstream_pressure_constraints(self, *, strict: bool = False) -> None: - self.closure.project_downstream_pressure_constraints(strict=strict) - - def snapshot( - self, - state_vector: list[float] | None = None, - *, - strict: bool = False, - ) -> TestModelSnapshot: - return self.closure.snapshot(state_vector, strict=strict) - - def rhs(self, _t: float, state_vector: list[float]) -> list[float]: - return self.closure.rhs(state_vector) - - @staticmethod - def _legacy_branch_series_key_map() -> tuple[tuple[str, str, str], tuple[str, str, str]]: - return ( - ("upper_branch", "branch_upper.in", "branch_upper.out"), - ("lower_branch", "branch_lower.in", "branch_lower.out"), - ) - - @classmethod - def _legacy_branch_series_keys_by_name(cls) -> dict[str, tuple[str, str]]: - return { - branch_name: (inlet_key, outlet_key) - for branch_name, inlet_key, outlet_key in cls._legacy_branch_series_key_map() - } - - @staticmethod - def _generic_branch_series_keys(branch_name: str) -> tuple[str, str, str]: - return ( - f"branch.{branch_name}.p", - f"branch.{branch_name}.in", - f"branch.{branch_name}.out", - ) - - @staticmethod - def _legacy_branch_pressure_keys_by_name() -> dict[str, str]: - return { - "upper_branch": "mypipe.p", - "lower_branch": "mypipe1.p", - } - - @classmethod - def _append_legacy_branch_series_aliases( - cls, - series: dict[str, list[float]], - ) -> dict[str, list[float]]: - legacy_branch_series_keys = cls._legacy_branch_series_keys_by_name() - legacy_branch_pressure_keys = cls._legacy_branch_pressure_keys_by_name() - for branch_name, (legacy_inlet_key, legacy_outlet_key) in legacy_branch_series_keys.items(): - pressure_key, generic_inlet_key, generic_outlet_key = cls._generic_branch_series_keys( - branch_name - ) - series[legacy_branch_pressure_keys[branch_name]] = list(series[pressure_key]) - series[legacy_inlet_key] = list(series[generic_inlet_key]) - series[legacy_outlet_key] = list(series[generic_outlet_key]) - return series - - def simulate( - self, - config: SolveIVPConfig | None = None, - t_eval: list[float] | None = None, - ) -> Any: - return integrate_ode( - rhs=self.rhs, - initial_state=self.consistent_initial_state_vector(), - config=config or SolveIVPConfig(), - t_eval=t_eval, - ) - - def evaluate_solution(self, solution: Any) -> dict[str, list[float]]: - series = { - "time": [], - "mycylinder.p": [], - "mycylinder.T": [], - "mytank.p": [], - "mytank.T": [], - } - for branch_name, _, _ in self._legacy_branch_series_key_map(): - pressure_key, inlet_key, outlet_key = self._generic_branch_series_keys(branch_name) - series[pressure_key] = [] - series[inlet_key] = [] - series[outlet_key] = [] - - for index, time_value in enumerate(solution.t): - state_vector = [row[index] for row in solution.y] - snapshot = self.snapshot(state_vector) - series["time"].append(float(time_value)) - series["mycylinder.p"].append(snapshot.cylinder.p) - series["mycylinder.T"].append(snapshot.cylinder.T) - series["mytank.p"].append(snapshot.tank.p) - series["mytank.T"].append(snapshot.tank.T) - for branch in snapshot.branches: - pressure_key, generic_inlet_key, generic_outlet_key = self._generic_branch_series_keys( - branch.name - ) - series[pressure_key].append(branch.pipe.p) - series[generic_inlet_key].append(branch.inlet_flow) - series[generic_outlet_key].append(branch.outlet_flow) - - return self._append_legacy_branch_series_aliases(series) - - -def build_testmodel() -> SimulationNetwork: - """Compatibility helper for callers that only need the topology.""" - - return TestModelSystem().network diff --git a/PythonModels/systems/testmodel_closure.py b/PythonModels/systems/testmodel_closure.py deleted file mode 100644 index cbcaff8..0000000 --- a/PythonModels/systems/testmodel_closure.py +++ /dev/null @@ -1,668 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass, field -from typing import Callable - -from PythonModels.components.cylinder import Cylinder -from PythonModels.components.orifice import Orifice -from PythonModels.components.pipe import Pipe -from PythonModels.components.tank import Tank -from PythonModels.components.tee import Tee -from PythonModels.core.medium import IdealGasMedium, ThermodynamicProperties -from PythonModels.core.state import VolumeState - - -@dataclass(frozen=True) -class BranchInletFlowDiagnostics: - converged: bool - iterations: int - residual: float - m_flow: float - inlet_pressure: float - - -@dataclass(frozen=True) -class DownstreamPressureDiagnostics: - converged: bool - iterations: int - residual: float - pressure: float - target_total_internal_energy: float - - -@dataclass(frozen=True) -class TestModelSolveDiagnostics: - upper_branch_inlet: BranchInletFlowDiagnostics - lower_branch_inlet: BranchInletFlowDiagnostics - downstream_pressure_projection: DownstreamPressureDiagnostics | None - - -@dataclass(frozen=True) -class BranchClosureComponents: - name: str - orifice: Orifice - pipe: Pipe - - -@dataclass(frozen=True) -class BranchClosureState: - name: str - pipe: ThermodynamicProperties - inlet_flow: float - outlet_flow: float - inlet_h: float - inlet_flow_diagnostics: BranchInletFlowDiagnostics - - -@dataclass(frozen=True) -class BranchSnapshot: - name: str - pipe: ThermodynamicProperties - inlet_flow: float - outlet_flow: float - inlet_h: float - inlet_flow_diagnostics: BranchInletFlowDiagnostics - - -@dataclass(frozen=True) -class TestModelSnapshot: - cylinder: ThermodynamicProperties - tank: ThermodynamicProperties - tee_upstream_h: float - tee_downstream_h: float - branches: tuple[BranchSnapshot, ...] = field(default_factory=tuple) - solve_diagnostics: TestModelSolveDiagnostics | None = None - - @property - def pipe_upper(self) -> ThermodynamicProperties: - return self.branches[0].pipe - - @property - def pipe_lower(self) -> ThermodynamicProperties: - return self.branches[1].pipe - - @property - def branch_inlet_flows(self) -> tuple[float, ...]: - return tuple(branch.inlet_flow for branch in self.branches) - - @property - def branch_outlet_flows(self) -> tuple[float, ...]: - return tuple(branch.outlet_flow for branch in self.branches) - - -@dataclass(frozen=True) -class InitializationDiagnostics: - converged: bool - iterations: int - max_state_delta: float - max_flow_delta: float - max_enthalpy_delta: float - downstream_pressure_spread: float - state_vector: tuple[float, ...] - - -@dataclass(frozen=True) -class TestModelClosureComponents: - cylinder: Cylinder - upstream_tee: Tee - upper_branch: BranchClosureComponents - lower_branch: BranchClosureComponents - downstream_tee: Tee - tank: Tank - - def branches(self) -> tuple[BranchClosureComponents, BranchClosureComponents]: - return (self.upper_branch, self.lower_branch) - - -class TestModelClosure: - """Owns Testmodel-specific closure, projection and port-writeback logic.""" - - def __init__( - self, - *, - medium: IdealGasMedium, - components: TestModelClosureComponents, - initial_state_vector: Callable[[], list[float]], - apply_state_vector: Callable[[list[float]], None], - ) -> None: - self.medium = medium - self.components = components - self._initial_state_vector = initial_state_vector - self._apply_state_vector = apply_state_vector - self.last_solve_diagnostics: TestModelSolveDiagnostics | None = None - self.last_downstream_pressure_diagnostics: DownstreamPressureDiagnostics | None = None - - @staticmethod - def _downstream_pressure_spread(snapshot: TestModelSnapshot) -> float: - downstream_pressures = tuple(branch.pipe.p for branch in snapshot.branches) + ( - snapshot.tank.p, - ) - return max(downstream_pressures) - min(downstream_pressures) - - @staticmethod - def _initialization_flow_delta( - previous_snapshot: TestModelSnapshot | None, - current_snapshot: TestModelSnapshot, - ) -> float: - if previous_snapshot is None: - return max(abs(branch.outlet_flow) for branch in current_snapshot.branches) - return max( - abs(curr - prev) - for curr, prev in zip( - current_snapshot.branch_outlet_flows, - previous_snapshot.branch_outlet_flows, - ) - ) - - @staticmethod - def _initialization_enthalpy_delta( - previous_snapshot: TestModelSnapshot | None, - current_snapshot: TestModelSnapshot, - ) -> float: - if previous_snapshot is None: - return abs(current_snapshot.tee_downstream_h - current_snapshot.tank.h) - return max( - abs(current_snapshot.tee_upstream_h - previous_snapshot.tee_upstream_h), - abs(current_snapshot.tee_downstream_h - previous_snapshot.tee_downstream_h), - ) - - def consistent_initial_state_vector(self) -> list[float]: - return list(self.initialize_consistent_state().state_vector) - - def initialize_consistent_state( - self, - max_iterations: int = 12, - state_tolerance: float = 1e-9, - flow_tolerance: float = 1e-9, - enthalpy_tolerance: float = 1e-6, - pressure_tolerance: float = 1e-6, - strict_internal_solvers: bool = False, - ) -> InitializationDiagnostics: - raw_state = self._initial_state_vector() - previous_snapshot: TestModelSnapshot | None = None - diagnostics: InitializationDiagnostics | None = None - - for iteration in range(1, max_iterations + 1): - state_before_projection = self._initial_state_vector() - self.snapshot(state_before_projection, strict=strict_internal_solvers) - - self.project_downstream_pressure_constraints(strict=strict_internal_solvers) - - state_after_projection = self._initial_state_vector() - snapshot_after_projection = self.snapshot( - state_after_projection, - strict=strict_internal_solvers, - ) - - max_state_delta = max( - abs(after - before) - for before, after in zip(state_before_projection, state_after_projection) - ) - max_flow_delta = self._initialization_flow_delta( - previous_snapshot, - snapshot_after_projection, - ) - max_enthalpy_delta = self._initialization_enthalpy_delta( - previous_snapshot, - snapshot_after_projection, - ) - downstream_pressure_spread = self._downstream_pressure_spread( - snapshot_after_projection, - ) - - diagnostics = InitializationDiagnostics( - converged=( - max_state_delta <= state_tolerance - and max_flow_delta <= flow_tolerance - and max_enthalpy_delta <= enthalpy_tolerance - and downstream_pressure_spread <= pressure_tolerance - ), - iterations=iteration, - max_state_delta=max_state_delta, - max_flow_delta=max_flow_delta, - max_enthalpy_delta=max_enthalpy_delta, - downstream_pressure_spread=downstream_pressure_spread, - state_vector=tuple(state_after_projection), - ) - previous_snapshot = snapshot_after_projection - - if diagnostics.converged: - self._apply_state_vector(raw_state) - return diagnostics - - assert diagnostics is not None - self._apply_state_vector(raw_state) - return diagnostics - - def _solve_branch_inlet_flow( - self, - orifice: Orifice, - pipe: Pipe, - p_upstream: float, - pipe_props: ThermodynamicProperties, - *, - strict: bool = False, - ) -> tuple[float, BranchInletFlowDiagnostics]: - m_flow = orifice.mass_flow(p_upstream, pipe_props.p) - rho = max(pipe_props.rho, 1e-9) - p_inlet = pipe.inlet_pressure(m_flow, rho, pipe_props.p) - residual = abs(orifice.mass_flow(p_upstream, p_inlet) - m_flow) - converged = False - iterations = 0 - for iteration in range(1, 9): - p_inlet = pipe.inlet_pressure(m_flow, rho, pipe_props.p) - next_m_flow = orifice.mass_flow(p_upstream, p_inlet) - residual = abs(next_m_flow - m_flow) - iterations = iteration - if residual <= 1e-9 * max(1.0, abs(next_m_flow)): - m_flow = next_m_flow - converged = True - break - m_flow = next_m_flow - diagnostics = BranchInletFlowDiagnostics( - converged=converged, - iterations=iterations, - residual=residual, - m_flow=m_flow, - inlet_pressure=p_inlet, - ) - if strict and not diagnostics.converged: - raise RuntimeError( - f"Branch inlet flow solve did not converge for {pipe.name}: residual={residual:.6e}" - ) - return m_flow, diagnostics - - def _solve_downstream_branch_flows( - self, - cylinder: ThermodynamicProperties, - tank: ThermodynamicProperties, - branch_states: tuple[BranchClosureState, BranchClosureState], - ) -> tuple[float, float]: - return self._solve_downstream_branch_flows_from_state( - inlet_h_upper=branch_states[0].inlet_h, - inlet_h_lower=branch_states[1].inlet_h, - pipe_upper_h=max(branch_states[0].pipe.h, 1e-9), - pipe_lower_h=max(branch_states[1].pipe.h, 1e-9), - tank_h=max(tank.h, 1e-9), - q_in_upper=branch_states[0].inlet_flow, - q_in_lower=branch_states[1].inlet_flow, - ) - - def _project_volume_energy_to_pressure( - self, - component: Pipe | Tank, - target_pressure: float, - ) -> None: - target_temperature = target_pressure * component.V / ( - max(component.state.m, 1e-12) * self.medium.R_gas - ) - target_internal_energy = ( - component.state.m * self.medium.specific_internal_energy(target_temperature) - ) - component.state = VolumeState(m=component.state.m, U=target_internal_energy) - - def _downstream_total_internal_energy_for_pressure( - self, - target_pressure: float, - downstream_components: tuple[Pipe | Tank, ...], - ) -> float: - total_internal_energy = 0.0 - for component in downstream_components: - target_temperature = target_pressure * component.V / ( - max(component.state.m, 1e-12) * self.medium.R_gas - ) - total_internal_energy += ( - component.state.m * self.medium.specific_internal_energy(target_temperature) - ) - return total_internal_energy - - def _solve_downstream_common_pressure( - self, - downstream_components: tuple[Pipe | Tank, ...], - target_total_internal_energy: float, - *, - strict: bool = False, - ) -> tuple[float, DownstreamPressureDiagnostics]: - lower_pressure = 1.0 - upper_pressure = max(component.properties().p for component in downstream_components) - upper_pressure = max(upper_pressure, 1e5) - - def residual(pressure: float) -> float: - return ( - self._downstream_total_internal_energy_for_pressure( - pressure, - downstream_components, - ) - - target_total_internal_energy - ) - - upper_residual = residual(upper_pressure) - iteration_count = 0 - - while upper_residual < 0.0: - upper_pressure *= 2.0 - upper_residual = residual(upper_pressure) - - final_pressure = 0.5 * (lower_pressure + upper_pressure) - final_residual = residual(final_pressure) - converged = False - for iteration in range(1, 81): - middle_pressure = 0.5 * (lower_pressure + upper_pressure) - middle_residual = residual(middle_pressure) - iteration_count = iteration - final_pressure = middle_pressure - final_residual = middle_residual - if abs(middle_residual) <= 1e-12 * max(1.0, target_total_internal_energy): - converged = True - break - if middle_residual > 0.0: - upper_pressure = middle_pressure - else: - lower_pressure = middle_pressure - - diagnostics = DownstreamPressureDiagnostics( - converged=converged, - iterations=iteration_count, - residual=final_residual, - pressure=final_pressure, - target_total_internal_energy=target_total_internal_energy, - ) - if strict and not diagnostics.converged: - raise RuntimeError( - "Downstream common-pressure solve did not converge: " - f"residual={final_residual:.6e}" - ) - return final_pressure, diagnostics - - def project_downstream_pressure_constraints(self, *, strict: bool = False) -> None: - downstream_components = ( - self.components.upper_branch.pipe, - self.components.lower_branch.pipe, - self.components.tank, - ) - total_internal_energy = sum(component.state.U for component in downstream_components) - common_pressure, diagnostics = self._solve_downstream_common_pressure( - downstream_components, - total_internal_energy, - strict=strict, - ) - self.last_downstream_pressure_diagnostics = diagnostics - - for component in downstream_components: - self._project_volume_energy_to_pressure(component, common_pressure) - - def _downstream_connection_enthalpy( - self, - q_out_upper: float, - q_out_lower: float, - pipe_upper_h: float, - pipe_lower_h: float, - tank_h: float, - ) -> float: - return self.components.downstream_tee.inlet_stream_enthalpy( - q_out_lower, - pipe_lower_h, - q_out_upper, - pipe_upper_h, - fallback_h=tank_h, - ) - - def _solve_downstream_branch_flows_from_state( - self, - *, - inlet_h_upper: float, - inlet_h_lower: float, - pipe_upper_h: float, - pipe_lower_h: float, - tank_h: float, - q_in_upper: float, - q_in_lower: float, - ) -> tuple[float, float]: - return self.components.downstream_tee.solve_branch_outlet_flows_from_energy_balance( - ratio_branch1=self.components.upper_branch.pipe.V / self.components.tank.V, - ratio_branch2=self.components.lower_branch.pipe.V / self.components.tank.V, - inlet_h_branch1=inlet_h_upper, - inlet_h_branch2=inlet_h_lower, - branch1_h=pipe_upper_h, - branch2_h=pipe_lower_h, - inlet_h=tank_h, - q_in_branch1=q_in_upper, - q_in_branch2=q_in_lower, - ) - - def _evaluate_branch_states( - self, - cylinder: ThermodynamicProperties, - ) -> tuple[BranchClosureState, BranchClosureState]: - states: list[BranchClosureState] = [] - for branch in self.components.branches(): - pipe_properties = branch.pipe.properties() - inlet_flow, inlet_flow_diagnostics = self._solve_branch_inlet_flow( - branch.orifice, - branch.pipe, - cylinder.p, - pipe_properties, - ) - inlet_h = branch.pipe.port_a_inlet_enthalpy( - port_a_m_flow=inlet_flow, - connected_h=cylinder.h, - internal_h=pipe_properties.h, - ) - states.append( - BranchClosureState( - name=branch.name, - pipe=pipe_properties, - inlet_flow=inlet_flow, - outlet_flow=0.0, - inlet_h=inlet_h, - inlet_flow_diagnostics=inlet_flow_diagnostics, - ) - ) - return (states[0], states[1]) - - @staticmethod - def _with_branch_outlet_flows( - branch_states: tuple[BranchClosureState, BranchClosureState], - outlet_flows: tuple[float, float], - ) -> tuple[BranchClosureState, BranchClosureState]: - return ( - BranchClosureState( - name=branch_states[0].name, - pipe=branch_states[0].pipe, - inlet_flow=branch_states[0].inlet_flow, - outlet_flow=outlet_flows[0], - inlet_h=branch_states[0].inlet_h, - inlet_flow_diagnostics=branch_states[0].inlet_flow_diagnostics, - ), - BranchClosureState( - name=branch_states[1].name, - pipe=branch_states[1].pipe, - inlet_flow=branch_states[1].inlet_flow, - outlet_flow=outlet_flows[1], - inlet_h=branch_states[1].inlet_h, - inlet_flow_diagnostics=branch_states[1].inlet_flow_diagnostics, - ), - ) - - @staticmethod - def _branch_snapshots( - branch_states: tuple[BranchClosureState, BranchClosureState], - ) -> tuple[BranchSnapshot, BranchSnapshot]: - return ( - BranchSnapshot( - name=branch_states[0].name, - pipe=branch_states[0].pipe, - inlet_flow=branch_states[0].inlet_flow, - outlet_flow=branch_states[0].outlet_flow, - inlet_h=branch_states[0].inlet_h, - inlet_flow_diagnostics=branch_states[0].inlet_flow_diagnostics, - ), - BranchSnapshot( - name=branch_states[1].name, - pipe=branch_states[1].pipe, - inlet_flow=branch_states[1].inlet_flow, - outlet_flow=branch_states[1].outlet_flow, - inlet_h=branch_states[1].inlet_h, - inlet_flow_diagnostics=branch_states[1].inlet_flow_diagnostics, - ), - ) - - def snapshot( - self, - state_vector: list[float] | None = None, - *, - strict: bool = False, - ) -> TestModelSnapshot: - if state_vector is not None: - self._apply_state_vector(list(state_vector)) - - cylinder = self.components.cylinder.properties() - tank = self.components.tank.properties() - branch_states = self._evaluate_branch_states(cylinder) - if strict: - for branch_state in branch_states: - if not branch_state.inlet_flow_diagnostics.converged: - raise RuntimeError( - "Branch inlet flow solve did not converge for " - f"{branch_state.name}: residual=" - f"{branch_state.inlet_flow_diagnostics.residual:.6e}" - ) - outlet_flows = self._solve_downstream_branch_flows(cylinder, tank, branch_states) - branch_states = self._with_branch_outlet_flows(branch_states, outlet_flows) - - tee_upstream_h = self.components.upstream_tee.inlet_stream_enthalpy( - -branch_states[0].inlet_flow, - branch_states[0].pipe.h, - -branch_states[1].inlet_flow, - branch_states[1].pipe.h, - fallback_h=cylinder.h, - ) - tee_downstream_h = self._downstream_connection_enthalpy( - branch_states[0].outlet_flow, - branch_states[1].outlet_flow, - branch_states[0].pipe.h, - branch_states[1].pipe.h, - tank.h, - ) - - self._write_port_states( - cylinder, - tank, - branch_states, - tee_upstream_h, - tee_downstream_h, - ) - - solve_diagnostics = TestModelSolveDiagnostics( - upper_branch_inlet=branch_states[0].inlet_flow_diagnostics, - lower_branch_inlet=branch_states[1].inlet_flow_diagnostics, - downstream_pressure_projection=self.last_downstream_pressure_diagnostics, - ) - self.last_solve_diagnostics = solve_diagnostics - branch_snapshots = self._branch_snapshots(branch_states) - - return TestModelSnapshot( - cylinder=cylinder, - tank=tank, - tee_upstream_h=tee_upstream_h, - tee_downstream_h=tee_downstream_h, - branches=branch_snapshots, - solve_diagnostics=solve_diagnostics, - ) - - def _write_port_states( - self, - cylinder: ThermodynamicProperties, - tank: ThermodynamicProperties, - branch_states: tuple[BranchClosureState, BranchClosureState], - tee_upstream_h: float, - tee_downstream_h: float, - ) -> None: - cylinder_m_flow = -sum(branch_state.inlet_flow for branch_state in branch_states) - tank_m_flow = sum(branch_state.outlet_flow for branch_state in branch_states) - - self.components.cylinder.port_b.m_flow = cylinder_m_flow - - self.components.upstream_tee.port_in.p = cylinder.p - self.components.upstream_tee.port_out1.p = cylinder.p - self.components.upstream_tee.port_out2.p = cylinder.p - self.components.upstream_tee.port_in.m_flow = cylinder_m_flow - self.components.upstream_tee.port_in.h_outflow = tee_upstream_h - self.components.upstream_tee.port_out1.h_outflow = cylinder.h - self.components.upstream_tee.port_out2.h_outflow = cylinder.h - self.components.upstream_tee.port_out1.m_flow = -branch_states[0].inlet_flow - self.components.upstream_tee.port_out2.m_flow = -branch_states[1].inlet_flow - - for branch_components, branch_state in zip(self.components.branches(), branch_states): - branch_components.orifice.port_a.p = cylinder.p - branch_components.orifice.port_b.p = branch_components.pipe.inlet_pressure( - branch_state.inlet_flow, - max(branch_state.pipe.rho, 1e-9), - branch_state.pipe.p, - ) - branch_components.orifice.port_a.m_flow = branch_state.inlet_flow - branch_components.orifice.port_b.m_flow = -branch_state.inlet_flow - branch_components.orifice.port_a.h_outflow = cylinder.h - branch_components.orifice.port_b.h_outflow = branch_state.pipe.h - - branch_components.pipe.port_a.p = branch_components.orifice.port_b.p - branch_components.pipe.port_a.m_flow = branch_state.inlet_flow - branch_components.pipe.port_b.m_flow = -branch_state.outlet_flow - branch_components.pipe.port_b.p = branch_state.pipe.p - - self.components.downstream_tee.port_in.p = tank.p - self.components.downstream_tee.port_out1.p = tank.p - self.components.downstream_tee.port_out2.p = tank.p - self.components.downstream_tee.port_in.m_flow = -tank_m_flow - self.components.downstream_tee.port_out1.m_flow = branch_states[1].outlet_flow - self.components.downstream_tee.port_out2.m_flow = branch_states[0].outlet_flow - self.components.downstream_tee.port_in.h_outflow = tee_downstream_h - self.components.downstream_tee.port_out1.h_outflow = tank.h - self.components.downstream_tee.port_out2.h_outflow = tank.h - - self.components.tank.port_a.m_flow = tank_m_flow - - def _branch_derivative_states( - self, - snapshot: TestModelSnapshot, - ) -> tuple[VolumeState, VolumeState]: - derivative_states: list[VolumeState] = [] - for branch_components, branch_snapshot in zip(self.components.branches(), snapshot.branches): - derivative_states.append( - branch_components.pipe.derivatives_from_connections( - port_a_m_flow=branch_snapshot.inlet_flow, - connected_h_a=snapshot.cylinder.h, - port_b_m_flow=-branch_snapshot.outlet_flow, - connected_h_b=snapshot.tank.h, - internal_h=branch_snapshot.pipe.h, - ) - ) - return (derivative_states[0], derivative_states[1]) - - def rhs(self, state_vector: list[float]) -> list[float]: - snapshot = self.snapshot(state_vector) - - cylinder_m_flow = -sum(branch.inlet_flow for branch in snapshot.branches) - tank_m_flow = sum(branch.outlet_flow for branch in snapshot.branches) - d_cylinder = self.components.cylinder.derivatives_from_connection( - connected_h=snapshot.tee_upstream_h, - port_m_flow=cylinder_m_flow, - internal_h=snapshot.cylinder.h, - ) - branch_derivatives = self._branch_derivative_states(snapshot) - d_tank = self.components.tank.derivatives_from_connection( - connected_h=snapshot.tee_downstream_h, - port_m_flow=tank_m_flow, - internal_h=snapshot.tank.h, - ) - - return [ - d_cylinder.m, - d_cylinder.U, - branch_derivatives[0].m, - branch_derivatives[0].U, - branch_derivatives[1].m, - branch_derivatives[1].U, - d_tank.m, - d_tank.U, - ] diff --git a/README.md b/README.md index 45799de..1ed85fa 100644 --- a/README.md +++ b/README.md @@ -8,16 +8,16 @@ ReactFlow 系统建模与 `app.simulation` 仿真后端。 - `POST /api/reactflow/system-xml`:导出 System XML v2。 - `POST /api/reactflow/compile-model`:将 ReactFlow 节点、参数和连线编译为仿真网络,并返回组件端口、无方向物理连接、压力-流量方程结构及未连接端口。 - `POST /api/reactflow/simulate-testmodel`:运行现有固定拓扑 TestModel;该接口暂时不是任意拓扑求解器。 -- `POST /api/reactflow/simulate-test-mql`:运行现有固定拓扑 AMESim `test_mql` 迁移模型;该接口不把 AMESim 子模型注册为公开拖拽组件。 +- `POST /api/reactflow/simulate-test-mql`:返回固定拓扑 AMESim `test_mql` 的结构与采样摘要;132 状态数值对比使用独立 comparison 入口。AMESim 子模型已有 19 个第一版公开模型,但该接口本身不是任意拖拽拓扑求解器。 - `POST /api/system-xml/validate`:接收原始 System XML v2,返回 XML、XSD 和模型语义三层诊断。 - `POST /api/system-xml/parse`:校验 XML 并返回规范化的 ReactFlow 工程对象。 - `POST /api/system-xml/compile-model`:校验并解析 XML,然后创建 `app.simulation` 组件网络。 -- `POST /api/system-xml/simulate`:按 XML 中的组件、物理连接、参数和仿真设置运行通用气动网络 MVP,并返回组件及端口时间序列。 +- `POST /api/system-xml/simulate`:按 XML 中的组件、连接、参数和仿真设置运行当前支持的气动、标量信号及一维机械网络 MVP,并返回组件及端口时间序列。 - `POST /api/simulation-results/csv`:校验结构化结果快照并导出 UTF-8 CSV 文件。 气动端口的后端契约采用 `p` 势变量相等、`m_flow` 流变量代数和为零、`h_outflow` 按 stream 规则混合。所有组件统一规定 `m_flow > 0` 表示流入组件,物理连接的端点顺序不表示流向。 -当前网络层可以从组件和连接生成压力-流量残差,使用 SciPy 完成非线性代数闭合和时间积分,并按实际流向传播 stream 焓。XML 通用仿真当前采用半显式 ODE/代数 MVP:气瓶和贮箱作为储能元件,孔板及 XML 管段作为阻性元件,三通作为等压零结点。它不是完整 DAE 求解器,也不等价于严格 Modelica.Fluid 实现。 +当前网络层可按端口域处理气动压力-流量残差与 stream 焓、标量信号传播,以及一维机械 `x/v` 等值和 `f` 平衡,并使用 SciPy 完成非线性代数闭合和时间积分。XML 通用仿真当前采用半显式 ODE/代数 MVP:气瓶和贮箱作为储能元件,孔板及 XML 管段作为阻性元件,三通作为等压零结点,同时支持已登记的信号和机械基础件。它不是完整 DAE 或事件求解器,也不等价于严格 Modelica.Fluid 实现。 XML 解析依赖 `lxml` 执行本地 XSD 校验。安装或更新 Python 环境时使用: diff --git a/app/main.py b/app/main.py index c722a86..02655dd 100644 --- a/app/main.py +++ b/app/main.py @@ -28,6 +28,7 @@ from app.system_xml import ( ) if TYPE_CHECKING: + from app.simulation.components.amesim.gases import AmesimGasRegistry from app.simulation.core.ports import PortDefinition from app.simulation.systems.network import SimulationNetwork @@ -135,6 +136,7 @@ class ReactFlowSimulationConfig(BaseModel): class ReactFlowProjectPayload(BaseModel): name: str = "untitled" + mediumReferenceVersion: Literal[1] | None = None nodes: list[ReactFlowNodePayload] = Field(default_factory=list) edges: list[ReactFlowEdgePayload] = Field(default_factory=list) simulation: ReactFlowSimulationConfig = Field(default_factory=ReactFlowSimulationConfig) @@ -891,12 +893,15 @@ def _compile_xml_document_or_422( def build_reactflow_system_xml(project: ReactFlowProjectPayload) -> bytes: + from app.simulation.registry import get_component_model_spec + system = ET.Element( "System", { "name": project.name, "schemaVersion": SYSTEM_XML_SCHEMA_VERSION, "unitSystem": SYSTEM_XML_UNIT_SYSTEM, + "mediumReferenceVersion": "1", }, ) ET.SubElement( @@ -953,7 +958,20 @@ def build_reactflow_system_xml(project: ReactFlowProjectPayload) -> bytes: port_definition.positiveFlowDirection or "intoComponent" ) ET.SubElement(component_node, "Port", port_attributes) - for name, value in node.data.parameters.items(): + parameter_values = dict(node.data.parameters) + try: + component_spec = get_component_model_spec(node.data.modelType) + except ValueError: + component_spec = None + if ( + component_spec is not None + and component_spec.display.role == "amesimGasMediumDefinition" + ): + for parameter in component_spec.parameters: + if parameter.editor == "amesimGasPropertyModel": + parameter_values.setdefault(parameter.name, parameter.default) + + for name, value in parameter_values.items(): ET.SubElement( component_node, "Parameter", @@ -1086,14 +1104,34 @@ def validate_compatible_ports( ) -def compile_reactflow_network(project: ReactFlowProjectPayload) -> "SimulationNetwork": - from app.simulation.core.medium import IdealGasMedium +def compile_reactflow_network( + project: ReactFlowProjectPayload, + *, + amesim_gas_registry: "AmesimGasRegistry | None" = None, +) -> "SimulationNetwork": + from app.simulation.components.amesim.gases import ( + AMESIM_BUILTIN_AIR_GAS_INDEX, + default_amesim_gas_registry, + ) + from app.simulation.components.amesim.media.properties import ( + AmesimGasMediumDefinitionComponent, + ) from app.simulation.registry import get_component_model_spec from app.simulation.systems.network import SimulationNetwork - medium = IdealGasMedium() - network = SimulationNetwork(name=project.name) + if amesim_gas_registry is None: + amesim_gas_registry = default_amesim_gas_registry() + else: + amesim_gas_registry = amesim_gas_registry.copy() + network = SimulationNetwork(name=project.name) + resolved_nodes = [] + specs_by_component_id = {} + gas_indices_by_component_id = {} + + # Phase 1: normalize every node and consume compile-time medium definitions. + # A definition node represents one complete property-method instance and is + # intentionally not added to the equation network. for node in project.nodes: spec = get_component_model_spec(node.data.modelType) parameter_values = { @@ -1107,7 +1145,73 @@ def compile_reactflow_network(project: ReactFlowProjectPayload) -> "SimulationNe + ", ".join(sorted(unknown_parameters)) + "." ) - component = spec.create(node.id, medium, parameter_values) + if issubclass( + spec.component_class, + AmesimGasMediumDefinitionComponent, + ): + definition_component = spec.create( + node.id, + amesim_gas_registry.resolve(AMESIM_BUILTIN_AIR_GAS_INDEX), + parameter_values, + ) + assert isinstance( + definition_component, + AmesimGasMediumDefinitionComponent, + ) + validate_component_port_interface( + node, + definition_component.port_definitions, + ) + try: + amesim_gas_registry.register( + definition_component.gas_definition() + ) + except ValueError as exc: + raise ValueError( + f"AMESim medium definition component '{node.id}' is invalid: " + f"{exc}" + ) from exc + continue + + resolved_nodes.append((node, spec, parameter_values)) + specs_by_component_id[node.id] = spec + gas_indices_by_component_id[node.id] = ( + parameter_values["gi"] + if spec.library.id == "amesim" and "gi" in spec.parameter_by_name + else None + ) + + # Phase 2: resolve one gas property model for each pneumatic circuit before + # any dynamic component initializes its thermodynamic state. + pneumatic_connections = [] + for edge in project.edges: + source_spec = specs_by_component_id.get(edge.source) + target_spec = specs_by_component_id.get(edge.target) + if source_spec is None or target_spec is None: + continue + source_ports = {port.name: port for port in source_spec.ports} + target_ports = {port.name: port for port in target_spec.ports} + source_port = source_ports.get(edge.sourceHandle or "") + target_port = target_ports.get(edge.targetHandle or "") + if ( + source_port is not None + and target_port is not None + and source_port.domain == "pneumatic" + and target_port.domain == "pneumatic" + ): + pneumatic_connections.append((edge.source, edge.target)) + + media_by_component_id = amesim_gas_registry.resolve_network_media( + gas_indices_by_component_id, + pneumatic_connections, + ) + + for node, spec, parameter_values in resolved_nodes: + component = spec.create( + node.id, + media_by_component_id[node.id], + parameter_values, + ) validate_component_port_interface(node, component.port_definitions) network.add_component(component) @@ -1260,7 +1364,7 @@ def run_reactflow_testmodel(project: ReactFlowProjectPayload) -> dict[str, objec def run_reactflow_test_mql(project: ReactFlowProjectPayload) -> dict[str, object]: from app.simulation.examples.test_mql.run import run_test_mql - from PythonModels.systems.test_mql import TestMqlRunConfig + from app.simulation.examples.test_mql.system import TestMqlRunConfig run_config = TestMqlRunConfig( t_start=project.simulation.t_start, diff --git a/app/simulation/README.md b/app/simulation/README.md index 512bf6e..d7195eb 100644 --- a/app/simulation/README.md +++ b/app/simulation/README.md @@ -16,8 +16,8 @@ - `components/experimental/junctions/`: 三通等连接节点。 - `components/amesim/`: AMESim 气动、信号和机械组件原语。 - `systems/`: 通用仿真网络与 XML 驱动系统装配。 -- `examples/testmodel/`: 固定 TestModel、专用闭合逻辑、基线运行入口,以及 test_mql 当前迁移过程中的系统装配和诊断脚本。 -- `examples/test_mql/`: AMESim `test_mql` 的前端调用运行入口。 +- `examples/testmodel/`: 固定 TestModel、专用闭合逻辑和基线运行入口。 +- `examples/test_mql/`: AMESim `test_mql` 的系统装配、校准原语、诊断和运行入口。 - `reporting/`: CSV、SVG、运行报告、Modelica 对比结果、AMESim 结果读取和诊断报告导出。 - `registry.py`: 从已启用库清单受控发现、校验和实例化组件。 - `paths.py`: 项目、运行产物、基准和 Modelica 参考结果路径。 @@ -29,7 +29,8 @@ FastAPI 的 `GET /api/components/catalog` 会把注册表转换成前端组件目录。ReactFlow 启动时自动读取该接口;接口暂时不可用时使用内置的同结构兜底定义。 -临时组件库的声明入口是 `components/experimental/library.py`。公开模型必须在 +临时组件库的声明入口是 `components/experimental/library.py`,AMESim 第一版 +公开临时库入口是 `components/amesim/library.py`。公开模型必须在 模型类中声明 `MODEL_TYPE / MODEL_VERSION / PORTS / PARAMETERS / RESULT_VARIABLES / DISPLAY / create()`,再把类路径加入库清单。完整规范参见 [`组件模型建模规范 v1`](../../docs/component-model-authoring-spec-v1.md)和 @@ -37,20 +38,24 @@ RESULT_VARIABLES / DISPLAY / create()`,再把类路径加入库清单。完整 当前关键文件: -- `core/medium.py`: 温度相关的理想气体近似介质 `IdealGasMedium` -- `core/peng_robinson.py`: `test_mql` 使用的氦气 Peng-Robinson 物性 +- `core/medium.py`: 气体介质协议 `GasMedium` 与通用理想气体实现 `IdealGasMedium` +- `components/amesim/media/`: AMESim 零端口介质物性定义元件;具体类型确定介质,`property_model` 下拉参数选择计算方法,当前提供空气理想气体和氦气 Peng-Robinson +- `components/amesim/gases.py`: AMESim `gi` 介质物性实例注册表;`gi=0` 固定为空气(理想气体,内置默认),`gi=1..99` 引用画布中的显式介质定义 +- `core/peng_robinson.py`: `test_mql` 与公开氦气介质共用的 Peng-Robinson 状态方程 - `systems/network.py`: `SimulationNetwork`,负责组件注册、连接拓扑和状态向量拼装 - `solvers/solver.py`: `integrate_ode()`,优先走 `SciPy solve_ivp`,缺依赖时回退到内置 RK4,并支持 `t_start == t_stop` 的零时长返回 - `examples/testmodel/dynamic_pipe.py`: TestModel 专用单阻容管道近似,入口压降 + 出口直连内容腔 - `components/experimental/junctions/tee.py`: 三通的最小 stream 混合 helper - `examples/testmodel/system.py`: `Testmodel` 的系统装配壳与外部运行入口 - `examples/testmodel/closure.py`: `Testmodel` 当前专用的闭合、初始化投影、分支求解与端口回写 -- `examples/testmodel/test_mql.py`: `test_mql` 系统装配、132 状态总闭包和关键输出映射 -- `examples/testmodel/test_mql_closure.py`: `test_mql` 气动网络 closure、snapshot、流量计算和端口写回 +- `examples/test_mql/system.py`: `test_mql` 系统装配、132 状态总闭包和关键输出映射 +- `examples/test_mql/closure.py`: `test_mql` 气动网络 closure、snapshot、流量计算和端口写回 +- `examples/test_mql/primitives/`: 固定算例专用的 Peng-Robinson 氦气、管路和机械校准原语 +- `examples/test_mql/structural_network.py`: 固定算例专用的结构网络;不替代带端口契约校验的通用网络 - `reporting/testmodel_outputs.py`: `Testmodel` 的 CSV/SVG/对比摘要导出 - `reporting/amesim_results.py`: AMESim 结果读取入口 -- `examples/testmodel/run_test_mql_full_state_comparison.py`: `test_mql` 短时域 AMESim comparison 和诊断入口 -- `examples/testmodel/run_test_mql.py`: test_mql 基线运行与程序化执行入口 +- `examples/test_mql/run_full_state_comparison.py`: `test_mql` 短时域 AMESim comparison 和诊断入口 +- `examples/test_mql/run.py`: `test_mql` 结构运行与程序化执行入口 - `tests/`: 当前组件契约、XML、通用系统、AMESim 迁移和结果导出测试 ## 当前阶段进度 @@ -278,7 +283,7 @@ print(result.used_modelica_reference) 当前四个主变量的最大误差为: -- `mytank.p`: `max_abs_error = 134.960857 Pa`, `max_rel_error = 0.006798%` +- `mytank.p`: `max_abs_error = 134.960858 Pa`, `max_rel_error = 0.006798%` - `mytank.T`: `max_abs_error = 0.035507 K`, `max_rel_error = 0.009016%` - `mycylinder.p`: `max_abs_error = 1391.986349 Pa`, `max_rel_error = 0.009447%` - `mycylinder.T`: `max_abs_error = 0.009069 K`, `max_rel_error = 0.003870%` @@ -287,31 +292,15 @@ print(result.used_modelica_reference) ## AMESim test_mql 当前进度 -`test_mql` 是从 `AmesimModels/test_mql.ame` 新增迁移的 AMESim 模型,当前只在独立路径下推进,不修改旧 `testmodel`。新增命名保持 AMESim 原始别名和 `Data_Path`,方便后续逐变量对齐。 +`test_mql` 从 `AmesimModels/test_mql.ame` 迁移,并与旧 `testmodel` 保持独立。 +固定算例实现统一位于 `examples/test_mql/`,结果读取和比较能力位于 +`reporting/`;公开拖拽组件由 `components/amesim/library.py` 单独登记。 -当前已经完成: - -- 解析 117 个组件、84 条 LINE 连接、直接组件接触、全局参数、仿真设置以及 AMESim 变量目录。 -- 直接读取 `.ame` 包内 `test_mql_.var` 和 `test_mql_.results`;baseline 包含 1002 个时间点和 1116 个保存变量。 -- 使用氦气 Peng-Robinson 物性,内部统一使用绝对压力,对外按 AMESim 表压和原始单位输出。 -- 实现 `PNCH023 / PNCH012 / PNOR001 / PNVO001`,以及 `PNL0001 / PNL0002 / PNL0003 / PNL00R` 管路和 `PN3NODE2 / P4NODE2` 节点语义。 -- 完成气动真实拓扑装配、canonical flow、端口写回、snapshot 和 112 状态气动 RHS。 -- 实现 `PNRP17 / MECMAS21 / LSTP00A / LMECHN1 / UD00 / FORC` 当前工况可确认的机械行为,并形成 20 状态机械闭包。 -- 将气动和机械部分组合成 132 状态总闭包,接入活塞体积反馈、气动力、外力、端止动和质量约束,可通过现有 solver 短时积分。 -- 建立关键 `Data_Path` 序列导出、output schema、validation、AMESim 插值比较、误差排序、端点诊断和 PNCH012 RHS 项拆解。 - -当前确认的关键细节: - -- `PNRP17` 活塞腔体积使用环形有效面积 `piston_area - rod_area`。 -- `LSTP00A` 的 `gap` 观测单位是 mm,计算接触力前必须转换为 m。 -- `PNCH023` 固定气室初始压力来自 `P0=153 bar` 的绝对压力;AMESim `press` 输出为相对 `101300 Pa` 的表压。 -- `PNCH012` 变容腔初始压力对齐 AMESim 的 `1 bar` 绝对压力,`vol` 输出单位为 cm3,且末端体积等于基础死容积加对应活塞 `vol1`。 -- `MECMAS21` 的 `x1dup / v1dup / acc1dup` 是第二机械端口观测,相对 `x1 / v1 / acc1` 为反号,不是重复同值。 -- 本算例中 `MECMAS21` 的 `Fmin / Fmax / Fvisc / Ffric` 在 AMESim 结果里为零;当前只把这一工况能验证的部分写入测试,没有硬猜未激活碰撞/摩擦状态机。 - -当前默认 `0 -> 1e-5 s` comparison 已定位最大偏差为 `press@pn_c1_8`:初值对齐,但末值绝对误差约 `9.22849 Pa`。RHS 拆解显示边界体积功约 `0.026 W`,端口焓流约 `32722 W`,因此当前首要工作是比较 Python 的 `p4_port3_remote_chamber_to_line_flow` 与 AMESim 的 `dm1@pneumatic_69`,检查单位、符号、PNL0001 阻力和 `pnnode4_16` 节点平衡。 - -当前还不能宣称 `test_mql` 的 Python 时域仿真已经和 AMESim 全局一致。完整说明、运行命令和下一步校准路径见 `AmesimModels/test_mql/README.md`。 +当前已形成 112 个气动状态和 20 个机械状态的总闭包、AMESim 原生结果读取、 +`Data_Path` 输出校验及短时域 comparison。这里不再复制易过期的数值进度; +最新对比结果、运行命令、限制和下一步校准路径以 +[`AmesimModels/test_mql/README.md`](../../AmesimModels/test_mql/README.md) +为唯一说明。当前仍不能宣称 Python 时域仿真与 AMESim 全局一致。 ## Testmodel 当前架构判断 diff --git a/app/simulation/components/amesim/flow/orifices.py b/app/simulation/components/amesim/flow/orifices.py index eef5d17..52da861 100644 --- a/app/simulation/components/amesim/flow/orifices.py +++ b/app/simulation/components/amesim/flow/orifices.py @@ -3,6 +3,10 @@ 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, PortDisplaySpec from app.simulation.core.equations import EquationResidual @@ -10,7 +14,7 @@ from app.simulation.core.metadata import ( ParameterDefinition, ResultVariableDefinition, ) -from app.simulation.core.medium import IdealGasMedium +from app.simulation.core.medium import GasMedium from app.simulation.core.ports import PortDefinition @@ -29,6 +33,7 @@ class AmesimPnor001(AlgebraicComponent): PortDefinition.pneumatic("port_2", nominal_role="bidirectional"), ) PARAMETERS = ( + AMESIM_GAS_INDEX_PARAMETER, ParameterDefinition( "cq", 0.72, @@ -37,6 +42,7 @@ class AmesimPnor001(AlgebraicComponent): unit="", minimum=1.0e-10, maximum=1.0, + description="孔口实际质量流量相对于理想可压缩流量的无量纲修正系数。", ), ParameterDefinition( "area", @@ -46,6 +52,7 @@ class AmesimPnor001(AlgebraicComponent): unit="m2", minimum=0.0, maximum=1.0, + description="选择 Cq/面积方式时用于流量计算的有效孔口面积。", ), ParameterDefinition( "Cv", @@ -54,6 +61,7 @@ class AmesimPnor001(AlgebraicComponent): quantity="dimensionless", unit="", minimum=0.0, + description="选择 Cv 方式时使用的英制流量系数。", ), ParameterDefinition( "Kv", @@ -62,15 +70,7 @@ class AmesimPnor001(AlgebraicComponent): quantity="dimensionless", unit="", minimum=0.0, - ), - ParameterDefinition( - "gi", - 1.0, - label="气体类型索引", - quantity="dimensionless", - unit="", - minimum=1.0, - maximum=99.0, + description="选择 Kv 方式时使用的公制流量系数。", ), ParameterDefinition( "flowset", @@ -80,6 +80,7 @@ class AmesimPnor001(AlgebraicComponent): unit="", minimum=1.0, maximum=3.0, + description="流量参数方式:1 使用 Cq 和面积,2 使用 Cv,3 使用 Kv。", ), ) RESULT_VARIABLES = ( @@ -115,7 +116,7 @@ class AmesimPnor001(AlgebraicComponent): def __init__( self, name: str, - medium: IdealGasMedium, + medium: GasMedium, *, cq: float = 0.72, area: float = 5.0e-6, @@ -140,7 +141,7 @@ class AmesimPnor001(AlgebraicComponent): self.area = float(area) self.Cv = float(Cv) self.Kv = float(Kv) - self.gi = self._integer_parameter("gi", gi) + 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.") @@ -163,7 +164,7 @@ class AmesimPnor001(AlgebraicComponent): cls, *, name: str, - medium: IdealGasMedium, + medium: GasMedium, parameters: Mapping[str, float], ) -> AmesimPnor001: return cls( @@ -206,7 +207,7 @@ class AmesimPnor001(AlgebraicComponent): def _upstream_temperature(self, port_name: str) -> float: port = self.get_port(port_name) if port.h_outflow > 0.0: - return max(port.h_outflow / self.medium.cp_ref, 1.0) + return max(self.medium.temperature_from_enthalpy(port.h_outflow), 1.0) return self.medium.T_ref def mass_flow(self, p_1: float, p_2: float) -> float: @@ -321,6 +322,7 @@ class AmesimPnvo001FixedOpening(AlgebraicComponent): PortDefinition.pneumatic("port_3", nominal_role="bidirectional"), ) PARAMETERS = ( + AMESIM_GAS_INDEX_PARAMETER, ParameterDefinition( "cq", 0.72, @@ -329,6 +331,7 @@ class AmesimPnvo001FixedOpening(AlgebraicComponent): unit="", minimum=1.0e-10, maximum=1.0, + description="孔口实际质量流量相对于理想可压缩流量的无量纲修正系数。", ), ParameterDefinition( "area0", @@ -338,6 +341,7 @@ class AmesimPnvo001FixedOpening(AlgebraicComponent): unit="m2", minimum=0.0, maximum=1.0, + description="阀门完全开启时的最大有效孔口面积。", ), ParameterDefinition( "Cv", @@ -346,6 +350,7 @@ class AmesimPnvo001FixedOpening(AlgebraicComponent): quantity="dimensionless", unit="", minimum=0.0, + description="选择 Cv 方式时使用的最大英制流量系数。", ), ParameterDefinition( "Kv", @@ -354,15 +359,7 @@ class AmesimPnvo001FixedOpening(AlgebraicComponent): quantity="dimensionless", unit="", minimum=0.0, - ), - ParameterDefinition( - "gi", - 1.0, - label="气体类型索引", - quantity="dimensionless", - unit="", - minimum=1.0, - maximum=99.0, + description="选择 Kv 方式时使用的最大公制流量系数。", ), ParameterDefinition( "flowset", @@ -372,6 +369,7 @@ class AmesimPnvo001FixedOpening(AlgebraicComponent): unit="", minimum=1.0, maximum=3.0, + description="流量参数方式:1 使用 Cq 和面积,2 使用 Cv,3 使用 Kv。", ), ParameterDefinition( "opening", @@ -381,6 +379,7 @@ class AmesimPnvo001FixedOpening(AlgebraicComponent): unit="", minimum=0.0, maximum=1.0, + description="固定的归一化阀门开度;0 表示关闭,1 表示完全开启。", ), ) RESULT_VARIABLES = ( @@ -424,7 +423,7 @@ class AmesimPnvo001FixedOpening(AlgebraicComponent): def __init__( self, name: str, - medium: IdealGasMedium, + medium: GasMedium, *, cq: float = 0.72, area0: float = 5.0e-6, @@ -451,7 +450,7 @@ class AmesimPnvo001FixedOpening(AlgebraicComponent): self.area0 = float(area0) self.Cv = float(Cv) self.Kv = float(Kv) - self.gi = self._integer_parameter("gi", gi) + 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.") @@ -475,7 +474,7 @@ class AmesimPnvo001FixedOpening(AlgebraicComponent): cls, *, name: str, - medium: IdealGasMedium, + medium: GasMedium, parameters: Mapping[str, float], ) -> AmesimPnvo001FixedOpening: return cls( @@ -509,7 +508,7 @@ class AmesimPnvo001FixedOpening(AlgebraicComponent): def _upstream_temperature(self, port_name: str) -> float: port = self.get_port(port_name) if port.h_outflow > 0.0: - return max(port.h_outflow / self.medium.cp_ref, 1.0) + return max(self.medium.temperature_from_enthalpy(port.h_outflow), 1.0) return self.medium.T_ref def mass_flow(self, p_2: float, p_3: float) -> float: @@ -621,11 +620,11 @@ class AmesimPnvo001SignalOpening(AmesimPnvo001FixedOpening): PortDefinition.pneumatic("port_3", nominal_role="bidirectional"), ) PARAMETERS = ( + AMESIM_GAS_INDEX_PARAMETER, ParameterDefinition("cq", 0.72, label="流量系数 Cq", quantity="dimensionless", unit="", minimum=1.0e-10, maximum=1.0), 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("gi", 1.0, label="气体类型索引", quantity="dimensionless", unit="", minimum=1.0, maximum=99.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), ) @@ -646,7 +645,7 @@ class AmesimPnvo001SignalOpening(AmesimPnvo001FixedOpening): def __init__( self, name: str, - medium: IdealGasMedium, + medium: GasMedium, *, cq: float = 0.72, area0: float = 5.0e-6, @@ -673,7 +672,7 @@ class AmesimPnvo001SignalOpening(AmesimPnvo001FixedOpening): self.area0 = float(area0) self.Cv = float(Cv) self.Kv = float(Kv) - self.gi = self._integer_parameter("gi", gi) + 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.") @@ -691,7 +690,7 @@ class AmesimPnvo001SignalOpening(AmesimPnvo001FixedOpening): cls, *, name: str, - medium: IdealGasMedium, + medium: GasMedium, parameters: Mapping[str, float], ) -> "AmesimPnvo001SignalOpening": return cls(name=name, medium=medium, **dict(parameters)) diff --git a/app/simulation/components/amesim/flow/pipes.py b/app/simulation/components/amesim/flow/pipes.py index 4f8fb31..3af34d9 100644 --- a/app/simulation/components/amesim/flow/pipes.py +++ b/app/simulation/components/amesim/flow/pipes.py @@ -3,6 +3,10 @@ from __future__ import annotations from collections.abc import Mapping from math import isclose, log10, pi, sqrt +from app.simulation.components.amesim.gases import ( + AMESIM_GAS_INDEX_PARAMETER, + 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.equations import EquationResidual @@ -11,7 +15,7 @@ from app.simulation.core.metadata import ( ResultVariableDefinition, THERMODYNAMIC_VOLUME_RESULT_VARIABLES, ) -from app.simulation.core.medium import IdealGasMedium, ThermodynamicProperties +from app.simulation.core.medium import GasMedium, ThermodynamicProperties from app.simulation.core.ports import PortDefinition from app.simulation.core.state import VolumeState @@ -31,6 +35,7 @@ class AmesimPnl00r(AlgebraicComponent): PortDefinition.pneumatic("port_2", nominal_role="bidirectional"), ) PARAMETERS = ( + AMESIM_GAS_INDEX_PARAMETER, ParameterDefinition( "diam", 0.01, @@ -39,6 +44,7 @@ class AmesimPnl00r(AlgebraicComponent): unit="m", minimum=0.0, minimum_exclusive=True, + description="管路的有效内径,用于计算流通面积和摩擦压降。", ), ParameterDefinition( "le", @@ -48,6 +54,7 @@ class AmesimPnl00r(AlgebraicComponent): unit="m", minimum=0.0, minimum_exclusive=True, + description="参与摩擦压降计算的管路有效长度。", ), ParameterDefinition( "rr", @@ -57,15 +64,7 @@ class AmesimPnl00r(AlgebraicComponent): unit="", minimum=0.0, maximum=0.1, - ), - ParameterDefinition( - "gi", - 1.0, - label="气体类型索引", - quantity="dimensionless", - unit="", - minimum=1.0, - maximum=99.0, + description="管壁绝对粗糙度与管径之比,用于计算 Darcy 摩擦因子。", ), ) RESULT_VARIABLES = ( @@ -117,7 +116,7 @@ class AmesimPnl00r(AlgebraicComponent): def __init__( self, name: str, - medium: IdealGasMedium, + medium: GasMedium, *, diam: float = 0.01, le: float = 1.0, @@ -130,7 +129,7 @@ class AmesimPnl00r(AlgebraicComponent): self.diam = float(diam) self.le = float(le) self.rr = float(rr) - self.gi = self._integer_parameter("gi", gi) + self.gi = normalize_amesim_gas_index(gi) self.area = pi * self.diam * self.diam / 4.0 initial_h = medium.specific_enthalpy(medium.T_ref) @@ -151,7 +150,7 @@ class AmesimPnl00r(AlgebraicComponent): cls, *, name: str, - medium: IdealGasMedium, + medium: GasMedium, parameters: Mapping[str, float], ) -> AmesimPnl00r: return cls( @@ -166,22 +165,11 @@ class AmesimPnl00r(AlgebraicComponent): def _port_temperature(self, port_name: str) -> float: port = self.get_port(port_name) if port.h_outflow > 0.0: - return max(port.h_outflow / self.medium.cp_ref, 1.0) + return max(self.medium.temperature_from_enthalpy(port.h_outflow), 1.0) return self.medium.T_ref - @staticmethod - def _dynamic_viscosity(temperature_k: float) -> float: - if temperature_k <= 0.0: - raise ValueError("temperature_k must be positive") - reference_temperature = 293.15 - reference_viscosity = 1.82e-5 - sutherland_constant = 110.4 - return ( - reference_viscosity - * (temperature_k / reference_temperature) ** 1.5 - * (reference_temperature + sutherland_constant) - / (temperature_k + sutherland_constant) - ) + def _dynamic_viscosity(self, temperature_k: float) -> float: + return self.medium.dynamic_viscosity(temperature_k) def reynolds_number(self, mass_flow: float, temperature: float) -> float: viscosity = self._dynamic_viscosity(temperature) @@ -322,6 +310,7 @@ class AmesimPnl0001(ThermodynamicVolumeComponent): PortDefinition.pneumatic("port_2", nominal_role="bidirectional"), ) PARAMETERS = ( + AMESIM_GAS_INDEX_PARAMETER, ParameterDefinition( "diam", 0.01, @@ -330,6 +319,7 @@ class AmesimPnl0001(ThermodynamicVolumeComponent): unit="m", minimum=0.0, minimum_exclusive=True, + description="管路的有效内径,用于计算流通面积、储气容积和摩擦压降。", ), ParameterDefinition( "le", @@ -339,6 +329,7 @@ class AmesimPnl0001(ThermodynamicVolumeComponent): unit="m", minimum=0.0, minimum_exclusive=True, + description="管路的有效长度,用于计算储气容积、换热面积和摩擦压降。", ), ParameterDefinition( "rr", @@ -348,6 +339,7 @@ class AmesimPnl0001(ThermodynamicVolumeComponent): unit="", minimum=0.0, maximum=0.1, + description="管壁绝对粗糙度与管径之比,用于计算 Darcy 摩擦因子。", ), ParameterDefinition( "k", @@ -358,6 +350,7 @@ class AmesimPnl0001(ThermodynamicVolumeComponent): minimum=0.0, minimum_exclusive=True, maximum=2.0, + description="AMESim 管路热力学配置使用的多方指数。", ), ParameterDefinition( "kth", @@ -366,6 +359,7 @@ class AmesimPnl0001(ThermodynamicVolumeComponent): quantity="heat_transfer_coefficient", unit="W/(m2*K)", minimum=0.0, + description="管内气体与外部环境之间的对流换热系数。", ), ParameterDefinition( "extemp", @@ -375,15 +369,7 @@ class AmesimPnl0001(ThermodynamicVolumeComponent): unit="K", minimum=0.0, minimum_exclusive=True, - ), - ParameterDefinition( - "gi", - 1.0, - label="气体类型索引", - quantity="dimensionless", - unit="", - minimum=1.0, - maximum=99.0, + description="管路外部环境的绝对温度,用于计算换热功率。", ), ParameterDefinition( "mode", @@ -393,6 +379,7 @@ class AmesimPnl0001(ThermodynamicVolumeComponent): unit="", minimum=1.0, maximum=2.0, + description="热模型选择:1 为绝热,2 为按换热系数计算环境换热。", ), ParameterDefinition( "p0", @@ -402,6 +389,7 @@ class AmesimPnl0001(ThermodynamicVolumeComponent): unit="Pa", minimum=0.0, minimum_exclusive=True, + description="仿真开始时管内气体的绝对压力。", ), ParameterDefinition( "T0", @@ -411,6 +399,7 @@ class AmesimPnl0001(ThermodynamicVolumeComponent): unit="K", minimum=0.0, minimum_exclusive=True, + description="仿真开始时管内气体的绝对温度。", ), ) RESULT_VARIABLES = THERMODYNAMIC_VOLUME_RESULT_VARIABLES + ( @@ -462,7 +451,7 @@ class AmesimPnl0001(ThermodynamicVolumeComponent): def __init__( self, name: str, - medium: IdealGasMedium, + medium: GasMedium, *, diam: float = 0.01, le: float = 1.0, @@ -497,14 +486,14 @@ class AmesimPnl0001(ThermodynamicVolumeComponent): self.k = float(k) self.kth = float(kth) self.extemp = float(extemp) - self.gi = self._integer_parameter("gi", gi) + self.gi = normalize_amesim_gas_index(gi) self.mode = self._integer_parameter("mode", mode) self.p0 = float(p0) self.T0 = float(T0) self.area = pi * self.diam * self.diam / 4.0 self.volume = self.area * self.le self.exchange_area = pi * self.diam * self.le - m0 = self.p0 * self.volume / (medium.R_gas * self.T0) + m0 = medium.density(self.p0, self.T0) * self.volume U0 = m0 * medium.specific_internal_energy(self.T0) self.state = VolumeState(m=m0, U=U0) initial_h = medium.specific_enthalpy(self.T0) @@ -527,7 +516,7 @@ class AmesimPnl0001(ThermodynamicVolumeComponent): cls, *, name: str, - medium: IdealGasMedium, + medium: GasMedium, parameters: Mapping[str, float], ) -> "AmesimPnl0001": return cls( @@ -566,9 +555,8 @@ class AmesimPnl0001(ThermodynamicVolumeComponent): return 0.0 return self.kth * self.exchange_area * (self.extemp - temperature) - @staticmethod - def _dynamic_viscosity(temperature_k: float) -> float: - return AmesimPnl00r._dynamic_viscosity(temperature_k) + def _dynamic_viscosity(self, temperature_k: float) -> float: + return self.medium.dynamic_viscosity(temperature_k) def reynolds_number(self, mass_flow: float, temperature: float) -> float: viscosity = self._dynamic_viscosity(temperature) @@ -745,7 +733,7 @@ class AmesimPnl0002(AmesimPnl0001): cls, *, name: str, - medium: IdealGasMedium, + medium: GasMedium, parameters: Mapping[str, float], ) -> "AmesimPnl0002": return cls( @@ -943,7 +931,7 @@ class AmesimPnl0003(DynamicComponent): def __init__( self, name: str, - medium: IdealGasMedium, + medium: GasMedium, *, diam: float = 0.01, le: float = 1.0, @@ -982,7 +970,7 @@ class AmesimPnl0003(DynamicComponent): self.k = float(k) self.kth = float(kth) self.extemp = float(extemp) - self.gi = AmesimPnl0001._integer_parameter("gi", gi) + self.gi = normalize_amesim_gas_index(gi) self.mode = AmesimPnl0001._integer_parameter("mode", mode) self.area = pi * self.diam * self.diam / 4.0 self.volume = self.area * self.le @@ -1004,13 +992,13 @@ class AmesimPnl0003(DynamicComponent): cls, *, name: str, - medium: IdealGasMedium, + medium: GasMedium, parameters: Mapping[str, float], ) -> "AmesimPnl0003": return cls(name=name, medium=medium, **dict(parameters)) def _initial_state(self, pressure: float, temperature: float) -> VolumeState: - mass = pressure * self.compliance_volume / (self.medium.R_gas * temperature) + mass = self.medium.density(pressure, temperature) * self.compliance_volume return VolumeState(m=mass, U=mass * self.medium.specific_internal_energy(temperature)) def get_state_vector(self) -> list[float]: @@ -1040,9 +1028,8 @@ class AmesimPnl0003(DynamicComponent): def refresh_thermodynamic_ports(self) -> tuple[ThermodynamicProperties, ThermodynamicProperties]: return self.properties_1(), self.properties_2() - @staticmethod - def _dynamic_viscosity(temperature_k: float) -> float: - return AmesimPnl00r._dynamic_viscosity(temperature_k) + def _dynamic_viscosity(self, temperature_k: float) -> float: + return self.medium.dynamic_viscosity(temperature_k) def reynolds_number(self, mass_flow: float, temperature: float) -> float: viscosity = self._dynamic_viscosity(temperature) diff --git a/app/simulation/components/amesim/gases.py b/app/simulation/components/amesim/gases.py new file mode 100644 index 0000000..98c71ad --- /dev/null +++ b/app/simulation/components/amesim/gases.py @@ -0,0 +1,264 @@ +from __future__ import annotations + +from collections.abc import Iterable, Mapping +from dataclasses import dataclass +from math import isclose, isfinite +from types import MappingProxyType + +from app.simulation.core.metadata import ParameterDefinition +from app.simulation.core.medium import GasMedium + + +AMESIM_BUILTIN_AIR_GAS_INDEX = 0 +AMESIM_DEFAULT_GAS_INDEX = AMESIM_BUILTIN_AIR_GAS_INDEX +AMESIM_MIN_GAS_INDEX = AMESIM_BUILTIN_AIR_GAS_INDEX +AMESIM_MIN_DEFINED_GAS_INDEX = 1 +AMESIM_MAX_GAS_INDEX = 99 + +AMESIM_GAS_INDEX_PARAMETER = ParameterDefinition( + "gi", + float(AMESIM_DEFAULT_GAS_INDEX), + label="介质物性模型(gi)", + quantity="dimensionless", + unit="", + minimum=float(AMESIM_MIN_GAS_INDEX), + maximum=float(AMESIM_MAX_GAS_INDEX), + editor="amesimGasReference", + description=( + "选择本元件使用的气体介质定义索引;0 表示内置空气," + "1–99 引用画布中的介质定义组件。" + ), +) + +AMESIM_GAS_DEFINITION_INDEX_PARAMETER = ParameterDefinition( + "gi", + float(AMESIM_MIN_DEFINED_GAS_INDEX), + label="介质定义索引(gi)", + quantity="dimensionless", + unit="", + minimum=float(AMESIM_MIN_DEFINED_GAS_INDEX), + maximum=float(AMESIM_MAX_GAS_INDEX), + description=( + "介质定义在当前模型中的唯一索引;由画布自动分配," + "0 保留给内置空气。" + ), +) + + +def normalize_amesim_gas_index(value: float | int) -> int: + """Validate an AMESim gas reference. + + Index 0 is reserved for the built-in ideal-gas air profile. Positive + indices refer to medium-definition components placed in the project. + """ + + if isinstance(value, bool) or not isinstance(value, (int, float)): + raise ValueError("AMESim gas type index gi must be a number.") + numeric = float(value) + if not isfinite(numeric): + raise ValueError("AMESim gas type index gi must be finite.") + rounded = round(numeric) + if not isclose(numeric, rounded, rel_tol=0.0, abs_tol=1.0e-12): + raise ValueError("AMESim gas type index gi must be an integer value.") + index = int(rounded) + if not AMESIM_MIN_GAS_INDEX <= index <= AMESIM_MAX_GAS_INDEX: + raise ValueError( + "AMESim gas type index gi must be between " + f"{AMESIM_MIN_GAS_INDEX} and {AMESIM_MAX_GAS_INDEX}." + ) + return index + + +def normalize_amesim_defined_gas_index(value: float | int) -> int: + """Validate a positive index owned by a project medium definition.""" + + index = normalize_amesim_gas_index(value) + if index < AMESIM_MIN_DEFINED_GAS_INDEX: + raise ValueError( + "AMESim medium definition index gi must be between " + f"{AMESIM_MIN_DEFINED_GAS_INDEX} and {AMESIM_MAX_GAS_INDEX}; " + "gi=0 is reserved for built-in ideal-gas air." + ) + return index + + +@dataclass(frozen=True) +class AmesimGasDefinition: + """One AMESim PNGD-style gas-definition slot. + + ``fluid_type`` and ``eos_type`` are intentionally optional today. They + reserve the metadata needed to map a future PNGD00 helium definition while + the executable behavior is supplied by ``medium``. + """ + + gi: int + label: str + medium: GasMedium + fluid_type: int | None = None + eos_type: int | None = None + + def __post_init__(self) -> None: + normalized = normalize_amesim_gas_index(self.gi) + object.__setattr__(self, "gi", normalized) + if not self.label.strip(): + raise ValueError("AMESim gas definition label must not be empty.") + + +class AmesimGasRegistry: + """Resolve AMESim component ``gi`` references to thermodynamic media.""" + + def __init__( + self, + definitions: Iterable[AmesimGasDefinition] = (), + *, + default_gi: int = AMESIM_DEFAULT_GAS_INDEX, + ) -> None: + from app.simulation.components.amesim.media.mediums import ( + AmesimIdealAirMedium, + ) + + self.default_gi = normalize_amesim_gas_index(default_gi) + self._definitions: dict[int, AmesimGasDefinition] = { + AMESIM_BUILTIN_AIR_GAS_INDEX: AmesimGasDefinition( + gi=AMESIM_BUILTIN_AIR_GAS_INDEX, + label="空气(理想气体,内置默认)", + medium=AmesimIdealAirMedium(), + ) + } + for definition in definitions: + self.register(definition) + + @property + def definitions(self) -> Mapping[int, AmesimGasDefinition]: + return MappingProxyType(self._definitions) + + def register(self, definition: AmesimGasDefinition) -> None: + if not isinstance(definition, AmesimGasDefinition): + raise TypeError("AMESim gas registry entries must use AmesimGasDefinition.") + if definition.gi == AMESIM_BUILTIN_AIR_GAS_INDEX: + raise ValueError( + "AMESim gas type index gi=0 is reserved for built-in " + "ideal-gas air and cannot be replaced." + ) + if definition.gi in self._definitions: + raise ValueError( + f"AMESim gas type index gi={definition.gi} is already defined." + ) + self._definitions[definition.gi] = definition + + def copy(self) -> AmesimGasRegistry: + """Return an independent registry for one project compilation.""" + + copied = AmesimGasRegistry( + ( + definition + for index, definition in self._definitions.items() + if index != AMESIM_BUILTIN_AIR_GAS_INDEX + ), + default_gi=self.default_gi, + ) + copied._definitions[AMESIM_BUILTIN_AIR_GAS_INDEX] = self._definitions[ + AMESIM_BUILTIN_AIR_GAS_INDEX + ] + return copied + + def resolve( + self, + gi: float | int, + *, + component_name: str | None = None, + ) -> GasMedium: + index = normalize_amesim_gas_index(gi) + try: + return self._definitions[index].medium + except KeyError as exc: + owner = f" for component '{component_name}'" if component_name else "" + available = ", ".join(str(index) for index in sorted(self._definitions)) + available_message = available or "none" + raise ValueError( + f"AMESim gas type index gi={index}{owner} is not defined. " + "Register a PNGD-style gas definition before using this index. " + f"Available indices: {available_message}." + ) from exc + + @property + def default_medium(self) -> GasMedium: + return self.resolve(self.default_gi) + + def resolve_network_media( + self, + component_gas_indices: Mapping[str, float | int | None], + pneumatic_connections: Iterable[tuple[str, str]], + ) -> dict[str, GasMedium]: + """Assign one medium to every connected pneumatic circuit. + + Components without ``gi`` inherit the explicit index used by their + circuit. Conflicting indices inside one circuit are rejected instead + of silently mixing different gases. + """ + + parents = {component_id: component_id for component_id in component_gas_indices} + + def find(component_id: str) -> str: + parent = parents[component_id] + while parent != parents[parent]: + parent = parents[parent] + while component_id != parent: + next_component = parents[component_id] + parents[component_id] = parent + component_id = next_component + return parent + + def union(left: str, right: str) -> None: + left_root = find(left) + right_root = find(right) + if left_root != right_root: + parents[right_root] = left_root + + for source, target in pneumatic_connections: + if source in parents and target in parents: + union(source, target) + + members_by_root: dict[str, list[str]] = {} + for component_id in component_gas_indices: + members_by_root.setdefault(find(component_id), []).append(component_id) + + media: dict[str, GasMedium] = {} + for members in members_by_root.values(): + indexed_components: dict[int, list[str]] = {} + for component_id in members: + raw_index = component_gas_indices[component_id] + if raw_index is None: + continue + index = normalize_amesim_gas_index(raw_index) + indexed_components.setdefault(index, []).append(component_id) + + if len(indexed_components) > 1: + details = ", ".join( + f"gi={index} ({', '.join(sorted(component_ids))})" + for index, component_ids in sorted(indexed_components.items()) + ) + raise ValueError( + "Connected pneumatic circuit contains conflicting AMESim " + f"gas definitions: {details}." + ) + + index = ( + next(iter(indexed_components)) + if indexed_components + else self.default_gi + ) + indexed_members = indexed_components.get(index, members) + medium = self.resolve( + index, + component_name=", ".join(sorted(indexed_members)), + ) + for component_id in members: + media[component_id] = medium + return media + + +def default_amesim_gas_registry() -> AmesimGasRegistry: + """Create a registry containing only built-in gi=0 ideal-gas air.""" + + return AmesimGasRegistry() diff --git a/app/simulation/components/amesim/library.py b/app/simulation/components/amesim/library.py index 3c3d63f..69d7b9e 100644 --- a/app/simulation/components/amesim/library.py +++ b/app/simulation/components/amesim/library.py @@ -9,11 +9,12 @@ from app.simulation.core.catalog import ( LIBRARY = ComponentLibrarySpec( id="amesim", label="AMESim 组件库", - version="0.1.0", + version="0.2.0", source_package="app.simulation.components.amesim", temporary=True, order=200, categories=( + ComponentCategorySpec(id="media", label="介质物性", order=5), ComponentCategorySpec(id="storage", label="储能元件", order=10), ComponentCategorySpec(id="flow", label="流动元件", order=20), ComponentCategorySpec(id="junctions", label="连接元件", order=30), @@ -22,6 +23,8 @@ LIBRARY = ComponentLibrarySpec( ComponentCategorySpec(id="mechanical", label="机械元件", order=60), ), models=( + "app.simulation.components.amesim.media.properties:AmesimIdealAirMediumDefinition", + "app.simulation.components.amesim.media.properties:AmesimHeliumMediumDefinition", "app.simulation.components.amesim.boundary.sources:AmesimPnpl01", "app.simulation.components.amesim.signals.sources:AmesimStep0", "app.simulation.components.amesim.signals.sources:AmesimUd00", diff --git a/app/simulation/components/amesim/media/__init__.py b/app/simulation/components/amesim/media/__init__.py new file mode 100644 index 0000000..cd5b1e4 --- /dev/null +++ b/app/simulation/components/amesim/media/__init__.py @@ -0,0 +1,33 @@ +"""AMESim medium-property definition components.""" + +from app.simulation.components.amesim.media.mediums import ( + AMESIM_AIR_IDEAL_GAS_PROPERTY_MODEL, + AMESIM_AIR_PROPERTY_MODELS, + AMESIM_HELIUM_PENG_ROBINSON_PROPERTY_MODEL, + AMESIM_HELIUM_PROPERTY_MODELS, + AmesimGasPropertyModelSpec, + AmesimHeliumPengRobinsonMedium, + AmesimIdealAirMedium, +) +from app.simulation.components.amesim.media.properties import ( + AMESIM_AIR_PROPERTY_MODEL_PARAMETER, + AMESIM_HELIUM_PROPERTY_MODEL_PARAMETER, + AmesimGasMediumDefinitionComponent, + AmesimHeliumMediumDefinition, + AmesimIdealAirMediumDefinition, +) + +__all__ = ( + "AMESIM_AIR_IDEAL_GAS_PROPERTY_MODEL", + "AMESIM_AIR_PROPERTY_MODELS", + "AMESIM_AIR_PROPERTY_MODEL_PARAMETER", + "AMESIM_HELIUM_PENG_ROBINSON_PROPERTY_MODEL", + "AMESIM_HELIUM_PROPERTY_MODELS", + "AMESIM_HELIUM_PROPERTY_MODEL_PARAMETER", + "AmesimGasMediumDefinitionComponent", + "AmesimGasPropertyModelSpec", + "AmesimHeliumMediumDefinition", + "AmesimHeliumPengRobinsonMedium", + "AmesimIdealAirMedium", + "AmesimIdealAirMediumDefinition", +) diff --git a/app/simulation/components/amesim/media/mediums.py b/app/simulation/components/amesim/media/mediums.py new file mode 100644 index 0000000..69bc002 --- /dev/null +++ b/app/simulation/components/amesim/media/mediums.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import ClassVar + +from app.simulation.core.medium import GasMedium, IdealGasMedium +from app.simulation.core.peng_robinson import HELIUM_PR, PengRobinsonFluid + + +@dataclass(frozen=True) +class AmesimIdealAirMedium(IdealGasMedium): + """AMESim air properties evaluated with the ideal-gas method. + + Substance identity and property method are part of the concrete Python + type. A future air correlation or helium Peng-Robinson implementation can + therefore coexist as a sibling type without turning ``gi`` into a fluid + enumeration. + """ + + SUBSTANCE_ID: ClassVar[str] = "air" + PROPERTY_METHOD_ID: ClassVar[str] = "ideal_gas" + + name: str = "AMESimAirIdealGas" + R_gas: float = 287.0 + cp_ref: float = 1005.0 + T_ref: float = 300.0 + cp_slope: float = 0.0 + viscosity_ref: float = 1.82e-5 + viscosity_T_ref: float = 293.15 + sutherland_constant: float = 110.4 + + +@dataclass(frozen=True) +class AmesimHeliumPengRobinsonMedium(IdealGasMedium): + """AMESim helium with a Peng-Robinson mechanical equation of state. + + The pressure-density-temperature relation is evaluated by the shared + ``HELIUM_PR`` fluid. The first public AMESim port keeps the committed + constant-heat-capacity caloric model so it can be consumed through the + same :class:`GasMedium` contract as ideal-gas air. + """ + + SUBSTANCE_ID: ClassVar[str] = "helium" + PROPERTY_METHOD_ID: ClassVar[str] = "peng_robinson" + fluid: ClassVar[PengRobinsonFluid] = HELIUM_PR + + name: str = "AMESimHeliumPengRobinson" + R_gas: float = HELIUM_PR.specific_gas_constant + cp_ref: float = 5193.0 + T_ref: float = 293.15 + cp_slope: float = 0.0 + viscosity_ref: float = 1.96e-5 + viscosity_T_ref: float = 293.15 + sutherland_constant: float = 79.4 + + @property + def cv(self) -> float: + return 3116.0 + + def cv_at_temperature(self, T: float) -> float: + del T + return self.cv + + def density(self, p: float, T: float) -> float: + return self.fluid.density(p, T) + + def pressure(self, m: float, T: float, V: float) -> float: + if V <= 0.0: + raise ValueError("Volume must stay positive.") + return self.fluid.pressure_from_density(T, m / V) + + +@dataclass(frozen=True) +class AmesimGasPropertyModelSpec: + """A selectable calculation method for one AMESim gas substance.""" + + value: int + label: str + method_id: str + factory: Callable[[], GasMedium] + eos_type: int + + def build_medium(self) -> GasMedium: + return self.factory() + + +AMESIM_AIR_IDEAL_GAS_PROPERTY_MODEL = 0 +AMESIM_AIR_PROPERTY_MODELS = ( + AmesimGasPropertyModelSpec( + value=AMESIM_AIR_IDEAL_GAS_PROPERTY_MODEL, + label="理想气体", + method_id=AmesimIdealAirMedium.PROPERTY_METHOD_ID, + factory=AmesimIdealAirMedium, + eos_type=1, + ), +) + + +AMESIM_HELIUM_PENG_ROBINSON_PROPERTY_MODEL = 0 +AMESIM_HELIUM_PROPERTY_MODELS = ( + AmesimGasPropertyModelSpec( + value=AMESIM_HELIUM_PENG_ROBINSON_PROPERTY_MODEL, + label="Peng–Robinson", + method_id=AmesimHeliumPengRobinsonMedium.PROPERTY_METHOD_ID, + factory=AmesimHeliumPengRobinsonMedium, + eos_type=6, + ), +) diff --git a/app/simulation/components/amesim/media/properties.py b/app/simulation/components/amesim/media/properties.py new file mode 100644 index 0000000..4446124 --- /dev/null +++ b/app/simulation/components/amesim/media/properties.py @@ -0,0 +1,196 @@ +from __future__ import annotations + +from abc import ABC +from collections.abc import Mapping + +from app.simulation.components.amesim.gases import ( + AMESIM_GAS_DEFINITION_INDEX_PARAMETER, + AmesimGasDefinition, + normalize_amesim_defined_gas_index, +) +from app.simulation.components.amesim.media.mediums import ( + AMESIM_AIR_IDEAL_GAS_PROPERTY_MODEL, + AMESIM_AIR_PROPERTY_MODELS, + AMESIM_HELIUM_PENG_ROBINSON_PROPERTY_MODEL, + AMESIM_HELIUM_PROPERTY_MODELS, + AmesimGasPropertyModelSpec, +) +from app.simulation.core.base import AlgebraicComponent +from app.simulation.core.catalog import ComponentDisplaySpec +from app.simulation.core.medium import GasMedium +from app.simulation.core.metadata import ParameterDefinition, ParameterOption + + +AMESIM_AIR_PROPERTY_MODEL_PARAMETER = ParameterDefinition( + "property_model", + float(AMESIM_AIR_IDEAL_GAS_PROPERTY_MODEL), + label="物性计算模型", + quantity="dimensionless", + unit="", + minimum=float(min(model.value for model in AMESIM_AIR_PROPERTY_MODELS)), + maximum=float(max(model.value for model in AMESIM_AIR_PROPERTY_MODELS)), + editor="amesimGasPropertyModel", + options=tuple( + ParameterOption(value=model.value, label=model.label) + for model in AMESIM_AIR_PROPERTY_MODELS + ), + description="选择空气介质的物性计算方法;当前首版提供理想气体模型。", +) + +AMESIM_HELIUM_PROPERTY_MODEL_PARAMETER = ParameterDefinition( + "property_model", + float(AMESIM_HELIUM_PENG_ROBINSON_PROPERTY_MODEL), + label="物性计算模型", + quantity="dimensionless", + unit="", + minimum=float(min(model.value for model in AMESIM_HELIUM_PROPERTY_MODELS)), + maximum=float(max(model.value for model in AMESIM_HELIUM_PROPERTY_MODELS)), + editor="amesimGasPropertyModel", + options=tuple( + ParameterOption(value=model.value, label=model.label) + for model in AMESIM_HELIUM_PROPERTY_MODELS + ), + description=( + "选择氦气介质的物性计算方法;当前首版提供 " + "Peng–Robinson 状态方程模型。" + ), +) + + +class AmesimGasMediumDefinitionComponent(AlgebraicComponent, ABC): + """Compile-time definition of one project-scoped AMESim gas medium. + + Concrete subclasses declare one substance and its available calculation + methods; each instance selects a method through ``property_model``. They + deliberately expose no physical ports or equations: the compiler consumes + them before it creates the simulation network. + """ + + IS_AMESIM_GAS_MEDIUM_DEFINITION = True + MEDIUM_LABEL = "" + FLUID_TYPE: int | None = None + PROPERTY_MODELS: tuple[AmesimGasPropertyModelSpec, ...] = () + + def __init__( + self, + name: str, + gi: float, + property_model: float = float(AMESIM_AIR_IDEAL_GAS_PROPERTY_MODEL), + ) -> None: + super().__init__(name) + self.gi = normalize_amesim_defined_gas_index(gi) + self.property_model = self._resolve_property_model(property_model).value + self.set_parameter_values( + { + "gi": self.gi, + "property_model": self.property_model, + } + ) + + def _resolve_property_model( + self, + value: float | int, + ) -> AmesimGasPropertyModelSpec: + for model in self.PROPERTY_MODELS: + if float(model.value) == float(value): + return model + available = ", ".join(str(model.value) for model in self.PROPERTY_MODELS) + raise ValueError( + f"AMESim medium definition '{self.name}' does not support property " + f"model {value:g}; available models: {available or 'none'}." + ) + + def build_medium(self) -> GasMedium: + """Create the executable property model selected by this instance.""" + + return self._resolve_property_model(self.property_model).build_medium() + + def gas_definition(self) -> AmesimGasDefinition: + model = self._resolve_property_model(self.property_model) + return AmesimGasDefinition( + gi=self.gi, + label=f"{self.MEDIUM_LABEL}({model.label})", + medium=self.build_medium(), + fluid_type=self.FLUID_TYPE, + eos_type=model.eos_type, + ) + + +class AmesimIdealAirMediumDefinition(AmesimGasMediumDefinitionComponent): + """Project gas slot using the built-in ideal-gas air property method.""" + + MODEL_TYPE = "amesim_ideal_air_medium" + MODEL_VERSION = "0.2.0" + PORTS = () + PARAMETERS = ( + AMESIM_GAS_DEFINITION_INDEX_PARAMETER, + AMESIM_AIR_PROPERTY_MODEL_PARAMETER, + ) + RESULT_VARIABLES = () + DISPLAY = ComponentDisplaySpec( + label="空气介质定义", + library_id="amesim", + category_id="media", + symbol="amesim_ideal_air_medium", + ports=(), + order=10, + role="amesimGasMediumDefinition", + ) + MEDIUM_LABEL = "空气" + FLUID_TYPE = 2 + PROPERTY_MODELS = AMESIM_AIR_PROPERTY_MODELS + + @classmethod + def create( + cls, + *, + name: str, + medium: GasMedium, + parameters: Mapping[str, float], + ) -> AmesimIdealAirMediumDefinition: + del medium + return cls( + name=name, + gi=parameters["gi"], + property_model=parameters["property_model"], + ) + + +class AmesimHeliumMediumDefinition(AmesimGasMediumDefinitionComponent): + """Project gas slot using the AMESim helium Peng-Robinson method.""" + + MODEL_TYPE = "amesim_helium_medium" + MODEL_VERSION = "0.1.0" + PORTS = () + PARAMETERS = ( + AMESIM_GAS_DEFINITION_INDEX_PARAMETER, + AMESIM_HELIUM_PROPERTY_MODEL_PARAMETER, + ) + RESULT_VARIABLES = () + DISPLAY = ComponentDisplaySpec( + label="氦气介质定义", + library_id="amesim", + category_id="media", + symbol="amesim_helium_medium", + ports=(), + order=20, + role="amesimGasMediumDefinition", + ) + MEDIUM_LABEL = "氦气" + FLUID_TYPE = 12 + PROPERTY_MODELS = AMESIM_HELIUM_PROPERTY_MODELS + + @classmethod + def create( + cls, + *, + name: str, + medium: GasMedium, + parameters: Mapping[str, float], + ) -> AmesimHeliumMediumDefinition: + del medium + return cls( + name=name, + gi=parameters["gi"], + property_model=parameters["property_model"], + ) diff --git a/app/simulation/components/amesim/storage/chambers.py b/app/simulation/components/amesim/storage/chambers.py index 0754d2a..5f0e9a2 100644 --- a/app/simulation/components/amesim/storage/chambers.py +++ b/app/simulation/components/amesim/storage/chambers.py @@ -1,8 +1,11 @@ from __future__ import annotations from collections.abc import Mapping -from math import isclose +from app.simulation.components.amesim.gases import ( + AMESIM_GAS_INDEX_PARAMETER, + normalize_amesim_gas_index, +) from app.simulation.core.base import ThermodynamicVolumeComponent from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec from app.simulation.core.equations import EquationResidual @@ -11,7 +14,7 @@ from app.simulation.core.metadata import ( ResultVariableDefinition, THERMODYNAMIC_VOLUME_RESULT_VARIABLES, ) -from app.simulation.core.medium import IdealGasMedium, ThermodynamicProperties +from app.simulation.core.medium import GasMedium, ThermodynamicProperties from app.simulation.core.ports import PortDefinition from app.simulation.core.state import VolumeState @@ -32,6 +35,7 @@ class AmesimPnch023(ThermodynamicVolumeComponent): PortDefinition.pneumatic("port_2", nominal_role="bidirectional"), ) PARAMETERS = ( + AMESIM_GAS_INDEX_PARAMETER, ParameterDefinition( "cvol", 0.057, @@ -40,6 +44,7 @@ class AmesimPnch023(ThermodynamicVolumeComponent): unit="m3", minimum=0.0, minimum_exclusive=True, + description="气室内部用于储存气体的固定有效容积。", ), ParameterDefinition( "kth", @@ -48,6 +53,7 @@ class AmesimPnch023(ThermodynamicVolumeComponent): quantity="heat_transfer_coefficient", unit="W/(m2*K)", minimum=0.0, + description="气室与环境之间的对流换热系数,与换热面积共同决定换热功率。", ), ParameterDefinition( "sth", @@ -56,6 +62,7 @@ class AmesimPnch023(ThermodynamicVolumeComponent): quantity="area", unit="m2", minimum=0.0, + description="气室与环境进行热交换的有效表面积。", ), ParameterDefinition( "extemp", @@ -65,15 +72,7 @@ class AmesimPnch023(ThermodynamicVolumeComponent): unit="K", minimum=0.0, minimum_exclusive=True, - ), - ParameterDefinition( - "gi", - 1.0, - label="气体类型索引", - quantity="dimensionless", - unit="", - minimum=1.0, - maximum=99.0, + description="气室外部环境的绝对温度,用于计算气体与环境之间的换热。", ), ParameterDefinition( "p0", @@ -83,6 +82,7 @@ class AmesimPnch023(ThermodynamicVolumeComponent): unit="Pa", minimum=0.0, minimum_exclusive=True, + description="仿真开始时气室内气体的绝对压力。", ), ParameterDefinition( "T0", @@ -92,6 +92,7 @@ class AmesimPnch023(ThermodynamicVolumeComponent): unit="K", minimum=0.0, minimum_exclusive=True, + description="仿真开始时气室内气体的绝对温度。", ), ) RESULT_VARIABLES = THERMODYNAMIC_VOLUME_RESULT_VARIABLES @@ -110,7 +111,7 @@ class AmesimPnch023(ThermodynamicVolumeComponent): def __init__( self, name: str, - medium: IdealGasMedium, + medium: GasMedium, *, cvol: float = 0.057, kth: float = 0.0, @@ -137,10 +138,10 @@ class AmesimPnch023(ThermodynamicVolumeComponent): self.kth = float(kth) self.sth = float(sth) self.extemp = float(extemp) - self.gi = self._integer_parameter("gi", gi) + self.gi = normalize_amesim_gas_index(gi) self.p0 = float(p0) self.T0 = float(T0) - m0 = self.p0 * self.cvol / (medium.R_gas * self.T0) + m0 = medium.density(self.p0, self.T0) * self.cvol U0 = m0 * medium.specific_internal_energy(self.T0) self.state = VolumeState(m=m0, U=U0) initial_h = medium.specific_enthalpy(self.T0) @@ -151,19 +152,12 @@ class AmesimPnch023(ThermodynamicVolumeComponent): self.port_2.p = self.p0 self.port_2.h_outflow = initial_h - @staticmethod - def _integer_parameter(name: str, value: float) -> int: - rounded = round(value) - if not isclose(value, rounded, rel_tol=0.0, abs_tol=1.0e-12): - raise ValueError(f"PNCH023 parameter {name} must be an integer value.") - return int(rounded) - @classmethod def create( cls, *, name: str, - medium: IdealGasMedium, + medium: GasMedium, parameters: Mapping[str, float], ) -> AmesimPnch023: return cls( @@ -270,6 +264,7 @@ class AmesimPnch012(ThermodynamicVolumeComponent): PortDefinition.pneumatic("port_4", nominal_role="bidirectional"), ) PARAMETERS = ( + AMESIM_GAS_INDEX_PARAMETER, ParameterDefinition( "cvol0", 0.015, @@ -278,6 +273,7 @@ class AmesimPnch012(ThermodynamicVolumeComponent): unit="m3", minimum=0.0, minimum_exclusive=True, + description="变容气室在所有外部容积为零时仍保留的基础容积。", ), ParameterDefinition( "kth", @@ -286,6 +282,7 @@ class AmesimPnch012(ThermodynamicVolumeComponent): quantity="heat_transfer_coefficient", unit="W/(m2*K)", minimum=0.0, + description="气室与环境之间的对流换热系数,与换热面积共同决定换热功率。", ), ParameterDefinition( "sth", @@ -294,6 +291,7 @@ class AmesimPnch012(ThermodynamicVolumeComponent): quantity="area", unit="m2", minimum=0.0, + description="气室与环境进行热交换的有效表面积。", ), ParameterDefinition( "extemp", @@ -303,15 +301,7 @@ class AmesimPnch012(ThermodynamicVolumeComponent): unit="K", minimum=0.0, minimum_exclusive=True, - ), - ParameterDefinition( - "gi", - 1.0, - label="气体类型索引", - quantity="dimensionless", - unit="", - minimum=1.0, - maximum=99.0, + description="气室外部环境的绝对温度,用于计算气体与环境之间的换热。", ), ParameterDefinition( "p0", @@ -321,6 +311,7 @@ class AmesimPnch012(ThermodynamicVolumeComponent): unit="Pa", minimum=0.0, minimum_exclusive=True, + description="仿真开始时气室内气体的绝对压力。", ), ParameterDefinition( "T0", @@ -330,6 +321,7 @@ class AmesimPnch012(ThermodynamicVolumeComponent): unit="K", minimum=0.0, minimum_exclusive=True, + description="仿真开始时气室内气体的绝对温度。", ), ParameterDefinition("vol1", 0.0, label="端口 1 外部容积", quantity="volume", unit="m3"), ParameterDefinition("vol2", 0.0, label="端口 2 外部容积", quantity="volume", unit="m3"), @@ -361,7 +353,7 @@ class AmesimPnch012(ThermodynamicVolumeComponent): def __init__( self, name: str, - medium: IdealGasMedium, + medium: GasMedium, *, cvol0: float = 0.015, kth: float = 0.0, @@ -404,7 +396,7 @@ class AmesimPnch012(ThermodynamicVolumeComponent): self.kth = float(kth) self.sth = float(sth) self.extemp = float(extemp) - self.gi = AmesimPnch023._integer_parameter("gi", gi) + self.gi = normalize_amesim_gas_index(gi) self.p0 = float(p0) self.T0 = float(T0) self.external_volumes = { @@ -421,7 +413,7 @@ class AmesimPnch012(ThermodynamicVolumeComponent): } if self.total_volume() <= 0.0: raise ValueError("PNCH012 total volume must be positive.") - m0 = self.p0 * self.total_volume() / (medium.R_gas * self.T0) + m0 = medium.density(self.p0, self.T0) * self.total_volume() U0 = m0 * medium.specific_internal_energy(self.T0) self.state = VolumeState(m=m0, U=U0) initial_h = medium.specific_enthalpy(self.T0) @@ -436,7 +428,7 @@ class AmesimPnch012(ThermodynamicVolumeComponent): cls, *, name: str, - medium: IdealGasMedium, + medium: GasMedium, parameters: Mapping[str, float], ) -> "AmesimPnch012": return cls(name=name, medium=medium, **dict(parameters)) diff --git a/app/simulation/core/base.py b/app/simulation/core/base.py index 93347aa..8c3f712 100644 --- a/app/simulation/core/base.py +++ b/app/simulation/core/base.py @@ -15,7 +15,7 @@ from app.simulation.core.metadata import ( from app.simulation.core.ports import PortDefinition, PortState if TYPE_CHECKING: - from app.simulation.core.medium import IdealGasMedium + from app.simulation.core.medium import GasMedium class Component(ABC): @@ -185,7 +185,7 @@ class Component(ABC): cls, *, name: str, - medium: IdealGasMedium, + medium: GasMedium, parameters: Mapping[str, float], ) -> Component: """Create a catalog model from normalized SI parameters.""" diff --git a/app/simulation/core/catalog.py b/app/simulation/core/catalog.py index bf92e72..bf67f35 100644 --- a/app/simulation/core/catalog.py +++ b/app/simulation/core/catalog.py @@ -5,6 +5,7 @@ from typing import Literal PortDisplaySide = Literal["left", "right"] +ComponentCatalogRole = Literal["amesimGasMediumDefinition"] @dataclass(frozen=True) @@ -42,6 +43,7 @@ class ComponentDisplaySpec: symbol: str ports: tuple[PortDisplaySpec, ...] order: int = 0 + role: ComponentCatalogRole | None = None @property def port_by_name(self) -> dict[str, PortDisplaySpec]: diff --git a/app/simulation/core/medium.py b/app/simulation/core/medium.py index 3410590..c18541c 100644 --- a/app/simulation/core/medium.py +++ b/app/simulation/core/medium.py @@ -1,6 +1,7 @@ from __future__ import annotations from dataclasses import dataclass +from typing import Protocol @dataclass(frozen=True) @@ -12,6 +13,53 @@ class ThermodynamicProperties: h: float +class GasMedium(Protocol): + """Thermodynamic contract required by pneumatic components. + + ``IdealGasMedium`` is the default implementation. Keeping the component + boundary structural allows a later helium/Peng-Robinson implementation to + be registered without changing every AMESim component constructor. + """ + + name: str + R_gas: float + cp_ref: float + T_ref: float + + @property + def cv(self) -> float: ... + + @property + def gamma(self) -> float: ... + + def cp_at_temperature(self, T: float) -> float: ... + + def cv_at_temperature(self, T: float) -> float: ... + + def density(self, p: float, T: float) -> float: ... + + def dynamic_viscosity(self, T: float) -> float: ... + + def specific_internal_energy(self, T: float) -> float: ... + + def specific_enthalpy(self, T: float) -> float: ... + + def temperature_from_internal_energy(self, u: float) -> float: ... + + def temperature_from_enthalpy(self, h: float) -> float: ... + + def temperature_from_mass_internal_energy(self, m: float, U: float) -> float: ... + + def pressure(self, m: float, T: float, V: float) -> float: ... + + def properties_from_mU( + self, + m: float, + U: float, + V: float, + ) -> ThermodynamicProperties: ... + + @dataclass(frozen=True) class IdealGasMedium: """Temperature-dependent ideal-gas air approximation. @@ -27,6 +75,9 @@ class IdealGasMedium: cp_ref: float = 1005.0 T_ref: float = 300.0 cp_slope: float = 0.0 + viscosity_ref: float = 1.82e-5 + viscosity_T_ref: float = 293.15 + sutherland_constant: float = 110.4 @property def cv(self) -> float: @@ -45,6 +96,18 @@ class IdealGasMedium: def density(self, p: float, T: float) -> float: return p / (self.R_gas * T) + def dynamic_viscosity(self, T: float) -> float: + """Return dynamic viscosity using the default air Sutherland law.""" + + if T <= 0.0: + raise ValueError("Temperature must be positive.") + return ( + self.viscosity_ref + * (T / self.viscosity_T_ref) ** 1.5 + * (self.viscosity_T_ref + self.sutherland_constant) + / (T + self.sutherland_constant) + ) + def specific_internal_energy(self, T: float) -> float: delta_T = T - self.T_ref return ( @@ -77,6 +140,22 @@ class IdealGasMedium: delta_T = positive_root if abs(positive_root) <= abs(negative_root) else negative_root return self.T_ref + delta_T + def temperature_from_enthalpy(self, h: float) -> float: + reference_enthalpy = self.cp_ref * self.T_ref + delta_h = h - reference_enthalpy + + if abs(self.cp_slope) <= 1e-15: + return self.T_ref + delta_h / self.cp_ref + + a = 0.5 * self.cp_slope + b = self.cp_ref + c = -delta_h + discriminant = max(b * b - 4.0 * a * c, 0.0) + positive_root = (-b + discriminant**0.5) / (2.0 * a) + negative_root = (-b - discriminant**0.5) / (2.0 * a) + delta_T = positive_root if abs(positive_root) <= abs(negative_root) else negative_root + return self.T_ref + delta_T + def temperature_from_mass_internal_energy(self, m: float, U: float) -> float: if m <= 0.0: raise ValueError("Mass must stay positive when recovering temperature.") diff --git a/app/simulation/core/metadata.py b/app/simulation/core/metadata.py index 75aa23d..eec2c57 100644 --- a/app/simulation/core/metadata.py +++ b/app/simulation/core/metadata.py @@ -6,6 +6,10 @@ from typing import Literal ResultVariableScope = Literal["component", "port"] +ParameterEditor = Literal[ + "amesimGasReference", + "amesimGasPropertyModel", +] SI_UNIT_BY_QUANTITY: dict[str, str] = { @@ -34,6 +38,20 @@ SI_UNIT_BY_QUANTITY: dict[str, str] = { } +@dataclass(frozen=True) +class ParameterOption: + """One numeric choice exposed by a catalog-backed parameter editor.""" + + value: float + label: str + + def as_interface_dict(self) -> dict[str, object]: + return { + "value": self.value, + "label": self.label, + } + + @dataclass(frozen=True) class ParameterDefinition: """User-configurable model input expressed in the backend SI contract.""" @@ -46,6 +64,9 @@ class ParameterDefinition: minimum: float | None = None maximum: float | None = None minimum_exclusive: bool = False + editor: ParameterEditor | None = None + options: tuple[ParameterOption, ...] = () + description: str = "" def validation_message(self, value: float) -> str | None: if not isfinite(value): @@ -57,6 +78,11 @@ class ParameterDefinition: return f"must be at least {self.minimum:g}" if self.maximum is not None and value > self.maximum: return f"must be at most {self.maximum:g}" + if self.options and value not in { + float(option.value) for option in self.options + }: + available = ", ".join(f"{option.value:g}" for option in self.options) + return f"must be one of {available}" return None def as_interface_dict(self, *, value: float | None = None) -> dict[str, object]: @@ -72,6 +98,14 @@ class ParameterDefinition: payload["minimum"] = self.minimum if self.maximum is not None: payload["maximum"] = self.maximum + if self.editor is not None: + payload["editor"] = self.editor + if self.options: + payload["options"] = [ + option.as_interface_dict() for option in self.options + ] + if self.description: + payload["description"] = self.description if value is not None: payload["value"] = value return payload diff --git a/app/simulation/examples/test_mql/__init__.py b/app/simulation/examples/test_mql/__init__.py index bf8d45d..72c62c3 100644 --- a/app/simulation/examples/test_mql/__init__.py +++ b/app/simulation/examples/test_mql/__init__.py @@ -1,5 +1,10 @@ from __future__ import annotations +from app.simulation.examples.test_mql.system import ( + TestMqlRunConfig, + TestMqlSimulationResult, + TestMqlSystem, +) from app.simulation.examples.test_mql.run import ( PreparedTestMqlRun, TestMqlRunResult, @@ -10,7 +15,10 @@ from app.simulation.examples.test_mql.run import ( __all__ = [ "PreparedTestMqlRun", + "TestMqlRunConfig", "TestMqlRunResult", + "TestMqlSimulationResult", + "TestMqlSystem", "prepare_test_mql_run", "run_prepared_test_mql", "run_test_mql", diff --git a/PythonModels/systems/test_mql_baseline.py b/app/simulation/examples/test_mql/baseline.py similarity index 85% rename from PythonModels/systems/test_mql_baseline.py rename to app/simulation/examples/test_mql/baseline.py index fff6c38..8bbeb45 100644 --- a/PythonModels/systems/test_mql_baseline.py +++ b/app/simulation/examples/test_mql/baseline.py @@ -3,17 +3,17 @@ from __future__ import annotations from dataclasses import dataclass from pathlib import Path -from PythonModels.reporting.amesim_results import AmesimResults, load_test_mql_amesim_results -from PythonModels.reporting.test_mql_comparison import TestMqlComparisonResult -from PythonModels.reporting.test_mql_observations import ( +from app.simulation.reporting.amesim_results import AmesimResults, load_test_mql_amesim_results +from app.simulation.reporting.test_mql_comparison import TestMqlComparisonResult +from app.simulation.reporting.test_mql_observations import ( TestMqlObservationCatalog, build_test_mql_observation_catalog, ) -from PythonModels.reporting.test_mql_output_schema import ( +from app.simulation.reporting.test_mql_output_schema import ( TestMqlOutputSchema, build_test_mql_output_schema, ) -from PythonModels.reporting.test_mql_output_validation import ( +from app.simulation.reporting.test_mql_output_validation import ( TestMqlValidatedOutput, compare_validated_test_mql_output, validate_test_mql_output, diff --git a/app/simulation/examples/testmodel/test_mql_closure.py b/app/simulation/examples/test_mql/closure.py similarity index 99% rename from app/simulation/examples/testmodel/test_mql_closure.py rename to app/simulation/examples/test_mql/closure.py index 984d9f7..f31d14c 100644 --- a/app/simulation/examples/testmodel/test_mql_closure.py +++ b/app/simulation/examples/test_mql/closure.py @@ -3,21 +3,21 @@ from __future__ import annotations from dataclasses import dataclass from typing import Callable -from PythonModels.components.amesim_pneumatic import ( +from app.simulation.examples.test_mql.primitives.pneumatic import ( AmesimPneumaticGas, AmesimPneumaticOrifice, AmesimPneumaticVolume, ) -from PythonModels.components.amesim_pneumatic_line import ( +from app.simulation.examples.test_mql.primitives.pneumatic_lines import ( AmesimPnl0001Pipe, AmesimPnl0002Pipe, AmesimPnl0003Pipe, AmesimPnl00rPipe, ) -from PythonModels.core.medium import ThermodynamicProperties -from PythonModels.core.ports import PortState -from PythonModels.core.state import VolumeState -from PythonModels.systems.test_mql_nodes import ( +from app.simulation.core.medium import ThermodynamicProperties +from app.simulation.core.ports import PortState +from app.simulation.core.state import VolumeState +from app.simulation.examples.test_mql.nodes import ( TestMqlPneumaticNode3, TestMqlPneumaticNode3Balance, TestMqlPneumaticNode4, diff --git a/PythonModels/systems/test_mql_computed.py b/app/simulation/examples/test_mql/computed.py similarity index 96% rename from PythonModels/systems/test_mql_computed.py rename to app/simulation/examples/test_mql/computed.py index e13bfb9..4dd2b79 100644 --- a/PythonModels/systems/test_mql_computed.py +++ b/app/simulation/examples/test_mql/computed.py @@ -3,28 +3,28 @@ from __future__ import annotations from dataclasses import dataclass from pathlib import Path -from PythonModels.components.amesim_pneumatic import m3_to_cm3 -from PythonModels.reporting.amesim_results import AmesimResults, load_test_mql_amesim_results -from PythonModels.reporting.test_mql_comparison import TestMqlComparisonResult -from PythonModels.reporting.test_mql_observations import ( +from app.simulation.examples.test_mql.primitives.pneumatic import m3_to_cm3 +from app.simulation.reporting.amesim_results import AmesimResults, load_test_mql_amesim_results +from app.simulation.reporting.test_mql_comparison import TestMqlComparisonResult +from app.simulation.reporting.test_mql_observations import ( TestMqlObservationCatalog, build_test_mql_observation_catalog, ) -from PythonModels.reporting.test_mql_output_schema import ( +from app.simulation.reporting.test_mql_output_schema import ( TestMqlOutputSchema, build_test_mql_output_schema, ) -from PythonModels.reporting.test_mql_output_validation import ( +from app.simulation.reporting.test_mql_output_validation import ( TestMqlValidatedOutput, compare_validated_test_mql_output, validate_test_mql_output, ) -from PythonModels.reporting.test_mql_variables import build_test_mql_variable_catalog -from PythonModels.systems.test_mql_mechanical import ( +from app.simulation.reporting.test_mql_variables import build_test_mql_variable_catalog +from app.simulation.examples.test_mql.mechanical import ( TestMqlMechanicalAssembly, build_test_mql_mechanical_assembly, ) -from PythonModels.systems.test_mql_pneumatic import ( +from app.simulation.examples.test_mql.pneumatic import ( TestMqlPneumaticAssembly, build_test_mql_pneumatic_assembly, ) diff --git a/app/simulation/examples/testmodel/test_mql_config.py b/app/simulation/examples/test_mql/config.py similarity index 96% rename from app/simulation/examples/testmodel/test_mql_config.py rename to app/simulation/examples/test_mql/config.py index 487936c..f43d88b 100644 --- a/app/simulation/examples/testmodel/test_mql_config.py +++ b/app/simulation/examples/test_mql/config.py @@ -6,8 +6,8 @@ from dataclasses import dataclass from math import isfinite from typing import Any -from PythonModels.core.peng_robinson import HELIUM_PR, PengRobinsonFluid -from PythonModels.systems.test_mql import COMPONENT_SPECS, GLOBAL_PARAMETERS +from app.simulation.core.peng_robinson import HELIUM_PR, PengRobinsonFluid +from app.simulation.examples.test_mql.system import COMPONENT_SPECS, GLOBAL_PARAMETERS _BINARY_OPERATORS = { diff --git a/app/simulation/examples/testmodel/test_mql_line_parameters.py b/app/simulation/examples/test_mql/line_parameters.py similarity index 99% rename from app/simulation/examples/testmodel/test_mql_line_parameters.py rename to app/simulation/examples/test_mql/line_parameters.py index e79298f..1632f37 100644 --- a/app/simulation/examples/testmodel/test_mql_line_parameters.py +++ b/app/simulation/examples/test_mql/line_parameters.py @@ -5,8 +5,8 @@ import tarfile from dataclasses import dataclass from pathlib import Path -from PythonModels.systems.test_mql import CONNECTION_SPECS, GLOBAL_PARAMETERS -from PythonModels.systems.test_mql_config import resolve_numeric_expression +from app.simulation.examples.test_mql.system import CONNECTION_SPECS, GLOBAL_PARAMETERS +from app.simulation.examples.test_mql.config import resolve_numeric_expression AMESIM_REFERENCE_PRESSURE_PA = 101_300.0 diff --git a/PythonModels/systems/test_mql_lines.py b/app/simulation/examples/test_mql/lines.py similarity index 93% rename from PythonModels/systems/test_mql_lines.py rename to app/simulation/examples/test_mql/lines.py index d437bc5..7715087 100644 --- a/PythonModels/systems/test_mql_lines.py +++ b/app/simulation/examples/test_mql/lines.py @@ -4,12 +4,12 @@ import re from collections import Counter from dataclasses import dataclass -from PythonModels.reporting.amesim_results import AmesimResults -from PythonModels.reporting.test_mql_variables import ( +from app.simulation.reporting.amesim_results import AmesimResults +from app.simulation.reporting.test_mql_variables import ( TestMqlVariableCatalog, build_test_mql_variable_catalog, ) -from PythonModels.systems.test_mql import CONNECTION_SPECS +from app.simulation.examples.test_mql.system import CONNECTION_SPECS TEST_MQL_PNEUMATIC_LINE_SUBMODELS = ("PNL0001", "PNL0002", "PNL0003", "PNL00R") diff --git a/app/simulation/examples/testmodel/test_mql_mechanical.py b/app/simulation/examples/test_mql/mechanical.py similarity index 98% rename from app/simulation/examples/testmodel/test_mql_mechanical.py rename to app/simulation/examples/test_mql/mechanical.py index 1b8481f..8086373 100644 --- a/app/simulation/examples/testmodel/test_mql_mechanical.py +++ b/app/simulation/examples/test_mql/mechanical.py @@ -2,19 +2,19 @@ from __future__ import annotations from dataclasses import dataclass -from PythonModels.components.amesim_mechanical import ( +from app.simulation.examples.test_mql.primitives.mechanical import ( AmesimElasticEndstop, AmesimMassFrictionEndstops, AmesimPistonGeometry, circular_area, mm_to_m, ) -from PythonModels.reporting.amesim_results import AmesimResults -from PythonModels.reporting.test_mql_variables import ( +from app.simulation.reporting.amesim_results import AmesimResults +from app.simulation.reporting.test_mql_variables import ( TestMqlVariableCatalog, build_test_mql_variable_catalog, ) -from PythonModels.systems.test_mql_config import TestMqlConfig, TestMqlResolvedComponent +from app.simulation.examples.test_mql.config import TestMqlConfig, TestMqlResolvedComponent MM_TO_M = 1.0e-3 diff --git a/app/simulation/examples/testmodel/test_mql_nodes.py b/app/simulation/examples/test_mql/nodes.py similarity index 99% rename from app/simulation/examples/testmodel/test_mql_nodes.py rename to app/simulation/examples/test_mql/nodes.py index 1227abc..001a782 100644 --- a/app/simulation/examples/testmodel/test_mql_nodes.py +++ b/app/simulation/examples/test_mql/nodes.py @@ -2,7 +2,7 @@ from __future__ import annotations from dataclasses import dataclass -from PythonModels.systems.test_mql import COMPONENT_SPECS +from app.simulation.examples.test_mql.system import COMPONENT_SPECS @dataclass(frozen=True) diff --git a/app/simulation/examples/testmodel/test_mql_pneumatic.py b/app/simulation/examples/test_mql/pneumatic.py similarity index 97% rename from app/simulation/examples/testmodel/test_mql_pneumatic.py rename to app/simulation/examples/test_mql/pneumatic.py index 208507c..edebf7d 100644 --- a/app/simulation/examples/testmodel/test_mql_pneumatic.py +++ b/app/simulation/examples/test_mql/pneumatic.py @@ -2,14 +2,14 @@ from __future__ import annotations from dataclasses import dataclass -from PythonModels.components.amesim_pneumatic import ( +from app.simulation.examples.test_mql.primitives.pneumatic import ( HELIUM_PNEUMATIC_GAS, AmesimPneumaticGas, AmesimPneumaticOrifice, AmesimPneumaticVolume, AmesimVariablePneumaticVolume, ) -from PythonModels.systems.test_mql_config import TestMqlConfig, TestMqlResolvedComponent +from app.simulation.examples.test_mql.config import TestMqlConfig, TestMqlResolvedComponent AMESIM_REFERENCE_PRESSURE_PA = 101_300.0 @@ -162,7 +162,7 @@ def build_test_mql_pneumatic_assembly( def _build_variable_orifice_controls( config: TestMqlConfig, ) -> dict[str, TestMqlVariableOrificeControl]: - from PythonModels.systems.test_mql import CONNECTION_SPECS + from app.simulation.examples.test_mql.system import CONNECTION_SPECS components_by_alias = {component.alias: component for component in config.components} variable_orifice_aliases = { diff --git a/app/simulation/examples/testmodel/test_mql_pneumatic_lines.py b/app/simulation/examples/test_mql/pneumatic_lines.py similarity index 96% rename from app/simulation/examples/testmodel/test_mql_pneumatic_lines.py rename to app/simulation/examples/test_mql/pneumatic_lines.py index 850991b..0eb9414 100644 --- a/app/simulation/examples/testmodel/test_mql_pneumatic_lines.py +++ b/app/simulation/examples/test_mql/pneumatic_lines.py @@ -3,17 +3,17 @@ from __future__ import annotations from dataclasses import dataclass from pathlib import Path -from PythonModels.components.amesim_pneumatic import ( +from app.simulation.examples.test_mql.primitives.pneumatic import ( HELIUM_PNEUMATIC_GAS, AmesimPneumaticGas, ) -from PythonModels.components.amesim_pneumatic_line import ( +from app.simulation.examples.test_mql.primitives.pneumatic_lines import ( AmesimPnl0001Pipe, AmesimPnl0002Pipe, AmesimPnl0003Pipe, AmesimPnl00rPipe, ) -from PythonModels.systems.test_mql_line_parameters import ( +from app.simulation.examples.test_mql.line_parameters import ( TestMqlPnl0001Spec, TestMqlPnl0002Spec, TestMqlPnl0003Spec, diff --git a/PythonModels/systems/test_mql_pneumatic_topology.py b/app/simulation/examples/test_mql/pneumatic_topology.py similarity index 96% rename from PythonModels/systems/test_mql_pneumatic_topology.py rename to app/simulation/examples/test_mql/pneumatic_topology.py index 70babcc..408133a 100644 --- a/PythonModels/systems/test_mql_pneumatic_topology.py +++ b/app/simulation/examples/test_mql/pneumatic_topology.py @@ -1,7 +1,7 @@ from __future__ import annotations -from PythonModels.systems.test_mql_closure import TestMqlPneumaticChamberSegmentSpec -from PythonModels.systems.test_mql_topology import TestMqlCirTopology +from app.simulation.examples.test_mql.closure import TestMqlPneumaticChamberSegmentSpec +from app.simulation.examples.test_mql.topology import TestMqlCirTopology def discover_fixed_chamber_segments( diff --git a/app/simulation/examples/test_mql/primitives/__init__.py b/app/simulation/examples/test_mql/primitives/__init__.py new file mode 100644 index 0000000..64650fa --- /dev/null +++ b/app/simulation/examples/test_mql/primitives/__init__.py @@ -0,0 +1 @@ +"""Calibrated component primitives used only by the ``test_mql`` example.""" diff --git a/PythonModels/components/amesim_mechanical.py b/app/simulation/examples/test_mql/primitives/mechanical.py similarity index 100% rename from PythonModels/components/amesim_mechanical.py rename to app/simulation/examples/test_mql/primitives/mechanical.py diff --git a/PythonModels/components/amesim_pneumatic.py b/app/simulation/examples/test_mql/primitives/pneumatic.py similarity index 97% rename from PythonModels/components/amesim_pneumatic.py rename to app/simulation/examples/test_mql/primitives/pneumatic.py index d705c0e..cd64f98 100644 --- a/PythonModels/components/amesim_pneumatic.py +++ b/app/simulation/examples/test_mql/primitives/pneumatic.py @@ -3,11 +3,11 @@ from __future__ import annotations from dataclasses import dataclass from math import pi, sqrt -from PythonModels.core.base import AlgebraicComponent, DynamicComponent -from PythonModels.core.medium import ThermodynamicProperties -from PythonModels.core.peng_robinson import HELIUM_PR, PengRobinsonFluid -from PythonModels.core.ports import PortState -from PythonModels.core.state import VolumeState +from app.simulation.core.base import AlgebraicComponent, DynamicComponent +from app.simulation.core.medium import ThermodynamicProperties +from app.simulation.core.peng_robinson import HELIUM_PR, PengRobinsonFluid +from app.simulation.core.ports import PortState +from app.simulation.core.state import VolumeState @dataclass(frozen=True) diff --git a/PythonModels/components/amesim_pneumatic_line.py b/app/simulation/examples/test_mql/primitives/pneumatic_lines.py similarity index 98% rename from PythonModels/components/amesim_pneumatic_line.py rename to app/simulation/examples/test_mql/primitives/pneumatic_lines.py index 7810d0d..c0b0ef7 100644 --- a/PythonModels/components/amesim_pneumatic_line.py +++ b/app/simulation/examples/test_mql/primitives/pneumatic_lines.py @@ -3,16 +3,16 @@ from __future__ import annotations from dataclasses import dataclass from math import log10, pi, sqrt -from PythonModels.components.amesim_pneumatic import ( +from app.simulation.examples.test_mql.primitives.pneumatic import ( HELIUM_PNEUMATIC_GAS, AmesimPneumaticGas, compressible_orifice_mass_flow, diameter_mm_to_area_m2, ) -from PythonModels.core.base import AlgebraicComponent, DynamicComponent -from PythonModels.core.medium import ThermodynamicProperties -from PythonModels.core.ports import PortState -from PythonModels.core.state import VolumeState +from app.simulation.core.base import AlgebraicComponent, DynamicComponent +from app.simulation.core.medium import ThermodynamicProperties +from app.simulation.core.ports import PortState +from app.simulation.core.state import VolumeState @dataclass(frozen=True) @@ -165,7 +165,7 @@ class AmesimPnl0001Pipe(_DarcyPipeResistanceMixin, DynamicComponent): """Physical first-pass implementation of AMESim ``PNL0001`` (C-R). Port 2 owns the lumped gas storage. Port 1 is connected through a Darcy - resistance. Both connection mass flows use the PythonModels convention: + resistance. Both connection mass flows use the simulation convention: positive values enter the pipe storage. AMESim's proprietary ``pn2pipefr`` utility is represented by an diff --git a/app/simulation/examples/test_mql/run.py b/app/simulation/examples/test_mql/run.py index 9fb3342..3d3dec3 100644 --- a/app/simulation/examples/test_mql/run.py +++ b/app/simulation/examples/test_mql/run.py @@ -5,8 +5,7 @@ from datetime import UTC, datetime from pathlib import Path from app.simulation.paths import PROJECT_ROOT, SIMULATION_RUNS_DIR -from PythonModels.scripts.run_test_mql import format_test_mql_summary -from PythonModels.systems.test_mql import ( +from app.simulation.examples.test_mql.system import ( TestMqlRunConfig, TestMqlSimulationResult, TestMqlSystem, @@ -29,6 +28,25 @@ class TestMqlRunResult: summary_path: Path +def format_test_mql_summary(system: TestMqlSystem) -> str: + snapshot = system.snapshot() + lines = [ + "Model: test_mql", + f"Source archive: {system.archive_path}", + f"Components: {snapshot.component_count}", + f"Connections: {snapshot.connection_count}", + f"Continuous states in AMESim modelinfo: {snapshot.continuous_state_count}", + f"Discrete states in AMESim modelinfo: {snapshot.discrete_state_count}", + "Global parameters:", + ] + for name, value in sorted(snapshot.global_parameters.items()): + lines.append(f" - {name}: {value}") + lines.append("Component submodels:") + for name, count in sorted(snapshot.submodel_counts.items()): + lines.append(f" - {name}: {count}") + return "\n".join(lines) + "\n" + + def _default_run_output_dir() -> Path: timestamp = datetime.now(UTC).strftime("test_mql_%Y%m%d_%H%M%S_%f") return SIMULATION_RUNS_DIR / timestamp diff --git a/PythonModels/scripts/run_test_mql_baseline.py b/app/simulation/examples/test_mql/run_baseline.py similarity index 89% rename from PythonModels/scripts/run_test_mql_baseline.py rename to app/simulation/examples/test_mql/run_baseline.py index eb37505..eed6a57 100644 --- a/PythonModels/scripts/run_test_mql_baseline.py +++ b/app/simulation/examples/test_mql/run_baseline.py @@ -4,16 +4,20 @@ from dataclasses import dataclass, field from datetime import UTC, datetime from pathlib import Path -from PythonModels.systems.test_mql_baseline import ( +from app.simulation.examples.test_mql.baseline import ( TestMqlBaselineRun, run_test_mql_baseline_passthrough, ) +from app.simulation.paths import ( + AMESIM_TEST_MQL_ARCHIVE_PATH, + SIMULATION_RUNS_DIR, +) @dataclass(frozen=True) class TestMqlBaselinePathConfig: archive_path: Path = field( - default_factory=lambda: Path(__file__).resolve().parents[2] / "AmesimModels" / "test_mql.ame" + default_factory=lambda: AMESIM_TEST_MQL_ARCHIVE_PATH ) output_dir: Path | None = None @@ -31,9 +35,8 @@ class TestMqlBaselineScriptConfig: def _default_output_dir() -> Path: - pythonmodels_root = Path(__file__).resolve().parents[1] timestamp = datetime.now(UTC).strftime("test_mql_baseline_%Y%m%d_%H%M%S_%f") - return pythonmodels_root / "runs" / timestamp + return SIMULATION_RUNS_DIR / timestamp def format_test_mql_baseline_summary(run: TestMqlBaselineRun) -> str: diff --git a/PythonModels/scripts/run_test_mql_full_state_comparison.py b/app/simulation/examples/test_mql/run_full_state_comparison.py similarity index 99% rename from PythonModels/scripts/run_test_mql_full_state_comparison.py rename to app/simulation/examples/test_mql/run_full_state_comparison.py index 1b3c8d4..a24083c 100644 --- a/PythonModels/scripts/run_test_mql_full_state_comparison.py +++ b/app/simulation/examples/test_mql/run_full_state_comparison.py @@ -7,30 +7,34 @@ from datetime import UTC, datetime from math import nextafter, sqrt from pathlib import Path -from PythonModels.components.amesim_pneumatic import AmesimPneumaticGas -from PythonModels.core.solver import SolveIVPConfig, integrate_ode -from PythonModels.reporting.amesim_results import AmesimResults, load_test_mql_amesim_results -from PythonModels.reporting.test_mql_comparison import ( +from app.simulation.examples.test_mql.primitives.pneumatic import AmesimPneumaticGas +from app.simulation.examples.test_mql.solver import SolveIVPConfig, integrate_ode +from app.simulation.reporting.amesim_results import AmesimResults, load_test_mql_amesim_results +from app.simulation.reporting.test_mql_comparison import ( TestMqlComparisonResult, interpolate_series_value, write_test_mql_comparison_csv, ) -from PythonModels.reporting.test_mql_output_schema import ( +from app.simulation.reporting.test_mql_output_schema import ( TestMqlOutputSchema, build_test_mql_output_schema, ) -from PythonModels.reporting.test_mql_output_validation import ( +from app.simulation.reporting.test_mql_output_validation import ( TestMqlValidatedOutput, compare_validated_test_mql_output, validate_test_mql_output, ) -from PythonModels.systems.test_mql import ( +from app.simulation.examples.test_mql.system import ( TestMqlPnl0001LineRhsDiagnostic, TestMqlSimulationResult, TestMqlSystem, TestMqlVariableChamberRhsDiagnostic, ) -from PythonModels.systems.test_mql_pneumatic import AMESIM_REFERENCE_PRESSURE_PA +from app.simulation.examples.test_mql.pneumatic import AMESIM_REFERENCE_PRESSURE_PA +from app.simulation.paths import ( + AMESIM_TEST_MQL_ARCHIVE_PATH, + SIMULATION_RUNS_DIR, +) DEFAULT_FULL_STATE_COMPARISON_DATA_PATHS = ( @@ -758,9 +762,7 @@ class TestMqlPnvoEventWindowCandidateComparisonDiagnostic: @dataclass(frozen=True) class TestMqlFullStateComparisonPathConfig: archive_path: Path = field( - default_factory=lambda: Path(__file__).resolve().parents[2] - / "AmesimModels" - / "test_mql.ame" + default_factory=lambda: AMESIM_TEST_MQL_ARCHIVE_PATH ) amesim_results_archive_path: Path | None = None output_dir: Path | None = None @@ -798,9 +800,8 @@ class TestMqlFullStateComparisonScriptConfig: def _default_output_dir() -> Path: - pythonmodels_root = Path(__file__).resolve().parents[1] timestamp = datetime.now(UTC).strftime("test_mql_full_state_%Y%m%d_%H%M%S_%f") - return pythonmodels_root / "runs" / timestamp + return SIMULATION_RUNS_DIR / timestamp def run_test_mql_full_state_comparison( diff --git a/app/simulation/examples/test_mql/solver.py b/app/simulation/examples/test_mql/solver.py new file mode 100644 index 0000000..e9ca298 --- /dev/null +++ b/app/simulation/examples/test_mql/solver.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from dataclasses import dataclass + +from app.simulation.solvers.solver import ( + SolveIVPConfig as _BaseSolveIVPConfig, + integrate_ode, +) + + +@dataclass(frozen=True) +class SolveIVPConfig(_BaseSolveIVPConfig): + """Solver defaults used by the calibrated ``test_mql`` example.""" + + atol: float = 1e-10 + + +__all__ = ["SolveIVPConfig", "integrate_ode"] diff --git a/PythonModels/core/network.py b/app/simulation/examples/test_mql/structural_network.py similarity index 97% rename from PythonModels/core/network.py rename to app/simulation/examples/test_mql/structural_network.py index e912867..5f96f43 100644 --- a/PythonModels/core/network.py +++ b/app/simulation/examples/test_mql/structural_network.py @@ -2,7 +2,7 @@ from __future__ import annotations from dataclasses import dataclass -from PythonModels.core.base import Component, DynamicComponent +from app.simulation.core.base import Component, DynamicComponent @dataclass(frozen=True) diff --git a/app/simulation/examples/testmodel/test_mql.py b/app/simulation/examples/test_mql/system.py similarity index 98% rename from app/simulation/examples/testmodel/test_mql.py rename to app/simulation/examples/test_mql/system.py index 1bd736b..1e5635f 100644 --- a/app/simulation/examples/testmodel/test_mql.py +++ b/app/simulation/examples/test_mql/system.py @@ -4,13 +4,13 @@ from dataclasses import dataclass, field from pathlib import Path from types import SimpleNamespace -from PythonModels.core.base import Component -from PythonModels.core.network import SimulationNetwork -from PythonModels.core.solver import SolveIVPConfig, integrate_ode +from app.simulation.core.base import Component +from app.simulation.examples.test_mql.solver import SolveIVPConfig, integrate_ode +from app.simulation.examples.test_mql.structural_network import SimulationNetwork +from app.simulation.paths import AMESIM_TEST_MQL_ARCHIVE_PATH MODEL_NAME = "test_mql" -AMESIM_ARCHIVE_RELATIVE_PATH = Path("AmesimModels/test_mql.ame") # Extracted from AmesimModels/test_mql.ame on the model-development branch. # This is a structural Python port scaffold: it preserves the AMESim component @@ -4237,7 +4237,7 @@ class TestMqlFullStateClosure: state_vector: list[float], data_paths: tuple[str, ...] | list[str] | None = None, ) -> dict[str, float]: - from PythonModels.systems.test_mql_pneumatic import pressure_to_amesim_gauge_pa + from app.simulation.examples.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_at(time_s, state_vector) @@ -4456,7 +4456,7 @@ class TestMqlFullStateClosure: } def _piston_force_by_mass_alias(self, pneumatic_snapshot: object) -> dict[str, float]: - from PythonModels.systems.test_mql_pneumatic import pressure_to_amesim_gauge_pa + from app.simulation.examples.test_mql.pneumatic import pressure_to_amesim_gauge_pa force_by_mass_alias: dict[str, float] = {} for mass_alias, piston_alias, chamber_field in _PISTON_FORCE_BINDINGS: @@ -4510,7 +4510,7 @@ class TestMqlSystem: """Structural Python port scaffold for AMESim model ``test_mql``.""" def __init__(self, archive_path: Path | None = None) -> None: - self.archive_path = archive_path or Path(__file__).resolve().parents[2] / AMESIM_ARCHIVE_RELATIVE_PATH + self.archive_path = archive_path or AMESIM_TEST_MQL_ARCHIVE_PATH self.network = SimulationNetwork(name=MODEL_NAME) self.pnl0001_assembly = self._build_pnl0001_assembly() self.pnl0002_assembly = self._build_pnl0002_assembly() @@ -4537,40 +4537,40 @@ class TestMqlSystem: @staticmethod def _build_pneumatic_assembly(): # Local import avoids a module cycle: test_mql_config reads constants from this module. - from PythonModels.systems.test_mql_pneumatic import build_test_mql_pneumatic_assembly + from app.simulation.examples.test_mql.pneumatic import build_test_mql_pneumatic_assembly return build_test_mql_pneumatic_assembly() @staticmethod def _build_mechanical_assembly(): # Local import avoids a module cycle: test_mql_config reads constants from this module. - from PythonModels.systems.test_mql_mechanical import build_test_mql_mechanical_assembly + from app.simulation.examples.test_mql.mechanical import build_test_mql_mechanical_assembly return build_test_mql_mechanical_assembly() def _build_pnl0001_assembly(self): - from PythonModels.systems.test_mql_pneumatic_lines import ( + from app.simulation.examples.test_mql.pneumatic_lines import ( build_test_mql_pnl0001_assembly, ) return build_test_mql_pnl0001_assembly(self.archive_path) def _build_pnl0002_assembly(self): - from PythonModels.systems.test_mql_pneumatic_lines import ( + from app.simulation.examples.test_mql.pneumatic_lines import ( build_test_mql_pnl0002_assembly, ) return build_test_mql_pnl0002_assembly(self.archive_path) def _build_pnl0003_assembly(self): - from PythonModels.systems.test_mql_pneumatic_lines import ( + from app.simulation.examples.test_mql.pneumatic_lines import ( build_test_mql_pnl0003_assembly, ) return build_test_mql_pnl0003_assembly(self.archive_path) def _build_pnl00r_assembly(self): - from PythonModels.systems.test_mql_pneumatic_lines import ( + from app.simulation.examples.test_mql.pneumatic_lines import ( build_test_mql_pnl00r_assembly, ) @@ -4578,13 +4578,13 @@ class TestMqlSystem: @staticmethod def _build_node3_assembly(): - from PythonModels.systems.test_mql_nodes import build_test_mql_node3_assembly + from app.simulation.examples.test_mql.nodes import build_test_mql_node3_assembly return build_test_mql_node3_assembly() @staticmethod def _build_node4_assembly(): - from PythonModels.systems.test_mql_nodes import build_test_mql_node4_assembly + from app.simulation.examples.test_mql.nodes import build_test_mql_node4_assembly return build_test_mql_node4_assembly() @@ -4637,7 +4637,7 @@ class TestMqlSystem: return self.pnl0001_assembly.lines[spec.alias] def _pnl0002_connection_for_node_port(self, node_alias: str, port_name: str): - from PythonModels.systems.test_mql_nodes import TestMqlP4NodePortConnection + from app.simulation.examples.test_mql.nodes import TestMqlP4NodePortConnection spec = self._pnl0002_spec_for_node_port(node_alias, port_name) if spec.source_component == node_alias and spec.source_port == port_name: @@ -4674,7 +4674,7 @@ class TestMqlSystem: return self.pnl0002_assembly.lines[spec.alias] def _p4_primary_connection_for_node(self, node_alias: str): - from PythonModels.systems.test_mql_nodes import TestMqlP4NodePrimaryConnection + from app.simulation.examples.test_mql.nodes import TestMqlP4NodePrimaryConnection spec = self._pnl0001_spec_for_node_port(node_alias, "port_2") if spec.source_component == node_alias and spec.source_port == "port_2": @@ -4694,7 +4694,7 @@ class TestMqlSystem: ) def _p4_port4_orifice_connection_for_node(self, node_alias: str): - from PythonModels.systems.test_mql_nodes import TestMqlP4NodeOrificeConnection + from app.simulation.examples.test_mql.nodes import TestMqlP4NodeOrificeConnection for spec in CONNECTION_SPECS: if spec["submodel"] != "DIRECT": @@ -4720,7 +4720,7 @@ class TestMqlSystem: raise KeyError(f"No PNVO orifice attached to {node_alias}.port_4") def p4_node_neighborhood(self, node_alias: str): - from PythonModels.systems.test_mql_nodes import TestMqlP4NodeNeighborhood + from app.simulation.examples.test_mql.nodes import TestMqlP4NodeNeighborhood if node_alias not in self.node4_assembly: raise KeyError(f"Unknown P4 node: {node_alias}") @@ -4774,7 +4774,7 @@ class TestMqlSystem: return self.network.initial_state_vector() def mechanical_mass_closure(self): - from PythonModels.systems.test_mql_mechanical import TestMqlMechanicalMassClosure + from app.simulation.examples.test_mql.mechanical import TestMqlMechanicalMassClosure return TestMqlMechanicalMassClosure(self.mechanical_assembly) @@ -4900,7 +4900,7 @@ class TestMqlSystem: downstream_volume_alias: str, source: str = "manual", ): - from PythonModels.systems.test_mql_closure import TestMqlPneumaticBranchSpec + from app.simulation.examples.test_mql.closure import TestMqlPneumaticBranchSpec return TestMqlPneumaticBranchSpec( name=name, @@ -4911,14 +4911,14 @@ class TestMqlSystem: ) def discover_pneumatic_branch_topology(self): - from PythonModels.systems.test_mql_closure import ( + from app.simulation.examples.test_mql.closure import ( TestMqlPneumaticTopologyCandidate, TestMqlPneumaticTopologyDiscovery, ) - from PythonModels.systems.test_mql_pneumatic_topology import ( + from app.simulation.examples.test_mql.pneumatic_topology import ( discover_fixed_chamber_segments, ) - from PythonModels.systems.test_mql_topology import load_test_mql_cir_topology + from app.simulation.examples.test_mql.topology import load_test_mql_cir_topology topology = load_test_mql_cir_topology(self.archive_path) chamber_segment_specs = discover_fixed_chamber_segments( @@ -4999,11 +4999,11 @@ class TestMqlSystem: Boundary values apply at the PNL0001/orifice interfaces, not at the nodes. """ - from PythonModels.components.amesim_pneumatic import ( + from app.simulation.examples.test_mql.primitives.pneumatic import ( AmesimPneumaticOrifice, AmesimPneumaticVolume, ) - from PythonModels.systems.test_mql_closure import ( + from app.simulation.examples.test_mql.closure import ( TestMqlPneumaticBoundaryCondition, TestMqlPneumaticChamberSegmentClosure, TestMqlPneumaticChamberSegmentComponents, @@ -5076,11 +5076,11 @@ class TestMqlSystem: outlet_temperature_k: float = 293.15, ): """Insert the topology-derived inlet PNL0001 into a chamber segment.""" - from PythonModels.components.amesim_pneumatic import ( + from app.simulation.examples.test_mql.primitives.pneumatic import ( AmesimPneumaticOrifice, AmesimPneumaticVolume, ) - from PythonModels.systems.test_mql_closure import ( + from app.simulation.examples.test_mql.closure import ( TestMqlPneumaticBoundaryCondition, TestMqlPnl0001ChamberSegmentClosure, TestMqlPnl0001ChamberSegmentComponents, @@ -5154,11 +5154,11 @@ class TestMqlSystem: outlet_node_temperature_k: float = 293.15, ): """Insert both topology-derived PNL0001 states into the segment.""" - from PythonModels.components.amesim_pneumatic import ( + from app.simulation.examples.test_mql.primitives.pneumatic import ( AmesimPneumaticOrifice, AmesimPneumaticVolume, ) - from PythonModels.systems.test_mql_closure import ( + from app.simulation.examples.test_mql.closure import ( TestMqlPneumaticBoundaryCondition, TestMqlPnl0001PairChamberSegmentClosure, TestMqlPnl0001PairChamberSegmentComponents, @@ -5241,11 +5241,11 @@ class TestMqlSystem: port-1 compliance. The PNL0003 port-2 side is connected through the real PNVO001 orifice to a prescribed P4 pressure boundary. """ - from PythonModels.components.amesim_pneumatic import ( + from app.simulation.examples.test_mql.primitives.pneumatic import ( AmesimPneumaticOrifice, AmesimPneumaticVolume, ) - from PythonModels.systems.test_mql_closure import ( + from app.simulation.examples.test_mql.closure import ( TestMqlPneumaticBoundaryCondition, TestMqlPn3NodeChamberSegmentClosure, TestMqlPn3NodeChamberSegmentComponents, @@ -5354,11 +5354,11 @@ class TestMqlSystem: PNVO upstream endpoints are closed through their adjacent PN3/PNL0003 subnets. """ - from PythonModels.components.amesim_pneumatic import ( + from app.simulation.examples.test_mql.primitives.pneumatic import ( AmesimPneumaticOrifice, AmesimPneumaticVolume, ) - from PythonModels.systems.test_mql_closure import ( + from app.simulation.examples.test_mql.closure import ( TestMqlPneumaticBoundaryCondition, TestMqlPn3P4NodeChamberSegmentClosure, TestMqlPn3P4NodeChamberSegmentComponents, @@ -6140,11 +6140,11 @@ class TestMqlSystem: downstream_volume_alias: str, spec=None, ): - from PythonModels.components.amesim_pneumatic import ( + from app.simulation.examples.test_mql.primitives.pneumatic import ( AmesimPneumaticOrifice, AmesimPneumaticVolume, ) - from PythonModels.systems.test_mql_closure import ( + from app.simulation.examples.test_mql.closure import ( TestMqlPneumaticBranchComponents, TestMqlPneumaticBranchSpec, TestMqlPneumaticClosure, diff --git a/PythonModels/systems/test_mql_topology.py b/app/simulation/examples/test_mql/topology.py similarity index 100% rename from PythonModels/systems/test_mql_topology.py rename to app/simulation/examples/test_mql/topology.py diff --git a/app/simulation/examples/testmodel/run_test_mql.py b/app/simulation/examples/testmodel/run_test_mql.py deleted file mode 100644 index 0fd7977..0000000 --- a/app/simulation/examples/testmodel/run_test_mql.py +++ /dev/null @@ -1,74 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass, field -from datetime import UTC, datetime -from pathlib import Path - -from PythonModels.systems.test_mql import TestMqlRunConfig, TestMqlSystem - - -@dataclass(frozen=True) -class TestMqlPathConfig: - output_dir: Path | None = None - - -@dataclass(frozen=True) -class TestMqlExecutionConfig: - write_summary: bool = True - - -@dataclass(frozen=True) -class TestMqlScriptConfig: - run: TestMqlRunConfig = field(default_factory=TestMqlRunConfig) - paths: TestMqlPathConfig = field(default_factory=TestMqlPathConfig) - execution: TestMqlExecutionConfig = field(default_factory=TestMqlExecutionConfig) - - -def _default_output_dir() -> Path: - pythonmodels_root = Path(__file__).resolve().parents[1] - timestamp = datetime.now(UTC).strftime("test_mql_%Y%m%d_%H%M%S_%f") - return pythonmodels_root / "runs" / timestamp - - -def format_test_mql_summary(system: TestMqlSystem) -> str: - snapshot = system.snapshot() - lines = [ - "Model: test_mql", - f"Source archive: {system.archive_path}", - f"Components: {snapshot.component_count}", - f"Connections: {snapshot.connection_count}", - f"Continuous states in AMESim modelinfo: {snapshot.continuous_state_count}", - f"Discrete states in AMESim modelinfo: {snapshot.discrete_state_count}", - "Global parameters:", - ] - for name, value in sorted(snapshot.global_parameters.items()): - lines.append(f" - {name}: {value}") - lines.append("Component submodels:") - for name, count in sorted(snapshot.submodel_counts.items()): - lines.append(f" - {name}: {count}") - return "\n".join(lines) + "\n" - - -def run_test_mql(config: TestMqlScriptConfig | None = None): - config = config or TestMqlScriptConfig() - system = TestMqlSystem() - result = system.simulate(config.run) - output_dir = config.paths.output_dir or _default_output_dir() - if config.execution.write_summary: - output_dir.mkdir(parents=True, exist_ok=True) - (output_dir / "test_mql_model_summary.txt").write_text( - format_test_mql_summary(system), - encoding="utf-8", - ) - return system, result, output_dir - - -def main() -> None: - system, result, output_dir = run_test_mql() - print(format_test_mql_summary(system), end="") - print(f"Samples: {len(result.t)}") - print(f"Output directory: {output_dir}") - - -if __name__ == "__main__": - main() diff --git a/app/simulation/examples/testmodel/run_test_mql_baseline.py b/app/simulation/examples/testmodel/run_test_mql_baseline.py deleted file mode 100644 index eb37505..0000000 --- a/app/simulation/examples/testmodel/run_test_mql_baseline.py +++ /dev/null @@ -1,77 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass, field -from datetime import UTC, datetime -from pathlib import Path - -from PythonModels.systems.test_mql_baseline import ( - TestMqlBaselineRun, - run_test_mql_baseline_passthrough, -) - - -@dataclass(frozen=True) -class TestMqlBaselinePathConfig: - archive_path: Path = field( - default_factory=lambda: Path(__file__).resolve().parents[2] / "AmesimModels" / "test_mql.ame" - ) - output_dir: Path | None = None - - -@dataclass(frozen=True) -class TestMqlBaselineExecutionConfig: - write_summary: bool = True - data_paths: tuple[str, ...] | None = None - - -@dataclass(frozen=True) -class TestMqlBaselineScriptConfig: - paths: TestMqlBaselinePathConfig = field(default_factory=TestMqlBaselinePathConfig) - execution: TestMqlBaselineExecutionConfig = field(default_factory=TestMqlBaselineExecutionConfig) - - -def _default_output_dir() -> Path: - pythonmodels_root = Path(__file__).resolve().parents[1] - timestamp = datetime.now(UTC).strftime("test_mql_baseline_%Y%m%d_%H%M%S_%f") - return pythonmodels_root / "runs" / timestamp - - -def format_test_mql_baseline_summary(run: TestMqlBaselineRun) -> str: - return "\n".join( - [ - "Model: test_mql", - "Mode: AMESim baseline passthrough", - f"Samples: {run.sample_count}", - f"Output schema signals: {run.output_schema.signal_count}", - f"Compared signals: {run.signal_count}", - f"Observation bindings: {run.observation_catalog.binding_count}", - f"Max absolute error: {run.comparison.max_abs_error}", - f"Max relative error: {run.comparison.max_rel_error}", - ] - ) + "\n" - - -def run_test_mql_baseline(config: TestMqlBaselineScriptConfig | None = None): - config = config or TestMqlBaselineScriptConfig() - run = run_test_mql_baseline_passthrough( - config.paths.archive_path, - data_paths=config.execution.data_paths, - ) - output_dir = config.paths.output_dir or _default_output_dir() - if config.execution.write_summary: - output_dir.mkdir(parents=True, exist_ok=True) - (output_dir / "test_mql_baseline_summary.txt").write_text( - format_test_mql_baseline_summary(run), - encoding="utf-8", - ) - return run, output_dir - - -def main() -> None: - run, output_dir = run_test_mql_baseline() - print(format_test_mql_baseline_summary(run), end="") - print(f"Output directory: {output_dir}") - - -if __name__ == "__main__": - main() diff --git a/app/simulation/examples/testmodel/run_test_mql_full_state_comparison.py b/app/simulation/examples/testmodel/run_test_mql_full_state_comparison.py deleted file mode 100644 index 1b3c8d4..0000000 --- a/app/simulation/examples/testmodel/run_test_mql_full_state_comparison.py +++ /dev/null @@ -1,4620 +0,0 @@ -from __future__ import annotations - -import argparse -from bisect import bisect_left -from dataclasses import dataclass, field, replace -from datetime import UTC, datetime -from math import nextafter, sqrt -from pathlib import Path - -from PythonModels.components.amesim_pneumatic import AmesimPneumaticGas -from PythonModels.core.solver import SolveIVPConfig, integrate_ode -from PythonModels.reporting.amesim_results import AmesimResults, load_test_mql_amesim_results -from PythonModels.reporting.test_mql_comparison import ( - TestMqlComparisonResult, - interpolate_series_value, - write_test_mql_comparison_csv, -) -from PythonModels.reporting.test_mql_output_schema import ( - TestMqlOutputSchema, - build_test_mql_output_schema, -) -from PythonModels.reporting.test_mql_output_validation import ( - TestMqlValidatedOutput, - compare_validated_test_mql_output, - validate_test_mql_output, -) -from PythonModels.systems.test_mql import ( - TestMqlPnl0001LineRhsDiagnostic, - TestMqlSimulationResult, - TestMqlSystem, - TestMqlVariableChamberRhsDiagnostic, -) -from PythonModels.systems.test_mql_pneumatic import AMESIM_REFERENCE_PRESSURE_PA - - -DEFAULT_FULL_STATE_COMPARISON_DATA_PATHS = ( - "press@pn_c1_8", - "vol@pn_c1_8", - "vol1@pn_brp2_8", - "vvol1@pn_brp2_8", - "x1@mass_friction_endstops_10", - "v1@mass_friction_endstops_10", - "acc1@mass_friction_endstops_10", - "x1@mass_friction_endstops_18", - "v1@mass_friction_endstops_18", - "acc1@mass_friction_endstops_18", - "dm1@pneumatic_69", - "xv@pn_morifice_1", - "dm2@pn_morifice_1", -) - - -@dataclass(frozen=True) -class TestMqlFullStateSignalDiagnostic: - data_path: str - initial_time_s: float - final_time_s: float - initial_python_value: float - initial_amesim_value: float - initial_abs_error: float - final_python_value: float - final_amesim_value: float - 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 TestMqlFullStatePnvoDiagnostic: - alias: str - initial_time_s: float - final_time_s: float - initial_python_opening: float - initial_amesim_opening: float - final_python_opening: float - final_amesim_opening: float - initial_python_mass_flow_kg_s: float - initial_amesim_mass_flow_kg_s: float - final_python_mass_flow_kg_s: float - final_amesim_mass_flow_kg_s: float - final_mass_flow_abs_error_kg_s: float - - -@dataclass(frozen=True) -class TestMqlFullStateComparisonRun: - system: TestMqlSystem - closure: object - amesim_results: AmesimResults - output_schema: TestMqlOutputSchema - result: TestMqlSimulationResult - output: TestMqlValidatedOutput - comparison: TestMqlComparisonResult - - @property - def sample_count(self) -> int: - return len(self.output.times) - - @property - def signal_count(self) -> int: - return len(self.output.data_paths) - - def metrics_by_max_abs_error(self): - return tuple( - sorted( - self.comparison.metrics, - key=lambda metric: metric.max_abs_error, - reverse=True, - ) - ) - - @property - def largest_abs_error_metric(self): - metrics = self.metrics_by_max_abs_error() - return metrics[0] if metrics else None - - def signal_diagnostic(self, data_path: str) -> TestMqlFullStateSignalDiagnostic: - times = self.output.times - python_values = self.output.series_by_data_path[data_path] - amesim_values = self.amesim_results.series(data_path) - initial_time = float(times[0]) - final_time = float(times[-1]) - initial_python = float(python_values[0]) - final_python = float(python_values[-1]) - initial_amesim = interpolate_series_value( - self.amesim_results.times, - amesim_values, - initial_time, - ) - final_amesim = interpolate_series_value( - self.amesim_results.times, - amesim_values, - final_time, - ) - return TestMqlFullStateSignalDiagnostic( - data_path=data_path, - initial_time_s=initial_time, - final_time_s=final_time, - initial_python_value=initial_python, - initial_amesim_value=initial_amesim, - initial_abs_error=abs(initial_python - initial_amesim), - final_python_value=final_python, - final_amesim_value=final_amesim, - 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 pnvo_diagnostic( - self, - alias: str = "pn_morifice_1", - ) -> TestMqlFullStatePnvoDiagnostic: - opening = self.signal_diagnostic(f"xv@{alias}") - mass_flow = self.signal_diagnostic(f"dm2@{alias}") - initial_python_mass_flow = mass_flow.initial_python_value * 1.0e-3 - initial_amesim_mass_flow = mass_flow.initial_amesim_value * 1.0e-3 - final_python_mass_flow = mass_flow.final_python_value * 1.0e-3 - final_amesim_mass_flow = mass_flow.final_amesim_value * 1.0e-3 - return TestMqlFullStatePnvoDiagnostic( - alias=alias, - initial_time_s=opening.initial_time_s, - final_time_s=opening.final_time_s, - initial_python_opening=opening.initial_python_value, - initial_amesim_opening=opening.initial_amesim_value, - final_python_opening=opening.final_python_value, - final_amesim_opening=opening.final_amesim_value, - initial_python_mass_flow_kg_s=initial_python_mass_flow, - initial_amesim_mass_flow_kg_s=initial_amesim_mass_flow, - final_python_mass_flow_kg_s=final_python_mass_flow, - final_amesim_mass_flow_kg_s=final_amesim_mass_flow, - final_mass_flow_abs_error_kg_s=abs( - final_python_mass_flow - final_amesim_mass_flow - ), - ) - - def diagnostics_by_final_abs_error(self): - return tuple( - sorted( - ( - self.signal_diagnostic(data_path) - for data_path in self.output.data_paths - ), - key=lambda diagnostic: diagnostic.final_abs_error, - reverse=True, - ) - ) - - @property - def largest_final_abs_error_diagnostic(self): - diagnostics = self.diagnostics_by_final_abs_error() - return diagnostics[0] if diagnostics else None - - def chamber_rhs_diagnostic(self, chamber_alias: str, sample_index: int = -1): - state_vector = [row[sample_index] for row in self.result.y] - return self.closure.variable_chamber_rhs_diagnostic( - chamber_alias=chamber_alias, - state_vector=state_vector, - time_s=float(self.result.t[sample_index]), - ) - - -@dataclass(frozen=True) -class TestMqlPnvoEventBoundaryDiagnostic: - orifice_alias: str - event_time_s: float - integration_stop_time_s: float - data_paths: tuple[str, ...] - python_values_by_data_path: dict[str, float] - amesim_values_by_data_path: dict[str, float] - - def abs_error(self, data_path: str) -> float: - return abs( - self.python_values_by_data_path[data_path] - - self.amesim_values_by_data_path[data_path] - ) - - -@dataclass(frozen=True) -class TestMqlPnvoEventWindowSegmentDiagnostic: - t_start: float - t_stop: float - method: str - rtol: float - atol: float - max_step: float - rhs_evaluations: int - success: bool - message: str - - -@dataclass(frozen=True) -class TestMqlPnl0001PressureLossCalibrationDiagnostic: - line_alias: str - chamber_alias: str - time_s: float - amesim_cm: float - amesim_dm1_g_s: float - amesim_mass_flow_magnitude_kg_s: float - amesim_line_gauge_pressure_pa: float - amesim_chamber_gauge_pressure_pa: float - amesim_line_temperature_k: float - amesim_pressure_drop_pa: float - current_darcy_pressure_drop_pa: float - pressure_drop_multiplier: float - candidate_pn2pipefr_dm1_g_s: float - candidate_pn2pipefr_to_amesim_dm1_ratio: float - amesim_linear_conductance_kg_s_sqrt_k_per_pa: float - python_linear_conductance_kg_s_sqrt_k_per_pa: float - python_to_amesim_linear_conductance_ratio: float - - -@dataclass(frozen=True) -class TestMqlPnvoFlowParameterDiagnostic: - orifice_alias: str - time_s: float - amesim_cm: float - amesim_dm2_g_s: float - amesim_opening: float - amesim_gas_velocity_m_s: float - python_opening: float - python_flow_coefficient: float - python_effective_area_m2: float - python_line_pressure_pa: float - python_boundary_pressure_pa: float - python_upstream_pressure_pa: float - python_upstream_temperature_k: float - python_mass_flow_kg_s: float - python_cm: float - python_to_amesim_cm_ratio: float - - -@dataclass(frozen=True) -class TestMqlPnl0001EnergyFlowDiagnostic: - line_alias: str - chamber_alias: str - node_alias: str - time_s: float - amesim_port_1_enthalpy_flow_w: float - amesim_port_1_mass_flow_g_s: float - amesim_node_port_2_enthalpy_flow_w: float - amesim_node_port_2_mass_flow_g_s: float - amesim_observed_port_enthalpy_sum_w: float - python_port_1_internal_energy_flow_w: float - python_port_2_internal_energy_flow_w: float - python_current_energy_derivative_w: float - python_direct_enthalpy_port_1_flow_w: float - python_direct_enthalpy_port_2_flow_w: float - python_direct_enthalpy_energy_derivative_w: float - python_direct_minus_current_energy_derivative_w: float - python_p4_node_port_2_enthalpy_flow_w: float - python_p4_node_port_2_mass_flow_g_s: float - python_p4_node_port_2_connected_h_j_kg: float - python_p4_node_port_2_connected_temperature_k: float - python_p4_node_port_2_connected_h_delta_to_line_h_j_kg: float - python_p4_node_port_2_counterfactual_dtemp_k_s: float - reference_temperature_k: float - amesim_port_1_reference_enthalpy_estimate_w: float - amesim_port_1_reference_enthalpy_error_w: float - amesim_candidate_pn2pipefr_dm2i_g_s: float - amesim_candidate_pn2pipefr_dh2i_w: float - amesim_candidate_storage_enthalpy_sum_w: float - amesim_candidate_pn2pipefr_dtemp_k_s: float - amesim_candidate_pn2pipefr_dtemp_residual_k_s: float - python_reference_port_1_enthalpy_flow_w: float - python_reference_node_port_2_enthalpy_flow_w: float - python_reference_observed_port_enthalpy_sum_w: float - python_reference_port_1_to_amesim_error_w: float - python_reference_node_port_2_to_amesim_error_w: float - python_reference_sum_to_amesim_error_w: float - python_current_line_temperature_derivative_k_s: float - amesim_line_temperature_derivative_fd_k_s: float - amesim_pn2vol2_dtemp_node_plus_dh1_k_s: float - amesim_pn2vol2_dtemp_node_minus_dh1_k_s: float - python_reference_pn2vol2_dtemp_node_minus_dh1_k_s: float - - -@dataclass(frozen=True) -class TestMqlP4NodeEnthalpyBreakdownDiagnostic: - node_alias: str - time_s: float - amesim_port_1_enthalpy_flow_w: float - amesim_port_1_mass_flow_g_s: float - amesim_port_3_enthalpy_flow_w: float - amesim_port_3_mass_flow_g_s: float - amesim_port_4_enthalpy_flow_w: float - amesim_port_4_mass_flow_g_s: float - amesim_port_2_enthalpy_flow_w: float - amesim_port_2_mass_flow_g_s: float - python_port_1_enthalpy_flow_w: float - python_port_1_mass_flow_g_s: float - python_port_3_enthalpy_flow_w: float - python_port_3_mass_flow_g_s: float - python_port_4_enthalpy_flow_w: float - python_port_4_mass_flow_g_s: float - python_port_2_enthalpy_flow_w: float - python_port_2_mass_flow_g_s: float - python_reference_port_1_enthalpy_flow_w: float - python_reference_port_3_enthalpy_flow_w: float - python_reference_port_4_enthalpy_flow_w: float - python_reference_port_2_enthalpy_flow_w: float - python_real_gas_reference_port_1_enthalpy_flow_w: float - python_real_gas_reference_port_3_enthalpy_flow_w: float - python_real_gas_reference_port_4_enthalpy_flow_w: float - python_real_gas_reference_port_2_enthalpy_flow_w: float - python_port_2_enthalpy_error_w: float - python_reference_port_2_enthalpy_error_w: float - python_real_gas_reference_port_2_enthalpy_error_w: float - - -@dataclass(frozen=True) -class TestMqlPnvoUpstreamEnthalpyDiagnostic: - orifice_alias: str - upstream_line_alias: str - time_s: float - amesim_orifice_port_2_enthalpy_flow_w: float - amesim_orifice_port_2_mass_flow_g_s: float - amesim_orifice_port_3_enthalpy_flow_w: float - amesim_orifice_port_3_mass_flow_g_s: float - amesim_upstream_line_center_enthalpy_flow_w: float - amesim_upstream_line_center_mass_flow_g_s: float - amesim_upstream_line_port_2_temperature_k: float - amesim_upstream_line_port_2_pressure_pa: float - amesim_orifice_port_2_implied_reference_temperature_k: float - amesim_orifice_port_2_implied_temperature_delta_to_line_k: float - amesim_upstream_line_port_2_reference_enthalpy_flow_w: float - amesim_upstream_line_port_2_reference_enthalpy_error_w: float - amesim_upstream_line_port_2_real_gas_reference_enthalpy_flow_w: float - amesim_upstream_line_port_2_real_gas_reference_enthalpy_error_w: float - python_orifice_to_node_flow_kg_s: float - python_upstream_line_port_2_temperature_k: float - python_upstream_line_port_2_pressure_pa: float - python_current_port_2_enthalpy_flow_w: float - python_reference_port_2_enthalpy_flow_w: float - python_real_gas_reference_port_2_enthalpy_flow_w: float - python_current_port_2_enthalpy_error_w: float - python_reference_port_2_enthalpy_error_w: float - python_real_gas_reference_port_2_enthalpy_error_w: float - -@dataclass(frozen=True) -class TestMqlPnl0003EnergyDiagnostic: - line_alias: str - time_s: float - amesim_port_1_temperature_k: float - amesim_port_2_temperature_k: float - amesim_port_1_pressure_pa: float - amesim_port_2_pressure_pa: float - amesim_port_1_mass_flow_g_s: float - amesim_port_1_enthalpy_flow_w: float - amesim_center_mass_flow_g_s: float - amesim_center_enthalpy_flow_w: float - amesim_port_2_mass_flow_g_s: float - amesim_port_2_enthalpy_flow_w: float - amesim_port_1_storage_mass_derivative_g_s: float - amesim_port_1_storage_enthalpy_sum_w: float - amesim_port_2_storage_mass_derivative_g_s: float - amesim_port_2_storage_enthalpy_sum_w: float - python_pn3_node_port_1_mass_flow_g_s: float - python_pn3_node_port_3_mass_flow_g_s: float - python_pn3_node_to_line_mass_flow_g_s: float - python_pn3_node_to_line_mass_flow_error_g_s: float - amesim_port_1_mass_estimate_g: float - amesim_port_2_mass_estimate_g: float - python_port_1_mass_g: float - python_port_2_mass_g: float - python_port_1_mass_error_to_amesim_g: float - python_port_2_mass_error_to_amesim_g: float - amesim_total_mass_derivative_fd_g_s: float - amesim_total_mass_derivative_backward_g_s: float - amesim_total_mass_derivative_forward_g_s: float - amesim_total_storage_mass_derivative_g_s: float - amesim_total_storage_mass_derivative_residual_g_s: float - amesim_center_flow_python_sign_equivalent_g_s: float - amesim_center_flow_python_sign_error_g_s: float - python_center_mass_flow_g_s: float - amesim_port_1_center_mass_derivative_g_s: float - amesim_port_2_center_mass_derivative_g_s: float - python_port_1_external_mass_flow_g_s: float - python_port_1_external_mass_flow_error_g_s: float - python_port_1_center_mass_derivative_g_s: float - python_port_1_center_mass_derivative_error_g_s: float - python_port_1_storage_mass_derivative_g_s: float - python_port_1_storage_mass_derivative_error_g_s: float - python_port_2_external_mass_flow_g_s: float - python_port_2_external_mass_flow_error_g_s: float - python_port_2_center_mass_derivative_g_s: float - python_port_2_center_mass_derivative_error_g_s: float - python_port_2_storage_mass_derivative_g_s: float - python_port_2_storage_mass_derivative_error_g_s: float - python_amesim_state_darcy_center_mass_flow_g_s: float - python_amesim_state_darcy_center_mass_flow_error_g_s: float - python_amesim_state_pn2pipefr_center_mass_flow_g_s: float - python_amesim_state_pn2pipefr_center_mass_flow_error_g_s: float - amesim_center_real_gas_reference_port_1_estimate_w: float - amesim_center_real_gas_reference_port_1_error_w: float - python_center_ideal_reference_port_1_estimate_w: float - python_center_ideal_reference_port_1_error_w: float - python_center_ideal_reference_port_2_estimate_w: float - python_center_ideal_reference_port_2_error_w: float - python_center_real_gas_reference_port_1_estimate_w: float - python_center_real_gas_reference_port_1_error_w: float - python_center_real_gas_reference_port_2_estimate_w: float - python_center_real_gas_reference_port_2_error_w: float - python_total_mass_derivative_kg_s: float - python_port_1_reference_external_enthalpy_flow_w: float - python_port_1_reference_center_enthalpy_flow_w: float - python_port_1_reference_storage_enthalpy_sum_w: float - python_port_1_reference_storage_enthalpy_error_w: float - python_port_2_reference_external_enthalpy_flow_w: float - python_port_2_reference_center_enthalpy_flow_w: float - python_port_2_reference_storage_enthalpy_sum_w: float - python_port_2_reference_storage_enthalpy_error_w: float - amesim_port_1_external_enthalpy_dtemp_component_k_s: float - amesim_port_1_center_enthalpy_dtemp_component_k_s: float - amesim_port_1_mass_offset_dtemp_component_k_s: float - amesim_port_1_heat_dtemp_component_k_s: float - python_port_1_external_enthalpy_dtemp_component_k_s: float - python_port_1_center_enthalpy_dtemp_component_k_s: float - python_port_1_mass_offset_dtemp_component_k_s: float - python_port_1_heat_dtemp_component_k_s: float - amesim_port_2_external_enthalpy_dtemp_component_k_s: float - amesim_port_2_center_enthalpy_dtemp_component_k_s: float - amesim_port_2_mass_offset_dtemp_component_k_s: float - amesim_port_2_heat_dtemp_component_k_s: float - python_port_2_external_enthalpy_dtemp_component_k_s: float - python_port_2_center_enthalpy_dtemp_component_k_s: float - python_port_2_mass_offset_dtemp_component_k_s: float - python_port_2_heat_dtemp_component_k_s: float - amesim_port_1_temperature_derivative_fd_k_s: float - amesim_port_1_temperature_derivative_backward_k_s: float - amesim_port_1_temperature_derivative_forward_k_s: float - amesim_port_2_temperature_derivative_fd_k_s: float - amesim_port_2_temperature_derivative_backward_k_s: float - amesim_port_2_temperature_derivative_forward_k_s: float - amesim_port_1_pn2vol_dtemp_k_s: float - amesim_port_2_pn2vol_dtemp_k_s: float - amesim_port_1_pn2vol_dtemp_residual_k_s: float - amesim_port_2_pn2vol_dtemp_residual_k_s: float - amesim_port_1_pressure_derivative_fd_pa_s: float - amesim_port_2_pressure_derivative_fd_pa_s: float - amesim_port_1_pressure_derivative_mass_component_pa_s: float - amesim_port_1_pressure_derivative_temperature_component_pa_s: float - amesim_port_1_pressure_derivative_eos_sum_pa_s: float - amesim_port_1_pressure_derivative_eos_residual_pa_s: float - amesim_port_2_pressure_derivative_mass_component_pa_s: float - amesim_port_2_pressure_derivative_temperature_component_pa_s: float - amesim_port_2_pressure_derivative_eos_sum_pa_s: float - amesim_port_2_pressure_derivative_eos_residual_pa_s: float - python_port_1_pressure_derivative_mass_component_pa_s: float - python_port_1_pressure_derivative_temperature_component_pa_s: float - python_port_1_pressure_derivative_eos_sum_pa_s: float - python_port_2_pressure_derivative_mass_component_pa_s: float - python_port_2_pressure_derivative_temperature_component_pa_s: float - python_port_2_pressure_derivative_eos_sum_pa_s: float - python_port_1_temperature_k: float - python_port_2_temperature_k: float - python_port_1_pressure_pa: float - python_port_2_pressure_pa: float - python_port_1_mass_derivative_kg_s: float - python_port_2_mass_derivative_kg_s: float - python_port_1_current_dtemp_k_s: float - python_port_2_current_dtemp_k_s: float - python_port_1_real_gas_reference_dtemp_k_s: float - python_port_2_real_gas_reference_dtemp_k_s: float - python_port_2_amesim_center_counterfactual_dtemp_k_s: float - python_port_2_amesim_external_counterfactual_dtemp_k_s: float - python_port_2_amesim_external_center_counterfactual_dtemp_k_s: float - python_port_2_temperature_error_to_amesim_k: float - - -@dataclass(frozen=True) -class TestMqlPnch012EnergyEquationDiagnostic: - chamber_alias: str - piston_alias: str - line_alias: str - time_s: float - amesim_port_1_enthalpy_flow_w: float - amesim_port_1_mass_flow_g_s: float - amesim_chamber_mass_derivative_fd_g_s: float - amesim_chamber_mass_derivative_residual_g_s: float - amesim_chamber_temperature_derivative_fd_k_s: float - amesim_chamber_volume_rate_fd_m3_s: float - amesim_piston_volume_rate_m3_s: float - amesim_heat_flow_w: float - amesim_boundary_work_fd_volume_w: float - amesim_pn2vol_reference_dtemp_fd_volume_k_s: float - amesim_pn2vol_reference_dtemp_fd_volume_residual_k_s: float - amesim_pn2vol_reference_dtemp_piston_volume_k_s: float - amesim_pn2vol_reference_dtemp_piston_volume_residual_k_s: float - python_port_a_mass_flow_kg_s: float - python_current_energy_derivative_w: float - python_current_chamber_temperature_derivative_k_s: float - reference_temperature_k: float - - -@dataclass(frozen=True) -class TestMqlPneumatic96InletLineDiagnostic: - line_alias: str - node_alias: str - orifice_alias: str - chamber_alias: str - time_s: float - amesim_line_pressure_pa: float - amesim_line_temperature_k: float - amesim_line_mass_g: float - amesim_line_mass_derivative_fd_g_s: float - amesim_line_to_node_mass_flow_g_s: float - amesim_orifice_to_line_mass_flow_g_s: float - amesim_storage_mass_derivative_from_ports_g_s: float - amesim_storage_mass_derivative_residual_g_s: float - amesim_chamber_pressure_pa: float - amesim_chamber_temperature_k: float - amesim_node_pressure_pa: float - amesim_node_temperature_k: float - amesim_line_temperature_derivative_fd_k_s: float - amesim_line_pressure_derivative_fd_pa_s: float - amesim_port_1_enthalpy_dtemp_component_k_s: float - amesim_port_2_enthalpy_dtemp_component_k_s: float - amesim_mass_offset_dtemp_component_k_s: float - amesim_reference_temperature_derivative_sum_k_s: float - amesim_reference_temperature_derivative_residual_k_s: float - amesim_pressure_derivative_mass_component_pa_s: float - amesim_pressure_derivative_temperature_component_pa_s: float - amesim_pressure_derivative_eos_sum_pa_s: float - amesim_pressure_derivative_eos_residual_pa_s: float - python_line_pressure_pa: float - python_line_temperature_k: float - python_line_mass_g: float - python_line_pressure_error_pa: float - python_line_temperature_error_k: float - python_line_mass_error_g: float - python_line_to_node_mass_flow_g_s: float - python_line_to_node_mass_flow_error_g_s: float - python_orifice_to_line_mass_flow_g_s: float - python_orifice_to_line_mass_flow_error_g_s: float - python_storage_mass_derivative_g_s: float - python_storage_mass_derivative_error_g_s: float - python_line_to_node_amesim_node_mass_flow_g_s: float - python_line_to_node_amesim_node_mass_flow_error_g_s: float - python_line_to_node_amesim_line_mass_flow_g_s: float - python_line_to_node_amesim_line_mass_flow_error_g_s: float - python_line_to_node_amesim_node_line_mass_flow_g_s: float - python_line_to_node_amesim_node_line_mass_flow_error_g_s: float - python_orifice_to_line_amesim_line_mass_flow_g_s: float - python_orifice_to_line_amesim_line_mass_flow_error_g_s: float - python_orifice_to_line_amesim_chamber_mass_flow_g_s: float - python_orifice_to_line_amesim_chamber_mass_flow_error_g_s: float - python_orifice_to_line_amesim_line_chamber_mass_flow_g_s: float - python_orifice_to_line_amesim_line_chamber_mass_flow_error_g_s: float - python_storage_amesim_boundary_states_mass_derivative_g_s: float - python_storage_amesim_boundary_states_mass_derivative_error_g_s: float - python_chamber_pressure_pa: float - python_chamber_temperature_k: float - python_current_temperature_derivative_k_s: float - python_port_1_current_dtemp_component_k_s: float - python_port_2_current_dtemp_component_k_s: float - python_heat_dtemp_component_k_s: float - python_temperature_derivative_error_k_s: float - python_pressure_derivative_mass_component_pa_s: float - python_pressure_derivative_temperature_component_pa_s: float - python_pressure_derivative_eos_sum_pa_s: float - python_pressure_derivative_eos_error_pa_s: float - python_reference_port_1_enthalpy_flow_w: float - python_reference_port_2_enthalpy_flow_w: float - python_reference_port_1_dtemp_component_k_s: float - python_reference_port_2_dtemp_component_k_s: float - python_reference_mass_offset_dtemp_component_k_s: float - python_reference_temperature_derivative_k_s: float - python_reference_temperature_derivative_error_k_s: float - python_reference_pressure_derivative_mass_component_pa_s: float - python_reference_pressure_derivative_temperature_component_pa_s: float - python_reference_pressure_derivative_eos_sum_pa_s: float - python_reference_pressure_derivative_eos_error_pa_s: float - python_reference_amesim_storage_mass_offset_dtemp_component_k_s: float - python_reference_amesim_storage_temperature_derivative_k_s: float - python_reference_amesim_storage_temperature_derivative_error_k_s: float - python_reference_amesim_storage_pressure_derivative_mass_component_pa_s: float - python_reference_amesim_storage_pressure_derivative_temperature_component_pa_s: float - python_reference_amesim_storage_pressure_derivative_eos_sum_pa_s: float - python_reference_amesim_storage_pressure_derivative_eos_error_pa_s: float - python_active_temperature_derivative_k_s: float - python_active_temperature_derivative_error_k_s: float - python_active_pressure_derivative_eos_sum_pa_s: float - python_active_pressure_derivative_eos_error_pa_s: float - python_chamber_pressure_error_pa: float - python_chamber_temperature_error_k: float - - -@dataclass(frozen=True) -class TestMqlPnvoEventWindowSampleDiagnostic: - time_s: float - data_paths: tuple[str, ...] - python_values_by_data_path: dict[str, float] - amesim_values_by_data_path: dict[str, float] - amesim_sample_left_time_s: float - amesim_sample_right_time_s: float - pnl0001_rhs_diagnostics: tuple[TestMqlPnl0001LineRhsDiagnostic, ...] = field( - default_factory=tuple - ) - pnl0001_pressure_loss_diagnostics: tuple[ - TestMqlPnl0001PressureLossCalibrationDiagnostic, ... - ] = field(default_factory=tuple) - pnvo_flow_parameter_diagnostics: tuple[ - TestMqlPnvoFlowParameterDiagnostic, ... - ] = field(default_factory=tuple) - pnl0001_energy_flow_diagnostics: tuple[ - TestMqlPnl0001EnergyFlowDiagnostic, ... - ] = field(default_factory=tuple) - p4_node_enthalpy_breakdown_diagnostics: tuple[ - TestMqlP4NodeEnthalpyBreakdownDiagnostic, ... - ] = field(default_factory=tuple) - pnvo_upstream_enthalpy_diagnostics: tuple[ - TestMqlPnvoUpstreamEnthalpyDiagnostic, ... - ] = field(default_factory=tuple) - pnl0003_energy_diagnostics: tuple[ - TestMqlPnl0003EnergyDiagnostic, ... - ] = field(default_factory=tuple) - pneumatic_96_inlet_line_diagnostics: tuple[ - TestMqlPneumatic96InletLineDiagnostic, ... - ] = field(default_factory=tuple) - pnch012_energy_equation_diagnostics: tuple[ - TestMqlPnch012EnergyEquationDiagnostic, ... - ] = field(default_factory=tuple) - - @property - def amesim_values_are_interpolated(self) -> bool: - return self.amesim_sample_left_time_s != self.amesim_sample_right_time_s - - def abs_error(self, data_path: str) -> float: - return abs( - self.python_values_by_data_path[data_path] - - self.amesim_values_by_data_path[data_path] - ) - - -@dataclass(frozen=True) -class TestMqlPnvoEventWindowDiagnostic: - orifice_alias: str - event_time_s: float - final_time_s: float - data_paths: tuple[str, ...] - segment_diagnostics: tuple[TestMqlPnvoEventWindowSegmentDiagnostic, ...] - sample_diagnostics: tuple[TestMqlPnvoEventWindowSampleDiagnostic, ...] - python_values_by_data_path: dict[str, float] - amesim_values_by_data_path: dict[str, float] - - def abs_error(self, data_path: str) -> float: - return abs( - self.python_values_by_data_path[data_path] - - self.amesim_values_by_data_path[data_path] - ) - - -@dataclass(frozen=True) -class TestMqlPnvoEventWindowCandidateMetricComparison: - time_s: float - metric_name: str - default_abs_error: float - candidate_abs_error: float - - @property - def abs_error_delta(self) -> float: - return self.candidate_abs_error - self.default_abs_error - - -@dataclass(frozen=True) -class TestMqlPnvoEventWindowCandidateComparisonDiagnostic: - orifice_alias: str - event_time_s: float - final_time_s: float - default_diagnostic: TestMqlPnvoEventWindowDiagnostic - candidate_diagnostic: TestMqlPnvoEventWindowDiagnostic - metric_comparisons: tuple[ - TestMqlPnvoEventWindowCandidateMetricComparison, ... - ] - - -@dataclass(frozen=True) -class TestMqlFullStateComparisonPathConfig: - archive_path: Path = field( - default_factory=lambda: Path(__file__).resolve().parents[2] - / "AmesimModels" - / "test_mql.ame" - ) - amesim_results_archive_path: Path | None = None - output_dir: Path | None = None - - @property - def resolved_amesim_results_archive_path(self) -> Path: - return self.amesim_results_archive_path or self.archive_path - - -@dataclass(frozen=True) -class TestMqlFullStateComparisonExecutionConfig: - write_summary: bool = True - write_comparison_csv: bool = True - data_paths: tuple[str, ...] | None = DEFAULT_FULL_STATE_COMPARISON_DATA_PATHS - solver: SolveIVPConfig = field( - default_factory=lambda: SolveIVPConfig(t_stop=1.0e-2, max_step=1.0e-3) - ) - t_eval: tuple[float, ...] | None = (0.0, 1.0e-2) - inlet_node_pressure_pa: float = 15.31e6 - resistance_boundary_pressure_pa: float = 15.29e6 - inlet_node_temperature_k: float = 293.15 - resistance_boundary_temperature_k: float = 293.15 - use_pneumatic_96_reference_rhs: bool = False - use_pneumatic_69_reference_rhs: bool = False - - -@dataclass(frozen=True) -class TestMqlFullStateComparisonScriptConfig: - paths: TestMqlFullStateComparisonPathConfig = field( - default_factory=TestMqlFullStateComparisonPathConfig - ) - execution: TestMqlFullStateComparisonExecutionConfig = field( - default_factory=TestMqlFullStateComparisonExecutionConfig - ) - - -def _default_output_dir() -> Path: - pythonmodels_root = Path(__file__).resolve().parents[1] - timestamp = datetime.now(UTC).strftime("test_mql_full_state_%Y%m%d_%H%M%S_%f") - return pythonmodels_root / "runs" / timestamp - - -def run_test_mql_full_state_comparison( - config: TestMqlFullStateComparisonScriptConfig | None = None, -) -> tuple[TestMqlFullStateComparisonRun, Path]: - config = config or TestMqlFullStateComparisonScriptConfig() - system = TestMqlSystem(archive_path=config.paths.archive_path) - amesim_results = load_test_mql_amesim_results( - config.paths.resolved_amesim_results_archive_path, - time_stop_s=config.execution.solver.t_stop, - ) - output_schema = build_test_mql_output_schema(amesim_results) - selected_paths = config.execution.data_paths - spec = system.discover_pneumatic_branch_topology().chamber_segment_specs[0] - result = system.simulate_full_state_series_from_spec( - spec, - inlet_node_pressure_pa=config.execution.inlet_node_pressure_pa, - resistance_boundary_pressure_pa=( - config.execution.resistance_boundary_pressure_pa - ), - inlet_node_temperature_k=config.execution.inlet_node_temperature_k, - resistance_boundary_temperature_k=( - config.execution.resistance_boundary_temperature_k - ), - use_pneumatic_96_reference_rhs=( - config.execution.use_pneumatic_96_reference_rhs - ), - use_pneumatic_69_reference_rhs=( - config.execution.use_pneumatic_69_reference_rhs - ), - config=config.execution.solver, - t_eval=list(config.execution.t_eval) if config.execution.t_eval is not None else None, - data_paths=selected_paths, - ) - closure = system.full_state_closure_from_spec( - spec, - inlet_node_pressure_pa=config.execution.inlet_node_pressure_pa, - resistance_boundary_pressure_pa=( - config.execution.resistance_boundary_pressure_pa - ), - inlet_node_temperature_k=config.execution.inlet_node_temperature_k, - resistance_boundary_temperature_k=( - config.execution.resistance_boundary_temperature_k - ), - use_pneumatic_96_reference_rhs=( - config.execution.use_pneumatic_96_reference_rhs - ), - use_pneumatic_69_reference_rhs=( - config.execution.use_pneumatic_69_reference_rhs - ), - ) - series_by_data_path = { - data_path: result.series[data_path] - for data_path in result.series - if data_path != "time" - } - output = validate_test_mql_output( - times=result.t, - series_by_data_path=series_by_data_path, - schema=output_schema, - data_paths=selected_paths, - ) - comparison = compare_validated_test_mql_output( - times=output.times, - series_by_data_path=output.series_by_data_path, - schema=output_schema, - amesim_results=amesim_results, - data_paths=output.data_paths, - ) - run = TestMqlFullStateComparisonRun( - system=system, - closure=closure, - amesim_results=amesim_results, - output_schema=output_schema, - result=result, - output=output, - comparison=comparison, - ) - output_dir = config.paths.output_dir or _default_output_dir() - if config.execution.write_summary or config.execution.write_comparison_csv: - output_dir.mkdir(parents=True, exist_ok=True) - if config.execution.write_summary: - (output_dir / "test_mql_full_state_comparison_summary.txt").write_text( - format_test_mql_full_state_comparison_summary(run), - encoding="utf-8", - ) - if config.execution.write_comparison_csv: - write_test_mql_comparison_csv( - output_dir=output_dir, - python_times=output.times, - python_series_by_data_path=output.series_by_data_path, - amesim_results=amesim_results, - data_paths=output.data_paths, - ) - return run, output_dir - - -def run_test_mql_pnvo_event_boundary_diagnostic( - config: TestMqlFullStateComparisonScriptConfig | None = None, - *, - orifice_alias: str = "pn_morifice_1", -) -> TestMqlPnvoEventBoundaryDiagnostic: - config = config or TestMqlFullStateComparisonScriptConfig() - system = TestMqlSystem(archive_path=config.paths.archive_path) - control = system.pneumatic_assembly.variable_orifice_controls[orifice_alias] - event_time_s = control.step.step_time_s - integration_stop_time_s = nextafter(event_time_s, 0.0) - solver_template = config.execution.solver - solver = SolveIVPConfig( - t_start=solver_template.t_start, - t_stop=integration_stop_time_s, - method=solver_template.method, - rtol=solver_template.rtol, - atol=solver_template.atol, - max_step=solver_template.max_step, - ) - spec = system.discover_pneumatic_branch_topology().chamber_segment_specs[0] - closure_kwargs = { - "inlet_node_pressure_pa": config.execution.inlet_node_pressure_pa, - "resistance_boundary_pressure_pa": ( - config.execution.resistance_boundary_pressure_pa - ), - "inlet_node_temperature_k": config.execution.inlet_node_temperature_k, - "resistance_boundary_temperature_k": ( - config.execution.resistance_boundary_temperature_k - ), - "use_pneumatic_96_reference_rhs": ( - config.execution.use_pneumatic_96_reference_rhs - ), - "use_pneumatic_69_reference_rhs": ( - config.execution.use_pneumatic_69_reference_rhs - ), - } - solution = system.simulate_full_state_from_spec( - spec, - config=solver, - t_eval=[solver.t_start, integration_stop_time_s], - **closure_kwargs, - ) - state_vector = [row[-1] for row in solution.y] - closure = system.full_state_closure_from_spec(spec, **closure_kwargs) - data_paths = ( - "press@pn_c1_8", - "dm1@pneumatic_69", - f"xv@{orifice_alias}", - f"dm2@{orifice_alias}", - ) - python_values = closure.data_path_values( - time_s=event_time_s, - state_vector=state_vector, - data_paths=data_paths, - ) - amesim_results = load_test_mql_amesim_results( - config.paths.resolved_amesim_results_archive_path, - time_stop_s=event_time_s, - ) - amesim_values = { - data_path: interpolate_series_value( - amesim_results.times, - amesim_results.series(data_path), - event_time_s, - ) - for data_path in data_paths - } - return TestMqlPnvoEventBoundaryDiagnostic( - orifice_alias=orifice_alias, - event_time_s=event_time_s, - integration_stop_time_s=integration_stop_time_s, - data_paths=data_paths, - python_values_by_data_path=python_values, - amesim_values_by_data_path=amesim_values, - ) - - -def format_test_mql_pnvo_event_boundary_summary( - diagnostic: TestMqlPnvoEventBoundaryDiagnostic, -) -> str: - lines = [ - "Model: test_mql", - f"Mode: PNVO event boundary diagnostic ({diagnostic.orifice_alias})", - f"Event time: {diagnostic.event_time_s}", - f"Integrated left limit: {diagnostic.integration_stop_time_s}", - "Observation side: right-continuous STEP0 opening", - ] - for data_path in diagnostic.data_paths: - lines.append( - f" - {data_path}: " - f"python={diagnostic.python_values_by_data_path[data_path]}, " - f"amesim={diagnostic.amesim_values_by_data_path[data_path]}, " - f"abs_error={diagnostic.abs_error(data_path)}" - ) - return "\n".join(lines) + "\n" - - -def _pnl0001_pressure_loss_calibration_diagnostic( - *, - closure: object, - amesim_results: AmesimResults, - python_values_by_data_path: dict[str, float], - line_alias: str, - chamber_alias: str, - time_s: float, -) -> TestMqlPnl0001PressureLossCalibrationDiagnostic: - if line_alias != "pneumatic_69" or chamber_alias != "pn_c1_8": - raise KeyError(f"Unsupported PNL0001 pressure-loss diagnostic: {line_alias}") - line = closure.pneumatic_closure.components.p4_port3_remote_primary_line - - def amesim_value(data_path: str) -> float: - return interpolate_series_value( - amesim_results.times, - amesim_results.series(data_path), - time_s, - ) - - amesim_cm = amesim_value(f"cm@{line_alias}") - amesim_dm1_g_s = amesim_value(f"dm1@{line_alias}") - amesim_line_gauge_pressure_pa = amesim_value(f"p2@{line_alias}") - amesim_chamber_gauge_pressure_pa = amesim_value(f"press@{chamber_alias}") - amesim_line_temperature_k = amesim_value(f"t2@{line_alias}") - amesim_chamber_temperature_k = amesim_value(f"temp@{chamber_alias}") - mass_flow_magnitude_kg_s = abs(amesim_dm1_g_s) * 1.0e-3 - amesim_pressure_drop_pa = abs( - amesim_line_gauge_pressure_pa - amesim_chamber_gauge_pressure_pa - ) - current_darcy_pressure_drop_pa = abs( - line.darcy_pressure_drop_for_state( - mass_flow_kg_s=mass_flow_magnitude_kg_s, - pressure_pa=( - amesim_line_gauge_pressure_pa + AMESIM_REFERENCE_PRESSURE_PA - ), - temperature_k=amesim_line_temperature_k, - ) - ) - if current_darcy_pressure_drop_pa > 0.0: - pressure_drop_multiplier = ( - amesim_pressure_drop_pa / current_darcy_pressure_drop_pa - ) - else: - pressure_drop_multiplier = ( - float("inf") if amesim_pressure_drop_pa > 0.0 else 1.0 - ) - candidate_port1_to_port2_kg_s = line.pn2pipefr_mass_flow( - port_1_pressure_pa=( - amesim_chamber_gauge_pressure_pa + AMESIM_REFERENCE_PRESSURE_PA - ), - port_1_temperature_k=amesim_chamber_temperature_k, - port_2_pressure_pa=( - amesim_line_gauge_pressure_pa + AMESIM_REFERENCE_PRESSURE_PA - ), - port_2_temperature_k=amesim_line_temperature_k, - ) - candidate_pn2pipefr_dm1_g_s = -candidate_port1_to_port2_kg_s * 1.0e3 - if amesim_dm1_g_s != 0.0: - candidate_pn2pipefr_to_amesim_dm1_ratio = ( - candidate_pn2pipefr_dm1_g_s / amesim_dm1_g_s - ) - else: - candidate_pn2pipefr_to_amesim_dm1_ratio = ( - float("inf") if candidate_pn2pipefr_dm1_g_s != 0.0 else 1.0 - ) - amesim_linear_conductance = _pnl0001_linear_conductance( - dm1_g_s=amesim_dm1_g_s, - temperature_k=amesim_line_temperature_k, - pressure_drop_pa=amesim_pressure_drop_pa, - ) - python_pressure_drop_pa = abs( - python_values_by_data_path[f"p2@{line_alias}"] - - python_values_by_data_path[f"press@{chamber_alias}"] - ) - python_linear_conductance = _pnl0001_linear_conductance( - dm1_g_s=python_values_by_data_path[f"dm1@{line_alias}"], - temperature_k=python_values_by_data_path[f"t2@{line_alias}"], - pressure_drop_pa=python_pressure_drop_pa, - ) - if amesim_linear_conductance > 0.0: - conductance_ratio = python_linear_conductance / amesim_linear_conductance - else: - conductance_ratio = ( - float("inf") if python_linear_conductance > 0.0 else 1.0 - ) - return TestMqlPnl0001PressureLossCalibrationDiagnostic( - line_alias=line_alias, - chamber_alias=chamber_alias, - time_s=time_s, - amesim_cm=amesim_cm, - amesim_dm1_g_s=amesim_dm1_g_s, - amesim_mass_flow_magnitude_kg_s=mass_flow_magnitude_kg_s, - amesim_line_gauge_pressure_pa=amesim_line_gauge_pressure_pa, - amesim_chamber_gauge_pressure_pa=amesim_chamber_gauge_pressure_pa, - amesim_line_temperature_k=amesim_line_temperature_k, - amesim_pressure_drop_pa=amesim_pressure_drop_pa, - current_darcy_pressure_drop_pa=current_darcy_pressure_drop_pa, - pressure_drop_multiplier=pressure_drop_multiplier, - candidate_pn2pipefr_dm1_g_s=candidate_pn2pipefr_dm1_g_s, - candidate_pn2pipefr_to_amesim_dm1_ratio=( - candidate_pn2pipefr_to_amesim_dm1_ratio - ), - amesim_linear_conductance_kg_s_sqrt_k_per_pa=( - amesim_linear_conductance - ), - python_linear_conductance_kg_s_sqrt_k_per_pa=( - python_linear_conductance - ), - python_to_amesim_linear_conductance_ratio=conductance_ratio, - ) - - -def _pnl0001_linear_conductance( - *, - dm1_g_s: float, - temperature_k: float, - pressure_drop_pa: float, -) -> float: - if temperature_k <= 0.0: - raise ValueError("temperature_k must be positive") - if pressure_drop_pa <= 0.0: - return 0.0 - return abs(dm1_g_s) * 1.0e-3 * sqrt(temperature_k) / pressure_drop_pa - - -def _ideal_pn2vol_reference_dtemp( - *, - gas: AmesimPneumaticGas, - mass_kg: float, - temperature_k: float, - mass_derivative_kg_s: float, - enthalpy_flow_w: float, - heat_flow_w: float, - reference_temperature_k: float = 298.15, -) -> float: - reference_offset_flow_w = ( - gas.cp * reference_temperature_k - gas.cv * temperature_k - ) * mass_derivative_kg_s - return ( - enthalpy_flow_w + reference_offset_flow_w + heat_flow_w - ) / (mass_kg * gas.cv) - - -def _series_finite_difference_at( - *, - times: tuple[float, ...] | list[float], - values: tuple[float, ...] | list[float], - time_s: float, -) -> float: - if len(times) != len(values): - raise ValueError("times and values must have equal length") - if len(times) < 2: - raise ValueError("at least two samples are required") - index = min(range(len(times)), key=lambda idx: abs(times[idx] - time_s)) - if index == 0: - left = 0 - right = 1 - elif index == len(times) - 1: - left = len(times) - 2 - right = len(times) - 1 - else: - left = index - 1 - right = index + 1 - dt = times[right] - times[left] - if dt == 0.0: - raise ValueError("finite difference time interval must be non-zero") - return (values[right] - values[left]) / dt - - -def _series_one_sided_differences_at( - *, - times: tuple[float, ...] | list[float], - values: tuple[float, ...] | list[float], - time_s: float, -) -> tuple[float, float]: - if len(times) != len(values): - raise ValueError("times and values must have equal length") - if len(times) < 2: - raise ValueError("at least two samples are required") - index = min(range(len(times)), key=lambda idx: abs(times[idx] - time_s)) - left = max(index - 1, 0) - right = min(index + 1, len(times) - 1) - backward_dt = times[index] - times[left] - forward_dt = times[right] - times[index] - if backward_dt == 0.0: - backward = (values[right] - values[index]) / forward_dt - else: - backward = (values[index] - values[left]) / backward_dt - if forward_dt == 0.0: - forward = (values[index] - values[left]) / backward_dt - else: - forward = (values[right] - values[index]) / forward_dt - return backward, forward - - -def _series_sample_bracket( - *, - times: tuple[float, ...] | list[float], - time_s: float, -) -> tuple[float, float]: - if not times: - raise ValueError("at least one sample time is required") - index = bisect_left(times, time_s) - tolerance = 1.0e-12 * max(1.0, abs(time_s)) - for candidate_index in (index - 1, index): - if 0 <= candidate_index < len(times): - candidate_time = times[candidate_index] - if abs(candidate_time - time_s) <= tolerance: - return candidate_time, candidate_time - if index <= 0: - return times[0], times[0] - if index >= len(times): - return times[-1], times[-1] - return times[index - 1], times[index] - - -def _reference_enthalpy_flow_for_node_port( - *, - gas: AmesimPneumaticGas, - reference_temperature_k: float, - flow_kg_s: float, - node_temperature_k: float, - connected_temperature_k: float, - port_mass_flow_kg_s: float, -) -> float: - stream_temperature_k = ( - node_temperature_k if flow_kg_s >= 0.0 else connected_temperature_k - ) - reference_h = gas.specific_reference_enthalpy( - stream_temperature_k, - reference_temperature_k, - ) - return port_mass_flow_kg_s * reference_h - - -def _real_gas_reference_enthalpy_flow_for_node_port( - *, - gas: AmesimPneumaticGas, - reference_temperature_k: float, - flow_kg_s: float, - node_pressure_pa: float, - node_temperature_k: float, - connected_pressure_pa: float, - connected_temperature_k: float, - port_mass_flow_kg_s: float, -) -> float: - stream_pressure_pa = node_pressure_pa if flow_kg_s >= 0.0 else connected_pressure_pa - stream_temperature_k = ( - node_temperature_k if flow_kg_s >= 0.0 else connected_temperature_k - ) - reference_h = gas.pressure_reference_enthalpy( - stream_pressure_pa, - stream_temperature_k, - reference_pressure=AMESIM_REFERENCE_PRESSURE_PA, - reference_temperature=reference_temperature_k, - ) - return port_mass_flow_kg_s * reference_h - - -def _pnl0001_energy_flow_diagnostic( - *, - closure: object, - amesim_results: AmesimResults, - state_vector: list[float], - rhs_diagnostic: TestMqlPnl0001LineRhsDiagnostic, - line_alias: str, - chamber_alias: str, - node_alias: str, - time_s: float, -) -> TestMqlPnl0001EnergyFlowDiagnostic: - if line_alias != "pneumatic_69" or chamber_alias != "pn_c1_8": - raise KeyError(f"Unsupported PNL0001 energy diagnostic: {line_alias}") - if node_alias != "pnnode4_16": - raise KeyError(f"Unsupported PNL0001 node diagnostic: {node_alias}") - - snapshot = closure.snapshot_at(time_s, state_vector).pneumatic - line = closure.pneumatic_closure.components.p4_port3_remote_primary_line - line_properties = snapshot.p4_port3_remote_primary_line - chamber_properties = snapshot.p4_port3_remote_primary_chamber - - def amesim_value(data_path: str) -> float: - return interpolate_series_value( - amesim_results.times, - amesim_results.series(data_path), - time_s, - ) - - amesim_dh1 = amesim_value(f"dh1@{line_alias}") - amesim_dm1 = amesim_value(f"dm1@{line_alias}") - amesim_node_dh2 = amesim_value(f"dh2@{node_alias}") - amesim_node_dm2 = amesim_value(f"dm2@{node_alias}") - direct_port_1 = rhs_diagnostic.chamber_to_line_flow_kg_s * ( - chamber_properties.h - if rhs_diagnostic.chamber_to_line_flow_kg_s > 0.0 - else line_properties.h - ) - direct_port_2 = rhs_diagnostic.node_to_line_flow_kg_s * line_properties.h - direct_energy_derivative = ( - direct_port_1 + direct_port_2 + rhs_diagnostic.thermal_energy_flow_w - ) - p4_node_port_2_mass_flow_kg_s = ( - snapshot.p4_port3_remote_balance.port_2_mass_flow_g_s * 1.0e-3 - ) - if abs(p4_node_port_2_mass_flow_kg_s) <= 1.0e-12: - p4_node_port_2_connected_h = line_properties.h - else: - p4_node_port_2_connected_h = ( - snapshot.p4_port3_remote_balance.port_2_enthalpy_flow_w - / p4_node_port_2_mass_flow_kg_s - ) - p4_node_port_2_inlet_h = ( - p4_node_port_2_connected_h - if rhs_diagnostic.node_to_line_flow_kg_s > 0.0 - else line_properties.h - ) - p4_node_port_2_counterfactual_energy_derivative = ( - rhs_diagnostic.port_1_energy_flow_w - + rhs_diagnostic.node_to_line_flow_kg_s * p4_node_port_2_inlet_h - + rhs_diagnostic.thermal_energy_flow_w - ) - p4_node_port_2_counterfactual_dtemp = ( - p4_node_port_2_counterfactual_energy_derivative - - line_properties.u * rhs_diagnostic.mass_derivative_kg_s - ) / (line.state.m * line.gas.cv) - reference_temperature_k = 298.15 - - def reference_h(temperature_k: float) -> float: - return line.gas.specific_reference_enthalpy( - temperature_k, - reference_temperature_k, - ) - - amesim_stream_temperature = ( - amesim_value(f"t2@{line_alias}") - if amesim_dm1 >= 0.0 - else amesim_value(f"temp@{chamber_alias}") - ) - reference_enthalpy_estimate = ( - amesim_dm1 * 1.0e-3 * reference_h(amesim_stream_temperature) - ) - python_dm1_kg_s = -rhs_diagnostic.chamber_to_line_flow_kg_s - python_port_1_stream_temperature_k = ( - line_properties.T if python_dm1_kg_s >= 0.0 else chamber_properties.T - ) - python_reference_port_1 = python_dm1_kg_s * reference_h( - python_port_1_stream_temperature_k - ) - python_reference_node_port_2 = sum( - ( - _reference_enthalpy_flow_for_node_port( - gas=line.gas, - reference_temperature_k=reference_temperature_k, - flow_kg_s=snapshot.p4_port3_remote_node_to_line_flow, - node_temperature_k=snapshot.p4_port3_remote_primary_line.T, - connected_temperature_k=snapshot.p4_port3_line.T, - port_mass_flow_kg_s=-snapshot.p4_port3_remote_node_to_line_flow, - ), - _reference_enthalpy_flow_for_node_port( - gas=line.gas, - reference_temperature_k=reference_temperature_k, - flow_kg_s=snapshot.p4_port3_remote_to_port3_line_flow, - node_temperature_k=snapshot.p4_port3_remote_primary_line.T, - connected_temperature_k=snapshot.p4_port3_remote_port3_line.T, - port_mass_flow_kg_s=-snapshot.p4_port3_remote_to_port3_line_flow, - ), - _reference_enthalpy_flow_for_node_port( - gas=line.gas, - reference_temperature_k=reference_temperature_k, - flow_kg_s=snapshot.p4_port3_remote_orifice_to_node_flow, - node_temperature_k=snapshot.p4_port3_remote_orifice_line_port_2.T, - connected_temperature_k=snapshot.p4_port3_remote_primary_line.T, - port_mass_flow_kg_s=snapshot.p4_port3_remote_orifice_to_node_flow, - ), - ) - ) - python_reference_sum = python_reference_port_1 + python_reference_node_port_2 - - amesim_mgas_kg = amesim_value(f"mgas@{line_alias}") * 1.0e-3 - amesim_line_temperature = amesim_value(f"t2@{line_alias}") - amesim_mass_derivative = (amesim_node_dm2 - amesim_dm1) * 1.0e-3 - amesim_heat_flow = ( - line.heat_transfer_coefficient - * line.heat_transfer_area - * (line.external_temperature - amesim_line_temperature) - ) - current_line_dtemp = ( - rhs_diagnostic.energy_derivative_w - - line_properties.u * rhs_diagnostic.mass_derivative_kg_s - ) / (line.state.m * line.gas.cv) - amesim_line_dtemp_fd = _series_finite_difference_at( - times=amesim_results.times, - values=amesim_results.series(f"t2@{line_alias}"), - time_s=time_s, - ) - candidate_pn2pipefr_dm2i_g_s = -amesim_dm1 - candidate_pn2pipefr_dh2i = -reference_enthalpy_estimate - candidate_storage_sdh = amesim_node_dh2 + candidate_pn2pipefr_dh2i - candidate_pn2pipefr_dtemp = _ideal_pn2vol_reference_dtemp( - gas=line.gas, - mass_kg=amesim_mgas_kg, - temperature_k=amesim_line_temperature, - mass_derivative_kg_s=amesim_mass_derivative, - enthalpy_flow_w=candidate_storage_sdh, - heat_flow_w=amesim_heat_flow, - ) - amesim_node_plus_dh1_dtemp = _ideal_pn2vol_reference_dtemp( - gas=line.gas, - mass_kg=amesim_mgas_kg, - temperature_k=amesim_line_temperature, - mass_derivative_kg_s=amesim_mass_derivative, - enthalpy_flow_w=amesim_node_dh2 + amesim_dh1, - heat_flow_w=amesim_heat_flow, - ) - amesim_node_minus_dh1_dtemp = _ideal_pn2vol_reference_dtemp( - gas=line.gas, - mass_kg=amesim_mgas_kg, - temperature_k=amesim_line_temperature, - mass_derivative_kg_s=amesim_mass_derivative, - enthalpy_flow_w=amesim_node_dh2 - amesim_dh1, - heat_flow_w=amesim_heat_flow, - ) - python_reference_node_minus_dh1_dtemp = _ideal_pn2vol_reference_dtemp( - gas=line.gas, - mass_kg=line.state.m, - temperature_k=line_properties.T, - mass_derivative_kg_s=rhs_diagnostic.mass_derivative_kg_s, - enthalpy_flow_w=python_reference_node_port_2 - python_reference_port_1, - heat_flow_w=rhs_diagnostic.thermal_energy_flow_w, - ) - return TestMqlPnl0001EnergyFlowDiagnostic( - line_alias=line_alias, - chamber_alias=chamber_alias, - node_alias=node_alias, - time_s=time_s, - amesim_port_1_enthalpy_flow_w=amesim_dh1, - amesim_port_1_mass_flow_g_s=amesim_dm1, - amesim_node_port_2_enthalpy_flow_w=amesim_node_dh2, - amesim_node_port_2_mass_flow_g_s=amesim_node_dm2, - amesim_observed_port_enthalpy_sum_w=amesim_dh1 + amesim_node_dh2, - python_port_1_internal_energy_flow_w=rhs_diagnostic.port_1_energy_flow_w, - python_port_2_internal_energy_flow_w=rhs_diagnostic.port_2_energy_flow_w, - python_current_energy_derivative_w=rhs_diagnostic.energy_derivative_w, - python_direct_enthalpy_port_1_flow_w=direct_port_1, - python_direct_enthalpy_port_2_flow_w=direct_port_2, - python_direct_enthalpy_energy_derivative_w=direct_energy_derivative, - python_direct_minus_current_energy_derivative_w=( - direct_energy_derivative - rhs_diagnostic.energy_derivative_w - ), - python_p4_node_port_2_enthalpy_flow_w=( - snapshot.p4_port3_remote_balance.port_2_enthalpy_flow_w - ), - python_p4_node_port_2_mass_flow_g_s=( - snapshot.p4_port3_remote_balance.port_2_mass_flow_g_s - ), - python_p4_node_port_2_connected_h_j_kg=p4_node_port_2_connected_h, - python_p4_node_port_2_connected_temperature_k=( - p4_node_port_2_connected_h / line.gas.cp - ), - python_p4_node_port_2_connected_h_delta_to_line_h_j_kg=( - p4_node_port_2_connected_h - line_properties.h - ), - python_p4_node_port_2_counterfactual_dtemp_k_s=( - p4_node_port_2_counterfactual_dtemp - ), - reference_temperature_k=reference_temperature_k, - amesim_port_1_reference_enthalpy_estimate_w=reference_enthalpy_estimate, - amesim_port_1_reference_enthalpy_error_w=( - reference_enthalpy_estimate - amesim_dh1 - ), - amesim_candidate_pn2pipefr_dm2i_g_s=candidate_pn2pipefr_dm2i_g_s, - amesim_candidate_pn2pipefr_dh2i_w=candidate_pn2pipefr_dh2i, - amesim_candidate_storage_enthalpy_sum_w=candidate_storage_sdh, - amesim_candidate_pn2pipefr_dtemp_k_s=candidate_pn2pipefr_dtemp, - amesim_candidate_pn2pipefr_dtemp_residual_k_s=( - candidate_pn2pipefr_dtemp - amesim_line_dtemp_fd - ), - python_reference_port_1_enthalpy_flow_w=python_reference_port_1, - python_reference_node_port_2_enthalpy_flow_w=python_reference_node_port_2, - python_reference_observed_port_enthalpy_sum_w=python_reference_sum, - python_reference_port_1_to_amesim_error_w=( - python_reference_port_1 - amesim_dh1 - ), - python_reference_node_port_2_to_amesim_error_w=( - python_reference_node_port_2 - amesim_node_dh2 - ), - python_reference_sum_to_amesim_error_w=( - python_reference_sum - (amesim_dh1 + amesim_node_dh2) - ), - python_current_line_temperature_derivative_k_s=current_line_dtemp, - amesim_line_temperature_derivative_fd_k_s=amesim_line_dtemp_fd, - amesim_pn2vol2_dtemp_node_plus_dh1_k_s=amesim_node_plus_dh1_dtemp, - amesim_pn2vol2_dtemp_node_minus_dh1_k_s=amesim_node_minus_dh1_dtemp, - python_reference_pn2vol2_dtemp_node_minus_dh1_k_s=( - python_reference_node_minus_dh1_dtemp - ), - ) - -def _p4_node_enthalpy_breakdown_diagnostic( - *, - closure: object, - amesim_results: AmesimResults, - state_vector: list[float], - node_alias: str, - time_s: float, -) -> TestMqlP4NodeEnthalpyBreakdownDiagnostic: - if node_alias != "pnnode4_16": - raise KeyError(f"Unsupported P4 node enthalpy diagnostic: {node_alias}") - - snapshot = closure.snapshot_at(time_s, state_vector).pneumatic - line = closure.pneumatic_closure.components.p4_port3_remote_primary_line - balance = snapshot.p4_port3_remote_balance - reference_temperature_k = 298.15 - - def amesim_value(data_path: str) -> float: - return interpolate_series_value( - amesim_results.times, - amesim_results.series(data_path), - time_s, - ) - - amesim_port_1_enthalpy = amesim_value("dh1@pneumatic_83") - amesim_port_1_mass = amesim_value("dm1@pneumatic_83") - amesim_port_3_enthalpy = amesim_value("dh2@pneumatic_95") - amesim_port_3_mass = amesim_value("dm2@pneumatic_95") - amesim_port_4_enthalpy = amesim_value("dh2@pn_morifice_1") - amesim_port_4_mass = amesim_value("dm2@pn_morifice_1") - amesim_port_2_enthalpy = amesim_value(f"dh2@{node_alias}") - amesim_port_2_mass = amesim_value(f"dm2@{node_alias}") - - reference_port_1 = _reference_enthalpy_flow_for_node_port( - gas=line.gas, - reference_temperature_k=reference_temperature_k, - flow_kg_s=snapshot.p4_port3_remote_node_to_line_flow, - node_temperature_k=snapshot.p4_port3_remote_primary_line.T, - connected_temperature_k=snapshot.p4_port3_line.T, - port_mass_flow_kg_s=-snapshot.p4_port3_remote_node_to_line_flow, - ) - reference_port_3 = _reference_enthalpy_flow_for_node_port( - gas=line.gas, - reference_temperature_k=reference_temperature_k, - flow_kg_s=snapshot.p4_port3_remote_to_port3_line_flow, - node_temperature_k=snapshot.p4_port3_remote_primary_line.T, - connected_temperature_k=snapshot.p4_port3_remote_port3_line.T, - port_mass_flow_kg_s=-snapshot.p4_port3_remote_to_port3_line_flow, - ) - reference_port_4 = _reference_enthalpy_flow_for_node_port( - gas=line.gas, - reference_temperature_k=reference_temperature_k, - flow_kg_s=snapshot.p4_port3_remote_orifice_to_node_flow, - node_temperature_k=snapshot.p4_port3_remote_orifice_line_port_2.T, - connected_temperature_k=snapshot.p4_port3_remote_primary_line.T, - port_mass_flow_kg_s=snapshot.p4_port3_remote_orifice_to_node_flow, - ) - reference_port_2 = reference_port_1 + reference_port_3 + reference_port_4 - real_gas_reference_port_1 = _real_gas_reference_enthalpy_flow_for_node_port( - gas=line.gas, - reference_temperature_k=reference_temperature_k, - flow_kg_s=snapshot.p4_port3_remote_node_to_line_flow, - node_pressure_pa=snapshot.p4_port3_remote_primary_line.p, - node_temperature_k=snapshot.p4_port3_remote_primary_line.T, - connected_pressure_pa=snapshot.p4_port3_line.p, - connected_temperature_k=snapshot.p4_port3_line.T, - port_mass_flow_kg_s=-snapshot.p4_port3_remote_node_to_line_flow, - ) - real_gas_reference_port_3 = _real_gas_reference_enthalpy_flow_for_node_port( - gas=line.gas, - reference_temperature_k=reference_temperature_k, - flow_kg_s=snapshot.p4_port3_remote_to_port3_line_flow, - node_pressure_pa=snapshot.p4_port3_remote_primary_line.p, - node_temperature_k=snapshot.p4_port3_remote_primary_line.T, - connected_pressure_pa=snapshot.p4_port3_remote_port3_line.p, - connected_temperature_k=snapshot.p4_port3_remote_port3_line.T, - port_mass_flow_kg_s=-snapshot.p4_port3_remote_to_port3_line_flow, - ) - real_gas_reference_port_4 = _real_gas_reference_enthalpy_flow_for_node_port( - gas=line.gas, - reference_temperature_k=reference_temperature_k, - flow_kg_s=snapshot.p4_port3_remote_orifice_to_node_flow, - node_pressure_pa=snapshot.p4_port3_remote_orifice_line_port_2.p, - node_temperature_k=snapshot.p4_port3_remote_orifice_line_port_2.T, - connected_pressure_pa=snapshot.p4_port3_remote_primary_line.p, - connected_temperature_k=snapshot.p4_port3_remote_primary_line.T, - port_mass_flow_kg_s=snapshot.p4_port3_remote_orifice_to_node_flow, - ) - real_gas_reference_port_2 = ( - real_gas_reference_port_1 - + real_gas_reference_port_3 - + real_gas_reference_port_4 - ) - - return TestMqlP4NodeEnthalpyBreakdownDiagnostic( - node_alias=node_alias, - time_s=time_s, - amesim_port_1_enthalpy_flow_w=amesim_port_1_enthalpy, - amesim_port_1_mass_flow_g_s=amesim_port_1_mass, - amesim_port_3_enthalpy_flow_w=amesim_port_3_enthalpy, - amesim_port_3_mass_flow_g_s=amesim_port_3_mass, - amesim_port_4_enthalpy_flow_w=amesim_port_4_enthalpy, - amesim_port_4_mass_flow_g_s=amesim_port_4_mass, - amesim_port_2_enthalpy_flow_w=amesim_port_2_enthalpy, - amesim_port_2_mass_flow_g_s=amesim_port_2_mass, - python_port_1_enthalpy_flow_w=balance.port_1_enthalpy_flow_w, - python_port_1_mass_flow_g_s=balance.port_1_mass_flow_g_s, - python_port_3_enthalpy_flow_w=balance.port_3_enthalpy_flow_w, - python_port_3_mass_flow_g_s=balance.port_3_mass_flow_g_s, - python_port_4_enthalpy_flow_w=balance.port_4_enthalpy_flow_w, - python_port_4_mass_flow_g_s=balance.port_4_mass_flow_g_s, - python_port_2_enthalpy_flow_w=balance.port_2_enthalpy_flow_w, - python_port_2_mass_flow_g_s=balance.port_2_mass_flow_g_s, - python_reference_port_1_enthalpy_flow_w=reference_port_1, - python_reference_port_3_enthalpy_flow_w=reference_port_3, - python_reference_port_4_enthalpy_flow_w=reference_port_4, - python_reference_port_2_enthalpy_flow_w=reference_port_2, - python_real_gas_reference_port_1_enthalpy_flow_w=real_gas_reference_port_1, - python_real_gas_reference_port_3_enthalpy_flow_w=real_gas_reference_port_3, - python_real_gas_reference_port_4_enthalpy_flow_w=real_gas_reference_port_4, - python_real_gas_reference_port_2_enthalpy_flow_w=real_gas_reference_port_2, - python_port_2_enthalpy_error_w=( - balance.port_2_enthalpy_flow_w - amesim_port_2_enthalpy - ), - python_reference_port_2_enthalpy_error_w=( - reference_port_2 - amesim_port_2_enthalpy - ), - python_real_gas_reference_port_2_enthalpy_error_w=( - real_gas_reference_port_2 - amesim_port_2_enthalpy - ), - ) - - -def _pnvo_upstream_enthalpy_diagnostic( - *, - closure: object, - amesim_results: AmesimResults, - state_vector: list[float], - orifice_alias: str, - upstream_line_alias: str, - downstream_node_alias: str, - time_s: float, -) -> TestMqlPnvoUpstreamEnthalpyDiagnostic: - if orifice_alias != "pn_morifice_1": - raise KeyError(f"Unsupported PNVO upstream diagnostic: {orifice_alias}") - if upstream_line_alias != "pneumatic_87": - raise KeyError(f"Unsupported PNVO upstream line diagnostic: {upstream_line_alias}") - if downstream_node_alias != "pnnode4_16": - raise KeyError(f"Unsupported PNVO downstream node diagnostic: {downstream_node_alias}") - - snapshot = closure.snapshot_at(time_s, state_vector).pneumatic - line = closure.pneumatic_closure.components.p4_port3_remote_orifice_line - flow_kg_s = snapshot.p4_port3_remote_orifice_to_node_flow - reference_temperature_k = 298.15 - - def amesim_value(data_path: str) -> float: - return interpolate_series_value( - amesim_results.times, - amesim_results.series(data_path), - time_s, - ) - - amesim_orifice_dh2 = amesim_value(f"dh2@{orifice_alias}") - amesim_orifice_dm2 = amesim_value(f"dm2@{orifice_alias}") - amesim_line_t2 = amesim_value(f"t2@{upstream_line_alias}") - if amesim_orifice_dm2 >= 0.0: - amesim_stream_temperature = amesim_line_t2 - amesim_stream_pressure = ( - amesim_value(f"p2@{upstream_line_alias}") - + AMESIM_REFERENCE_PRESSURE_PA - ) - else: - amesim_stream_temperature = amesim_value(f"temp1@{downstream_node_alias}") - amesim_stream_pressure = ( - amesim_value(f"press1@{downstream_node_alias}") - + AMESIM_REFERENCE_PRESSURE_PA - ) - amesim_dm2_kg_s = amesim_orifice_dm2 * 1.0e-3 - amesim_reference_dh2 = amesim_dm2_kg_s * line.gas.specific_reference_enthalpy( - amesim_stream_temperature, - reference_temperature_k, - ) - amesim_real_gas_reference_dh2 = ( - amesim_dm2_kg_s - * line.gas.pressure_reference_enthalpy( - amesim_stream_pressure, - amesim_stream_temperature, - reference_pressure=AMESIM_REFERENCE_PRESSURE_PA, - reference_temperature=reference_temperature_k, - ) - ) - if abs(amesim_orifice_dm2) <= 1.0e-12: - amesim_implied_temperature = amesim_stream_temperature - else: - amesim_implied_temperature = line.gas.reference_temperature_from_specific_enthalpy( - amesim_orifice_dh2 / (amesim_orifice_dm2 * 1.0e-3), - reference_temperature_k, - ) - - python_stream_properties = ( - snapshot.p4_port3_remote_orifice_line_port_2 - if flow_kg_s >= 0.0 - else snapshot.p4_port3_remote_primary_line - ) - python_current_dh2 = flow_kg_s * python_stream_properties.h - python_reference_dh2 = flow_kg_s * line.gas.specific_reference_enthalpy( - python_stream_properties.T, - reference_temperature_k, - ) - python_real_gas_reference_dh2 = flow_kg_s * line.gas.pressure_reference_enthalpy( - python_stream_properties.p, - python_stream_properties.T, - reference_pressure=AMESIM_REFERENCE_PRESSURE_PA, - reference_temperature=reference_temperature_k, - ) - - return TestMqlPnvoUpstreamEnthalpyDiagnostic( - orifice_alias=orifice_alias, - upstream_line_alias=upstream_line_alias, - time_s=time_s, - amesim_orifice_port_2_enthalpy_flow_w=amesim_orifice_dh2, - amesim_orifice_port_2_mass_flow_g_s=amesim_orifice_dm2, - amesim_orifice_port_3_enthalpy_flow_w=amesim_value( - f"dh3@{orifice_alias}" - ), - amesim_orifice_port_3_mass_flow_g_s=amesim_value( - f"dm3@{orifice_alias}" - ), - amesim_upstream_line_center_enthalpy_flow_w=amesim_value( - f"dhctr@{upstream_line_alias}" - ), - amesim_upstream_line_center_mass_flow_g_s=amesim_value( - f"dmctr@{upstream_line_alias}" - ), - amesim_upstream_line_port_2_temperature_k=amesim_line_t2, - amesim_upstream_line_port_2_pressure_pa=amesim_stream_pressure, - amesim_orifice_port_2_implied_reference_temperature_k=( - amesim_implied_temperature - ), - amesim_orifice_port_2_implied_temperature_delta_to_line_k=( - amesim_implied_temperature - amesim_line_t2 - ), - amesim_upstream_line_port_2_reference_enthalpy_flow_w=amesim_reference_dh2, - amesim_upstream_line_port_2_reference_enthalpy_error_w=( - amesim_reference_dh2 - amesim_orifice_dh2 - ), - amesim_upstream_line_port_2_real_gas_reference_enthalpy_flow_w=( - amesim_real_gas_reference_dh2 - ), - amesim_upstream_line_port_2_real_gas_reference_enthalpy_error_w=( - amesim_real_gas_reference_dh2 - amesim_orifice_dh2 - ), - python_orifice_to_node_flow_kg_s=flow_kg_s, - python_upstream_line_port_2_temperature_k=( - snapshot.p4_port3_remote_orifice_line_port_2.T - ), - python_upstream_line_port_2_pressure_pa=python_stream_properties.p, - python_current_port_2_enthalpy_flow_w=python_current_dh2, - python_reference_port_2_enthalpy_flow_w=python_reference_dh2, - python_real_gas_reference_port_2_enthalpy_flow_w=( - python_real_gas_reference_dh2 - ), - python_current_port_2_enthalpy_error_w=( - python_current_dh2 - amesim_orifice_dh2 - ), - python_reference_port_2_enthalpy_error_w=( - python_reference_dh2 - amesim_orifice_dh2 - ), - python_real_gas_reference_port_2_enthalpy_error_w=( - python_real_gas_reference_dh2 - amesim_orifice_dh2 - ), - ) - - -def _pn2vol_reference_dtemp( - *, - gas: AmesimPneumaticGas, - temperature_k: float, - mass_kg: float, - mass_derivative_kg_s: float, - enthalpy_flow_w: float, - heat_flow_w: float, - reference_temperature_k: float = 298.15, -) -> float: - reference_offset_flow_w = ( - gas.cp * reference_temperature_k - gas.cv * temperature_k - ) * mass_derivative_kg_s - return (enthalpy_flow_w + reference_offset_flow_w + heat_flow_w) / ( - mass_kg * gas.cv - ) - - -def _actual_reference_enthalpy_flow( - *, - gas: AmesimPneumaticGas, - port_m_flow: float, - connected_pressure_pa: float, - connected_temperature_k: float, - internal_pressure_pa: float, - internal_temperature_k: float, - reference_temperature_k: float = 298.15, -) -> float: - if port_m_flow > 0.0: - pressure = connected_pressure_pa - temperature = connected_temperature_k - else: - pressure = internal_pressure_pa - temperature = internal_temperature_k - return port_m_flow * gas.pressure_reference_enthalpy( - pressure, - temperature, - reference_pressure=AMESIM_REFERENCE_PRESSURE_PA, - reference_temperature=reference_temperature_k, - ) - - -def _pressure_derivative_components_from_density_temperature( - *, - gas: AmesimPneumaticGas, - density_kg_m3: float, - temperature_k: float, - density_derivative_kg_m3_s: float, - temperature_derivative_k_s: float, -) -> tuple[float, float, float]: - density_step = max(abs(density_kg_m3) * 1.0e-6, 1.0e-9) - if density_kg_m3 <= density_step: - density_step = density_kg_m3 * 0.5 - temperature_step = max(abs(temperature_k) * 1.0e-6, 1.0e-4) - dp_drho = ( - gas.pressure(density_kg_m3 + density_step, temperature_k) - - gas.pressure(density_kg_m3 - density_step, temperature_k) - ) / (2.0 * density_step) - dp_dtemp = ( - gas.pressure(density_kg_m3, temperature_k + temperature_step) - - gas.pressure(density_kg_m3, temperature_k - temperature_step) - ) / (2.0 * temperature_step) - mass_component = dp_drho * density_derivative_kg_m3_s - temperature_component = dp_dtemp * temperature_derivative_k_s - return mass_component, temperature_component, mass_component + temperature_component - - -def _pnl0003_energy_diagnostic( - *, - closure: object, - amesim_results: AmesimResults, - state_vector: list[float], - line_alias: str, - time_s: float, -) -> TestMqlPnl0003EnergyDiagnostic: - if line_alias != "pneumatic_87": - raise KeyError(f"Unsupported PNL0003 energy diagnostic: {line_alias}") - - snapshot = closure.snapshot_at(time_s, state_vector).pneumatic - line = closure.pneumatic_closure.components.p4_port3_remote_orifice_line - port_1 = snapshot.p4_port3_remote_orifice_line_port_1 - port_2 = snapshot.p4_port3_remote_orifice_line_port_2 - connected_port_2 = snapshot.p4_port3_remote_primary_line - center_flow = snapshot.p4_port3_remote_orifice_line_center_flow - port_1_flow = snapshot.p4_port3_remote_node_to_orifice_line_flow - port_2_flow = -snapshot.p4_port3_remote_orifice_to_node_flow - reference_temperature_k = 298.15 - pn3_port_2_mass_flow_kg_s = ( - snapshot.p4_port3_remote_orifice_node_balance.port_2_mass_flow_g_s * 1.0e-3 - ) - if abs(pn3_port_2_mass_flow_kg_s) <= 1.0e-12: - port_1_connected_h = port_1.h - else: - port_1_connected_h = ( - snapshot.p4_port3_remote_orifice_node_balance.port_2_enthalpy_flow_w - / pn3_port_2_mass_flow_kg_s - ) - - def amesim_value(data_path: str) -> float: - return interpolate_series_value( - amesim_results.times, - amesim_results.series(data_path), - time_s, - ) - - amesim_t1 = amesim_value(f"t1@{line_alias}") - amesim_t2 = amesim_value(f"t2@{line_alias}") - amesim_p1 = amesim_value(f"p1@{line_alias}") + AMESIM_REFERENCE_PRESSURE_PA - amesim_p2 = amesim_value(f"p2@{line_alias}") + AMESIM_REFERENCE_PRESSURE_PA - amesim_dm1 = amesim_value("dm2@pn_node3_8") - amesim_dh1 = amesim_value("dh2@pn_node3_8") - python_node_dm1 = snapshot.p4_port3_remote_orifice_node_balance.port_1_mass_flow_g_s - python_node_dm3 = snapshot.p4_port3_remote_orifice_node_balance.port_3_mass_flow_g_s - python_node_dm2_sum = python_node_dm1 + python_node_dm3 - python_node_to_line_dm = -python_node_dm2_sum - amesim_dm2 = amesim_value("dm3@pn_morifice_1") - amesim_dh2 = amesim_value("dh3@pn_morifice_1") - amesim_dmctr = amesim_value(f"dmctr@{line_alias}") - amesim_dhctr = amesim_value(f"dhctr@{line_alias}") - amesim_sdm1_kg_s = (amesim_dm1 + amesim_dmctr) * 1.0e-3 - amesim_sdm2_kg_s = (amesim_dm2 - amesim_dmctr) * 1.0e-3 - amesim_sdh1 = amesim_dh1 + amesim_dhctr - amesim_sdh2 = amesim_dh2 - amesim_dhctr - amesim_mgas_series = amesim_results.series(f"mgas@{line_alias}") - amesim_total_mass_derivative_fd = _series_finite_difference_at( - times=amesim_results.times, - values=amesim_mgas_series, - time_s=time_s, - ) - ( - amesim_total_mass_derivative_backward, - amesim_total_mass_derivative_forward, - ) = _series_one_sided_differences_at( - times=amesim_results.times, - values=amesim_mgas_series, - time_s=time_s, - ) - amesim_total_storage_mass_derivative = ( - amesim_sdm1_kg_s + amesim_sdm2_kg_s - ) * 1.0e3 - python_center_mass_flow_g_s = center_flow * 1.0e3 - amesim_center_python_sign_equivalent_g_s = -amesim_dmctr - amesim_center_mass_flow_python_sign_kg_s = ( - amesim_center_python_sign_equivalent_g_s * 1.0e-3 - ) - amesim_port_1_center_mass_derivative_g_s = amesim_dmctr - amesim_port_2_center_mass_derivative_g_s = -amesim_dmctr - python_port_1_external_mass_flow_g_s = port_1_flow * 1.0e3 - python_port_1_center_mass_derivative_g_s = -center_flow * 1.0e3 - python_port_1_storage_mass_derivative_g_s = ( - python_port_1_external_mass_flow_g_s - + python_port_1_center_mass_derivative_g_s - ) - python_port_2_external_mass_flow_g_s = port_2_flow * 1.0e3 - python_port_2_center_mass_derivative_g_s = center_flow * 1.0e3 - python_port_2_storage_mass_derivative_g_s = ( - python_port_2_external_mass_flow_g_s - + python_port_2_center_mass_derivative_g_s - ) - - def current_darcy_center_flow_for_state( - port_1_pressure_pa: float, - port_1_temperature_k: float, - port_2_pressure_pa: float, - port_2_temperature_k: float, - ) -> float: - pressure_difference = port_1_pressure_pa - port_2_pressure_pa - if pressure_difference == 0.0: - return 0.0 - upstream_pressure = ( - port_1_pressure_pa if pressure_difference > 0.0 else port_2_pressure_pa - ) - upstream_temperature = ( - port_1_temperature_k if pressure_difference > 0.0 else port_2_temperature_k - ) - upstream_density = line.gas.density(upstream_pressure, upstream_temperature) - magnitude = line._mass_flow_for_pressure_drop( - abs(pressure_difference), - density=upstream_density, - temperature=upstream_temperature, - ) - return magnitude if pressure_difference > 0.0 else -magnitude - - amesim_state_darcy_center_flow = current_darcy_center_flow_for_state( - amesim_p1, - amesim_t1, - amesim_p2, - amesim_t2, - ) - amesim_state_pn2pipefr_center_flow = line.pn2pipefr_mass_flow( - port_1_pressure_pa=amesim_p1, - port_1_temperature_k=amesim_t1, - port_2_pressure_pa=amesim_p2, - port_2_temperature_k=amesim_t2, - ) - amesim_center_real_port_1 = ( - -amesim_center_mass_flow_python_sign_kg_s - * line.gas.pressure_reference_enthalpy( - amesim_p1, - amesim_t1, - reference_pressure=AMESIM_REFERENCE_PRESSURE_PA, - reference_temperature=reference_temperature_k, - ) - ) - amesim_mass_1 = line.gas.density(amesim_p1, amesim_t1) * line.compliance_volume - amesim_mass_2 = line.gas.density(amesim_p2, amesim_t2) * line.compliance_volume - heat_flow_1 = ( - line.heat_transfer_coefficient - * line.heat_transfer_area - * 0.5 - * (line.external_temperature - amesim_t1) - ) - heat_flow_2 = ( - line.heat_transfer_coefficient - * line.heat_transfer_area - * 0.5 - * (line.external_temperature - amesim_t2) - ) - amesim_pn2vol_dtemp_1 = _pn2vol_reference_dtemp( - gas=line.gas, - temperature_k=amesim_t1, - mass_kg=amesim_mass_1, - mass_derivative_kg_s=amesim_sdm1_kg_s, - enthalpy_flow_w=amesim_sdh1, - heat_flow_w=heat_flow_1, - reference_temperature_k=reference_temperature_k, - ) - amesim_pn2vol_dtemp_2 = _pn2vol_reference_dtemp( - gas=line.gas, - temperature_k=amesim_t2, - mass_kg=amesim_mass_2, - mass_derivative_kg_s=amesim_sdm2_kg_s, - enthalpy_flow_w=amesim_sdh2, - heat_flow_w=heat_flow_2, - reference_temperature_k=reference_temperature_k, - ) - amesim_t1_series = amesim_results.series(f"t1@{line_alias}") - amesim_t2_series = amesim_results.series(f"t2@{line_alias}") - amesim_fd_dtemp_1 = _series_finite_difference_at( - times=amesim_results.times, - values=amesim_t1_series, - time_s=time_s, - ) - amesim_fd_dtemp_2 = _series_finite_difference_at( - times=amesim_results.times, - values=amesim_t2_series, - time_s=time_s, - ) - amesim_backward_dtemp_1, amesim_forward_dtemp_1 = ( - _series_one_sided_differences_at( - times=amesim_results.times, - values=amesim_t1_series, - time_s=time_s, - ) - ) - amesim_backward_dtemp_2, amesim_forward_dtemp_2 = ( - _series_one_sided_differences_at( - times=amesim_results.times, - values=amesim_t2_series, - time_s=time_s, - ) - ) - amesim_p1_series = amesim_results.series(f"p1@{line_alias}") - amesim_p2_series = amesim_results.series(f"p2@{line_alias}") - amesim_fd_dpressure_1 = _series_finite_difference_at( - times=amesim_results.times, - values=amesim_p1_series, - time_s=time_s, - ) - amesim_fd_dpressure_2 = _series_finite_difference_at( - times=amesim_results.times, - values=amesim_p2_series, - time_s=time_s, - ) - ( - amesim_dpressure_1_mass, - amesim_dpressure_1_temperature, - amesim_dpressure_1_sum, - ) = _pressure_derivative_components_from_density_temperature( - gas=line.gas, - density_kg_m3=amesim_mass_1 / line.compliance_volume, - temperature_k=amesim_t1, - density_derivative_kg_m3_s=amesim_sdm1_kg_s / line.compliance_volume, - temperature_derivative_k_s=amesim_fd_dtemp_1, - ) - ( - amesim_dpressure_2_mass, - amesim_dpressure_2_temperature, - amesim_dpressure_2_sum, - ) = _pressure_derivative_components_from_density_temperature( - gas=line.gas, - density_kg_m3=amesim_mass_2 / line.compliance_volume, - temperature_k=amesim_t2, - density_derivative_kg_m3_s=amesim_sdm2_kg_s / line.compliance_volume, - temperature_derivative_k_s=amesim_fd_dtemp_2, - ) - - current_derivative_1, current_derivative_2 = line.derivatives_from_connections( - port_1_m_flow=port_1_flow, - connected_h_1=port_1_connected_h, - port_2_m_flow=port_2_flow, - connected_h_2=connected_port_2.h, - ) - python_current_dtemp_1 = ( - current_derivative_1.U - port_1.u * current_derivative_1.m - ) / (line.state_1.m * line.gas.cv) - python_current_dtemp_2 = ( - current_derivative_2.U - port_2.u * current_derivative_2.m - ) / (line.state_2.m * line.gas.cv) - ( - python_dpressure_1_mass, - python_dpressure_1_temperature, - python_dpressure_1_sum, - ) = _pressure_derivative_components_from_density_temperature( - gas=line.gas, - density_kg_m3=line.state_1.m / line.compliance_volume, - temperature_k=port_1.T, - density_derivative_kg_m3_s=current_derivative_1.m / line.compliance_volume, - temperature_derivative_k_s=python_current_dtemp_1, - ) - ( - python_dpressure_2_mass, - python_dpressure_2_temperature, - python_dpressure_2_sum, - ) = _pressure_derivative_components_from_density_temperature( - gas=line.gas, - density_kg_m3=line.state_2.m / line.compliance_volume, - temperature_k=port_2.T, - density_derivative_kg_m3_s=current_derivative_2.m / line.compliance_volume, - temperature_derivative_k_s=python_current_dtemp_2, - ) - - python_center_dh_1 = _actual_reference_enthalpy_flow( - gas=line.gas, - port_m_flow=-center_flow, - connected_pressure_pa=port_2.p, - connected_temperature_k=port_2.T, - internal_pressure_pa=port_1.p, - internal_temperature_k=port_1.T, - reference_temperature_k=reference_temperature_k, - ) - python_center_dh_2 = _actual_reference_enthalpy_flow( - gas=line.gas, - port_m_flow=center_flow, - connected_pressure_pa=port_1.p, - connected_temperature_k=port_1.T, - internal_pressure_pa=port_2.p, - internal_temperature_k=port_2.T, - reference_temperature_k=reference_temperature_k, - ) - if port_1_flow > 0.0: - python_port_1_dh = port_1_flow * ( - port_1_connected_h - line.gas.cp * reference_temperature_k - ) - else: - python_port_1_dh = _actual_reference_enthalpy_flow( - gas=line.gas, - port_m_flow=port_1_flow, - connected_pressure_pa=port_1.p, - connected_temperature_k=port_1.T, - internal_pressure_pa=port_1.p, - internal_temperature_k=port_1.T, - reference_temperature_k=reference_temperature_k, - ) - python_port_2_dh = _actual_reference_enthalpy_flow( - gas=line.gas, - port_m_flow=port_2_flow, - connected_pressure_pa=connected_port_2.p, - connected_temperature_k=connected_port_2.T, - internal_pressure_pa=port_2.p, - internal_temperature_k=port_2.T, - reference_temperature_k=reference_temperature_k, - ) - python_center_ideal_port_1 = -center_flow * line.gas.specific_reference_enthalpy( - port_1.T, - reference_temperature_k, - ) - python_center_ideal_port_2 = -center_flow * line.gas.specific_reference_enthalpy( - port_2.T, - reference_temperature_k, - ) - python_center_real_port_1 = -center_flow * line.gas.pressure_reference_enthalpy( - port_1.p, - port_1.T, - reference_pressure=AMESIM_REFERENCE_PRESSURE_PA, - reference_temperature=reference_temperature_k, - ) - python_center_real_port_2 = -center_flow * line.gas.pressure_reference_enthalpy( - port_2.p, - port_2.T, - reference_pressure=AMESIM_REFERENCE_PRESSURE_PA, - reference_temperature=reference_temperature_k, - ) - - python_heat_flow_1 = ( - line.heat_transfer_coefficient - * line.heat_transfer_area - * 0.5 - * (line.external_temperature - port_1.T) - ) - python_heat_flow_2 = ( - line.heat_transfer_coefficient - * line.heat_transfer_area - * 0.5 - * (line.external_temperature - port_2.T) - ) - python_ref_dtemp_1 = _pn2vol_reference_dtemp( - gas=line.gas, - temperature_k=port_1.T, - mass_kg=line.state_1.m, - mass_derivative_kg_s=port_1_flow - center_flow, - enthalpy_flow_w=python_port_1_dh + python_center_dh_1, - heat_flow_w=python_heat_flow_1, - reference_temperature_k=reference_temperature_k, - ) - python_ref_dtemp_2 = _pn2vol_reference_dtemp( - gas=line.gas, - temperature_k=port_2.T, - mass_kg=line.state_2.m, - mass_derivative_kg_s=port_2_flow + center_flow, - enthalpy_flow_w=python_port_2_dh + python_center_dh_2, - heat_flow_w=python_heat_flow_2, - reference_temperature_k=reference_temperature_k, - ) - - def dtemp_component(flow_w: float, mass_kg: float) -> float: - return flow_w / (mass_kg * line.gas.cv) - - amesim_port_1_mass_offset_flow_w = ( - line.gas.cp * reference_temperature_k - line.gas.cv * amesim_t1 - ) * amesim_sdm1_kg_s - amesim_port_2_mass_offset_flow_w = ( - line.gas.cp * reference_temperature_k - line.gas.cv * amesim_t2 - ) * amesim_sdm2_kg_s - python_port_1_mass_offset_flow_w = ( - line.gas.cp * reference_temperature_k - line.gas.cv * port_1.T - ) * (port_1_flow - center_flow) - python_port_2_mass_offset_flow_w = ( - line.gas.cp * reference_temperature_k - line.gas.cv * port_2.T - ) * (port_2_flow + center_flow) - amesim_port_1_external_dtemp_component = dtemp_component( - amesim_dh1, - amesim_mass_1, - ) - amesim_port_1_center_dtemp_component = dtemp_component( - amesim_dhctr, - amesim_mass_1, - ) - amesim_port_1_mass_offset_dtemp_component = dtemp_component( - amesim_port_1_mass_offset_flow_w, - amesim_mass_1, - ) - amesim_port_1_heat_dtemp_component = dtemp_component( - heat_flow_1, - amesim_mass_1, - ) - amesim_port_2_external_dtemp_component = dtemp_component( - amesim_dh2, - amesim_mass_2, - ) - amesim_port_2_center_dtemp_component = dtemp_component( - -amesim_dhctr, - amesim_mass_2, - ) - amesim_port_2_mass_offset_dtemp_component = dtemp_component( - amesim_port_2_mass_offset_flow_w, - amesim_mass_2, - ) - amesim_port_2_heat_dtemp_component = dtemp_component( - heat_flow_2, - amesim_mass_2, - ) - python_port_1_external_dtemp_component = dtemp_component( - python_port_1_dh, - line.state_1.m, - ) - python_port_1_center_dtemp_component = dtemp_component( - python_center_dh_1, - line.state_1.m, - ) - python_port_1_mass_offset_dtemp_component = dtemp_component( - python_port_1_mass_offset_flow_w, - line.state_1.m, - ) - python_port_1_heat_dtemp_component = dtemp_component( - python_heat_flow_1, - line.state_1.m, - ) - python_port_2_external_dtemp_component = dtemp_component( - python_port_2_dh, - line.state_2.m, - ) - python_port_2_center_dtemp_component = dtemp_component( - python_center_dh_2, - line.state_2.m, - ) - python_port_2_mass_offset_dtemp_component = dtemp_component( - python_port_2_mass_offset_flow_w, - line.state_2.m, - ) - python_port_2_heat_dtemp_component = dtemp_component( - python_heat_flow_2, - line.state_2.m, - ) - - amesim_center_flow_python_sign_kg_s = ( - amesim_center_python_sign_equivalent_g_s * 1.0e-3 - ) - amesim_external_flow_kg_s = amesim_dm2 * 1.0e-3 - python_dtemp_2_amesim_center = _pn2vol_reference_dtemp( - gas=line.gas, - temperature_k=port_2.T, - mass_kg=line.state_2.m, - mass_derivative_kg_s=port_2_flow + amesim_center_flow_python_sign_kg_s, - enthalpy_flow_w=python_port_2_dh - amesim_dhctr, - heat_flow_w=python_heat_flow_2, - reference_temperature_k=reference_temperature_k, - ) - python_dtemp_2_amesim_external = _pn2vol_reference_dtemp( - gas=line.gas, - temperature_k=port_2.T, - mass_kg=line.state_2.m, - mass_derivative_kg_s=amesim_external_flow_kg_s + center_flow, - enthalpy_flow_w=amesim_dh2 + python_center_dh_2, - heat_flow_w=python_heat_flow_2, - reference_temperature_k=reference_temperature_k, - ) - python_dtemp_2_amesim_external_center = _pn2vol_reference_dtemp( - gas=line.gas, - temperature_k=port_2.T, - mass_kg=line.state_2.m, - mass_derivative_kg_s=( - amesim_external_flow_kg_s + amesim_center_flow_python_sign_kg_s - ), - enthalpy_flow_w=amesim_dh2 - amesim_dhctr, - heat_flow_w=python_heat_flow_2, - reference_temperature_k=reference_temperature_k, - ) - - return TestMqlPnl0003EnergyDiagnostic( - line_alias=line_alias, - time_s=time_s, - amesim_port_1_temperature_k=amesim_t1, - amesim_port_2_temperature_k=amesim_t2, - amesim_port_1_pressure_pa=amesim_p1, - amesim_port_2_pressure_pa=amesim_p2, - amesim_port_1_mass_flow_g_s=amesim_dm1, - amesim_port_1_enthalpy_flow_w=amesim_dh1, - amesim_center_mass_flow_g_s=amesim_dmctr, - amesim_center_enthalpy_flow_w=amesim_dhctr, - amesim_port_2_mass_flow_g_s=amesim_dm2, - amesim_port_2_enthalpy_flow_w=amesim_dh2, - amesim_port_1_storage_mass_derivative_g_s=amesim_sdm1_kg_s * 1.0e3, - amesim_port_1_storage_enthalpy_sum_w=amesim_sdh1, - amesim_port_2_storage_mass_derivative_g_s=amesim_sdm2_kg_s * 1.0e3, - amesim_port_2_storage_enthalpy_sum_w=amesim_sdh2, - python_pn3_node_port_1_mass_flow_g_s=python_node_dm1, - python_pn3_node_port_3_mass_flow_g_s=python_node_dm3, - python_pn3_node_to_line_mass_flow_g_s=python_node_to_line_dm, - python_pn3_node_to_line_mass_flow_error_g_s=( - python_node_to_line_dm - amesim_dm1 - ), - amesim_port_1_mass_estimate_g=amesim_mass_1 * 1.0e3, - amesim_port_2_mass_estimate_g=amesim_mass_2 * 1.0e3, - python_port_1_mass_g=line.state_1.m * 1.0e3, - python_port_2_mass_g=line.state_2.m * 1.0e3, - python_port_1_mass_error_to_amesim_g=( - line.state_1.m * 1.0e3 - amesim_mass_1 * 1.0e3 - ), - python_port_2_mass_error_to_amesim_g=( - line.state_2.m * 1.0e3 - amesim_mass_2 * 1.0e3 - ), - amesim_total_mass_derivative_fd_g_s=amesim_total_mass_derivative_fd, - amesim_total_mass_derivative_backward_g_s=( - amesim_total_mass_derivative_backward - ), - amesim_total_mass_derivative_forward_g_s=( - amesim_total_mass_derivative_forward - ), - amesim_total_storage_mass_derivative_g_s=( - amesim_total_storage_mass_derivative - ), - amesim_total_storage_mass_derivative_residual_g_s=( - amesim_total_storage_mass_derivative - amesim_total_mass_derivative_fd - ), - amesim_center_flow_python_sign_equivalent_g_s=( - amesim_center_python_sign_equivalent_g_s - ), - amesim_center_flow_python_sign_error_g_s=( - python_center_mass_flow_g_s - amesim_center_python_sign_equivalent_g_s - ), - python_center_mass_flow_g_s=python_center_mass_flow_g_s, - amesim_port_1_center_mass_derivative_g_s=( - amesim_port_1_center_mass_derivative_g_s - ), - amesim_port_2_center_mass_derivative_g_s=( - amesim_port_2_center_mass_derivative_g_s - ), - python_port_1_external_mass_flow_g_s=python_port_1_external_mass_flow_g_s, - python_port_1_external_mass_flow_error_g_s=( - python_port_1_external_mass_flow_g_s - amesim_dm1 - ), - python_port_1_center_mass_derivative_g_s=( - python_port_1_center_mass_derivative_g_s - ), - python_port_1_center_mass_derivative_error_g_s=( - python_port_1_center_mass_derivative_g_s - - amesim_port_1_center_mass_derivative_g_s - ), - python_port_1_storage_mass_derivative_g_s=( - python_port_1_storage_mass_derivative_g_s - ), - python_port_1_storage_mass_derivative_error_g_s=( - python_port_1_storage_mass_derivative_g_s - amesim_sdm1_kg_s * 1.0e3 - ), - python_port_2_external_mass_flow_g_s=python_port_2_external_mass_flow_g_s, - python_port_2_external_mass_flow_error_g_s=( - python_port_2_external_mass_flow_g_s - amesim_dm2 - ), - python_port_2_center_mass_derivative_g_s=( - python_port_2_center_mass_derivative_g_s - ), - python_port_2_center_mass_derivative_error_g_s=( - python_port_2_center_mass_derivative_g_s - - amesim_port_2_center_mass_derivative_g_s - ), - python_port_2_storage_mass_derivative_g_s=( - python_port_2_storage_mass_derivative_g_s - ), - python_port_2_storage_mass_derivative_error_g_s=( - python_port_2_storage_mass_derivative_g_s - amesim_sdm2_kg_s * 1.0e3 - ), - python_amesim_state_darcy_center_mass_flow_g_s=( - amesim_state_darcy_center_flow * 1.0e3 - ), - python_amesim_state_darcy_center_mass_flow_error_g_s=( - amesim_state_darcy_center_flow * 1.0e3 - - amesim_center_python_sign_equivalent_g_s - ), - python_amesim_state_pn2pipefr_center_mass_flow_g_s=( - amesim_state_pn2pipefr_center_flow * 1.0e3 - ), - python_amesim_state_pn2pipefr_center_mass_flow_error_g_s=( - amesim_state_pn2pipefr_center_flow * 1.0e3 - - amesim_center_python_sign_equivalent_g_s - ), - amesim_center_real_gas_reference_port_1_estimate_w=amesim_center_real_port_1, - amesim_center_real_gas_reference_port_1_error_w=( - amesim_center_real_port_1 - amesim_dhctr - ), - python_center_ideal_reference_port_1_estimate_w=python_center_ideal_port_1, - python_center_ideal_reference_port_1_error_w=( - python_center_ideal_port_1 - amesim_dhctr - ), - python_center_ideal_reference_port_2_estimate_w=python_center_ideal_port_2, - python_center_ideal_reference_port_2_error_w=( - python_center_ideal_port_2 - amesim_dhctr - ), - python_center_real_gas_reference_port_1_estimate_w=python_center_real_port_1, - python_center_real_gas_reference_port_1_error_w=( - python_center_real_port_1 - amesim_dhctr - ), - python_center_real_gas_reference_port_2_estimate_w=python_center_real_port_2, - python_center_real_gas_reference_port_2_error_w=( - python_center_real_port_2 - amesim_dhctr - ), - python_total_mass_derivative_kg_s=( - current_derivative_1.m + current_derivative_2.m - ), - python_port_1_reference_external_enthalpy_flow_w=python_port_1_dh, - python_port_1_reference_center_enthalpy_flow_w=python_center_dh_1, - python_port_1_reference_storage_enthalpy_sum_w=( - python_port_1_dh + python_center_dh_1 - ), - python_port_1_reference_storage_enthalpy_error_w=( - python_port_1_dh + python_center_dh_1 - amesim_sdh1 - ), - python_port_2_reference_external_enthalpy_flow_w=python_port_2_dh, - python_port_2_reference_center_enthalpy_flow_w=python_center_dh_2, - python_port_2_reference_storage_enthalpy_sum_w=( - python_port_2_dh + python_center_dh_2 - ), - python_port_2_reference_storage_enthalpy_error_w=( - python_port_2_dh + python_center_dh_2 - amesim_sdh2 - ), - amesim_port_1_external_enthalpy_dtemp_component_k_s=( - amesim_port_1_external_dtemp_component - ), - amesim_port_1_center_enthalpy_dtemp_component_k_s=( - amesim_port_1_center_dtemp_component - ), - amesim_port_1_mass_offset_dtemp_component_k_s=( - amesim_port_1_mass_offset_dtemp_component - ), - amesim_port_1_heat_dtemp_component_k_s=amesim_port_1_heat_dtemp_component, - python_port_1_external_enthalpy_dtemp_component_k_s=( - python_port_1_external_dtemp_component - ), - python_port_1_center_enthalpy_dtemp_component_k_s=( - python_port_1_center_dtemp_component - ), - python_port_1_mass_offset_dtemp_component_k_s=( - python_port_1_mass_offset_dtemp_component - ), - python_port_1_heat_dtemp_component_k_s=python_port_1_heat_dtemp_component, - amesim_port_2_external_enthalpy_dtemp_component_k_s=( - amesim_port_2_external_dtemp_component - ), - amesim_port_2_center_enthalpy_dtemp_component_k_s=( - amesim_port_2_center_dtemp_component - ), - amesim_port_2_mass_offset_dtemp_component_k_s=( - amesim_port_2_mass_offset_dtemp_component - ), - amesim_port_2_heat_dtemp_component_k_s=amesim_port_2_heat_dtemp_component, - python_port_2_external_enthalpy_dtemp_component_k_s=( - python_port_2_external_dtemp_component - ), - python_port_2_center_enthalpy_dtemp_component_k_s=( - python_port_2_center_dtemp_component - ), - python_port_2_mass_offset_dtemp_component_k_s=( - python_port_2_mass_offset_dtemp_component - ), - python_port_2_heat_dtemp_component_k_s=python_port_2_heat_dtemp_component, - amesim_port_1_temperature_derivative_fd_k_s=amesim_fd_dtemp_1, - amesim_port_1_temperature_derivative_backward_k_s=amesim_backward_dtemp_1, - amesim_port_1_temperature_derivative_forward_k_s=amesim_forward_dtemp_1, - amesim_port_2_temperature_derivative_fd_k_s=amesim_fd_dtemp_2, - amesim_port_2_temperature_derivative_backward_k_s=amesim_backward_dtemp_2, - amesim_port_2_temperature_derivative_forward_k_s=amesim_forward_dtemp_2, - amesim_port_1_pn2vol_dtemp_k_s=amesim_pn2vol_dtemp_1, - amesim_port_2_pn2vol_dtemp_k_s=amesim_pn2vol_dtemp_2, - amesim_port_1_pn2vol_dtemp_residual_k_s=( - amesim_pn2vol_dtemp_1 - amesim_fd_dtemp_1 - ), - amesim_port_2_pn2vol_dtemp_residual_k_s=( - amesim_pn2vol_dtemp_2 - amesim_fd_dtemp_2 - ), - amesim_port_1_pressure_derivative_fd_pa_s=amesim_fd_dpressure_1, - amesim_port_2_pressure_derivative_fd_pa_s=amesim_fd_dpressure_2, - amesim_port_1_pressure_derivative_mass_component_pa_s=( - amesim_dpressure_1_mass - ), - amesim_port_1_pressure_derivative_temperature_component_pa_s=( - amesim_dpressure_1_temperature - ), - amesim_port_1_pressure_derivative_eos_sum_pa_s=amesim_dpressure_1_sum, - amesim_port_1_pressure_derivative_eos_residual_pa_s=( - amesim_dpressure_1_sum - amesim_fd_dpressure_1 - ), - amesim_port_2_pressure_derivative_mass_component_pa_s=( - amesim_dpressure_2_mass - ), - amesim_port_2_pressure_derivative_temperature_component_pa_s=( - amesim_dpressure_2_temperature - ), - amesim_port_2_pressure_derivative_eos_sum_pa_s=amesim_dpressure_2_sum, - amesim_port_2_pressure_derivative_eos_residual_pa_s=( - amesim_dpressure_2_sum - amesim_fd_dpressure_2 - ), - python_port_1_pressure_derivative_mass_component_pa_s=( - python_dpressure_1_mass - ), - python_port_1_pressure_derivative_temperature_component_pa_s=( - python_dpressure_1_temperature - ), - python_port_1_pressure_derivative_eos_sum_pa_s=python_dpressure_1_sum, - python_port_2_pressure_derivative_mass_component_pa_s=( - python_dpressure_2_mass - ), - python_port_2_pressure_derivative_temperature_component_pa_s=( - python_dpressure_2_temperature - ), - python_port_2_pressure_derivative_eos_sum_pa_s=python_dpressure_2_sum, - python_port_1_temperature_k=port_1.T, - python_port_2_temperature_k=port_2.T, - python_port_1_pressure_pa=port_1.p, - python_port_2_pressure_pa=port_2.p, - python_port_1_mass_derivative_kg_s=current_derivative_1.m, - python_port_2_mass_derivative_kg_s=current_derivative_2.m, - python_port_1_current_dtemp_k_s=python_current_dtemp_1, - python_port_2_current_dtemp_k_s=python_current_dtemp_2, - python_port_1_real_gas_reference_dtemp_k_s=python_ref_dtemp_1, - python_port_2_real_gas_reference_dtemp_k_s=python_ref_dtemp_2, - python_port_2_amesim_center_counterfactual_dtemp_k_s=( - python_dtemp_2_amesim_center - ), - python_port_2_amesim_external_counterfactual_dtemp_k_s=( - python_dtemp_2_amesim_external - ), - python_port_2_amesim_external_center_counterfactual_dtemp_k_s=( - python_dtemp_2_amesim_external_center - ), - python_port_2_temperature_error_to_amesim_k=port_2.T - amesim_t2, - ) - - -def _pneumatic_96_inlet_line_diagnostic( - *, - closure: object, - amesim_results: AmesimResults, - state_vector: list[float], - line_alias: str, - node_alias: str, - orifice_alias: str, - chamber_alias: str, - time_s: float, -) -> TestMqlPneumatic96InletLineDiagnostic: - if line_alias != "pneumatic_96": - raise KeyError(f"Unsupported inlet line diagnostic: {line_alias}") - if node_alias != "pn_node3_8" or orifice_alias != "pn_orifice_18": - raise KeyError( - f"Unsupported inlet path diagnostic: {node_alias}/{orifice_alias}" - ) - - snapshot = closure.snapshot_at(time_s, state_vector).pneumatic - line = closure.pneumatic_closure.components.inlet_line - - def amesim_value(data_path: str) -> float: - return interpolate_series_value( - amesim_results.times, - amesim_results.series(data_path), - time_s, - ) - - amesim_line_pressure = ( - amesim_value(f"p2@{line_alias}") + AMESIM_REFERENCE_PRESSURE_PA - ) - amesim_line_temperature = amesim_value(f"t2@{line_alias}") - amesim_line_mass = amesim_value(f"mgas@{line_alias}") - amesim_line_mass_fd = _series_finite_difference_at( - times=amesim_results.times, - values=amesim_results.series(f"mgas@{line_alias}"), - time_s=time_s, - ) - amesim_line_to_node_flow = amesim_value(f"dm1@{line_alias}") - amesim_orifice_to_line_flow = amesim_value(f"dm2@{orifice_alias}") - amesim_storage_from_ports = ( - amesim_orifice_to_line_flow - amesim_line_to_node_flow - ) - amesim_chamber_pressure = ( - amesim_value(f"press@{chamber_alias}") + AMESIM_REFERENCE_PRESSURE_PA - ) - amesim_chamber_temperature = amesim_value(f"temp@{chamber_alias}") - amesim_node_pressure = ( - amesim_value(f"press1@{node_alias}") + AMESIM_REFERENCE_PRESSURE_PA - ) - amesim_node_temperature = amesim_value(f"temp1@{node_alias}") - - python_line_to_node_flow = -snapshot.inlet_node_to_line_flow * 1.0e3 - python_orifice_to_line_flow = -snapshot.inlet_line_to_chamber_flow * 1.0e3 - python_storage_derivative = ( - snapshot.inlet_node_to_line_flow - snapshot.inlet_line_to_chamber_flow - ) * 1.0e3 - - orifice = closure.pneumatic_closure.components.inlet_orifice - - def node_to_line_flow_from_states( - *, - node_pressure_pa: float, - node_temperature_k: float, - line_pressure_pa: float, - line_temperature_k: float, - ) -> float: - pressure_difference = node_pressure_pa - line_pressure_pa - if pressure_difference == 0.0: - return 0.0 - if line.calibrated_linear_conductance is not None: - return ( - line.calibrated_linear_conductance - * pressure_difference - / sqrt(line_temperature_k) - ) - upstream_pressure = max(node_pressure_pa, line_pressure_pa) - upstream_temperature = ( - node_temperature_k if pressure_difference > 0.0 else line_temperature_k - ) - density = line.gas.density(upstream_pressure, upstream_temperature) - magnitude = line._mass_flow_for_pressure_drop( - abs(pressure_difference), - density=density, - temperature=upstream_temperature, - ) - return magnitude if pressure_difference > 0.0 else -magnitude - - def line_to_node_flow_from_states_g_s( - *, - node_pressure_pa: float, - node_temperature_k: float, - line_pressure_pa: float, - line_temperature_k: float, - ) -> float: - return -1.0e3 * node_to_line_flow_from_states( - node_pressure_pa=node_pressure_pa, - node_temperature_k=node_temperature_k, - line_pressure_pa=line_pressure_pa, - line_temperature_k=line_temperature_k, - ) - - def orifice_to_line_flow_from_states_g_s( - *, - line_pressure_pa: float, - line_temperature_k: float, - chamber_pressure_pa: float, - chamber_temperature_k: float, - ) -> float: - upstream_temperature = ( - line_temperature_k - if line_pressure_pa >= chamber_pressure_pa - else chamber_temperature_k - ) - line_to_chamber = orifice.mass_flow( - line_pressure_pa, - chamber_pressure_pa, - upstream_temperature, - ) - return -1.0e3 * line_to_chamber - - python_line_to_node_amesim_node_flow = line_to_node_flow_from_states_g_s( - node_pressure_pa=amesim_node_pressure, - node_temperature_k=amesim_node_temperature, - line_pressure_pa=snapshot.inlet_line.p, - line_temperature_k=snapshot.inlet_line.T, - ) - python_line_to_node_amesim_line_flow = line_to_node_flow_from_states_g_s( - node_pressure_pa=snapshot.inlet_node.p, - node_temperature_k=snapshot.inlet_node.T, - line_pressure_pa=amesim_line_pressure, - line_temperature_k=amesim_line_temperature, - ) - python_line_to_node_amesim_node_line_flow = line_to_node_flow_from_states_g_s( - node_pressure_pa=amesim_node_pressure, - node_temperature_k=amesim_node_temperature, - line_pressure_pa=amesim_line_pressure, - line_temperature_k=amesim_line_temperature, - ) - python_orifice_to_line_amesim_line_flow = orifice_to_line_flow_from_states_g_s( - line_pressure_pa=amesim_line_pressure, - line_temperature_k=amesim_line_temperature, - chamber_pressure_pa=snapshot.chamber.p, - chamber_temperature_k=snapshot.chamber.T, - ) - python_orifice_to_line_amesim_chamber_flow = orifice_to_line_flow_from_states_g_s( - line_pressure_pa=snapshot.inlet_line.p, - line_temperature_k=snapshot.inlet_line.T, - chamber_pressure_pa=amesim_chamber_pressure, - chamber_temperature_k=amesim_chamber_temperature, - ) - python_orifice_to_line_amesim_line_chamber_flow = ( - orifice_to_line_flow_from_states_g_s( - line_pressure_pa=amesim_line_pressure, - line_temperature_k=amesim_line_temperature, - chamber_pressure_pa=amesim_chamber_pressure, - chamber_temperature_k=amesim_chamber_temperature, - ) - ) - python_storage_amesim_boundary_states = ( - python_orifice_to_line_amesim_line_chamber_flow - - python_line_to_node_amesim_node_line_flow - ) - - amesim_line_temperature_fd = _series_finite_difference_at( - times=amesim_results.times, - values=amesim_results.series(f"t2@{line_alias}"), - time_s=time_s, - ) - amesim_line_pressure_fd = _series_finite_difference_at( - times=amesim_results.times, - values=amesim_results.series(f"p2@{line_alias}"), - time_s=time_s, - ) - amesim_dh1 = amesim_value(f"dh1@{line_alias}") - amesim_dh2 = amesim_value(f"dh2@{orifice_alias}") - amesim_line_mass_kg = amesim_line_mass * 1.0e-3 - amesim_storage_kg_s = amesim_storage_from_ports * 1.0e-3 - amesim_reference_temperature_k = 298.15 - amesim_mass_offset_flow = ( - line.gas.cp * amesim_reference_temperature_k - - line.gas.cv * amesim_line_temperature - ) * amesim_storage_kg_s - - def amesim_dtemp_component(flow_w: float) -> float: - return flow_w / (amesim_line_mass_kg * line.gas.cv) - - amesim_port_1_dtemp = amesim_dtemp_component(-amesim_dh1) - amesim_port_2_dtemp = amesim_dtemp_component(amesim_dh2) - amesim_mass_offset_dtemp = amesim_dtemp_component(amesim_mass_offset_flow) - amesim_reference_dtemp_sum = ( - amesim_port_1_dtemp + amesim_port_2_dtemp + amesim_mass_offset_dtemp - ) - ( - amesim_pressure_mass_component, - amesim_pressure_temperature_component, - amesim_pressure_eos_sum, - ) = _pressure_derivative_components_from_density_temperature( - gas=line.gas, - density_kg_m3=amesim_line_mass_kg / line.volume, - temperature_k=amesim_line_temperature, - density_derivative_kg_m3_s=amesim_storage_kg_s / line.volume, - temperature_derivative_k_s=amesim_line_temperature_fd, - ) - - python_port_1_flow = snapshot.inlet_node_to_line_flow - python_port_2_flow = -snapshot.inlet_line_to_chamber_flow - python_heat_flow = ( - line.heat_transfer_coefficient - * line.heat_transfer_area - * (line.external_temperature - snapshot.inlet_line.T) - ) - python_port_1_u = ( - snapshot.inlet_node.h / line.gas.gamma - if python_port_1_flow > 0.0 - else snapshot.inlet_line.u - ) - python_port_2_u = ( - snapshot.chamber.h / line.gas.gamma - if python_port_2_flow > 0.0 - else snapshot.inlet_line.u - ) - - def python_current_dtemp_component( - *, - mass_flow_kg_s: float, - inlet_u_j_kg: float, - ) -> float: - return ( - mass_flow_kg_s - * (inlet_u_j_kg - snapshot.inlet_line.u) - / (line.state.m * line.gas.cv) - ) - - python_port_1_dtemp = python_current_dtemp_component( - mass_flow_kg_s=python_port_1_flow, - inlet_u_j_kg=python_port_1_u, - ) - python_port_2_dtemp = python_current_dtemp_component( - mass_flow_kg_s=python_port_2_flow, - inlet_u_j_kg=python_port_2_u, - ) - python_heat_dtemp = python_heat_flow / (line.state.m * line.gas.cv) - python_current_dtemp = ( - python_port_1_dtemp + python_port_2_dtemp + python_heat_dtemp - ) - python_density_derivative = ( - (snapshot.inlet_node_to_line_flow - snapshot.inlet_line_to_chamber_flow) - / line.volume - ) - ( - python_pressure_mass_component, - python_pressure_temperature_component, - python_pressure_eos_sum, - ) = _pressure_derivative_components_from_density_temperature( - gas=line.gas, - density_kg_m3=line.state.m / line.volume, - temperature_k=snapshot.inlet_line.T, - density_derivative_kg_m3_s=python_density_derivative, - temperature_derivative_k_s=python_current_dtemp, - ) - - python_reference_port_1_dh = _actual_reference_enthalpy_flow( - gas=line.gas, - port_m_flow=python_port_1_flow, - connected_pressure_pa=snapshot.inlet_node.p, - connected_temperature_k=snapshot.inlet_node.T, - internal_pressure_pa=snapshot.inlet_line.p, - internal_temperature_k=snapshot.inlet_line.T, - reference_temperature_k=amesim_reference_temperature_k, - ) - python_reference_port_2_dh = _actual_reference_enthalpy_flow( - gas=line.gas, - port_m_flow=python_port_2_flow, - connected_pressure_pa=snapshot.chamber.p, - connected_temperature_k=snapshot.chamber.T, - internal_pressure_pa=snapshot.inlet_line.p, - internal_temperature_k=snapshot.inlet_line.T, - reference_temperature_k=amesim_reference_temperature_k, - ) - python_reference_mass_offset_flow = ( - line.gas.cp * amesim_reference_temperature_k - - line.gas.cv * snapshot.inlet_line.T - ) * (python_port_1_flow + python_port_2_flow) - - def python_reference_dtemp_component(flow_w: float) -> float: - return flow_w / (line.state.m * line.gas.cv) - - python_reference_port_1_dtemp = python_reference_dtemp_component( - python_reference_port_1_dh - ) - python_reference_port_2_dtemp = python_reference_dtemp_component( - python_reference_port_2_dh - ) - python_reference_mass_offset_dtemp = python_reference_dtemp_component( - python_reference_mass_offset_flow - ) - python_reference_dtemp = ( - python_reference_port_1_dtemp - + python_reference_port_2_dtemp - + python_reference_mass_offset_dtemp - + python_heat_dtemp - ) - ( - python_reference_pressure_mass_component, - python_reference_pressure_temperature_component, - python_reference_pressure_eos_sum, - ) = _pressure_derivative_components_from_density_temperature( - gas=line.gas, - density_kg_m3=line.state.m / line.volume, - temperature_k=snapshot.inlet_line.T, - density_derivative_kg_m3_s=python_density_derivative, - temperature_derivative_k_s=python_reference_dtemp, - ) - - python_reference_amesim_storage_mass_offset_flow = ( - line.gas.cp * amesim_reference_temperature_k - - line.gas.cv * snapshot.inlet_line.T - ) * amesim_storage_kg_s - python_reference_amesim_storage_mass_offset_dtemp = ( - python_reference_dtemp_component( - python_reference_amesim_storage_mass_offset_flow - ) - ) - python_reference_amesim_storage_dtemp = ( - python_reference_port_1_dtemp - + python_reference_port_2_dtemp - + python_reference_amesim_storage_mass_offset_dtemp - + python_heat_dtemp - ) - use_reference_rhs = bool( - getattr(closure.pneumatic_closure, "use_inlet_line_reference_rhs", False) - ) - python_active_dtemp = ( - python_reference_dtemp if use_reference_rhs else python_current_dtemp - ) - ( - python_reference_amesim_storage_pressure_mass_component, - python_reference_amesim_storage_pressure_temperature_component, - python_reference_amesim_storage_pressure_eos_sum, - ) = _pressure_derivative_components_from_density_temperature( - gas=line.gas, - density_kg_m3=line.state.m / line.volume, - temperature_k=snapshot.inlet_line.T, - density_derivative_kg_m3_s=amesim_storage_kg_s / line.volume, - temperature_derivative_k_s=python_reference_amesim_storage_dtemp, - ) - - return TestMqlPneumatic96InletLineDiagnostic( - line_alias=line_alias, - node_alias=node_alias, - orifice_alias=orifice_alias, - chamber_alias=chamber_alias, - time_s=time_s, - amesim_line_pressure_pa=amesim_line_pressure, - amesim_line_temperature_k=amesim_line_temperature, - amesim_line_mass_g=amesim_line_mass, - amesim_line_mass_derivative_fd_g_s=amesim_line_mass_fd, - amesim_line_to_node_mass_flow_g_s=amesim_line_to_node_flow, - amesim_orifice_to_line_mass_flow_g_s=amesim_orifice_to_line_flow, - amesim_storage_mass_derivative_from_ports_g_s=amesim_storage_from_ports, - amesim_storage_mass_derivative_residual_g_s=( - amesim_storage_from_ports - amesim_line_mass_fd - ), - amesim_chamber_pressure_pa=amesim_chamber_pressure, - amesim_chamber_temperature_k=amesim_chamber_temperature, - amesim_node_pressure_pa=amesim_node_pressure, - amesim_node_temperature_k=amesim_node_temperature, - amesim_line_temperature_derivative_fd_k_s=amesim_line_temperature_fd, - amesim_line_pressure_derivative_fd_pa_s=amesim_line_pressure_fd, - amesim_port_1_enthalpy_dtemp_component_k_s=amesim_port_1_dtemp, - amesim_port_2_enthalpy_dtemp_component_k_s=amesim_port_2_dtemp, - amesim_mass_offset_dtemp_component_k_s=amesim_mass_offset_dtemp, - amesim_reference_temperature_derivative_sum_k_s=( - amesim_reference_dtemp_sum - ), - amesim_reference_temperature_derivative_residual_k_s=( - amesim_reference_dtemp_sum - amesim_line_temperature_fd - ), - amesim_pressure_derivative_mass_component_pa_s=( - amesim_pressure_mass_component - ), - amesim_pressure_derivative_temperature_component_pa_s=( - amesim_pressure_temperature_component - ), - amesim_pressure_derivative_eos_sum_pa_s=amesim_pressure_eos_sum, - amesim_pressure_derivative_eos_residual_pa_s=( - amesim_pressure_eos_sum - amesim_line_pressure_fd - ), - python_line_pressure_pa=snapshot.inlet_line.p, - python_line_temperature_k=snapshot.inlet_line.T, - python_line_mass_g=line.state.m * 1.0e3, - python_line_pressure_error_pa=snapshot.inlet_line.p - amesim_line_pressure, - python_line_temperature_error_k=( - snapshot.inlet_line.T - amesim_line_temperature - ), - python_line_mass_error_g=line.state.m * 1.0e3 - amesim_line_mass, - python_line_to_node_mass_flow_g_s=python_line_to_node_flow, - python_line_to_node_mass_flow_error_g_s=( - python_line_to_node_flow - amesim_line_to_node_flow - ), - python_orifice_to_line_mass_flow_g_s=python_orifice_to_line_flow, - python_orifice_to_line_mass_flow_error_g_s=( - python_orifice_to_line_flow - amesim_orifice_to_line_flow - ), - python_storage_mass_derivative_g_s=python_storage_derivative, - python_storage_mass_derivative_error_g_s=( - python_storage_derivative - amesim_storage_from_ports - ), - python_line_to_node_amesim_node_mass_flow_g_s=( - python_line_to_node_amesim_node_flow - ), - python_line_to_node_amesim_node_mass_flow_error_g_s=( - python_line_to_node_amesim_node_flow - amesim_line_to_node_flow - ), - python_line_to_node_amesim_line_mass_flow_g_s=( - python_line_to_node_amesim_line_flow - ), - python_line_to_node_amesim_line_mass_flow_error_g_s=( - python_line_to_node_amesim_line_flow - amesim_line_to_node_flow - ), - python_line_to_node_amesim_node_line_mass_flow_g_s=( - python_line_to_node_amesim_node_line_flow - ), - python_line_to_node_amesim_node_line_mass_flow_error_g_s=( - python_line_to_node_amesim_node_line_flow - amesim_line_to_node_flow - ), - python_orifice_to_line_amesim_line_mass_flow_g_s=( - python_orifice_to_line_amesim_line_flow - ), - python_orifice_to_line_amesim_line_mass_flow_error_g_s=( - python_orifice_to_line_amesim_line_flow - amesim_orifice_to_line_flow - ), - python_orifice_to_line_amesim_chamber_mass_flow_g_s=( - python_orifice_to_line_amesim_chamber_flow - ), - python_orifice_to_line_amesim_chamber_mass_flow_error_g_s=( - python_orifice_to_line_amesim_chamber_flow - amesim_orifice_to_line_flow - ), - python_orifice_to_line_amesim_line_chamber_mass_flow_g_s=( - python_orifice_to_line_amesim_line_chamber_flow - ), - python_orifice_to_line_amesim_line_chamber_mass_flow_error_g_s=( - python_orifice_to_line_amesim_line_chamber_flow - - amesim_orifice_to_line_flow - ), - python_storage_amesim_boundary_states_mass_derivative_g_s=( - python_storage_amesim_boundary_states - ), - python_storage_amesim_boundary_states_mass_derivative_error_g_s=( - python_storage_amesim_boundary_states - amesim_storage_from_ports - ), - python_chamber_pressure_pa=snapshot.chamber.p, - python_chamber_temperature_k=snapshot.chamber.T, - python_current_temperature_derivative_k_s=python_current_dtemp, - python_port_1_current_dtemp_component_k_s=python_port_1_dtemp, - python_port_2_current_dtemp_component_k_s=python_port_2_dtemp, - python_heat_dtemp_component_k_s=python_heat_dtemp, - python_temperature_derivative_error_k_s=( - python_current_dtemp - amesim_line_temperature_fd - ), - python_pressure_derivative_mass_component_pa_s=( - python_pressure_mass_component - ), - python_pressure_derivative_temperature_component_pa_s=( - python_pressure_temperature_component - ), - python_pressure_derivative_eos_sum_pa_s=python_pressure_eos_sum, - python_pressure_derivative_eos_error_pa_s=( - python_pressure_eos_sum - amesim_line_pressure_fd - ), - python_reference_port_1_enthalpy_flow_w=python_reference_port_1_dh, - python_reference_port_2_enthalpy_flow_w=python_reference_port_2_dh, - python_reference_port_1_dtemp_component_k_s=( - python_reference_port_1_dtemp - ), - python_reference_port_2_dtemp_component_k_s=( - python_reference_port_2_dtemp - ), - python_reference_mass_offset_dtemp_component_k_s=( - python_reference_mass_offset_dtemp - ), - python_reference_temperature_derivative_k_s=python_reference_dtemp, - python_reference_temperature_derivative_error_k_s=( - python_reference_dtemp - amesim_line_temperature_fd - ), - python_reference_pressure_derivative_mass_component_pa_s=( - python_reference_pressure_mass_component - ), - python_reference_pressure_derivative_temperature_component_pa_s=( - python_reference_pressure_temperature_component - ), - python_reference_pressure_derivative_eos_sum_pa_s=( - python_reference_pressure_eos_sum - ), - python_reference_pressure_derivative_eos_error_pa_s=( - python_reference_pressure_eos_sum - amesim_line_pressure_fd - ), - python_reference_amesim_storage_mass_offset_dtemp_component_k_s=( - python_reference_amesim_storage_mass_offset_dtemp - ), - python_reference_amesim_storage_temperature_derivative_k_s=( - python_reference_amesim_storage_dtemp - ), - python_reference_amesim_storage_temperature_derivative_error_k_s=( - python_reference_amesim_storage_dtemp - amesim_line_temperature_fd - ), - python_reference_amesim_storage_pressure_derivative_mass_component_pa_s=( - python_reference_amesim_storage_pressure_mass_component - ), - python_reference_amesim_storage_pressure_derivative_temperature_component_pa_s=( - python_reference_amesim_storage_pressure_temperature_component - ), - python_reference_amesim_storage_pressure_derivative_eos_sum_pa_s=( - python_reference_amesim_storage_pressure_eos_sum - ), - python_reference_amesim_storage_pressure_derivative_eos_error_pa_s=( - python_reference_amesim_storage_pressure_eos_sum - amesim_line_pressure_fd - ), - python_active_temperature_derivative_k_s=python_active_dtemp, - python_active_temperature_derivative_error_k_s=( - python_active_dtemp - amesim_line_temperature_fd - ), - python_active_pressure_derivative_eos_sum_pa_s=( - python_reference_pressure_eos_sum - if use_reference_rhs - else python_pressure_eos_sum - ), - python_active_pressure_derivative_eos_error_pa_s=( - (python_reference_pressure_eos_sum if use_reference_rhs else python_pressure_eos_sum) - - amesim_line_pressure_fd - ), - python_chamber_pressure_error_pa=( - snapshot.chamber.p - amesim_chamber_pressure - ), - python_chamber_temperature_error_k=( - snapshot.chamber.T - amesim_chamber_temperature - ), - ) - - -def _pnch012_energy_equation_diagnostic( - *, - closure: object, - amesim_results: AmesimResults, - state_vector: list[float], - rhs_diagnostic: TestMqlVariableChamberRhsDiagnostic, - chamber_alias: str, - piston_alias: str, - line_alias: str, - time_s: float, -) -> TestMqlPnch012EnergyEquationDiagnostic: - if chamber_alias != "pn_c1_8" or piston_alias != "pn_brp2_8": - raise KeyError(f"Unsupported PNCH012 energy diagnostic: {chamber_alias}") - if line_alias != "pneumatic_69": - raise KeyError(f"Unsupported PNCH012 line diagnostic: {line_alias}") - - snapshot = closure.snapshot_at(time_s, state_vector).pneumatic - chamber = closure.pneumatic_closure.components.p4_port3_remote_primary_chamber - chamber_properties = snapshot.p4_port3_remote_primary_chamber - reference_temperature_k = 298.15 - - def amesim_value(data_path: str) -> float: - return interpolate_series_value( - amesim_results.times, - amesim_results.series(data_path), - time_s, - ) - - amesim_dh1 = amesim_value(f"dh1@{line_alias}") - amesim_dm1_g_s = amesim_value(f"dm1@{line_alias}") - amesim_chamber_temperature = amesim_value(f"temp@{chamber_alias}") - amesim_chamber_pressure_abs = ( - amesim_value(f"press@{chamber_alias}") + AMESIM_REFERENCE_PRESSURE_PA - ) - amesim_mgas_kg = amesim_value(f"mgas1@{chamber_alias}") * 1.0e-3 - amesim_mass_derivative_fd_g_s = _series_finite_difference_at( - times=amesim_results.times, - values=amesim_results.series(f"mgas1@{chamber_alias}"), - time_s=time_s, - ) - amesim_temperature_derivative_fd = _series_finite_difference_at( - times=amesim_results.times, - values=amesim_results.series(f"temp@{chamber_alias}"), - time_s=time_s, - ) - amesim_volume_rate_fd = ( - _series_finite_difference_at( - times=amesim_results.times, - values=amesim_results.series(f"vol@{chamber_alias}"), - time_s=time_s, - ) - * 1.0e-6 - ) - amesim_piston_volume_rate = amesim_value(f"vvol1@{piston_alias}") * (1.0 / 60000.0) - amesim_heat_flow = ( - chamber.heat_transfer_coefficient - * chamber.heat_transfer_area - * (chamber.external_temperature - amesim_chamber_temperature) - ) - - def reference_pn2vol_dtemp(volume_rate_m3_s: float) -> float: - mass_flow_kg_s = amesim_dm1_g_s * 1.0e-3 - reference_offset_flow_w = ( - chamber.gas.cp * reference_temperature_k - - chamber.gas.cv * amesim_chamber_temperature - ) * mass_flow_kg_s - return ( - amesim_dh1 - + reference_offset_flow_w - + amesim_heat_flow - - amesim_chamber_pressure_abs * volume_rate_m3_s - ) / (amesim_mgas_kg * chamber.gas.cv) - - dtemp_from_fd_volume = reference_pn2vol_dtemp(amesim_volume_rate_fd) - dtemp_from_piston_volume = reference_pn2vol_dtemp(amesim_piston_volume_rate) - python_current_dtemp = ( - rhs_diagnostic.energy_derivative_w - - chamber_properties.u * rhs_diagnostic.mass_derivative_kg_s - ) / (chamber.state.m * chamber.gas.cv) - - return TestMqlPnch012EnergyEquationDiagnostic( - chamber_alias=chamber_alias, - piston_alias=piston_alias, - line_alias=line_alias, - time_s=time_s, - amesim_port_1_enthalpy_flow_w=amesim_dh1, - amesim_port_1_mass_flow_g_s=amesim_dm1_g_s, - amesim_chamber_mass_derivative_fd_g_s=amesim_mass_derivative_fd_g_s, - amesim_chamber_mass_derivative_residual_g_s=( - amesim_dm1_g_s - amesim_mass_derivative_fd_g_s - ), - amesim_chamber_temperature_derivative_fd_k_s=amesim_temperature_derivative_fd, - amesim_chamber_volume_rate_fd_m3_s=amesim_volume_rate_fd, - amesim_piston_volume_rate_m3_s=amesim_piston_volume_rate, - amesim_heat_flow_w=amesim_heat_flow, - amesim_boundary_work_fd_volume_w=( - -amesim_chamber_pressure_abs * amesim_volume_rate_fd - ), - amesim_pn2vol_reference_dtemp_fd_volume_k_s=dtemp_from_fd_volume, - amesim_pn2vol_reference_dtemp_fd_volume_residual_k_s=( - dtemp_from_fd_volume - amesim_temperature_derivative_fd - ), - amesim_pn2vol_reference_dtemp_piston_volume_k_s=dtemp_from_piston_volume, - amesim_pn2vol_reference_dtemp_piston_volume_residual_k_s=( - dtemp_from_piston_volume - amesim_temperature_derivative_fd - ), - python_port_a_mass_flow_kg_s=rhs_diagnostic.port_a_mass_flow_kg_s, - python_current_energy_derivative_w=rhs_diagnostic.energy_derivative_w, - python_current_chamber_temperature_derivative_k_s=python_current_dtemp, - reference_temperature_k=reference_temperature_k, - ) - -def _pnvo_flow_parameter_diagnostic( - *, - closure: object, - amesim_results: AmesimResults, - state_vector: list[float], - orifice_alias: str, - time_s: float, -) -> TestMqlPnvoFlowParameterDiagnostic: - if orifice_alias != "pn_morifice_1": - raise KeyError(f"Unsupported PNVO flow parameter diagnostic: {orifice_alias}") - snapshot = closure.snapshot_at(time_s, state_vector).pneumatic - orifice = closure.pneumatic_closure.components.p4_port3_remote_orifice - if orifice.name != orifice_alias: - raise KeyError(f"Unexpected PNVO diagnostic orifice: {orifice.name}") - line_properties = snapshot.p4_port3_remote_orifice_line_port_2 - boundary_properties = snapshot.p4_port3_remote_primary_line - upstream_properties = ( - line_properties if line_properties.p >= boundary_properties.p else boundary_properties - ) - python_mass_flow_kg_s = snapshot.p4_port3_remote_orifice_to_node_flow - denominator = ( - orifice.flow_coefficient * orifice.effective_area * upstream_properties.p - ) - python_cm = ( - abs(python_mass_flow_kg_s) * sqrt(upstream_properties.T) / denominator - if denominator > 0.0 and upstream_properties.T > 0.0 - else 0.0 - ) - - def amesim_value(data_path: str) -> float: - return interpolate_series_value( - amesim_results.times, - amesim_results.series(data_path), - time_s, - ) - - amesim_cm = amesim_value(f"cm@{orifice_alias}") - cm_ratio = python_cm / amesim_cm if amesim_cm != 0.0 else 0.0 - return TestMqlPnvoFlowParameterDiagnostic( - orifice_alias=orifice_alias, - time_s=time_s, - amesim_cm=amesim_cm, - amesim_dm2_g_s=amesim_value(f"dm2@{orifice_alias}"), - amesim_opening=amesim_value(f"xv@{orifice_alias}"), - amesim_gas_velocity_m_s=amesim_value(f"gasvel@{orifice_alias}"), - python_opening=orifice.opening, - python_flow_coefficient=orifice.flow_coefficient, - python_effective_area_m2=orifice.effective_area, - python_line_pressure_pa=line_properties.p, - python_boundary_pressure_pa=boundary_properties.p, - python_upstream_pressure_pa=upstream_properties.p, - python_upstream_temperature_k=upstream_properties.T, - python_mass_flow_kg_s=python_mass_flow_kg_s, - python_cm=python_cm, - python_to_amesim_cm_ratio=cm_ratio, - ) - - -def run_test_mql_pnvo_event_window_diagnostic( - config: TestMqlFullStateComparisonScriptConfig | None = None, - *, - orifice_alias: str = "pn_morifice_1", - final_time_s: float = 0.05, -) -> TestMqlPnvoEventWindowDiagnostic: - config = config or TestMqlFullStateComparisonScriptConfig() - system = TestMqlSystem(archive_path=config.paths.archive_path) - control = system.pneumatic_assembly.variable_orifice_controls[orifice_alias] - event_time_s = control.step.step_time_s - if final_time_s <= event_time_s: - raise ValueError("final_time_s must be greater than the PNVO event time") - spec = system.discover_pneumatic_branch_topology().chamber_segment_specs[0] - closure_kwargs = { - "inlet_node_pressure_pa": config.execution.inlet_node_pressure_pa, - "resistance_boundary_pressure_pa": ( - config.execution.resistance_boundary_pressure_pa - ), - "inlet_node_temperature_k": config.execution.inlet_node_temperature_k, - "resistance_boundary_temperature_k": ( - config.execution.resistance_boundary_temperature_k - ), - "use_pneumatic_96_reference_rhs": ( - config.execution.use_pneumatic_96_reference_rhs - ), - "use_pneumatic_69_reference_rhs": ( - config.execution.use_pneumatic_69_reference_rhs - ), - } - closure = system.full_state_closure_from_spec(spec, **closure_kwargs) - state_vector = closure.initial_state_vector() - solver_template = config.execution.solver - segment_templates = ( - SolveIVPConfig( - t_start=solver_template.t_start, - t_stop=nextafter(event_time_s, 0.0), - method=solver_template.method, - rtol=solver_template.rtol, - atol=solver_template.atol, - max_step=solver_template.max_step, - ), - SolveIVPConfig( - t_start=event_time_s, - t_stop=0.040001, - method="Radau", - rtol=solver_template.rtol, - atol=solver_template.atol, - max_step=1.0e-7, - first_step=1.0e-10, - ), - SolveIVPConfig( - t_start=0.040001, - t_stop=0.04001, - method="Radau", - rtol=solver_template.rtol, - atol=solver_template.atol, - max_step=1.0e-6, - ), - SolveIVPConfig( - t_start=0.04001, - t_stop=0.0401, - method="Radau", - rtol=solver_template.rtol, - atol=solver_template.atol, - max_step=1.0e-5, - ), - SolveIVPConfig( - t_start=0.0401, - t_stop=0.041, - method="Radau", - rtol=solver_template.rtol, - atol=solver_template.atol, - max_step=1.0e-5, - ), - SolveIVPConfig( - t_start=0.041, - t_stop=0.048, - method="BDF", - rtol=solver_template.rtol, - atol=solver_template.atol, - max_step=1.0e-5, - ), - SolveIVPConfig( - t_start=0.048, - t_stop=0.05, - method="BDF", - rtol=1.0e-5, - atol=1.0e-8, - max_step=1.0e-5, - ), - ) - segments = tuple( - replace(segment, t_stop=min(segment.t_stop, final_time_s)) - for segment in segment_templates - if segment.t_start < final_time_s - ) - data_paths = ( - "press@pn_c1_8", - "temp@pn_c1_8", - "vol@pn_c1_8", - "mgas1@pn_c1_8", - "p2@pneumatic_69", - "t2@pneumatic_69", - "mgas@pneumatic_69", - "re@pneumatic_69", - "v@pneumatic_69", - "ff@pneumatic_69", - "dm1@pneumatic_69", - f"xv@{orifice_alias}", - f"dm2@{orifice_alias}", - ) - sample_times = tuple( - sorted( - { - sample_time - for sample_time in ( - nextafter(event_time_s, 0.0), - 0.0401, - 0.0402, - 0.041, - 0.042, - 0.045, - 0.048, - final_time_s, - ) - if sample_time <= final_time_s - } - ) - ) - state_vector_by_sample_time: dict[float, list[float]] = {} - segment_diagnostics: list[TestMqlPnvoEventWindowSegmentDiagnostic] = [] - for segment in segments: - rhs_evaluations = 0 - segment_sample_times = tuple( - sample_time - for sample_time in sample_times - if segment.t_start <= sample_time <= segment.t_stop - ) - t_eval = tuple( - sorted({segment.t_start, segment.t_stop, *segment_sample_times}) - ) - - def rhs(time_s, values): - nonlocal rhs_evaluations - rhs_evaluations += 1 - return closure.rhs_at(time_s, values) - - solution = integrate_ode( - rhs=rhs, - initial_state=state_vector, - config=segment, - t_eval=list(t_eval), - ) - segment_diagnostics.append( - TestMqlPnvoEventWindowSegmentDiagnostic( - t_start=segment.t_start, - t_stop=segment.t_stop, - method=segment.method, - rtol=segment.rtol, - atol=segment.atol, - max_step=segment.max_step, - rhs_evaluations=rhs_evaluations, - success=bool(solution.success), - message=str(solution.message), - ) - ) - for sample_time in segment_sample_times: - if sample_time in solution.t: - sample_index = list(solution.t).index(sample_time) - state_vector_by_sample_time[sample_time] = [ - row[sample_index] for row in solution.y - ] - state_vector = [row[-1] for row in solution.y] - if not solution.success: - break - amesim_results = load_test_mql_amesim_results( - config.paths.resolved_amesim_results_archive_path, - time_stop_s=final_time_s, - ) - sample_diagnostics = [] - for sample_time in sample_times: - if sample_time not in state_vector_by_sample_time: - continue - python_sample_values = closure.data_path_values( - time_s=sample_time, - state_vector=state_vector_by_sample_time[sample_time], - data_paths=data_paths, - ) - amesim_sample_values = { - data_path: interpolate_series_value( - amesim_results.times, - amesim_results.series(data_path), - sample_time, - ) - for data_path in data_paths - } - pnl0001_rhs_diagnostics = ( - closure.pnl0001_line_rhs_diagnostic( - line_alias="pneumatic_69", - state_vector=state_vector_by_sample_time[sample_time], - time_s=sample_time, - ), - ) - pnl0001_pressure_loss_diagnostics = ( - _pnl0001_pressure_loss_calibration_diagnostic( - closure=closure, - amesim_results=amesim_results, - python_values_by_data_path=python_sample_values, - line_alias="pneumatic_69", - chamber_alias="pn_c1_8", - time_s=sample_time, - ), - ) - pnvo_flow_parameter_diagnostics = ( - _pnvo_flow_parameter_diagnostic( - closure=closure, - amesim_results=amesim_results, - state_vector=state_vector_by_sample_time[sample_time], - orifice_alias=orifice_alias, - time_s=sample_time, - ), - ) - pnl0001_energy_flow_diagnostics = ( - _pnl0001_energy_flow_diagnostic( - closure=closure, - amesim_results=amesim_results, - state_vector=state_vector_by_sample_time[sample_time], - rhs_diagnostic=pnl0001_rhs_diagnostics[0], - line_alias="pneumatic_69", - chamber_alias="pn_c1_8", - node_alias="pnnode4_16", - time_s=sample_time, - ), - ) - p4_node_enthalpy_breakdown_diagnostics = ( - _p4_node_enthalpy_breakdown_diagnostic( - closure=closure, - amesim_results=amesim_results, - state_vector=state_vector_by_sample_time[sample_time], - node_alias="pnnode4_16", - time_s=sample_time, - ), - ) - pnvo_upstream_enthalpy_diagnostics = ( - _pnvo_upstream_enthalpy_diagnostic( - closure=closure, - amesim_results=amesim_results, - state_vector=state_vector_by_sample_time[sample_time], - orifice_alias=orifice_alias, - upstream_line_alias="pneumatic_87", - downstream_node_alias="pnnode4_16", - time_s=sample_time, - ), - ) - pnl0003_energy_diagnostics = ( - _pnl0003_energy_diagnostic( - closure=closure, - amesim_results=amesim_results, - state_vector=state_vector_by_sample_time[sample_time], - line_alias="pneumatic_87", - time_s=sample_time, - ), - ) - pneumatic_96_inlet_line_diagnostics = ( - _pneumatic_96_inlet_line_diagnostic( - closure=closure, - amesim_results=amesim_results, - state_vector=state_vector_by_sample_time[sample_time], - line_alias="pneumatic_96", - node_alias="pn_node3_8", - orifice_alias="pn_orifice_18", - chamber_alias="pn_general_chamber", - time_s=sample_time, - ), - ) - pnch012_rhs_diagnostics = ( - closure.variable_chamber_rhs_diagnostic( - chamber_alias="pn_c1_8", - state_vector=state_vector_by_sample_time[sample_time], - time_s=sample_time, - ), - ) - pnch012_energy_equation_diagnostics = ( - _pnch012_energy_equation_diagnostic( - closure=closure, - amesim_results=amesim_results, - state_vector=state_vector_by_sample_time[sample_time], - rhs_diagnostic=pnch012_rhs_diagnostics[0], - chamber_alias="pn_c1_8", - piston_alias="pn_brp2_8", - line_alias="pneumatic_69", - time_s=sample_time, - ), - ) - ( - amesim_sample_left_time_s, - amesim_sample_right_time_s, - ) = _series_sample_bracket(times=amesim_results.times, time_s=sample_time) - sample_diagnostics.append( - TestMqlPnvoEventWindowSampleDiagnostic( - time_s=sample_time, - data_paths=data_paths, - python_values_by_data_path=python_sample_values, - amesim_values_by_data_path=amesim_sample_values, - amesim_sample_left_time_s=amesim_sample_left_time_s, - amesim_sample_right_time_s=amesim_sample_right_time_s, - pnl0001_rhs_diagnostics=pnl0001_rhs_diagnostics, - pnl0001_pressure_loss_diagnostics=pnl0001_pressure_loss_diagnostics, - pnvo_flow_parameter_diagnostics=pnvo_flow_parameter_diagnostics, - pnl0001_energy_flow_diagnostics=pnl0001_energy_flow_diagnostics, - p4_node_enthalpy_breakdown_diagnostics=( - p4_node_enthalpy_breakdown_diagnostics - ), - pnvo_upstream_enthalpy_diagnostics=( - pnvo_upstream_enthalpy_diagnostics - ), - pnl0003_energy_diagnostics=pnl0003_energy_diagnostics, - pneumatic_96_inlet_line_diagnostics=( - pneumatic_96_inlet_line_diagnostics - ), - pnch012_energy_equation_diagnostics=( - pnch012_energy_equation_diagnostics - ), - ) - ) - python_values = closure.data_path_values( - time_s=final_time_s, - state_vector=state_vector, - data_paths=data_paths, - ) - amesim_values = { - data_path: interpolate_series_value( - amesim_results.times, - amesim_results.series(data_path), - final_time_s, - ) - for data_path in data_paths - } - return TestMqlPnvoEventWindowDiagnostic( - orifice_alias=orifice_alias, - event_time_s=event_time_s, - final_time_s=final_time_s, - data_paths=data_paths, - segment_diagnostics=tuple(segment_diagnostics), - sample_diagnostics=tuple(sample_diagnostics), - python_values_by_data_path=python_values, - amesim_values_by_data_path=amesim_values, - ) - - - -def _event_window_config_with_pneumatic_96_reference_rhs( - config: TestMqlFullStateComparisonScriptConfig | None, - *, - enabled: bool, -) -> TestMqlFullStateComparisonScriptConfig: - base = config or TestMqlFullStateComparisonScriptConfig() - return replace( - base, - execution=replace( - base.execution, - use_pneumatic_96_reference_rhs=enabled, - ), - ) - - -def _pnvo_event_window_sample_by_time( - diagnostic: TestMqlPnvoEventWindowDiagnostic, -) -> dict[float, TestMqlPnvoEventWindowSampleDiagnostic]: - return {sample.time_s: sample for sample in diagnostic.sample_diagnostics} - - -def _append_metric_comparison( - comparisons: list[TestMqlPnvoEventWindowCandidateMetricComparison], - *, - time_s: float, - metric_name: str, - default_abs_error: float, - candidate_abs_error: float, -) -> None: - comparisons.append( - TestMqlPnvoEventWindowCandidateMetricComparison( - time_s=time_s, - metric_name=metric_name, - default_abs_error=default_abs_error, - candidate_abs_error=candidate_abs_error, - ) - ) - - -def _append_pneumatic_96_candidate_metric_comparisons( - comparisons: list[TestMqlPnvoEventWindowCandidateMetricComparison], - *, - time_s: float, - default_sample: TestMqlPnvoEventWindowSampleDiagnostic, - candidate_sample: TestMqlPnvoEventWindowSampleDiagnostic, -) -> None: - if ( - not default_sample.pneumatic_96_inlet_line_diagnostics - or not candidate_sample.pneumatic_96_inlet_line_diagnostics - ): - return - default_line = default_sample.pneumatic_96_inlet_line_diagnostics[0] - candidate_line = candidate_sample.pneumatic_96_inlet_line_diagnostics[0] - for metric_name, default_value, candidate_value in ( - ( - "p96:p2_abs_error_pa", - default_line.python_line_pressure_error_pa, - candidate_line.python_line_pressure_error_pa, - ), - ( - "p96:t2_abs_error_k", - default_line.python_line_temperature_error_k, - candidate_line.python_line_temperature_error_k, - ), - ( - "p96:storage_dm_abs_error_g_s", - default_line.python_storage_mass_derivative_error_g_s, - candidate_line.python_storage_mass_derivative_error_g_s, - ), - ( - "p96:active_dT_abs_error_k_s", - default_line.python_active_temperature_derivative_error_k_s, - candidate_line.python_active_temperature_derivative_error_k_s, - ), - ( - "p96:active_dP_abs_error_pa_s", - default_line.python_active_pressure_derivative_eos_error_pa_s, - candidate_line.python_active_pressure_derivative_eos_error_pa_s, - ), - ): - _append_metric_comparison( - comparisons, - time_s=time_s, - metric_name=metric_name, - default_abs_error=abs(default_value), - candidate_abs_error=abs(candidate_value), - ) - - -def _append_pneumatic_87_candidate_metric_comparisons( - comparisons: list[TestMqlPnvoEventWindowCandidateMetricComparison], - *, - time_s: float, - default_sample: TestMqlPnvoEventWindowSampleDiagnostic, - candidate_sample: TestMqlPnvoEventWindowSampleDiagnostic, -) -> None: - if ( - not default_sample.pnl0003_energy_diagnostics - or not candidate_sample.pnl0003_energy_diagnostics - ): - return - default_line = default_sample.pnl0003_energy_diagnostics[0] - candidate_line = candidate_sample.pnl0003_energy_diagnostics[0] - for metric_name, default_value, candidate_value in ( - ( - "p87:node_to_line_dm_abs_error_g_s", - default_line.python_pn3_node_to_line_mass_flow_error_g_s, - candidate_line.python_pn3_node_to_line_mass_flow_error_g_s, - ), - ( - "p87:center_dm_abs_error_g_s", - default_line.amesim_center_flow_python_sign_error_g_s, - candidate_line.amesim_center_flow_python_sign_error_g_s, - ), - ( - "p87:sdm1_abs_error_g_s", - default_line.python_port_1_storage_mass_derivative_error_g_s, - candidate_line.python_port_1_storage_mass_derivative_error_g_s, - ), - ( - "p87:sdm2_abs_error_g_s", - default_line.python_port_2_storage_mass_derivative_error_g_s, - candidate_line.python_port_2_storage_mass_derivative_error_g_s, - ), - ( - "p87:t2_abs_error_k", - default_line.python_port_2_temperature_error_to_amesim_k, - candidate_line.python_port_2_temperature_error_to_amesim_k, - ), - ): - _append_metric_comparison( - comparisons, - time_s=time_s, - metric_name=metric_name, - default_abs_error=abs(default_value), - candidate_abs_error=abs(candidate_value), - ) - - -def _pnvo_event_window_candidate_metric_comparisons( - default_diagnostic: TestMqlPnvoEventWindowDiagnostic, - candidate_diagnostic: TestMqlPnvoEventWindowDiagnostic, -) -> tuple[TestMqlPnvoEventWindowCandidateMetricComparison, ...]: - candidate_samples = _pnvo_event_window_sample_by_time(candidate_diagnostic) - comparisons: list[TestMqlPnvoEventWindowCandidateMetricComparison] = [] - for default_sample in default_diagnostic.sample_diagnostics: - candidate_sample = candidate_samples.get(default_sample.time_s) - if candidate_sample is None: - continue - for data_path in default_sample.data_paths: - _append_metric_comparison( - comparisons, - time_s=default_sample.time_s, - metric_name=f"path:{data_path}", - default_abs_error=default_sample.abs_error(data_path), - candidate_abs_error=candidate_sample.abs_error(data_path), - ) - _append_pneumatic_96_candidate_metric_comparisons( - comparisons, - time_s=default_sample.time_s, - default_sample=default_sample, - candidate_sample=candidate_sample, - ) - _append_pneumatic_87_candidate_metric_comparisons( - comparisons, - time_s=default_sample.time_s, - default_sample=default_sample, - candidate_sample=candidate_sample, - ) - return tuple(comparisons) - - -def run_test_mql_pnvo_event_window_candidate_comparison( - config: TestMqlFullStateComparisonScriptConfig | None = None, - *, - orifice_alias: str = "pn_morifice_1", - final_time_s: float = 0.041, -) -> TestMqlPnvoEventWindowCandidateComparisonDiagnostic: - default_diagnostic = run_test_mql_pnvo_event_window_diagnostic( - _event_window_config_with_pneumatic_96_reference_rhs( - config, - enabled=False, - ), - orifice_alias=orifice_alias, - final_time_s=final_time_s, - ) - candidate_diagnostic = run_test_mql_pnvo_event_window_diagnostic( - _event_window_config_with_pneumatic_96_reference_rhs( - config, - enabled=True, - ), - orifice_alias=orifice_alias, - final_time_s=final_time_s, - ) - return TestMqlPnvoEventWindowCandidateComparisonDiagnostic( - orifice_alias=orifice_alias, - event_time_s=default_diagnostic.event_time_s, - final_time_s=default_diagnostic.final_time_s, - default_diagnostic=default_diagnostic, - candidate_diagnostic=candidate_diagnostic, - metric_comparisons=_pnvo_event_window_candidate_metric_comparisons( - default_diagnostic, - candidate_diagnostic, - ), - ) - - -def format_test_mql_pnvo_event_window_summary( - diagnostic: TestMqlPnvoEventWindowDiagnostic, -) -> str: - lines = [ - "Model: test_mql", - f"Mode: PNVO event window diagnostic ({diagnostic.orifice_alias})", - f"Event time: {diagnostic.event_time_s}", - f"Final time: {diagnostic.final_time_s}", - "Segments:", - ] - for segment in diagnostic.segment_diagnostics: - lines.append( - f" - {segment.t_start} -> {segment.t_stop}: " - f"method={segment.method}, rtol={segment.rtol}, " - f"atol={segment.atol}, max_step={segment.max_step}, " - f"rhs={segment.rhs_evaluations}, success={segment.success}" - ) - if diagnostic.sample_diagnostics: - lines.append("Sample comparisons:") - for sample in diagnostic.sample_diagnostics: - if sample.amesim_values_are_interpolated: - amesim_sample_source = ( - "linear-interpolation@" - f"{sample.amesim_sample_left_time_s}" - f"..{sample.amesim_sample_right_time_s}" - ) - else: - amesim_sample_source = f"exact@{sample.amesim_sample_left_time_s}" - lines.append( - f" t={sample.time_s} (amesim={amesim_sample_source})" - ) - for data_path in sample.data_paths: - lines.append( - f" - {data_path}: " - f"python={sample.python_values_by_data_path[data_path]}, " - f"amesim={sample.amesim_values_by_data_path[data_path]}, " - f"abs_error={sample.abs_error(data_path)}" - ) - for rhs in sample.pnl0001_rhs_diagnostics: - lines.append( - f" - rhs@{rhs.line_alias}: " - f"chamber_to_line={rhs.chamber_to_line_flow_kg_s}, " - f"node_to_line={rhs.node_to_line_flow_kg_s}, " - f"dm_dt={rhs.mass_derivative_kg_s}, " - f"dU_dt={rhs.energy_derivative_w}" - ) - for pressure_loss in sample.pnl0001_pressure_loss_diagnostics: - lines.append( - f" - pressure_loss@{pressure_loss.line_alias}: " - f"cm={pressure_loss.amesim_cm}, " - f"amesim_dm1={pressure_loss.amesim_dm1_g_s}, " - f"amesim_dp={pressure_loss.amesim_pressure_drop_pa}, " - f"darcy_dp={pressure_loss.current_darcy_pressure_drop_pa}, " - f"dp_multiplier={pressure_loss.pressure_drop_multiplier}, " - f"candidate_pn2pipefr_dm1=" - f"{pressure_loss.candidate_pn2pipefr_dm1_g_s}, " - f"candidate_pn2pipefr_ratio=" - f"{pressure_loss.candidate_pn2pipefr_to_amesim_dm1_ratio}, " - f"amesim_linear_k=" - f"{pressure_loss.amesim_linear_conductance_kg_s_sqrt_k_per_pa}, " - f"python_linear_k=" - f"{pressure_loss.python_linear_conductance_kg_s_sqrt_k_per_pa}, " - f"linear_k_ratio=" - f"{pressure_loss.python_to_amesim_linear_conductance_ratio}" - ) - for flow_parameter in sample.pnvo_flow_parameter_diagnostics: - lines.append( - f" - flow_parameter@{flow_parameter.orifice_alias}: " - f"amesim_cm={flow_parameter.amesim_cm}, " - f"python_cm={flow_parameter.python_cm}, " - f"cm_ratio={flow_parameter.python_to_amesim_cm_ratio}, " - f"amesim_dm2={flow_parameter.amesim_dm2_g_s}, " - f"python_m={flow_parameter.python_mass_flow_kg_s}, " - f"opening={flow_parameter.python_opening}, " - f"effective_area={flow_parameter.python_effective_area_m2}, " - f"upstream_p={flow_parameter.python_upstream_pressure_pa}, " - f"upstream_t={flow_parameter.python_upstream_temperature_k}, " - f"amesim_gasvel={flow_parameter.amesim_gas_velocity_m_s}" - ) - for pnvo_enthalpy in sample.pnvo_upstream_enthalpy_diagnostics: - lines.append( - f" - pnvo_upstream_enthalpy@{pnvo_enthalpy.orifice_alias}: " - f"amesim_dh2=" - f"{pnvo_enthalpy.amesim_orifice_port_2_enthalpy_flow_w}, " - f"amesim_dm2=" - f"{pnvo_enthalpy.amesim_orifice_port_2_mass_flow_g_s}, " - f"amesim_dh3=" - f"{pnvo_enthalpy.amesim_orifice_port_3_enthalpy_flow_w}, " - f"amesim_dm3=" - f"{pnvo_enthalpy.amesim_orifice_port_3_mass_flow_g_s}, " - f"amesim_line_dhctr=" - f"{pnvo_enthalpy.amesim_upstream_line_center_enthalpy_flow_w}, " - f"amesim_line_dmctr=" - f"{pnvo_enthalpy.amesim_upstream_line_center_mass_flow_g_s}, " - f"amesim_line_t2=" - f"{pnvo_enthalpy.amesim_upstream_line_port_2_temperature_k}, " - f"amesim_line_p2_abs=" - f"{pnvo_enthalpy.amesim_upstream_line_port_2_pressure_pa}, " - f"amesim_implied_t2=" - f"{pnvo_enthalpy.amesim_orifice_port_2_implied_reference_temperature_k}, " - f"amesim_implied_t2_delta=" - f"{pnvo_enthalpy.amesim_orifice_port_2_implied_temperature_delta_to_line_k}, " - f"amesim_ref_dh2=" - f"{pnvo_enthalpy.amesim_upstream_line_port_2_reference_enthalpy_flow_w}, " - f"amesim_ref_dh2_error=" - f"{pnvo_enthalpy.amesim_upstream_line_port_2_reference_enthalpy_error_w}, " - f"amesim_real_ref_dh2=" - f"{pnvo_enthalpy.amesim_upstream_line_port_2_real_gas_reference_enthalpy_flow_w}, " - f"amesim_real_ref_dh2_error=" - f"{pnvo_enthalpy.amesim_upstream_line_port_2_real_gas_reference_enthalpy_error_w}, " - f"python_flow=" - f"{pnvo_enthalpy.python_orifice_to_node_flow_kg_s}, " - f"python_line_t2=" - f"{pnvo_enthalpy.python_upstream_line_port_2_temperature_k}, " - f"python_line_p2_abs=" - f"{pnvo_enthalpy.python_upstream_line_port_2_pressure_pa}, " - f"python_current_dh2=" - f"{pnvo_enthalpy.python_current_port_2_enthalpy_flow_w}, " - f"python_ref_dh2=" - f"{pnvo_enthalpy.python_reference_port_2_enthalpy_flow_w}, " - f"python_real_ref_dh2=" - f"{pnvo_enthalpy.python_real_gas_reference_port_2_enthalpy_flow_w}, " - f"python_current_dh2_error=" - f"{pnvo_enthalpy.python_current_port_2_enthalpy_error_w}, " - f"python_ref_dh2_error=" - f"{pnvo_enthalpy.python_reference_port_2_enthalpy_error_w}, " - f"python_real_ref_dh2_error=" - f"{pnvo_enthalpy.python_real_gas_reference_port_2_enthalpy_error_w}" - ) - for inlet_line in sample.pneumatic_96_inlet_line_diagnostics: - lines.append( - f" - inlet_line@{inlet_line.line_alias}: " - f"node={inlet_line.node_alias}, " - f"orifice={inlet_line.orifice_alias}, " - f"chamber={inlet_line.chamber_alias}, " - f"amesim_p2_abs={inlet_line.amesim_line_pressure_pa}, " - f"amesim_t2={inlet_line.amesim_line_temperature_k}, " - f"amesim_mgas={inlet_line.amesim_line_mass_g}, " - f"amesim_fd_dmgas={inlet_line.amesim_line_mass_derivative_fd_g_s}, " - f"amesim_line_to_node_dm1={inlet_line.amesim_line_to_node_mass_flow_g_s}, " - f"amesim_orifice_to_line_dm2={inlet_line.amesim_orifice_to_line_mass_flow_g_s}, " - f"amesim_storage_dm_ports={inlet_line.amesim_storage_mass_derivative_from_ports_g_s}, " - f"amesim_storage_dm_residual={inlet_line.amesim_storage_mass_derivative_residual_g_s}, " - f"amesim_chamber_p_abs={inlet_line.amesim_chamber_pressure_pa}, " - f"amesim_chamber_t={inlet_line.amesim_chamber_temperature_k}, " - f"python_p2_abs={inlet_line.python_line_pressure_pa}, " - f"python_t2={inlet_line.python_line_temperature_k}, " - f"python_mgas={inlet_line.python_line_mass_g}, " - f"python_p2_error={inlet_line.python_line_pressure_error_pa}, " - f"python_t2_error={inlet_line.python_line_temperature_error_k}, " - f"python_mgas_error={inlet_line.python_line_mass_error_g}, " - f"python_line_to_node_dm={inlet_line.python_line_to_node_mass_flow_g_s}, " - f"python_line_to_node_dm_error={inlet_line.python_line_to_node_mass_flow_error_g_s}, " - f"python_orifice_to_line_dm={inlet_line.python_orifice_to_line_mass_flow_g_s}, " - f"python_orifice_to_line_dm_error={inlet_line.python_orifice_to_line_mass_flow_error_g_s}, " - f"python_storage_dm={inlet_line.python_storage_mass_derivative_g_s}, " - f"python_storage_dm_error={inlet_line.python_storage_mass_derivative_error_g_s}, " - f"amesim_node_p_abs={inlet_line.amesim_node_pressure_pa}, " - f"amesim_node_t={inlet_line.amesim_node_temperature_k}, " - f"python_line_to_node_amesim_node_dm={inlet_line.python_line_to_node_amesim_node_mass_flow_g_s}, " - f"python_line_to_node_amesim_node_dm_error={inlet_line.python_line_to_node_amesim_node_mass_flow_error_g_s}, " - f"python_line_to_node_amesim_line_dm={inlet_line.python_line_to_node_amesim_line_mass_flow_g_s}, " - f"python_line_to_node_amesim_line_dm_error={inlet_line.python_line_to_node_amesim_line_mass_flow_error_g_s}, " - f"python_line_to_node_amesim_node_line_dm={inlet_line.python_line_to_node_amesim_node_line_mass_flow_g_s}, " - f"python_line_to_node_amesim_node_line_dm_error={inlet_line.python_line_to_node_amesim_node_line_mass_flow_error_g_s}, " - f"python_orifice_to_line_amesim_line_dm={inlet_line.python_orifice_to_line_amesim_line_mass_flow_g_s}, " - f"python_orifice_to_line_amesim_line_dm_error={inlet_line.python_orifice_to_line_amesim_line_mass_flow_error_g_s}, " - f"python_orifice_to_line_amesim_chamber_dm={inlet_line.python_orifice_to_line_amesim_chamber_mass_flow_g_s}, " - f"python_orifice_to_line_amesim_chamber_dm_error={inlet_line.python_orifice_to_line_amesim_chamber_mass_flow_error_g_s}, " - f"python_orifice_to_line_amesim_line_chamber_dm={inlet_line.python_orifice_to_line_amesim_line_chamber_mass_flow_g_s}, " - f"python_orifice_to_line_amesim_line_chamber_dm_error={inlet_line.python_orifice_to_line_amesim_line_chamber_mass_flow_error_g_s}, " - f"python_storage_amesim_boundary_states_dm={inlet_line.python_storage_amesim_boundary_states_mass_derivative_g_s}, " - f"python_storage_amesim_boundary_states_dm_error={inlet_line.python_storage_amesim_boundary_states_mass_derivative_error_g_s}, " - f"python_chamber_p_abs={inlet_line.python_chamber_pressure_pa}, " - f"python_chamber_t={inlet_line.python_chamber_temperature_k}, " - f"amesim_fd_dT={inlet_line.amesim_line_temperature_derivative_fd_k_s}, " - f"amesim_dT_p1_h={inlet_line.amesim_port_1_enthalpy_dtemp_component_k_s}, " - f"amesim_dT_p2_h={inlet_line.amesim_port_2_enthalpy_dtemp_component_k_s}, " - f"amesim_dT_m_offset={inlet_line.amesim_mass_offset_dtemp_component_k_s}, " - f"amesim_ref_dT_sum={inlet_line.amesim_reference_temperature_derivative_sum_k_s}, " - f"amesim_ref_dT_residual={inlet_line.amesim_reference_temperature_derivative_residual_k_s}, " - f"python_current_dT={inlet_line.python_current_temperature_derivative_k_s}, " - f"python_dT_p1={inlet_line.python_port_1_current_dtemp_component_k_s}, " - f"python_dT_p2={inlet_line.python_port_2_current_dtemp_component_k_s}, " - f"python_dT_heat={inlet_line.python_heat_dtemp_component_k_s}, " - f"python_dT_error={inlet_line.python_temperature_derivative_error_k_s}, " - f"amesim_fd_dP={inlet_line.amesim_line_pressure_derivative_fd_pa_s}, " - f"amesim_dP_m={inlet_line.amesim_pressure_derivative_mass_component_pa_s}, " - f"amesim_dP_T={inlet_line.amesim_pressure_derivative_temperature_component_pa_s}, " - f"amesim_dP_sum={inlet_line.amesim_pressure_derivative_eos_sum_pa_s}, " - f"amesim_dP_residual={inlet_line.amesim_pressure_derivative_eos_residual_pa_s}, " - f"python_dP_m={inlet_line.python_pressure_derivative_mass_component_pa_s}, " - f"python_dP_T={inlet_line.python_pressure_derivative_temperature_component_pa_s}, " - f"python_dP_sum={inlet_line.python_pressure_derivative_eos_sum_pa_s}, " - f"python_dP_error={inlet_line.python_pressure_derivative_eos_error_pa_s}, " - f"python_ref_dh1={inlet_line.python_reference_port_1_enthalpy_flow_w}, " - f"python_ref_dh2={inlet_line.python_reference_port_2_enthalpy_flow_w}, " - f"python_ref_dT_p1={inlet_line.python_reference_port_1_dtemp_component_k_s}, " - f"python_ref_dT_p2={inlet_line.python_reference_port_2_dtemp_component_k_s}, " - f"python_ref_dT_m_offset={inlet_line.python_reference_mass_offset_dtemp_component_k_s}, " - f"python_ref_dT={inlet_line.python_reference_temperature_derivative_k_s}, " - f"python_ref_dT_error={inlet_line.python_reference_temperature_derivative_error_k_s}, " - f"python_ref_dP_m={inlet_line.python_reference_pressure_derivative_mass_component_pa_s}, " - f"python_ref_dP_T={inlet_line.python_reference_pressure_derivative_temperature_component_pa_s}, " - f"python_ref_dP_sum={inlet_line.python_reference_pressure_derivative_eos_sum_pa_s}, " - f"python_ref_dP_error={inlet_line.python_reference_pressure_derivative_eos_error_pa_s}, " - f"python_ref_amesim_storage_dT_m_offset={inlet_line.python_reference_amesim_storage_mass_offset_dtemp_component_k_s}, " - f"python_ref_amesim_storage_dT={inlet_line.python_reference_amesim_storage_temperature_derivative_k_s}, " - f"python_ref_amesim_storage_dT_error={inlet_line.python_reference_amesim_storage_temperature_derivative_error_k_s}, " - f"python_ref_amesim_storage_dP_m={inlet_line.python_reference_amesim_storage_pressure_derivative_mass_component_pa_s}, " - f"python_ref_amesim_storage_dP_T={inlet_line.python_reference_amesim_storage_pressure_derivative_temperature_component_pa_s}, " - f"python_ref_amesim_storage_dP_sum={inlet_line.python_reference_amesim_storage_pressure_derivative_eos_sum_pa_s}, " - f"python_ref_amesim_storage_dP_error={inlet_line.python_reference_amesim_storage_pressure_derivative_eos_error_pa_s}, " - f"python_active_dT={inlet_line.python_active_temperature_derivative_k_s}, " - f"python_active_dT_error={inlet_line.python_active_temperature_derivative_error_k_s}, " - f"python_active_dP_sum={inlet_line.python_active_pressure_derivative_eos_sum_pa_s}, " - f"python_active_dP_error={inlet_line.python_active_pressure_derivative_eos_error_pa_s}, " - f"python_chamber_p_error={inlet_line.python_chamber_pressure_error_pa}, " - f"python_chamber_t_error={inlet_line.python_chamber_temperature_error_k}" - ) - for pnl0003_energy in sample.pnl0003_energy_diagnostics: - lines.append( - f" - pnl0003_energy@{pnl0003_energy.line_alias}: " - f"amesim_t1={pnl0003_energy.amesim_port_1_temperature_k}, " - f"amesim_t2={pnl0003_energy.amesim_port_2_temperature_k}, " - f"amesim_p1_abs={pnl0003_energy.amesim_port_1_pressure_pa}, " - f"amesim_p2_abs={pnl0003_energy.amesim_port_2_pressure_pa}, " - f"amesim_dm1={pnl0003_energy.amesim_port_1_mass_flow_g_s}, " - f"amesim_dh1={pnl0003_energy.amesim_port_1_enthalpy_flow_w}, " - f"amesim_dmctr={pnl0003_energy.amesim_center_mass_flow_g_s}, " - f"amesim_dhctr={pnl0003_energy.amesim_center_enthalpy_flow_w}, " - f"amesim_dm2={pnl0003_energy.amesim_port_2_mass_flow_g_s}, " - f"amesim_dh2={pnl0003_energy.amesim_port_2_enthalpy_flow_w}, " - f"amesim_sdm1={pnl0003_energy.amesim_port_1_storage_mass_derivative_g_s}, " - f"amesim_sdh1={pnl0003_energy.amesim_port_1_storage_enthalpy_sum_w}, " - f"amesim_sdm2={pnl0003_energy.amesim_port_2_storage_mass_derivative_g_s}, " - f"amesim_sdh2={pnl0003_energy.amesim_port_2_storage_enthalpy_sum_w}, " - f"python_node_dm1={pnl0003_energy.python_pn3_node_port_1_mass_flow_g_s}, " - f"python_node_dm3={pnl0003_energy.python_pn3_node_port_3_mass_flow_g_s}, " - f"python_node_to_line_dm={pnl0003_energy.python_pn3_node_to_line_mass_flow_g_s}, " - f"python_node_to_line_dm_error={pnl0003_energy.python_pn3_node_to_line_mass_flow_error_g_s}, " - f"amesim_m1={pnl0003_energy.amesim_port_1_mass_estimate_g}, " - f"amesim_m2={pnl0003_energy.amesim_port_2_mass_estimate_g}, " - f"python_m1={pnl0003_energy.python_port_1_mass_g}, " - f"python_m2={pnl0003_energy.python_port_2_mass_g}, " - f"python_m1_error={pnl0003_energy.python_port_1_mass_error_to_amesim_g}, " - f"python_m2_error={pnl0003_energy.python_port_2_mass_error_to_amesim_g}, " - f"amesim_fd_dmgas={pnl0003_energy.amesim_total_mass_derivative_fd_g_s}, " - f"amesim_back_dmgas={pnl0003_energy.amesim_total_mass_derivative_backward_g_s}, " - f"amesim_fwd_dmgas={pnl0003_energy.amesim_total_mass_derivative_forward_g_s}, " - f"amesim_storage_dmgas={pnl0003_energy.amesim_total_storage_mass_derivative_g_s}, " - f"amesim_storage_dmgas_residual={pnl0003_energy.amesim_total_storage_mass_derivative_residual_g_s}, " - f"amesim_dmctr_py_sign={pnl0003_energy.amesim_center_flow_python_sign_equivalent_g_s}, " - f"python_center_dm={pnl0003_energy.python_center_mass_flow_g_s}, " - f"python_center_dm_error={pnl0003_energy.amesim_center_flow_python_sign_error_g_s}, " - f"amesim_sdm1_center={pnl0003_energy.amesim_port_1_center_mass_derivative_g_s}, " - f"amesim_sdm2_center={pnl0003_energy.amesim_port_2_center_mass_derivative_g_s}, " - f"python_sdm1_ext={pnl0003_energy.python_port_1_external_mass_flow_g_s}, " - f"python_sdm1_ext_error={pnl0003_energy.python_port_1_external_mass_flow_error_g_s}, " - f"python_sdm1_center={pnl0003_energy.python_port_1_center_mass_derivative_g_s}, " - f"python_sdm1_center_error={pnl0003_energy.python_port_1_center_mass_derivative_error_g_s}, " - f"python_sdm1={pnl0003_energy.python_port_1_storage_mass_derivative_g_s}, " - f"python_sdm1_error={pnl0003_energy.python_port_1_storage_mass_derivative_error_g_s}, " - f"python_sdm2_ext={pnl0003_energy.python_port_2_external_mass_flow_g_s}, " - f"python_sdm2_ext_error={pnl0003_energy.python_port_2_external_mass_flow_error_g_s}, " - f"python_sdm2_center={pnl0003_energy.python_port_2_center_mass_derivative_g_s}, " - f"python_sdm2_center_error={pnl0003_energy.python_port_2_center_mass_derivative_error_g_s}, " - f"python_sdm2={pnl0003_energy.python_port_2_storage_mass_derivative_g_s}, " - f"python_sdm2_error={pnl0003_energy.python_port_2_storage_mass_derivative_error_g_s}, " - f"python_amesim_state_darcy_center_dm=" - f"{pnl0003_energy.python_amesim_state_darcy_center_mass_flow_g_s}, " - f"python_amesim_state_darcy_center_dm_error=" - f"{pnl0003_energy.python_amesim_state_darcy_center_mass_flow_error_g_s}, " - f"python_amesim_state_pn2pipefr_center_dm=" - f"{pnl0003_energy.python_amesim_state_pn2pipefr_center_mass_flow_g_s}, " - f"python_amesim_state_pn2pipefr_center_dm_error=" - f"{pnl0003_energy.python_amesim_state_pn2pipefr_center_mass_flow_error_g_s}, " - f"amesim_center_real_p1=" - f"{pnl0003_energy.amesim_center_real_gas_reference_port_1_estimate_w}, " - f"amesim_center_real_p1_error=" - f"{pnl0003_energy.amesim_center_real_gas_reference_port_1_error_w}, " - f"python_center_ideal_p1=" - f"{pnl0003_energy.python_center_ideal_reference_port_1_estimate_w}, " - f"python_center_ideal_p1_error=" - f"{pnl0003_energy.python_center_ideal_reference_port_1_error_w}, " - f"python_center_ideal_p2=" - f"{pnl0003_energy.python_center_ideal_reference_port_2_estimate_w}, " - f"python_center_ideal_p2_error=" - f"{pnl0003_energy.python_center_ideal_reference_port_2_error_w}, " - f"python_center_real_p1=" - f"{pnl0003_energy.python_center_real_gas_reference_port_1_estimate_w}, " - f"python_center_real_p1_error=" - f"{pnl0003_energy.python_center_real_gas_reference_port_1_error_w}, " - f"python_center_real_p2=" - f"{pnl0003_energy.python_center_real_gas_reference_port_2_estimate_w}, " - f"python_center_real_p2_error=" - f"{pnl0003_energy.python_center_real_gas_reference_port_2_error_w}, " - f"python_dmgas_dt={pnl0003_energy.python_total_mass_derivative_kg_s}, " - f"python_ref_ext1={pnl0003_energy.python_port_1_reference_external_enthalpy_flow_w}, " - f"python_ref_ctr1={pnl0003_energy.python_port_1_reference_center_enthalpy_flow_w}, " - f"python_ref_sdh1={pnl0003_energy.python_port_1_reference_storage_enthalpy_sum_w}, " - f"python_ref_sdh1_error={pnl0003_energy.python_port_1_reference_storage_enthalpy_error_w}, " - f"python_ref_ext2={pnl0003_energy.python_port_2_reference_external_enthalpy_flow_w}, " - f"python_ref_ctr2={pnl0003_energy.python_port_2_reference_center_enthalpy_flow_w}, " - f"python_ref_sdh2={pnl0003_energy.python_port_2_reference_storage_enthalpy_sum_w}, " - f"python_ref_sdh2_error={pnl0003_energy.python_port_2_reference_storage_enthalpy_error_w}, " - f"amesim_dT1_ext_h={pnl0003_energy.amesim_port_1_external_enthalpy_dtemp_component_k_s}, " - f"amesim_dT1_ctr_h={pnl0003_energy.amesim_port_1_center_enthalpy_dtemp_component_k_s}, " - f"amesim_dT1_m_offset={pnl0003_energy.amesim_port_1_mass_offset_dtemp_component_k_s}, " - f"amesim_dT1_heat={pnl0003_energy.amesim_port_1_heat_dtemp_component_k_s}, " - f"python_dT1_ext_h={pnl0003_energy.python_port_1_external_enthalpy_dtemp_component_k_s}, " - f"python_dT1_ctr_h={pnl0003_energy.python_port_1_center_enthalpy_dtemp_component_k_s}, " - f"python_dT1_m_offset={pnl0003_energy.python_port_1_mass_offset_dtemp_component_k_s}, " - f"python_dT1_heat={pnl0003_energy.python_port_1_heat_dtemp_component_k_s}, " - f"amesim_dT2_ext_h={pnl0003_energy.amesim_port_2_external_enthalpy_dtemp_component_k_s}, " - f"amesim_dT2_ctr_h={pnl0003_energy.amesim_port_2_center_enthalpy_dtemp_component_k_s}, " - f"amesim_dT2_m_offset={pnl0003_energy.amesim_port_2_mass_offset_dtemp_component_k_s}, " - f"amesim_dT2_heat={pnl0003_energy.amesim_port_2_heat_dtemp_component_k_s}, " - f"python_dT2_ext_h={pnl0003_energy.python_port_2_external_enthalpy_dtemp_component_k_s}, " - f"python_dT2_ctr_h={pnl0003_energy.python_port_2_center_enthalpy_dtemp_component_k_s}, " - f"python_dT2_m_offset={pnl0003_energy.python_port_2_mass_offset_dtemp_component_k_s}, " - f"python_dT2_heat={pnl0003_energy.python_port_2_heat_dtemp_component_k_s}, " - f"amesim_fd_dT1={pnl0003_energy.amesim_port_1_temperature_derivative_fd_k_s}, " - f"amesim_back_dT1={pnl0003_energy.amesim_port_1_temperature_derivative_backward_k_s}, " - f"amesim_fwd_dT1={pnl0003_energy.amesim_port_1_temperature_derivative_forward_k_s}, " - f"amesim_fd_dT2={pnl0003_energy.amesim_port_2_temperature_derivative_fd_k_s}, " - f"amesim_back_dT2={pnl0003_energy.amesim_port_2_temperature_derivative_backward_k_s}, " - f"amesim_fwd_dT2={pnl0003_energy.amesim_port_2_temperature_derivative_forward_k_s}, " - f"amesim_pn2vol_dT1={pnl0003_energy.amesim_port_1_pn2vol_dtemp_k_s}, " - f"amesim_pn2vol_dT2={pnl0003_energy.amesim_port_2_pn2vol_dtemp_k_s}, " - f"amesim_pn2vol_dT1_residual={pnl0003_energy.amesim_port_1_pn2vol_dtemp_residual_k_s}, " - f"amesim_pn2vol_dT2_residual={pnl0003_energy.amesim_port_2_pn2vol_dtemp_residual_k_s}, " - f"amesim_fd_dP1={pnl0003_energy.amesim_port_1_pressure_derivative_fd_pa_s}, " - f"amesim_fd_dP2={pnl0003_energy.amesim_port_2_pressure_derivative_fd_pa_s}, " - f"amesim_dP1_m={pnl0003_energy.amesim_port_1_pressure_derivative_mass_component_pa_s}, " - f"amesim_dP1_T={pnl0003_energy.amesim_port_1_pressure_derivative_temperature_component_pa_s}, " - f"amesim_dP1_sum={pnl0003_energy.amesim_port_1_pressure_derivative_eos_sum_pa_s}, " - f"amesim_dP1_residual={pnl0003_energy.amesim_port_1_pressure_derivative_eos_residual_pa_s}, " - f"amesim_dP2_m={pnl0003_energy.amesim_port_2_pressure_derivative_mass_component_pa_s}, " - f"amesim_dP2_T={pnl0003_energy.amesim_port_2_pressure_derivative_temperature_component_pa_s}, " - f"amesim_dP2_sum={pnl0003_energy.amesim_port_2_pressure_derivative_eos_sum_pa_s}, " - f"amesim_dP2_residual={pnl0003_energy.amesim_port_2_pressure_derivative_eos_residual_pa_s}, " - f"python_dP1_m={pnl0003_energy.python_port_1_pressure_derivative_mass_component_pa_s}, " - f"python_dP1_T={pnl0003_energy.python_port_1_pressure_derivative_temperature_component_pa_s}, " - f"python_dP1_sum={pnl0003_energy.python_port_1_pressure_derivative_eos_sum_pa_s}, " - f"python_dP2_m={pnl0003_energy.python_port_2_pressure_derivative_mass_component_pa_s}, " - f"python_dP2_T={pnl0003_energy.python_port_2_pressure_derivative_temperature_component_pa_s}, " - f"python_dP2_sum={pnl0003_energy.python_port_2_pressure_derivative_eos_sum_pa_s}, " - f"python_t1={pnl0003_energy.python_port_1_temperature_k}, " - f"python_t2={pnl0003_energy.python_port_2_temperature_k}, " - f"python_p1_abs={pnl0003_energy.python_port_1_pressure_pa}, " - f"python_p2_abs={pnl0003_energy.python_port_2_pressure_pa}, " - f"python_dm1_dt={pnl0003_energy.python_port_1_mass_derivative_kg_s}, " - f"python_dm2_dt={pnl0003_energy.python_port_2_mass_derivative_kg_s}, " - f"python_current_dT1={pnl0003_energy.python_port_1_current_dtemp_k_s}, " - f"python_current_dT2={pnl0003_energy.python_port_2_current_dtemp_k_s}, " - f"python_real_ref_dT1={pnl0003_energy.python_port_1_real_gas_reference_dtemp_k_s}, " - f"python_real_ref_dT2={pnl0003_energy.python_port_2_real_gas_reference_dtemp_k_s}, " - f"python_amesim_center_dT2=" - f"{pnl0003_energy.python_port_2_amesim_center_counterfactual_dtemp_k_s}, " - f"python_amesim_ext_dT2=" - f"{pnl0003_energy.python_port_2_amesim_external_counterfactual_dtemp_k_s}, " - f"python_amesim_ext_ctr_dT2=" - f"{pnl0003_energy.python_port_2_amesim_external_center_counterfactual_dtemp_k_s}, " - f"python_t2_error={pnl0003_energy.python_port_2_temperature_error_to_amesim_k}" - ) - for energy_flow in sample.pnl0001_energy_flow_diagnostics: - lines.append( - f" - energy_flow@{energy_flow.line_alias}: " - f"amesim_dh1={energy_flow.amesim_port_1_enthalpy_flow_w}, " - f"amesim_dm1={energy_flow.amesim_port_1_mass_flow_g_s}, " - f"amesim_node_dh2={energy_flow.amesim_node_port_2_enthalpy_flow_w}, " - f"amesim_node_dm2={energy_flow.amesim_node_port_2_mass_flow_g_s}, " - f"amesim_observed_sdh=" - f"{energy_flow.amesim_observed_port_enthalpy_sum_w}, " - f"python_port1_u=" - f"{energy_flow.python_port_1_internal_energy_flow_w}, " - f"python_port2_u=" - f"{energy_flow.python_port_2_internal_energy_flow_w}, " - f"python_dU_dt={energy_flow.python_current_energy_derivative_w}, " - f"direct_h_dU_dt=" - f"{energy_flow.python_direct_enthalpy_energy_derivative_w}, " - f"direct_minus_current=" - f"{energy_flow.python_direct_minus_current_energy_derivative_w}, " - f"python_p4_node_dh2=" - f"{energy_flow.python_p4_node_port_2_enthalpy_flow_w}, " - f"python_p4_node_dm2=" - f"{energy_flow.python_p4_node_port_2_mass_flow_g_s}, " - f"python_p4_node_h2=" - f"{energy_flow.python_p4_node_port_2_connected_h_j_kg}, " - f"python_p4_node_t2=" - f"{energy_flow.python_p4_node_port_2_connected_temperature_k}, " - f"python_p4_node_h2_delta=" - f"{energy_flow.python_p4_node_port_2_connected_h_delta_to_line_h_j_kg}, " - f"python_p4_node_counterfactual_dT=" - f"{energy_flow.python_p4_node_port_2_counterfactual_dtemp_k_s}, " - f"href_t={energy_flow.reference_temperature_k}, " - f"ref_dh1_est=" - f"{energy_flow.amesim_port_1_reference_enthalpy_estimate_w}, " - f"ref_dh1_error=" - f"{energy_flow.amesim_port_1_reference_enthalpy_error_w}, " - f"candidate_dm2i=" - f"{energy_flow.amesim_candidate_pn2pipefr_dm2i_g_s}, " - f"candidate_dh2i=" - f"{energy_flow.amesim_candidate_pn2pipefr_dh2i_w}, " - f"candidate_storage_sdh=" - f"{energy_flow.amesim_candidate_storage_enthalpy_sum_w}, " - f"candidate_pn2pipefr_dT=" - f"{energy_flow.amesim_candidate_pn2pipefr_dtemp_k_s}, " - f"candidate_pn2pipefr_dT_residual=" - f"{energy_flow.amesim_candidate_pn2pipefr_dtemp_residual_k_s}, " - f"python_ref_dh1=" - f"{energy_flow.python_reference_port_1_enthalpy_flow_w}, " - f"python_ref_node_dh2=" - f"{energy_flow.python_reference_node_port_2_enthalpy_flow_w}, " - f"python_ref_sdh=" - f"{energy_flow.python_reference_observed_port_enthalpy_sum_w}, " - f"python_ref_dh1_error=" - f"{energy_flow.python_reference_port_1_to_amesim_error_w}, " - f"python_ref_node_dh2_error=" - f"{energy_flow.python_reference_node_port_2_to_amesim_error_w}, " - f"python_ref_sdh_error=" - f"{energy_flow.python_reference_sum_to_amesim_error_w}, " - f"python_current_dT=" - f"{energy_flow.python_current_line_temperature_derivative_k_s}, " - f"amesim_fd_dT=" - f"{energy_flow.amesim_line_temperature_derivative_fd_k_s}, " - f"amesim_pn2vol2_dT_node_plus_dh1=" - f"{energy_flow.amesim_pn2vol2_dtemp_node_plus_dh1_k_s}, " - f"amesim_pn2vol2_dT_node_minus_dh1=" - f"{energy_flow.amesim_pn2vol2_dtemp_node_minus_dh1_k_s}, " - f"python_ref_pn2vol2_dT_node_minus_dh1=" - f"{energy_flow.python_reference_pn2vol2_dtemp_node_minus_dh1_k_s}" - ) - for p4_node in sample.p4_node_enthalpy_breakdown_diagnostics: - lines.append( - f" - p4_node_enthalpy@{p4_node.node_alias}: " - f"amesim_p1_dh={p4_node.amesim_port_1_enthalpy_flow_w}, " - f"amesim_p1_dm={p4_node.amesim_port_1_mass_flow_g_s}, " - f"amesim_p3_dh={p4_node.amesim_port_3_enthalpy_flow_w}, " - f"amesim_p3_dm={p4_node.amesim_port_3_mass_flow_g_s}, " - f"amesim_p4_dh={p4_node.amesim_port_4_enthalpy_flow_w}, " - f"amesim_p4_dm={p4_node.amesim_port_4_mass_flow_g_s}, " - f"amesim_p2_dh={p4_node.amesim_port_2_enthalpy_flow_w}, " - f"amesim_p2_dm={p4_node.amesim_port_2_mass_flow_g_s}, " - f"python_p1_dh={p4_node.python_port_1_enthalpy_flow_w}, " - f"python_p1_dm={p4_node.python_port_1_mass_flow_g_s}, " - f"python_p3_dh={p4_node.python_port_3_enthalpy_flow_w}, " - f"python_p3_dm={p4_node.python_port_3_mass_flow_g_s}, " - f"python_p4_dh={p4_node.python_port_4_enthalpy_flow_w}, " - f"python_p4_dm={p4_node.python_port_4_mass_flow_g_s}, " - f"python_p2_dh={p4_node.python_port_2_enthalpy_flow_w}, " - f"python_p2_dm={p4_node.python_port_2_mass_flow_g_s}, " - f"python_ref_p1_dh=" - f"{p4_node.python_reference_port_1_enthalpy_flow_w}, " - f"python_ref_p3_dh=" - f"{p4_node.python_reference_port_3_enthalpy_flow_w}, " - f"python_ref_p4_dh=" - f"{p4_node.python_reference_port_4_enthalpy_flow_w}, " - f"python_ref_p2_dh=" - f"{p4_node.python_reference_port_2_enthalpy_flow_w}, " - f"python_real_ref_p1_dh=" - f"{p4_node.python_real_gas_reference_port_1_enthalpy_flow_w}, " - f"python_real_ref_p3_dh=" - f"{p4_node.python_real_gas_reference_port_3_enthalpy_flow_w}, " - f"python_real_ref_p4_dh=" - f"{p4_node.python_real_gas_reference_port_4_enthalpy_flow_w}, " - f"python_real_ref_p2_dh=" - f"{p4_node.python_real_gas_reference_port_2_enthalpy_flow_w}, " - f"python_p2_dh_error=" - f"{p4_node.python_port_2_enthalpy_error_w}, " - f"python_ref_p2_dh_error=" - f"{p4_node.python_reference_port_2_enthalpy_error_w}, " - f"python_real_ref_p2_dh_error=" - f"{p4_node.python_real_gas_reference_port_2_enthalpy_error_w}" - ) - for chamber_energy in sample.pnch012_energy_equation_diagnostics: - lines.append( - f" - pnch012_energy@{chamber_energy.chamber_alias}: " - f"amesim_dh1={chamber_energy.amesim_port_1_enthalpy_flow_w}, " - f"amesim_dm1={chamber_energy.amesim_port_1_mass_flow_g_s}, " - f"amesim_fd_dmgas=" - f"{chamber_energy.amesim_chamber_mass_derivative_fd_g_s}, " - f"amesim_dmgas_residual=" - f"{chamber_energy.amesim_chamber_mass_derivative_residual_g_s}, " - f"amesim_fd_dT=" - f"{chamber_energy.amesim_chamber_temperature_derivative_fd_k_s}, " - f"amesim_fd_dvol=" - f"{chamber_energy.amesim_chamber_volume_rate_fd_m3_s}, " - f"amesim_piston_dvol=" - f"{chamber_energy.amesim_piston_volume_rate_m3_s}, " - f"amesim_dq={chamber_energy.amesim_heat_flow_w}, " - f"amesim_boundary_work_fd=" - f"{chamber_energy.amesim_boundary_work_fd_volume_w}, " - f"amesim_pn2vol_ref_dT_fdvol=" - f"{chamber_energy.amesim_pn2vol_reference_dtemp_fd_volume_k_s}, " - f"amesim_pn2vol_ref_dT_fdvol_residual=" - f"{chamber_energy.amesim_pn2vol_reference_dtemp_fd_volume_residual_k_s}, " - f"amesim_pn2vol_ref_dT_pistonvol=" - f"{chamber_energy.amesim_pn2vol_reference_dtemp_piston_volume_k_s}, " - f"amesim_pn2vol_ref_dT_pistonvol_residual=" - f"{chamber_energy.amesim_pn2vol_reference_dtemp_piston_volume_residual_k_s}, " - f"python_port_a_dm=" - f"{chamber_energy.python_port_a_mass_flow_kg_s}, " - f"python_dU_dt=" - f"{chamber_energy.python_current_energy_derivative_w}, " - f"python_current_dT=" - f"{chamber_energy.python_current_chamber_temperature_derivative_k_s}, " - f"href_t={chamber_energy.reference_temperature_k}" - ) - lines.append("Final comparison:") - for data_path in diagnostic.data_paths: - lines.append( - f" - {data_path}: " - f"python={diagnostic.python_values_by_data_path[data_path]}, " - f"amesim={diagnostic.amesim_values_by_data_path[data_path]}, " - f"abs_error={diagnostic.abs_error(data_path)}" - ) - return "\n".join(lines) + "\n" - - - -def format_test_mql_pnvo_event_window_candidate_comparison_summary( - diagnostic: TestMqlPnvoEventWindowCandidateComparisonDiagnostic, -) -> str: - lines = [ - "Model: test_mql", - f"Mode: PNVO event window candidate comparison ({diagnostic.orifice_alias})", - f"Event time: {diagnostic.event_time_s}", - f"Final time: {diagnostic.final_time_s}", - "Default RHS: current pneumatic_96 internal-energy RHS", - "Candidate RHS: pneumatic_96 reference-enthalpy/mass-offset RHS", - "Metric comparisons: negative delta means the candidate reduced error", - ] - for comparison in diagnostic.metric_comparisons: - lines.append( - f" t={comparison.time_s} - {comparison.metric_name}: " - f"default_abs_error={comparison.default_abs_error}, " - f"candidate_abs_error={comparison.candidate_abs_error}, " - f"delta={comparison.abs_error_delta}" - ) - lines.append("Final comparison:") - for data_path in diagnostic.default_diagnostic.data_paths: - lines.append( - f" - {data_path}: " - f"default_abs_error={diagnostic.default_diagnostic.abs_error(data_path)}, " - f"candidate_abs_error={diagnostic.candidate_diagnostic.abs_error(data_path)}, " - f"delta=" - f"{diagnostic.candidate_diagnostic.abs_error(data_path) - diagnostic.default_diagnostic.abs_error(data_path)}" - ) - return "\n".join(lines) + "\n" - - -def format_test_mql_full_state_comparison_summary( - run: TestMqlFullStateComparisonRun, -) -> str: - lines = [ - "Model: test_mql", - "Mode: Python 132 full-state closure comparison", - f"Samples: {run.sample_count}", - f"Compared signals: {run.signal_count}", - f"Output schema signals: {run.output_schema.signal_count}", - f"AMESim first saved interval: " - f"{run.amesim_results.times[1] - run.amesim_results.times[0]}", - f"Final time aligns with AMESim sample: " - f"{_matches_amesim_sample(run, run.output.times[-1])}", - f"Max absolute error: {run.comparison.max_abs_error}", - f"Max relative error: {run.comparison.max_rel_error}", - ] - largest_metric = run.largest_abs_error_metric - if largest_metric is not None: - lines.append( - "Largest absolute error: " - f"{largest_metric.data_path}={largest_metric.max_abs_error}" - ) - largest_diagnostic = run.largest_final_abs_error_diagnostic - if largest_diagnostic is not None: - lines.append( - "Largest final endpoint error: " - f"{largest_diagnostic.data_path}={largest_diagnostic.final_abs_error}" - ) - lines.append("Metrics by max absolute error:") - for metric in run.metrics_by_max_abs_error(): - lines.append( - f" - {metric.data_path}: max_abs_error={metric.max_abs_error}, " - f"final_abs_error={metric.final_abs_error}" - ) - lines.append("Endpoint diagnostics by final absolute error:") - for diagnostic in run.diagnostics_by_final_abs_error(): - lines.append( - f" - {diagnostic.data_path}: " - f"initial_python={diagnostic.initial_python_value}, " - f"initial_amesim={diagnostic.initial_amesim_value}, " - f"final_python={diagnostic.final_python_value}, " - 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}", - ] - ) - pnvo_diagnostic = _pn_morifice_1_diagnostic(run) - if pnvo_diagnostic is not None: - lines.append(f"PNVO diagnostic: {pnvo_diagnostic.alias}") - lines.extend( - [ - f" - initial_python_opening=" - f"{pnvo_diagnostic.initial_python_opening}", - f" - initial_amesim_opening=" - f"{pnvo_diagnostic.initial_amesim_opening}", - f" - final_python_opening=" - f"{pnvo_diagnostic.final_python_opening}", - f" - final_amesim_opening=" - f"{pnvo_diagnostic.final_amesim_opening}", - f" - final_python_mass_flow_kg_s=" - f"{pnvo_diagnostic.final_python_mass_flow_kg_s}", - f" - final_amesim_mass_flow_kg_s=" - f"{pnvo_diagnostic.final_amesim_mass_flow_kg_s}", - f" - final_mass_flow_abs_error_kg_s=" - f"{pnvo_diagnostic.final_mass_flow_abs_error_kg_s}", - ] - ) - chamber_diagnostic = _largest_chamber_rhs_diagnostic(run) - if chamber_diagnostic is not None: - lines.append( - "Largest endpoint chamber RHS breakdown: " - f"{chamber_diagnostic.chamber_alias}" - ) - lines.extend( - [ - f" - piston_alias={chamber_diagnostic.piston_alias}", - f" - pressure_pa={chamber_diagnostic.chamber_pressure_pa}", - f" - volume_m3={chamber_diagnostic.chamber_volume_m3}", - f" - volume_rate_m3_s={chamber_diagnostic.chamber_volume_rate_m3_s}", - f" - mass_derivative_kg_s={chamber_diagnostic.mass_derivative_kg_s}", - f" - port_a_energy_flow_w={chamber_diagnostic.port_a_energy_flow_w}", - f" - boundary_work_w={chamber_diagnostic.boundary_work_w}", - f" - thermal_energy_flow_w=" - f"{chamber_diagnostic.thermal_energy_flow_w}", - f" - energy_derivative_w={chamber_diagnostic.energy_derivative_w}", - ] - ) - return "\n".join(lines) + "\n" - - -def _matches_amesim_sample( - run: TestMqlFullStateComparisonRun, - time_s: float, -) -> bool: - return any( - abs(float(sample_time) - float(time_s)) <= 1.0e-12 - for sample_time in run.amesim_results.times - ) - - -def _pneumatic_69_flow_diagnostic(run: TestMqlFullStateComparisonRun): - try: - return run.pnl0001_mass_flow_diagnostic() - except KeyError: - return None - - -def _pn_morifice_1_diagnostic(run: TestMqlFullStateComparisonRun): - try: - return run.pnvo_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: - return None - _signal, alias = diagnostic.data_path.split("@", 1) - try: - return run.chamber_rhs_diagnostic(alias) - except KeyError: - return None - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument( - "--pnvo-event-boundary", - action="store_true", - help="compare the STEP0 left-state/right-opening boundary at t=0.04 s", - ) - parser.add_argument( - "--pnvo-event-window", - action="store_true", - help="run the segmented PNVO opening window through the t=0.05 s save point", - ) - parser.add_argument( - "--pnvo-event-window-candidate-comparison", - action="store_true", - help="compare the PNVO event window with and without the pneumatic_96 RHS candidate", - ) - parser.add_argument( - "--pnvo-event-window-final-time", - type=float, - help="override PNVO event window final time; candidate comparison defaults to 0.041 s", - ) - parser.add_argument( - "--amesim-results-archive", - type=Path, - help="read AMESim observations from a separate .ame results package", - ) - parser.add_argument( - "--pneumatic-96-reference-rhs", - action="store_true", - help="enable the local reference/mass-offset RHS candidate for pneumatic_96", - ) - parser.add_argument( - "--pneumatic-69-reference-rhs", - action="store_true", - help="enable the local reference/mass-offset RHS candidate for pneumatic_69", - ) - args = parser.parse_args() - config = None - if ( - args.amesim_results_archive is not None - or args.pneumatic_96_reference_rhs - or args.pneumatic_69_reference_rhs - ): - config = TestMqlFullStateComparisonScriptConfig( - paths=TestMqlFullStateComparisonPathConfig( - amesim_results_archive_path=args.amesim_results_archive, - ), - execution=TestMqlFullStateComparisonExecutionConfig( - use_pneumatic_96_reference_rhs=args.pneumatic_96_reference_rhs, - use_pneumatic_69_reference_rhs=args.pneumatic_69_reference_rhs, - ), - ) - if args.pnvo_event_boundary: - diagnostic = run_test_mql_pnvo_event_boundary_diagnostic(config) - print(format_test_mql_pnvo_event_boundary_summary(diagnostic), end="") - return - if args.pnvo_event_window_candidate_comparison: - diagnostic = run_test_mql_pnvo_event_window_candidate_comparison( - config, - final_time_s=args.pnvo_event_window_final_time or 0.041, - ) - print( - format_test_mql_pnvo_event_window_candidate_comparison_summary( - diagnostic - ), - end="", - ) - return - if args.pnvo_event_window: - if args.pnvo_event_window_final_time is None: - diagnostic = run_test_mql_pnvo_event_window_diagnostic(config) - else: - diagnostic = run_test_mql_pnvo_event_window_diagnostic( - config, - final_time_s=args.pnvo_event_window_final_time, - ) - print(format_test_mql_pnvo_event_window_summary(diagnostic), end="") - return - - run, output_dir = run_test_mql_full_state_comparison(config) - print(format_test_mql_full_state_comparison_summary(run), end="") - print(f"Output directory: {output_dir}") - - -if __name__ == "__main__": - main() diff --git a/app/simulation/examples/testmodel/test_mql_baseline.py b/app/simulation/examples/testmodel/test_mql_baseline.py deleted file mode 100644 index fff6c38..0000000 --- a/app/simulation/examples/testmodel/test_mql_baseline.py +++ /dev/null @@ -1,77 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass -from pathlib import Path - -from PythonModels.reporting.amesim_results import AmesimResults, load_test_mql_amesim_results -from PythonModels.reporting.test_mql_comparison import TestMqlComparisonResult -from PythonModels.reporting.test_mql_observations import ( - TestMqlObservationCatalog, - build_test_mql_observation_catalog, -) -from PythonModels.reporting.test_mql_output_schema import ( - TestMqlOutputSchema, - build_test_mql_output_schema, -) -from PythonModels.reporting.test_mql_output_validation import ( - TestMqlValidatedOutput, - compare_validated_test_mql_output, - validate_test_mql_output, -) - - -@dataclass(frozen=True) -class TestMqlBaselineRun: - amesim_results: AmesimResults - observation_catalog: TestMqlObservationCatalog - output_schema: TestMqlOutputSchema - output: TestMqlValidatedOutput - comparison: TestMqlComparisonResult - - @property - def sample_count(self) -> int: - return len(self.output.times) - - @property - def signal_count(self) -> int: - return len(self.output.data_paths) - - -def run_test_mql_baseline_passthrough( - archive_path: Path, - *, - data_paths: tuple[str, ...] | list[str] | None = None, -) -> TestMqlBaselineRun: - amesim_results = load_test_mql_amesim_results(archive_path) - observation_catalog = build_test_mql_observation_catalog(amesim_results) - output_schema = build_test_mql_output_schema( - amesim_results, - observation_catalog=observation_catalog, - ) - selected_paths = tuple(data_paths) if data_paths is not None else output_schema.data_paths() - baseline_series = observation_catalog.baseline_series_by_data_path( - amesim_results, - selected_paths, - ) - output = validate_test_mql_output( - times=amesim_results.times, - series_by_data_path=baseline_series, - schema=output_schema, - data_paths=selected_paths, - require_all_schema_paths=data_paths is None, - ) - comparison = compare_validated_test_mql_output( - times=output.times, - series_by_data_path=output.series_by_data_path, - schema=output_schema, - amesim_results=amesim_results, - data_paths=output.data_paths, - require_all_schema_paths=data_paths is None, - ) - return TestMqlBaselineRun( - amesim_results=amesim_results, - observation_catalog=observation_catalog, - output_schema=output_schema, - output=output, - comparison=comparison, - ) diff --git a/app/simulation/examples/testmodel/test_mql_computed.py b/app/simulation/examples/testmodel/test_mql_computed.py deleted file mode 100644 index e13bfb9..0000000 --- a/app/simulation/examples/testmodel/test_mql_computed.py +++ /dev/null @@ -1,468 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass -from pathlib import Path - -from PythonModels.components.amesim_pneumatic import m3_to_cm3 -from PythonModels.reporting.amesim_results import AmesimResults, load_test_mql_amesim_results -from PythonModels.reporting.test_mql_comparison import TestMqlComparisonResult -from PythonModels.reporting.test_mql_observations import ( - TestMqlObservationCatalog, - build_test_mql_observation_catalog, -) -from PythonModels.reporting.test_mql_output_schema import ( - TestMqlOutputSchema, - build_test_mql_output_schema, -) -from PythonModels.reporting.test_mql_output_validation import ( - TestMqlValidatedOutput, - compare_validated_test_mql_output, - validate_test_mql_output, -) -from PythonModels.reporting.test_mql_variables import build_test_mql_variable_catalog -from PythonModels.systems.test_mql_mechanical import ( - TestMqlMechanicalAssembly, - build_test_mql_mechanical_assembly, -) -from PythonModels.systems.test_mql_pneumatic import ( - TestMqlPneumaticAssembly, - build_test_mql_pneumatic_assembly, -) - - -@dataclass(frozen=True) -class TestMqlComputedPistonGeometryRun: - amesim_results: AmesimResults - observation_catalog: TestMqlObservationCatalog - output_schema: TestMqlOutputSchema - mechanical_assembly: TestMqlMechanicalAssembly - output: TestMqlValidatedOutput - comparison: TestMqlComparisonResult - - @property - def sample_count(self) -> int: - return len(self.output.times) - - @property - def signal_count(self) -> int: - return len(self.output.data_paths) - - -@dataclass(frozen=True) -class TestMqlComputedGeometryRun: - amesim_results: AmesimResults - observation_catalog: TestMqlObservationCatalog - output_schema: TestMqlOutputSchema - mechanical_assembly: TestMqlMechanicalAssembly - pneumatic_assembly: TestMqlPneumaticAssembly - output: TestMqlValidatedOutput - comparison: TestMqlComparisonResult - - @property - def sample_count(self) -> int: - return len(self.output.times) - - @property - def signal_count(self) -> int: - return len(self.output.data_paths) - - -@dataclass(frozen=True) -class TestMqlComputedLineRelationsRun: - amesim_results: AmesimResults - observation_catalog: TestMqlObservationCatalog - output_schema: TestMqlOutputSchema - output: TestMqlValidatedOutput - comparison: TestMqlComparisonResult - - @property - def sample_count(self) -> int: - return len(self.output.times) - - @property - def signal_count(self) -> int: - return len(self.output.data_paths) - - -@dataclass(frozen=True) -class TestMqlComputedPneumaticRelationsRun: - amesim_results: AmesimResults - observation_catalog: TestMqlObservationCatalog - output_schema: TestMqlOutputSchema - output: TestMqlValidatedOutput - comparison: TestMqlComparisonResult - - @property - def sample_count(self) -> int: - return len(self.output.times) - - @property - def signal_count(self) -> int: - return len(self.output.data_paths) - - -@dataclass(frozen=True) -class TestMqlComputedMechanicalRelationsRun: - amesim_results: AmesimResults - observation_catalog: TestMqlObservationCatalog - output_schema: TestMqlOutputSchema - mechanical_assembly: TestMqlMechanicalAssembly - output: TestMqlValidatedOutput - comparison: TestMqlComparisonResult - - @property - def sample_count(self) -> int: - return len(self.output.times) - - @property - def signal_count(self) -> int: - return len(self.output.data_paths) - - -def run_test_mql_computed_piston_geometry( - archive_path: Path, -) -> TestMqlComputedPistonGeometryRun: - amesim_results = load_test_mql_amesim_results(archive_path) - observation_catalog = build_test_mql_observation_catalog(amesim_results) - output_schema = build_test_mql_output_schema( - amesim_results, - observation_catalog=observation_catalog, - ) - variable_catalog = build_test_mql_variable_catalog(amesim_results) - mechanical_assembly = build_test_mql_mechanical_assembly( - amesim_results=amesim_results, - variable_catalog=variable_catalog, - ) - output_series = _compute_piston_geometry_series(amesim_results, mechanical_assembly) - output_data_paths = tuple(output_series) - output = validate_test_mql_output( - times=amesim_results.times, - series_by_data_path=output_series, - schema=output_schema, - data_paths=output_data_paths, - ) - comparison = compare_validated_test_mql_output( - times=output.times, - series_by_data_path=output.series_by_data_path, - schema=output_schema, - amesim_results=amesim_results, - data_paths=output.data_paths, - ) - return TestMqlComputedPistonGeometryRun( - amesim_results=amesim_results, - observation_catalog=observation_catalog, - output_schema=output_schema, - mechanical_assembly=mechanical_assembly, - output=output, - comparison=comparison, - ) - - -def run_test_mql_computed_geometry( - archive_path: Path, -) -> TestMqlComputedGeometryRun: - amesim_results = load_test_mql_amesim_results(archive_path) - observation_catalog = build_test_mql_observation_catalog(amesim_results) - output_schema = build_test_mql_output_schema( - amesim_results, - observation_catalog=observation_catalog, - ) - variable_catalog = build_test_mql_variable_catalog(amesim_results) - mechanical_assembly = build_test_mql_mechanical_assembly( - amesim_results=amesim_results, - variable_catalog=variable_catalog, - ) - pneumatic_assembly = build_test_mql_pneumatic_assembly() - output_series = { - **_compute_piston_geometry_series(amesim_results, mechanical_assembly), - **_compute_variable_chamber_volume_series( - amesim_results, - mechanical_assembly, - pneumatic_assembly, - ), - } - output_data_paths = tuple(output_series) - output = validate_test_mql_output( - times=amesim_results.times, - series_by_data_path=output_series, - schema=output_schema, - data_paths=output_data_paths, - ) - comparison = compare_validated_test_mql_output( - times=output.times, - series_by_data_path=output.series_by_data_path, - schema=output_schema, - amesim_results=amesim_results, - data_paths=output.data_paths, - ) - return TestMqlComputedGeometryRun( - amesim_results=amesim_results, - observation_catalog=observation_catalog, - output_schema=output_schema, - mechanical_assembly=mechanical_assembly, - pneumatic_assembly=pneumatic_assembly, - output=output, - comparison=comparison, - ) - - -def run_test_mql_computed_line_relations( - archive_path: Path, -) -> TestMqlComputedLineRelationsRun: - amesim_results = load_test_mql_amesim_results(archive_path) - observation_catalog = build_test_mql_observation_catalog(amesim_results) - output_schema = build_test_mql_output_schema( - amesim_results, - observation_catalog=observation_catalog, - ) - output_series = _compute_line_reversed_series(amesim_results, observation_catalog) - output_data_paths = tuple(output_series) - output = validate_test_mql_output( - times=amesim_results.times, - series_by_data_path=output_series, - schema=output_schema, - data_paths=output_data_paths, - ) - comparison = compare_validated_test_mql_output( - times=output.times, - series_by_data_path=output.series_by_data_path, - schema=output_schema, - amesim_results=amesim_results, - data_paths=output.data_paths, - ) - return TestMqlComputedLineRelationsRun( - amesim_results=amesim_results, - observation_catalog=observation_catalog, - output_schema=output_schema, - output=output, - comparison=comparison, - ) - - -def run_test_mql_computed_pneumatic_relations( - archive_path: Path, -) -> TestMqlComputedPneumaticRelationsRun: - amesim_results = load_test_mql_amesim_results(archive_path) - observation_catalog = build_test_mql_observation_catalog(amesim_results) - output_schema = build_test_mql_output_schema( - amesim_results, - observation_catalog=observation_catalog, - ) - output_series = { - **_compute_chamber_duplicate_series(amesim_results, observation_catalog), - **_compute_orifice_reversed_series(amesim_results, observation_catalog), - } - output_data_paths = tuple(output_series) - output = validate_test_mql_output( - times=amesim_results.times, - series_by_data_path=output_series, - schema=output_schema, - data_paths=output_data_paths, - ) - comparison = compare_validated_test_mql_output( - times=output.times, - series_by_data_path=output.series_by_data_path, - schema=output_schema, - amesim_results=amesim_results, - data_paths=output.data_paths, - ) - return TestMqlComputedPneumaticRelationsRun( - amesim_results=amesim_results, - observation_catalog=observation_catalog, - output_schema=output_schema, - output=output, - comparison=comparison, - ) - - -def run_test_mql_computed_mechanical_relations( - archive_path: Path, -) -> TestMqlComputedMechanicalRelationsRun: - amesim_results = load_test_mql_amesim_results(archive_path) - observation_catalog = build_test_mql_observation_catalog(amesim_results) - output_schema = build_test_mql_output_schema( - amesim_results, - observation_catalog=observation_catalog, - ) - variable_catalog = build_test_mql_variable_catalog(amesim_results) - mechanical_assembly = build_test_mql_mechanical_assembly( - amesim_results=amesim_results, - variable_catalog=variable_catalog, - ) - output_series = { - **_compute_mass_duplicate_series(amesim_results, mechanical_assembly), - **_compute_inactive_mass_force_series(amesim_results, mechanical_assembly), - **_compute_zero_force_source_series(amesim_results, mechanical_assembly), - } - output_data_paths = tuple(output_series) - output = validate_test_mql_output( - times=amesim_results.times, - series_by_data_path=output_series, - schema=output_schema, - data_paths=output_data_paths, - ) - comparison = compare_validated_test_mql_output( - times=output.times, - series_by_data_path=output.series_by_data_path, - schema=output_schema, - amesim_results=amesim_results, - data_paths=output.data_paths, - ) - return TestMqlComputedMechanicalRelationsRun( - amesim_results=amesim_results, - observation_catalog=observation_catalog, - output_schema=output_schema, - mechanical_assembly=mechanical_assembly, - output=output, - comparison=comparison, - ) - - -def _compute_piston_geometry_series( - amesim_results: AmesimResults, - mechanical_assembly: TestMqlMechanicalAssembly, -) -> dict[str, tuple[float, ...]]: - series_by_data_path: dict[str, tuple[float, ...]] = {} - for alias in sorted(mechanical_assembly.pistons): - piston = mechanical_assembly.pistons[alias] - geometry = piston.geometry() - x4 = amesim_results.series(f"x4@{alias}") - x5 = amesim_results.series(f"x5@{alias}") - v4 = amesim_results.series(f"v4@{alias}") - v5 = amesim_results.series(f"v5@{alias}") - series_by_data_path[f"length@{alias}"] = tuple( - geometry.chamber_length_mm(port4, port5) - for port4, port5 in zip(x4, x5) - ) - series_by_data_path[f"vol1@{alias}"] = tuple( - geometry.chamber_volume_cm3(port4, port5) - for port4, port5 in zip(x4, x5) - ) - series_by_data_path[f"vvol1@{alias}"] = tuple( - geometry.chamber_volume_rate_l_min(port4, port5) - for port4, port5 in zip(v4, v5) - ) - return series_by_data_path - - -def _compute_variable_chamber_volume_series( - amesim_results: AmesimResults, - mechanical_assembly: TestMqlMechanicalAssembly, - pneumatic_assembly: TestMqlPneumaticAssembly, -) -> dict[str, tuple[float, ...]]: - series_by_data_path: dict[str, tuple[float, ...]] = {} - for chamber_alias in sorted(pneumatic_assembly.variable_chambers): - chamber = pneumatic_assembly.variable_chambers[chamber_alias] - piston_alias = _piston_alias_for_variable_chamber(chamber_alias) - piston = mechanical_assembly.pistons[piston_alias] - geometry = piston.geometry() - x4 = amesim_results.series(f"x4@{piston_alias}") - x5 = amesim_results.series(f"x5@{piston_alias}") - dead_volume_cm3 = m3_to_cm3(chamber.dead_volume) - series_by_data_path[f"vol@{chamber_alias}"] = tuple( - dead_volume_cm3 + geometry.chamber_volume_cm3(port4, port5) - for port4, port5 in zip(x4, x5) - ) - return series_by_data_path - - -def _piston_alias_for_variable_chamber(chamber_alias: str) -> str: - if not chamber_alias.startswith("pn_c1"): - raise ValueError(f"Unexpected PNCH012 alias: {chamber_alias}") - return chamber_alias.replace("pn_c1", "pn_brp2", 1) - - -def _compute_mass_duplicate_series( - amesim_results: AmesimResults, - mechanical_assembly: TestMqlMechanicalAssembly, -) -> dict[str, tuple[float, ...]]: - series_by_data_path: dict[str, tuple[float, ...]] = {} - for alias in sorted(mechanical_assembly.masses): - for signal_name in ("x1", "v1", "acc1"): - source_path = f"{signal_name}@{alias}" - duplicate_path = f"{signal_name}dup@{alias}" - series_by_data_path[duplicate_path] = tuple( - -value for value in amesim_results.series(source_path) - ) - return series_by_data_path - - -def _compute_inactive_mass_force_series( - amesim_results: AmesimResults, - mechanical_assembly: TestMqlMechanicalAssembly, -) -> dict[str, tuple[float, ...]]: - series_by_data_path: dict[str, tuple[float, ...]] = {} - for alias in sorted(mechanical_assembly.masses): - mass = mechanical_assembly.masses[alias].endstop() - x1 = amesim_results.series(f"x1@{alias}") - v1 = amesim_results.series(f"v1@{alias}") - series_by_data_path[f"Fmin@{alias}"] = tuple( - mass.lower_static_force_magnitude(displacement) - for displacement in x1 - ) - series_by_data_path[f"Fvisc@{alias}"] = tuple( - mass.viscous_friction_force(velocity) - for velocity in v1 - ) - series_by_data_path[f"Ffric@{alias}"] = tuple(0.0 for _ in x1) - return series_by_data_path - - -def _compute_zero_force_source_series( - amesim_results: AmesimResults, - mechanical_assembly: TestMqlMechanicalAssembly, -) -> dict[str, tuple[float, ...]]: - return { - f"fzero@{alias}": tuple(0.0 for _ in amesim_results.times) - for alias in sorted(mechanical_assembly.zero_force_sources) - } - - -def _compute_chamber_duplicate_series( - amesim_results: AmesimResults, - observation_catalog: TestMqlObservationCatalog, -) -> dict[str, tuple[float, ...]]: - series_by_data_path: dict[str, tuple[float, ...]] = {} - for binding in observation_catalog.chambers.bindings: - pressure_series = amesim_results.series(binding.pressure_path) - temperature_series = amesim_results.series(binding.temperature_path) - for duplicate_path in binding.pressure_duplicate_paths: - series_by_data_path[duplicate_path] = tuple(pressure_series) - for duplicate_path in binding.temperature_duplicate_paths: - series_by_data_path[duplicate_path] = tuple(temperature_series) - return series_by_data_path - - -def _compute_orifice_reversed_series( - amesim_results: AmesimResults, - observation_catalog: TestMqlObservationCatalog, -) -> dict[str, tuple[float, ...]]: - series_by_data_path: dict[str, tuple[float, ...]] = {} - for binding in observation_catalog.orifices.bindings: - series_by_data_path[binding.reversed_mass_flow_path] = tuple( - -value for value in amesim_results.series(binding.primary_mass_flow_path) - ) - series_by_data_path[binding.reversed_enthalpy_flow_path] = tuple( - -value for value in amesim_results.series(binding.primary_enthalpy_flow_path) - ) - return series_by_data_path - - -def _compute_line_reversed_series( - amesim_results: AmesimResults, - observation_catalog: TestMqlObservationCatalog, -) -> dict[str, tuple[float, ...]]: - series_by_data_path: dict[str, tuple[float, ...]] = {} - for binding in observation_catalog.lines.by_submodel("PNL00R"): - if len(binding.mass_flow_paths) != 2 or len(binding.enthalpy_flow_paths) != 2: - raise ValueError(f"Expected two PNL00R flow paths for {binding.alias}.") - primary_mass_path, reversed_mass_path = binding.mass_flow_paths - primary_enthalpy_path, reversed_enthalpy_path = binding.enthalpy_flow_paths - series_by_data_path[reversed_mass_path] = tuple( - -value for value in amesim_results.series(primary_mass_path) - ) - series_by_data_path[reversed_enthalpy_path] = tuple( - -value for value in amesim_results.series(primary_enthalpy_path) - ) - return series_by_data_path - diff --git a/app/simulation/examples/testmodel/test_mql_lines.py b/app/simulation/examples/testmodel/test_mql_lines.py deleted file mode 100644 index d437bc5..0000000 --- a/app/simulation/examples/testmodel/test_mql_lines.py +++ /dev/null @@ -1,102 +0,0 @@ -from __future__ import annotations - -import re -from collections import Counter -from dataclasses import dataclass - -from PythonModels.reporting.amesim_results import AmesimResults -from PythonModels.reporting.test_mql_variables import ( - TestMqlVariableCatalog, - build_test_mql_variable_catalog, -) -from PythonModels.systems.test_mql import CONNECTION_SPECS - - -TEST_MQL_PNEUMATIC_LINE_SUBMODELS = ("PNL0001", "PNL0002", "PNL0003", "PNL00R") -_LINE_PATTERN_RE = re.compile(r"\(([^()]+)\)\s*$") - - -@dataclass(frozen=True) -class TestMqlLineConnection: - index: int - alias: str - submodel: str - pattern: str - source_component: str - source_port: str - target_component: str - target_port: str - label: str - data_paths: tuple[str, ...] - signal_names: tuple[str, ...] - - @property - def has_compliance(self) -> bool: - return "C" in self.pattern - - @property - def has_resistance(self) -> bool: - return "R" in self.pattern - - -@dataclass(frozen=True) -class TestMqlLineAssembly: - lines: tuple[TestMqlLineConnection, ...] - - @property - def line_count(self) -> int: - return len(self.lines) - - def by_alias(self, alias: str) -> TestMqlLineConnection: - for line in self.lines: - if line.alias == alias: - return line - raise KeyError(alias) - - def by_submodel(self, submodel: str) -> tuple[TestMqlLineConnection, ...]: - return tuple(line for line in self.lines if line.submodel == submodel) - - def counts_by_submodel(self) -> dict[str, int]: - return dict(Counter(line.submodel for line in self.lines)) - - def aliases(self) -> tuple[str, ...]: - return tuple(line.alias for line in self.lines) - - -def build_test_mql_line_assembly( - amesim_results: AmesimResults, - variable_catalog: TestMqlVariableCatalog | None = None, -) -> TestMqlLineAssembly: - variable_catalog = variable_catalog or build_test_mql_variable_catalog(amesim_results) - lines = [] - for spec in CONNECTION_SPECS: - submodel = str(spec["submodel"]) - if submodel not in TEST_MQL_PNEUMATIC_LINE_SUBMODELS: - continue - data_paths = variable_catalog.data_paths_for_owner(str(spec["alias"])) - signal_names = tuple(path.rsplit("@", 1)[0] for path in data_paths) - lines.append( - TestMqlLineConnection( - index=int(spec["index"]), - alias=str(spec["alias"]), - submodel=submodel, - pattern=_line_pattern(str(spec["label"]), submodel), - source_component=str(spec["source_component"]), - source_port=str(spec["source_port"]), - target_component=str(spec["target_component"]), - target_port=str(spec["target_port"]), - label=str(spec["label"]), - data_paths=data_paths, - signal_names=signal_names, - ) - ) - return TestMqlLineAssembly(lines=tuple(lines)) - - -def _line_pattern(label: str, submodel: str) -> str: - match = _LINE_PATTERN_RE.search(label) - if match is not None: - return match.group(1) - if submodel == "PNL00R": - return "R" - return submodel diff --git a/app/simulation/examples/testmodel/test_mql_pneumatic_topology.py b/app/simulation/examples/testmodel/test_mql_pneumatic_topology.py deleted file mode 100644 index 70babcc..0000000 --- a/app/simulation/examples/testmodel/test_mql_pneumatic_topology.py +++ /dev/null @@ -1,128 +0,0 @@ -from __future__ import annotations - -from PythonModels.systems.test_mql_closure import TestMqlPneumaticChamberSegmentSpec -from PythonModels.systems.test_mql_topology import TestMqlCirTopology - - -def discover_fixed_chamber_segments( - topology: TestMqlCirTopology, - component_specs: list[dict[str, object]], - connection_specs: list[dict[str, object]], -) -> tuple[TestMqlPneumaticChamberSegmentSpec, ...]: - submodel_by_alias = { - str(component["alias"]): str(component["submodel"]) - for component in component_specs - } - segments = [] - for component in component_specs: - volume_alias = str(component["alias"]) - if component["submodel"] != "PNCH023": - continue - - orifice_contacts = [] - for contact in topology.contacts_for(volume_alias): - other_alias, other_port = contact.other_endpoint(volume_alias) - if submodel_by_alias.get(other_alias) == "PNOR001": - orifice_contacts.append( - ( - other_alias, - other_port, - contact.port_for(volume_alias), - ) - ) - if len(orifice_contacts) != 2: - raise ValueError( - f"{volume_alias} must contact exactly two PNOR001 orifices; " - f"found {len(orifice_contacts)}" - ) - - sides = [ - _resolve_orifice_boundary( - orifice_alias=orifice_alias, - orifice_volume_port=orifice_volume_port, - volume_port=volume_port, - connection_specs=connection_specs, - submodel_by_alias=submodel_by_alias, - ) - for orifice_alias, orifice_volume_port, volume_port in orifice_contacts - ] - inlet_sides = [side for side in sides if side["role"] == "inlet"] - outlet_sides = [side for side in sides if side["role"] == "outlet"] - if len(inlet_sides) != 1 or len(outlet_sides) != 1: - raise ValueError( - f"{volume_alias} requires one inlet and one outlet topology side" - ) - inlet = inlet_sides[0] - outlet = outlet_sides[0] - segments.append( - TestMqlPneumaticChamberSegmentSpec( - name=f"{volume_alias}_segment", - inlet_node_alias=inlet["node_alias"], - inlet_line_alias=inlet["line_alias"], - inlet_orifice_alias=inlet["orifice_alias"], - inlet_orifice_boundary_port=inlet["orifice_boundary_port"], - inlet_orifice_volume_port=inlet["orifice_volume_port"], - volume_alias=volume_alias, - volume_inlet_port=inlet["volume_port"], - volume_outlet_port=outlet["volume_port"], - outlet_orifice_alias=outlet["orifice_alias"], - outlet_orifice_volume_port=outlet["orifice_volume_port"], - outlet_orifice_boundary_port=outlet["orifice_boundary_port"], - outlet_line_alias=outlet["line_alias"], - outlet_node_alias=outlet["node_alias"], - ) - ) - return tuple(segments) - - -def _resolve_orifice_boundary( - *, - orifice_alias: str, - orifice_volume_port: str, - volume_port: str, - connection_specs: list[dict[str, object]], - submodel_by_alias: dict[str, str], -) -> dict[str, str]: - boundary_connections = [] - for connection in connection_specs: - if ( - connection["source_component"] == orifice_alias - and connection["source_port"] != orifice_volume_port - ) or ( - connection["target_component"] == orifice_alias - and connection["target_port"] != orifice_volume_port - ): - boundary_connections.append(connection) - if len(boundary_connections) != 1: - raise ValueError( - f"{orifice_alias} must have exactly one non-volume boundary connection; " - f"found {len(boundary_connections)}" - ) - - connection = boundary_connections[0] - if connection["submodel"] != "PNL0001": - raise ValueError( - f"{orifice_alias} boundary must use PNL0001, got {connection['submodel']}" - ) - if connection["target_component"] == orifice_alias: - role = "inlet" - node_alias = str(connection["source_component"]) - orifice_boundary_port = str(connection["target_port"]) - else: - role = "outlet" - node_alias = str(connection["target_component"]) - orifice_boundary_port = str(connection["source_port"]) - if submodel_by_alias.get(node_alias) != "PN3NODE2": - raise ValueError( - f"{orifice_alias} PNL0001 boundary must terminate at PN3NODE2, " - f"got {node_alias}" - ) - return { - "role": role, - "node_alias": node_alias, - "line_alias": str(connection["alias"]), - "orifice_alias": orifice_alias, - "orifice_boundary_port": orifice_boundary_port, - "orifice_volume_port": orifice_volume_port, - "volume_port": volume_port, - } diff --git a/app/simulation/examples/testmodel/test_mql_topology.py b/app/simulation/examples/testmodel/test_mql_topology.py deleted file mode 100644 index 4fac424..0000000 --- a/app/simulation/examples/testmodel/test_mql_topology.py +++ /dev/null @@ -1,118 +0,0 @@ -from __future__ import annotations - -import re -import tarfile -import xml.etree.ElementTree as ET -from dataclasses import dataclass -from pathlib import Path - - -@dataclass(frozen=True) -class TestMqlComponentContact: - component_a: str - port_a: str - component_b: str - port_b: str - - def other_endpoint(self, component_alias: str) -> tuple[str, str]: - if component_alias == self.component_a: - return self.component_b, self.port_b - if component_alias == self.component_b: - return self.component_a, self.port_a - raise KeyError(component_alias) - - def port_for(self, component_alias: str) -> str: - if component_alias == self.component_a: - return self.port_a - if component_alias == self.component_b: - return self.port_b - raise KeyError(component_alias) - - -@dataclass(frozen=True) -class TestMqlCirTopology: - component_contacts: tuple[TestMqlComponentContact, ...] - - def contacts_for(self, component_alias: str) -> tuple[TestMqlComponentContact, ...]: - return tuple( - contact - for contact in self.component_contacts - if component_alias in (contact.component_a, contact.component_b) - ) - - -def load_test_mql_cir_topology( - archive_path: str | Path, - *, - cir_member: str = "test_mql_.cir", -) -> TestMqlCirTopology: - with tarfile.open(archive_path) as archive: - cir_file = archive.extractfile(cir_member) - if cir_file is None: - raise ValueError(f"Missing AMESim circuit member: {cir_member}") - cir_text = cir_file.read().decode("latin1") - - root = ET.fromstring(_topology_only_xml(cir_text)) - components = root.findall(".//COMPS_LIST/COMP") - aliases = tuple(_required_text(component, "ALIAS") for component in components) - contacts: dict[ - tuple[tuple[int, int], tuple[int, int]], - TestMqlComponentContact, - ] = {} - directed_contacts: set[tuple[tuple[int, int], tuple[int, int]]] = set() - - for component_index, component in enumerate(components): - ports = component.findall("./COMP_PORTS_LIST/COMP_PORT") - for port_index, port in enumerate(ports): - if port.findtext("PORT_CONNECT") != "1": - continue - for connection in port.findall("./CONNECT_LIST/CONNECT"): - target_index = int(_required_text(connection, "CONNECT_ENTITY_NUM")) - target_port_index = int(_required_text(connection, "CONNECT_ENTITY_PORT")) - if target_index < 0 or target_index >= len(components): - raise ValueError(f"Component contact references unknown entity {target_index}") - target_ports = components[target_index].findall("./COMP_PORTS_LIST/COMP_PORT") - if target_port_index < 0 or target_port_index >= len(target_ports): - raise ValueError( - f"Component contact references unknown port {target_port_index} " - f"on {aliases[target_index]}" - ) - - endpoint = (component_index, port_index) - target_endpoint = (target_index, target_port_index) - directed_contacts.add((endpoint, target_endpoint)) - key = tuple(sorted((endpoint, target_endpoint))) - first, second = key - contacts[key] = TestMqlComponentContact( - component_a=aliases[first[0]], - port_a=f"port_{first[1] + 1}", - component_b=aliases[second[0]], - port_b=f"port_{second[1] + 1}", - ) - - for endpoint, target_endpoint in directed_contacts: - if (target_endpoint, endpoint) not in directed_contacts: - raise ValueError( - "AMESim component contact is not reciprocal: " - f"{endpoint} -> {target_endpoint}" - ) - - return TestMqlCirTopology(component_contacts=tuple(contacts.values())) - - -def _topology_only_xml(cir_text: str) -> str: - # AMESim expressions inside SUBMODEL contain unescaped && and <= operators. - # Topology lives outside those blocks, so omit them before XML parsing. - return re.sub( - r".*?", - "", - cir_text, - flags=re.DOTALL, - ) - - -def _required_text(element: ET.Element, child_name: str) -> str: - value = element.findtext(child_name) - if value is None: - raise ValueError(f"Missing AMESim circuit element: {child_name}") - return value diff --git a/app/simulation/paths.py b/app/simulation/paths.py index 15ed5e4..3697b07 100644 --- a/app/simulation/paths.py +++ b/app/simulation/paths.py @@ -16,6 +16,7 @@ DATA_DIR = ( SIMULATION_RUNS_DIR = DATA_DIR / "simulation-runs" SIMULATION_BASELINES_DIR = PROJECT_ROOT / "tests" / "baselines" / "simulation" +AMESIM_TEST_MQL_ARCHIVE_PATH = PROJECT_ROOT / "AmesimModels" / "test_mql.ame" MODELICA_TESTMODEL_RESULT_PATH = ( PROJECT_ROOT / "ModelicaModels" / "Simulation" / "Testmodel_res.csv" ) diff --git a/app/simulation/registry.py b/app/simulation/registry.py index 63f7421..9754e71 100644 --- a/app/simulation/registry.py +++ b/app/simulation/registry.py @@ -17,9 +17,10 @@ from app.simulation.core.catalog import ( from app.simulation.core.metadata import ( SI_UNIT_BY_QUANTITY, ParameterDefinition, + ParameterOption, ResultVariableDefinition, ) -from app.simulation.core.medium import IdealGasMedium +from app.simulation.core.medium import GasMedium, IdealGasMedium from app.simulation.core.ports import PortDefinition, PortVariableDefinition @@ -81,7 +82,7 @@ class ComponentModelSpec: ports.append(payload) ports.sort(key=lambda item: (int(item["order"]), str(item["name"]))) - return { + payload: dict[str, object] = { "type": self.model_type, "modelType": self.model_type, "modelVersion": self.model_version, @@ -94,11 +95,14 @@ class ComponentModelSpec: parameter.as_interface_dict() for parameter in self.parameters ], } + if self.display.role is not None: + payload["role"] = self.display.role + return payload def create( self, name: str, - medium: IdealGasMedium, + medium: GasMedium, values: Mapping[str, float], ) -> Component: unknown = sorted(set(values) - set(self.parameter_by_name)) @@ -410,6 +414,14 @@ def _validate_parameter( raise ValueError(f"{field} declarations must use ParameterDefinition.") _validate_member_id(parameter.name, field=f"{field} name") _validate_label(parameter.label, field=f"{field} '{parameter.name}' label") + if not isinstance(parameter.description, str): + raise ValueError( + f"{field} '{parameter.name}' description must be a string." + ) + if parameter.description and not parameter.description.strip(): + raise ValueError( + f"{field} '{parameter.name}' description must not be blank." + ) _validate_quantity_unit( parameter.quantity, parameter.unit, @@ -433,6 +445,74 @@ def _validate_parameter( raise ValueError( f"{field} '{parameter.name}' minimum_exclusive must be a boolean." ) + if parameter.editor not in { + None, + "amesimGasReference", + "amesimGasPropertyModel", + }: + raise ValueError( + f"{field} '{parameter.name}' uses unsupported editor " + f"'{parameter.editor}'." + ) + if not isinstance(parameter.options, tuple): + raise ValueError( + f"{field} '{parameter.name}' options must use a tuple." + ) + if parameter.editor == "amesimGasPropertyModel" and not parameter.options: + raise ValueError( + f"{field} '{parameter.name}' property-model editor must declare options." + ) + if parameter.options and parameter.editor != "amesimGasPropertyModel": + raise ValueError( + f"{field} '{parameter.name}' options require the " + "'amesimGasPropertyModel' editor." + ) + option_values: list[float] = [] + for option in parameter.options: + if not isinstance(option, ParameterOption): + raise ValueError( + f"{field} '{parameter.name}' options must use ParameterOption." + ) + if ( + isinstance(option.value, bool) + or not isinstance(option.value, (int, float)) + or not isfinite(option.value) + ): + raise ValueError( + f"{field} '{parameter.name}' option values must be finite numbers." + ) + _validate_label( + option.label, + field=( + f"{field} '{parameter.name}' option " + f"'{option.value:g}' label" + ), + ) + numeric_option = float(option.value) + if ( + parameter.editor == "amesimGasPropertyModel" + and not numeric_option.is_integer() + ): + raise ValueError( + f"{field} '{parameter.name}' property-model option values " + "must be integers." + ) + option_message = parameter.validation_message(numeric_option) + if option_message is not None: + raise ValueError( + f"{field} '{parameter.name}' option {numeric_option:g} " + f"{option_message}." + ) + option_values.append(numeric_option) + duplicate_option_values = sorted( + {value for value in option_values if option_values.count(value) > 1} + ) + if duplicate_option_values: + duplicates = ", ".join(f"{value:g}" for value in duplicate_option_values) + raise ValueError( + f"{field} '{parameter.name}' contains duplicate option values: " + f"{duplicates}." + ) if ( parameter.minimum is not None and parameter.maximum is not None @@ -532,6 +612,11 @@ def validate_component_model_class( field=f"Component '{model_type}' display symbol", ) _validate_order(display.order, field=f"Component '{model_type}' display order") + if display.role not in {None, "amesimGasMediumDefinition"}: + raise ValueError( + f"Component '{model_type}' DISPLAY uses unsupported role " + f"'{display.role}'." + ) if not isinstance(display.ports, tuple): raise ValueError(f"Component '{model_type}' DISPLAY ports must be a tuple.") if display.library_id != library.id: diff --git a/app/simulation/reporting/test_mql_chamber_observations.py b/app/simulation/reporting/test_mql_chamber_observations.py index fd60209..49d9361 100644 --- a/app/simulation/reporting/test_mql_chamber_observations.py +++ b/app/simulation/reporting/test_mql_chamber_observations.py @@ -2,13 +2,13 @@ from __future__ import annotations from dataclasses import dataclass -from PythonModels.reporting.amesim_results import AmesimResults -from PythonModels.reporting.test_mql_variables import ( +from app.simulation.reporting.amesim_results import AmesimResults +from app.simulation.reporting.test_mql_variables import ( TestMqlVariableBinding, TestMqlVariableCatalog, build_test_mql_variable_catalog, ) -from PythonModels.systems.test_mql_pneumatic import ( +from app.simulation.examples.test_mql.pneumatic import ( TestMqlPneumaticAssembly, build_test_mql_pneumatic_assembly, ) diff --git a/app/simulation/reporting/test_mql_comparison.py b/app/simulation/reporting/test_mql_comparison.py index 3d65cf2..5e26b29 100644 --- a/app/simulation/reporting/test_mql_comparison.py +++ b/app/simulation/reporting/test_mql_comparison.py @@ -5,7 +5,7 @@ import csv from dataclasses import dataclass from pathlib import Path -from PythonModels.reporting.amesim_results import AmesimResults +from app.simulation.reporting.amesim_results import AmesimResults DEFAULT_TEST_MQL_ALIGNMENT_PATHS = ( diff --git a/app/simulation/reporting/test_mql_line_observations.py b/app/simulation/reporting/test_mql_line_observations.py index 0a85c8a..86904b3 100644 --- a/app/simulation/reporting/test_mql_line_observations.py +++ b/app/simulation/reporting/test_mql_line_observations.py @@ -2,13 +2,13 @@ from __future__ import annotations from dataclasses import dataclass -from PythonModels.reporting.amesim_results import AmesimResults -from PythonModels.reporting.test_mql_variables import ( +from app.simulation.reporting.amesim_results import AmesimResults +from app.simulation.reporting.test_mql_variables import ( TestMqlVariableBinding, TestMqlVariableCatalog, build_test_mql_variable_catalog, ) -from PythonModels.systems.test_mql_lines import ( +from app.simulation.examples.test_mql.lines import ( TestMqlLineAssembly, build_test_mql_line_assembly, ) diff --git a/app/simulation/reporting/test_mql_mechanical_observations.py b/app/simulation/reporting/test_mql_mechanical_observations.py index 645be29..f2c8776 100644 --- a/app/simulation/reporting/test_mql_mechanical_observations.py +++ b/app/simulation/reporting/test_mql_mechanical_observations.py @@ -2,13 +2,13 @@ from __future__ import annotations from dataclasses import dataclass -from PythonModels.reporting.amesim_results import AmesimResults -from PythonModels.reporting.test_mql_variables import ( +from app.simulation.reporting.amesim_results import AmesimResults +from app.simulation.reporting.test_mql_variables import ( TestMqlVariableBinding, TestMqlVariableCatalog, build_test_mql_variable_catalog, ) -from PythonModels.systems.test_mql_mechanical import ( +from app.simulation.examples.test_mql.mechanical import ( TestMqlMechanicalAssembly, build_test_mql_mechanical_assembly, ) diff --git a/app/simulation/reporting/test_mql_observations.py b/app/simulation/reporting/test_mql_observations.py index fe48070..1680a9f 100644 --- a/app/simulation/reporting/test_mql_observations.py +++ b/app/simulation/reporting/test_mql_observations.py @@ -2,24 +2,24 @@ from __future__ import annotations from dataclasses import dataclass -from PythonModels.reporting.amesim_results import AmesimResults -from PythonModels.reporting.test_mql_chamber_observations import ( +from app.simulation.reporting.amesim_results import AmesimResults +from app.simulation.reporting.test_mql_chamber_observations import ( TestMqlChamberObservationCatalog, build_test_mql_chamber_observation_catalog, ) -from PythonModels.reporting.test_mql_line_observations import ( +from app.simulation.reporting.test_mql_line_observations import ( TestMqlLineObservationCatalog, build_test_mql_line_observation_catalog, ) -from PythonModels.reporting.test_mql_mechanical_observations import ( +from app.simulation.reporting.test_mql_mechanical_observations import ( TestMqlMechanicalObservationCatalog, build_test_mql_mechanical_observation_catalog, ) -from PythonModels.reporting.test_mql_orifice_observations import ( +from app.simulation.reporting.test_mql_orifice_observations import ( TestMqlOrificeObservationCatalog, build_test_mql_orifice_observation_catalog, ) -from PythonModels.reporting.test_mql_variables import ( +from app.simulation.reporting.test_mql_variables import ( TestMqlVariableCatalog, build_test_mql_variable_catalog, ) diff --git a/app/simulation/reporting/test_mql_orifice_observations.py b/app/simulation/reporting/test_mql_orifice_observations.py index defe414..3279d12 100644 --- a/app/simulation/reporting/test_mql_orifice_observations.py +++ b/app/simulation/reporting/test_mql_orifice_observations.py @@ -2,13 +2,13 @@ from __future__ import annotations from dataclasses import dataclass -from PythonModels.reporting.amesim_results import AmesimResults -from PythonModels.reporting.test_mql_variables import ( +from app.simulation.reporting.amesim_results import AmesimResults +from app.simulation.reporting.test_mql_variables import ( TestMqlVariableBinding, TestMqlVariableCatalog, build_test_mql_variable_catalog, ) -from PythonModels.systems.test_mql_pneumatic import ( +from app.simulation.examples.test_mql.pneumatic import ( TestMqlPneumaticAssembly, build_test_mql_pneumatic_assembly, ) diff --git a/app/simulation/reporting/test_mql_output_schema.py b/app/simulation/reporting/test_mql_output_schema.py index 3a5661c..ae24b99 100644 --- a/app/simulation/reporting/test_mql_output_schema.py +++ b/app/simulation/reporting/test_mql_output_schema.py @@ -3,12 +3,12 @@ from __future__ import annotations from collections import Counter from dataclasses import dataclass -from PythonModels.reporting.amesim_results import AmesimResults -from PythonModels.reporting.test_mql_observations import ( +from app.simulation.reporting.amesim_results import AmesimResults +from app.simulation.reporting.test_mql_observations import ( TestMqlObservationCatalog, build_test_mql_observation_catalog, ) -from PythonModels.reporting.test_mql_variables import TestMqlVariableBinding +from app.simulation.reporting.test_mql_variables import TestMqlVariableBinding @dataclass(frozen=True) diff --git a/app/simulation/reporting/test_mql_output_validation.py b/app/simulation/reporting/test_mql_output_validation.py index 39333e2..9785aa6 100644 --- a/app/simulation/reporting/test_mql_output_validation.py +++ b/app/simulation/reporting/test_mql_output_validation.py @@ -3,12 +3,12 @@ from __future__ import annotations from dataclasses import dataclass from math import isfinite -from PythonModels.reporting.amesim_results import AmesimResults -from PythonModels.reporting.test_mql_comparison import ( +from app.simulation.reporting.amesim_results import AmesimResults +from app.simulation.reporting.test_mql_comparison import ( TestMqlComparisonResult, compare_test_mql_series, ) -from PythonModels.reporting.test_mql_output_schema import TestMqlOutputSchema +from app.simulation.reporting.test_mql_output_schema import TestMqlOutputSchema class TestMqlOutputValidationError(ValueError): diff --git a/app/simulation/reporting/test_mql_variables.py b/app/simulation/reporting/test_mql_variables.py index c453928..6a120c7 100644 --- a/app/simulation/reporting/test_mql_variables.py +++ b/app/simulation/reporting/test_mql_variables.py @@ -4,8 +4,8 @@ import re from collections import Counter from dataclasses import dataclass -from PythonModels.reporting.amesim_results import AmesimResults, AmesimVariable -from PythonModels.systems.test_mql import COMPONENT_SPECS, CONNECTION_SPECS +from app.simulation.reporting.amesim_results import AmesimResults, AmesimVariable +from app.simulation.examples.test_mql.system import COMPONENT_SPECS, CONNECTION_SPECS _UNIT_RE = re.compile(r"\[([^\]]+)\]\s*$") diff --git a/app/simulation/solvers/solver.py b/app/simulation/solvers/solver.py index 086a5df..acdb17f 100644 --- a/app/simulation/solvers/solver.py +++ b/app/simulation/solvers/solver.py @@ -21,6 +21,7 @@ class SolveIVPConfig: rtol: float = 1e-6 atol: float = 1e-8 max_step: float = 1e-3 + first_step: float | None = None @dataclass(frozen=True) @@ -162,15 +163,21 @@ def _integrate_scipy_stepwise( status="cancelled", ) + solver_options = { + "rtol": config.rtol, + "atol": config.atol, + "max_step": config.max_step, + } + if config.first_step is not None: + solver_options["first_step"] = config.first_step + try: solver = solver_type( cancellable_rhs, config.t_start, np.asarray(initial_state, dtype=float), config.t_stop, - rtol=config.rtol, - atol=config.atol, - max_step=config.max_step, + **solver_options, ) except _IntegrationCancelled: return ODESolution( @@ -296,13 +303,16 @@ def integrate_ode( accepted_step_callback, ) - return solve_ivp( - fun=rhs, - t_span=(config.t_start, config.t_stop), - y0=initial_state, - method=config.method, - rtol=config.rtol, - atol=config.atol, - max_step=config.max_step, - t_eval=t_eval, - ) + solve_options = { + "fun": rhs, + "t_span": (config.t_start, config.t_stop), + "y0": initial_state, + "method": config.method, + "rtol": config.rtol, + "atol": config.atol, + "max_step": config.max_step, + "t_eval": t_eval, + } + if config.first_step is not None: + solve_options["first_step"] = config.first_step + return solve_ivp(**solve_options) diff --git a/app/system_xml.py b/app/system_xml.py index 7c94e83..8bc06e0 100644 --- a/app/system_xml.py +++ b/app/system_xml.py @@ -1,7 +1,7 @@ from __future__ import annotations from collections.abc import Mapping -from dataclasses import dataclass +from dataclasses import dataclass, replace from functools import lru_cache from math import isfinite from pathlib import Path @@ -138,18 +138,22 @@ class SystemXmlDocument: name: str schema_version: str unit_system: str + medium_reference_version: str | None simulation: SystemXmlSimulation components: tuple[SystemXmlComponent, ...] connections: tuple[SystemXmlConnection, ...] def summary(self) -> dict[str, object]: - return { + result: dict[str, object] = { "name": self.name, "schemaVersion": self.schema_version, "unitSystem": self.unit_system, "componentCount": len(self.components), "connectionCount": len(self.connections), } + if self.medium_reference_version is not None: + result["mediumReferenceVersion"] = self.medium_reference_version + return result def as_project_data(self) -> dict[str, object]: edges = [] @@ -171,6 +175,7 @@ class SystemXmlDocument: return { "name": self.name, + "mediumReferenceVersion": 1, "nodes": [ { "id": component.id, @@ -281,7 +286,20 @@ def validate_system_xml_document( return SystemXmlValidationReport(document=None, issues=issues) document = _parse_validated_root(root) - issues = tuple(_semantic_issues(document)) + document, compatibility_issues = _normalize_legacy_amesim_gas_references( + document + ) + document, property_model_issues = _normalize_amesim_gas_property_models( + document + ) + document = replace(document, medium_reference_version="1") + issues = tuple( + [ + *compatibility_issues, + *property_model_issues, + *_semantic_issues(document), + ] + ) return SystemXmlValidationReport(document=document, issues=issues) @@ -331,6 +349,7 @@ def _parse_validated_root(root: etree._Element) -> SystemXmlDocument: name=str(root.get("name")), schema_version=str(root.get("schemaVersion")), unit_system=str(root.get("unitSystem")), + medium_reference_version=root.get("mediumReferenceVersion"), simulation=simulation, components=components, connections=connections, @@ -373,6 +392,167 @@ def _parse_component(element: etree._Element) -> SystemXmlComponent: ) +def _normalize_legacy_amesim_gas_references( + document: SystemXmlDocument, +) -> tuple[SystemXmlDocument, list[ValidationIssue]]: + """Safely normalize the pre-v1 implicit-air ``gi`` convention. + + System XML v2 files without ``mediumReferenceVersion`` predate explicit + project medium definitions. Such a document can only be migrated when + every component type is known and no component declares the + ``amesimGasMediumDefinition`` catalog role. Parameter discovery is driven + by the catalog editor contract instead of hard-coded AMESim model names. + """ + + if document.medium_reference_version is not None: + return document, [] + + specs = [ + COMPONENT_MODEL_REGISTRY.get(component.model_type) + for component in document.components + ] + if any(spec is None for spec in specs): + return document, [] + if any( + spec is not None + and spec.display.role == "amesimGasMediumDefinition" + for spec in specs + ): + return document, [] + + issues: list[ValidationIssue] = [] + normalized_components: list[SystemXmlComponent] = [] + changed = False + for component, spec in zip(document.components, specs, strict=True): + assert spec is not None + reference_parameters = { + parameter.name: parameter + for parameter in spec.parameters + if parameter.editor == "amesimGasReference" + and parameter.default == 0.0 + } + if not reference_parameters: + normalized_components.append(component) + continue + + parameters_by_name = { + parameter.name: parameter for parameter in component.parameters + } + normalized_parameters = list(component.parameters) + for name, definition in reference_parameters.items(): + existing = parameters_by_name.get(name) + path = ( + "/System/Components/" + f"Component[@id='{component.id}']/Parameter[@name='{name}']" + ) + if existing is None: + normalized_parameters.append( + SystemXmlParameter( + name=name, + value=definition.default, + line=component.line, + ) + ) + issues.append( + _semantic_issue( + "AMESIM_GI_DEFAULT_INJECTED", + f"Legacy component {component.id} omitted {name}; " + "the implicit ideal-air reference 0 was applied.", + path, + component.line, + severity="warning", + ) + ) + changed = True + continue + if existing.value != 1.0: + continue + normalized_parameters = [ + replace(parameter, value=0.0) + if parameter is existing + else parameter + for parameter in normalized_parameters + ] + issues.append( + _semantic_issue( + "AMESIM_GI_LEGACY_AIR_MAPPED", + f"Legacy component {component.id} used {name}=1 without " + "a medium-definition component; it was mapped to the " + "implicit ideal-air reference 0.", + path, + existing.line, + severity="warning", + ) + ) + changed = True + normalized_components.append( + replace(component, parameters=tuple(normalized_parameters)) + ) + + if not changed: + return document, issues + return replace(document, components=tuple(normalized_components)), issues + + +def _normalize_amesim_gas_property_models( + document: SystemXmlDocument, +) -> tuple[SystemXmlDocument, list[ValidationIssue]]: + """Add the catalog default for pre-selector medium definitions. + + Medium-definition nodes created before the property-model selector existed + only contain ``gi``. The selector is catalog driven, so its default can be + injected without hard-coding a component type or a calculation method. + """ + + issues: list[ValidationIssue] = [] + normalized_components: list[SystemXmlComponent] = [] + changed = False + + for component in document.components: + spec = COMPONENT_MODEL_REGISTRY.get(component.model_type) + if spec is None or spec.display.role != "amesimGasMediumDefinition": + normalized_components.append(component) + continue + + existing_names = {parameter.name for parameter in component.parameters} + normalized_parameters = list(component.parameters) + for definition in spec.parameters: + if ( + definition.editor != "amesimGasPropertyModel" + or definition.name in existing_names + ): + continue + normalized_parameters.append( + SystemXmlParameter( + name=definition.name, + value=definition.default, + line=component.line, + ) + ) + issues.append( + _semantic_issue( + "AMESIM_GAS_PROPERTY_MODEL_DEFAULTED", + f"Medium definition {component.id} omitted " + f"{definition.name}; catalog default {definition.default:g} " + "was applied.", + "/System/Components/" + f"Component[@id='{component.id}']/" + f"Parameter[@name='{definition.name}']", + component.line, + severity="warning", + ) + ) + changed = True + + normalized_components.append( + replace(component, parameters=tuple(normalized_parameters)) + ) + + if not changed: + return document, issues + return replace(document, components=tuple(normalized_components)), issues + + def _parse_connection(element: etree._Element) -> SystemXmlConnection: endpoints = tuple( SystemXmlEndpoint( @@ -398,6 +578,7 @@ def _semantic_issues(document: SystemXmlDocument) -> list[ValidationIssue]: _validate_system_and_simulation(document, issues) component_by_id = _validate_components(document, issues) _validate_connections(document, component_by_id, issues) + _validate_amesim_medium_references(document, component_by_id, issues) return issues @@ -664,6 +845,229 @@ def _validate_component_parameters( ) +def _validate_amesim_medium_references( + document: SystemXmlDocument, + component_by_id: Mapping[str, SystemXmlComponent], + issues: list[ValidationIssue], +) -> None: + """Validate project-scoped AMESim gas slots before model compilation.""" + + component_paths = { + id(component): f"/System/Components/Component[{index}]" + for index, component in enumerate(document.components, start=1) + } + definitions_by_index: dict[int, SystemXmlComponent] = {} + + for component in document.components: + spec = COMPONENT_MODEL_REGISTRY.get(component.model_type) + if spec is None or spec.display.role != "amesimGasMediumDefinition": + continue + component_path = component_paths[id(component)] + parameters = [ + parameter for parameter in component.parameters if parameter.name == "gi" + ] + if len(parameters) != 1: + if not parameters: + issues.append( + _semantic_issue( + "AMESIM_GAS_MEDIUM_INDEX_MISSING", + f"AMESim gas medium definition {component.id} must " + "declare exactly one gi parameter.", + component_path, + component.line, + ) + ) + continue + + parameter = parameters[0] + parameter_path = f"{component_path}/Parameter[@name='gi']" + gas_index = _integer_index(parameter.value, minimum=1, maximum=99) + if gas_index is None: + issues.append( + _semantic_issue( + "AMESIM_GAS_MEDIUM_INDEX_INVALID", + f"AMESim gas medium definition {component.id} gi must be " + "an integer between 1 and 99.", + parameter_path, + parameter.line, + ) + ) + continue + + existing = definitions_by_index.get(gas_index) + if existing is not None: + issues.append( + _semantic_issue( + "AMESIM_GAS_MEDIUM_INDEX_DUPLICATE", + f"AMESim gas medium index gi={gas_index} is defined by " + f"both {existing.id} and {component.id}.", + parameter_path, + parameter.line, + ) + ) + continue + definitions_by_index[gas_index] = component + + resolved_references: dict[ + str, + list[tuple[str, int, SystemXmlParameter]], + ] = {} + for component in document.components: + spec = COMPONENT_MODEL_REGISTRY.get(component.model_type) + if spec is None: + continue + component_path = component_paths[id(component)] + for definition in spec.parameters: + if definition.editor != "amesimGasReference": + continue + parameters = [ + parameter + for parameter in component.parameters + if parameter.name == definition.name + ] + if len(parameters) != 1: + continue + + parameter = parameters[0] + parameter_path = ( + f"{component_path}/Parameter[@name='{definition.name}']" + ) + gas_index = _integer_index(parameter.value, minimum=0, maximum=99) + if gas_index is None: + issues.append( + _semantic_issue( + "AMESIM_GAS_REFERENCE_INDEX_INVALID", + f"AMESim gas reference {component.id}.{definition.name} " + "must be an integer between 0 and 99.", + parameter_path, + parameter.line, + ) + ) + continue + if gas_index > 0 and gas_index not in definitions_by_index: + issues.append( + _semantic_issue( + "AMESIM_GAS_REFERENCE_UNDEFINED", + f"AMESim gas reference {component.id}.{definition.name} " + f"uses gi={gas_index}, but no gas medium definition " + "declares that index.", + parameter_path, + parameter.line, + ) + ) + continue + resolved_references.setdefault(component.id, []).append( + (definition.name, gas_index, parameter) + ) + + _validate_connected_amesim_gas_references( + document, + component_by_id, + resolved_references, + issues, + ) + + +def _integer_index(value: float, *, minimum: int, maximum: int) -> int | None: + if not isfinite(value) or value != int(value): + return None + index = int(value) + if not minimum <= index <= maximum: + return None + return index + + +def _validate_connected_amesim_gas_references( + document: SystemXmlDocument, + component_by_id: Mapping[str, SystemXmlComponent], + resolved_references: Mapping[ + str, + list[tuple[str, int, SystemXmlParameter]], + ], + issues: list[ValidationIssue], +) -> None: + if len(component_by_id) != len(document.components): + return + + parents = {component_id: component_id for component_id in component_by_id} + + def find(component_id: str) -> str: + parent = parents[component_id] + while parent != parents[parent]: + parent = parents[parent] + while component_id != parent: + next_component = parents[component_id] + parents[component_id] = parent + component_id = next_component + return parent + + def union(left: str, right: str) -> None: + left_root = find(left) + right_root = find(right) + if left_root != right_root: + parents[right_root] = left_root + + for connection in document.connections: + if connection.kind != "physical" or connection.domain != "pneumatic": + continue + first_endpoint, second_endpoint = connection.endpoints + first_component = component_by_id.get(first_endpoint.component) + second_component = component_by_id.get(second_endpoint.component) + if first_component is None or second_component is None: + continue + first_port = first_component.port_by_name.get(first_endpoint.port) + second_port = second_component.port_by_name.get(second_endpoint.port) + if ( + first_port is None + or second_port is None + or first_port.kind != "physical" + or second_port.kind != "physical" + or first_port.domain != "pneumatic" + or second_port.domain != "pneumatic" + ): + continue + union(first_component.id, second_component.id) + + references_by_root: dict[ + str, + dict[int, list[tuple[str, str, SystemXmlParameter]]], + ] = {} + for component_id, references in resolved_references.items(): + if component_id not in parents: + continue + by_index = references_by_root.setdefault(find(component_id), {}) + for parameter_name, gas_index, parameter in references: + by_index.setdefault(gas_index, []).append( + (component_id, parameter_name, parameter) + ) + + for references_by_index in references_by_root.values(): + if len(references_by_index) <= 1: + continue + details = ", ".join( + f"gi={gas_index} " + f"({', '.join(sorted(component_id for component_id, _, _ in references))})" + for gas_index, references in sorted(references_by_index.items()) + ) + first_reference = next( + reference + for references in references_by_index.values() + for reference in references + ) + component_id, parameter_name, parameter = first_reference + issues.append( + _semantic_issue( + "AMESIM_GAS_REFERENCE_CONFLICT", + "Connected pneumatic circuit contains conflicting AMESim " + f"gas references: {details}.", + "/System/Components/" + f"Component[@id='{component_id}']/" + f"Parameter[@name='{parameter_name}']", + parameter.line, + ) + ) + + def _validate_connections( document: SystemXmlDocument, component_by_id: dict[str, SystemXmlComponent], diff --git a/docs/README.md b/docs/README.md index e811c2d..24105a4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -12,6 +12,8 @@ `GET /api/components/catalog` 的机器可读结构。 4. [AMESim 子模型公开组件迁移矩阵](amesim-component-migration-matrix.md) 用于划分 `test_mql` 子模型族的公开组件、内部模型和暂不支持范围。 +5. [AMESim 氦气 Peng-Robinson 介质模型](amesim-helium-peng-robinson.md) + 记录本地 AMESim 资料、氦气参数、索引映射和首版计算边界。 建议人工和 AI 先阅读建模规范,再阅读读取规范,然后参考目标分类中最接近的现有 模型。不要从前端兜底数据反推后端物理契约。 @@ -36,6 +38,8 @@ | 库和显示声明 | [`app/simulation/core/catalog.py`](../app/simulation/core/catalog.py) | | 库发现与注册校验 | [`app/simulation/registry.py`](../app/simulation/registry.py) | | 临时库清单 | [`app/simulation/components/experimental/library.py`](../app/simulation/components/experimental/library.py) | +| AMESim 第一版公开临时库清单 | [`app/simulation/components/amesim/library.py`](../app/simulation/components/amesim/library.py) | +| `test_mql` 固定算例入口 | [`app/simulation/examples/test_mql/system.py`](../app/simulation/examples/test_mql/system.py) | | 元件完整示例 | [`app/simulation/components/example.md`](../app/simulation/components/example.md) | ## AI 使用原则 diff --git a/docs/amesim-component-migration-matrix.md b/docs/amesim-component-migration-matrix.md index 6c9138b..7ab5770 100644 --- a/docs/amesim-component-migration-matrix.md +++ b/docs/amesim-component-migration-matrix.md @@ -1,62 +1,64 @@ # AMESim 子模型公开组件迁移矩阵 -状态:规划中 -适用模型:`AmesimModels/test_mql.ame` / `PythonModels.systems.test_mql` +状态:21 个模型已在临时 AMESim 库完成第一版公开,持续校准中 + +适用模型:`AmesimModels/test_mql.ame` / `app.simulation.examples.test_mql.system` + 配套规范:[`component-model-authoring-spec-v1.md`](component-model-authoring-spec-v1.md) ## 目标 -本文档把 `test_mql` 中的 AMESim 子模型族拆成可执行的迁移批次。它不是组件注册清单;只有完成公开模型契约、方程实现、最小 XML 编译/仿真测试后,子模型才能加入某个 `library.py` 的 `models` 清单并出现在前端组件库中。 +本文档记录 `test_mql` 中 AMESim 子模型族的公开状态和后续校准批次。实际注册清单以 `app/simulation/components/amesim/library.py` 为准。 当前原则: - 不把现有固定拓扑 `TestMqlSystem` 整体注册为公开拖拽组件。 -- 不把机械、信号、事件元件提前伪装成可由当前气动求解器仿真的公开组件。 -- 可先公开当前 `app.simulation` 求解链路能承载的气动元件:储能、阻性、孔口、三通/四通、准稳态管段。 -- 对 AMESim 精确语义尚未复刻的元件,允许先做内部模型或文档化候选,不能在目录中标记为完成。 +- 第一版公开只表示模型契约、目录、XML 编译和最小仿真链路已接通,不表示 AMESim 精确语义已完成复刻。 +- 气动、标量信号和一维机械模型按当前求解链路能力公开;事件与跨域耦合继续保留明确限制。 +- 固定算例专用的校准原语留在 `examples/test_mql/`,不与公共组件目录混为同一实现。 ## 当前求解器能力边界 `app.simulation` 当前支持: -- 物理域:`pneumatic`。 -- 端口变量:`p`、`m_flow`、`h_outflow`。 +- 物理域:`pneumatic`、标量 `signal` 和一维 `mechanical`。 +- 端口变量:气动 `p/m_flow/h_outflow`、信号 `signal`、机械 `x/v/f`。 - 流量符号:`m_flow > 0` 表示流入当前组件。 -- 网络方程:连接压力相等、连接流量和为零、组件压力-流量残差、stream 焓传播。 +- 网络方程:气动连接压力相等、流量和为零、组件压力-流量残差与 stream 焓传播;标量信号传播;机械 `x/v` 等值和 `f` 平衡。 - 动态:半显式 ODE,动态组件提供质量和内能状态导数。 -- 暂不支持:机械端口、信号端口仿真、事件系统、一般高指数 DAE、AMESim 气体定义组件对全局 medium 的动态切换。 +- 暂不支持:完整事件系统、一般高指数 DAE、气动-机械实时跨域组件,以及从 PNGD00 自动批量导入其他气体。公共编译链已经支持“介质 + 物性计算方法”定义元件:`gi=0` 使用内置空气理想气体,`gi=1..99` 引用画布中的显式介质物性实例;介质定义内部通过 `property_model` 下拉接口选择计算方法。氦气已提供 Peng-Robinson 首版。 ## 子模型族矩阵 | AMESim 子模型 | 数量 | AMESim 角色 | 当前公开状态 | 建议目标 | 先决条件 / 限制 | | --- | ---: | --- | --- | --- | --- | -| `PNCH023` | 4 | 固定容积气室,带换热 | 候选公开 | `amesim_pnch023`,`storage` | 可基于 `ThermodynamicVolumeComponent`;需按 AMESim `cvol/extemp/kth/sth/gi` 复核质量、能量、换热方程。 | -| `PNCH012` | 8 | 变容气室,带换热 | 参数化第一版公开 | `amesim_pnch012`,`storage` | 已将 AMESim `vol1..4/dvol1..4` 外部体积输入映射为 SI 参数,支持固定/预设体积场景;实时机械耦合仍需后续机械域或输入端口协议。 | -| `PNOR001` | 8 | 常系数气动孔口 | 候选公开 | `amesim_pnor001`,`flow` | 可基于 `AlgebraicComponent`;需按 AMESim `cq/area/Cv/Kv/flowset/gi` 复核双向流、零压差正则化和单位换算。 | -| `PNVO001` | 8 | 信号调制气动孔口 | 第一版公开 | `amesim_pnvo001`,`flow` | 已接入标量信号端口 `res`,可由 `amesim_step0` 驱动开度;精确事件语义和 AMESim baseline 仍留后续修模。固定开度变体 `amesim_pnvo001_fixed` 继续保留。 | -| `PN3NODE2` | 8 | 三端气动节点,压力温度由 port 2 固定 | 候选公开 | `amesim_pn3node2`,`junctions` | 当前 `Tee` 是通用三通近似;AMESim port 2 参考温压语义和 stream 混合需单独测试。 | -| `P4NODE2` | 8 | 四端气动节点,压力温度由 port 2 固定 | 候选公开 | `amesim_p4node2`,`junctions` | 需要新增四端 junction 基类/模型;复核 port 2 参考温压和多支路混合。 | -| `PNL00R` | 4 | 管路纯阻性摩擦段 | 候选公开 | `amesim_pnl00r`,`flow` | 可基于准稳态阻性管;需按 AMESim `PNL00R` 参数和摩擦公式复核。 | +| `PNCH023` | 4 | 固定容积气室,带换热 | 第一版公开 | `amesim_pnch023`,`storage` | 已基于 `ThermodynamicVolumeComponent` 接入公开契约;仍需按 AMESim `cvol/extemp/kth/sth/gi` 复核质量、能量、换热方程。 | +| `PNCH012` | 8 | 变容气室,带换热 | 参数化第一版公开 | `amesim_pnch012`,`storage` | 已将 AMESim `vol1..4/dvol1..4` 外部体积输入映射为 SI 参数,支持固定/预设体积场景;实时机械耦合仍需跨域组件契约和闭合支持。 | +| `PNOR001` | 8 | 常系数气动孔口 | 第一版公开 | `amesim_pnor001`,`flow` | 已接入代数组件公开契约;仍需按 AMESim `cq/area/Cv/Kv/flowset/gi` 复核双向流、零压差正则化和单位换算。 | +| `PNVO001` | 8 | 信号调制气动孔口 | 第一版公开 | `amesim_pnvo001`,`flow` | 已接入名为 `res` 的标量信号输入端口,可由 `amesim_step0` 驱动开度;精确事件语义和 AMESim baseline 仍留后续修模。固定开度变体 `amesim_pnvo001_fixed` 继续保留。 | +| `PN3NODE2` | 8 | 三端气动节点,压力温度由 port 2 固定 | 第一版公开 | `amesim_pn3node2`,`junctions` | 已接入三端等压、流量守恒基础版;AMESim port 2 参考温压语义和 stream 混合仍需单独测试。 | +| `P4NODE2` | 8 | 四端气动节点,压力温度由 port 2 固定 | 第一版公开 | `amesim_p4node2`,`junctions` | 已接入四端等压、流量守恒基础版;仍需复核 port 2 参考温压和多支路混合。 | +| `PNL00R` | 4 | 管路纯阻性摩擦段 | 第一版公开 | `amesim_pnl00r`,`flow` | 已接入准稳态阻性管公开契约;仍需按 AMESim `PNL00R` 参数和摩擦公式复核。 | | `PNL0001` | 20 | C-R 动态管路 | 第一版公开 | `amesim_pnl0001`,`flow` | 已按公开契约接入两状态管内容积 + port 1 摩擦残差 + mode 2 换热项;仍需后续按 AMESim baseline 复核 `pn2pipefr_` 和 mode 1 多方语义。 | | `PNL0002` | 8 | R-C-R 动态管路 | 第一版公开 | `amesim_pnl0002`,`flow` | 已按公开契约接入中心两状态容积 + 两端半长摩擦残差 + mode 2 换热项;仍需后续按 AMESim baseline 复核 `pn2pipefr_` 和 mode 1 多方语义。 | | `PNL0003` | 8 | C-R-C 动态管路 | 第一版公开 | `amesim_pnl0003`,`flow` | 已按公开契约接入两端四状态容积 + 中心摩擦流 + mode 2 换热项;大压差动态闭合和 AMESim baseline 误差仍留后续修模。 | | `PNPL01` | 16 | 零气动流源 | 第一版公开 | `amesim_pnpl01`,`boundary` | 当前实现一端零流边界,只约束端口质量流量为 0;压力源/外部边界语义留后续扩展。 | -| `PNGD00` | 1 | 氦气气体定义 | 暂不公开 | medium 配置,不是画布物理组件 | 应映射为系统/介质设置;不能作为普通可连接组件注册。 | -| `PNRP17` | 8 | 气动活塞与移动体耦合 | 暂不公开 | 内部固定算例;后续跨域组件 | 依赖气动端口和机械端口耦合;需要新增机械域、跨域状态和连接规则。 | +| `PNGD00` | 1 | 氦气气体定义 | 氦气 Peng-Robinson 首版公开 | `amesim_helium_medium`,`media` | 已映射源模型 `fluidType=12/eosType=6`;密度和压力使用 PR EOS,热量学暂用手册参考点的定比热闭合。完整压力相关残余焓、比热和真实气体临界流仍待状态相关物性接口。 | +| `PNRP17` | 8 | 气动活塞与移动体耦合 | 暂不公开 | 内部固定算例;后续跨域组件 | 需要跨域组件契约、实时气动-机械耦合,以及对应的事件、状态和闭合规则。 | | `MECMAS21` | 10 | 一维平动质量 | 第一版公开 | `amesim_mecmas21`,`mechanical` | 已接入一维机械端口 `x/v/f`、双端质量状态和基本摩擦/限位项,并跑通零力源与信号力源最小 System XML;完整 AMESim 接触/事件语义仍留后续对齐。 | | `LMECHN1` | 2 | 动态线性机械节点 | 第一版公开 | `amesim_lmechn1`,`mechanical` | 已接入 9 端一维机械节点、端口位移/速度等值和节点力平衡,并跑通 `FORC -> LMECHN1 -> MECMAS21` 最小 System XML;端口朝向/符号细节留后续 AMESim baseline 对齐。 | | `LSTP00A` | 8 | 弹性接触/端止动 | 第一版公开 | `amesim_lstp00a`,`mechanical` | 已接入两端一维机械端口、相对位移/速度接触力和最小 System XML 仿真;当前是连续罚函数基础版,完整 AMESim 事件/非光滑接触语义留后续 baseline 对齐。 | | `F000` | 16 | 零力源 | 第一版公开 | `amesim_f000`,`mechanical` | 已作为一端机械零力边界公开,约束端口力为 0。 | | `FORC` | 2 | 信号转力 | 第一版公开 | `amesim_forc`,`mechanical` | 已接入信号输入 `res` 到机械端口力源,可由 `STEP0/UD00` 驱动质量组件。 | | `STEP0` | 8 | 阶跃信号源 | 第一版公开 | `amesim_step0`,`signals` | 已接入标量信号输出端口和求解时信号传播;当前是基础阶跃,不含更复杂事件调度语义。 | -| `UD00` | 2 | 分段线性信号源 | 第一版公开 | `amesim_ud00`,`signals` | 已接入标量信号输出端口、8 段 start/end/t 参数、循环模式和 System XML signal connection;当前按已转换 PythonModels 语义处理最后一段外推,AMESim baseline 仍留后续复核。 | +| `UD00` | 2 | 分段线性信号源 | 第一版公开 | `amesim_ud00`,`signals` | 已接入标量信号输出端口、8 段 start/end/t 参数、循环模式和 System XML signal connection;当前按迁移实现语义处理最后一段外推,AMESim baseline 仍留后续复核。 | | `DIRECT` | 44 | 直接连接 | 不注册为组件 | System XML `Connection` | 连接不是组件;物理连接必须保持无方向端点语义。 | ## 建议迁移批次 -### 批次 1:可进入当前气动求解器的公开组件 +### 批次 1:已进入当前气动求解器的公开组件 -优先级最高,但仍需先补 AMESim 公式与测试: +以下模型已完成第一版公开,后续重点是 AMESim 公式和 baseline 精修: - `amesim_pnor001` - `amesim_pnl00r` @@ -64,7 +66,7 @@ - `amesim_pn3node2` - `amesim_p4node2` -完成定义:每个模型必须有 `MODEL_TYPE/MODEL_VERSION/PORTS/PARAMETERS/RESULT_VARIABLES/DISPLAY/create()`,并补目录、参数边界、残差、零流量/反向流、最小 XML 编译和短时仿真测试。 +这些模型已有 `MODEL_TYPE/MODEL_VERSION/PORTS/PARAMETERS/RESULT_VARIABLES/DISPLAY/create()` 契约和目录、XML、最小仿真测试;第一版状态不代表数值对齐完成。 ### 批次 2:动态管路 @@ -86,7 +88,7 @@ ## 下一步执行建议 -1. 先为 AMESim 公开组件建立独立库,例如 `app.simulation.components.amesim`,不要混入 `experimental`。 -2. 先只登记一个完成度最高的气动代数组件,例如 `PNOR001` 或 `PNL00R`。 -3. 每登记一个模型,都同步补充测试和目录校验,确认 `/api/components/catalog`、System XML 编译和最小仿真都通过。 -4. 动态管路 `PNL0001/2/3`、参数化 `PNCH012`、`STEP0`、`UD00`、信号版 `PNVO001` 和机械基础件 `F000/FORC/MECMAS21/LSTP00A/LMECHN1` 已完成第一版公开接入;下一步转向 `PNRP17` 或完整实时耦合。 +1. 以 `app.simulation.components.amesim.library` 的 21 个模型为公开清单唯一来源。 +2. 优先校准 `PNCH023 / PNOR001 / PN3NODE2 / P4NODE2 / PNL00R` 与动态管路的 AMESim baseline 误差。 +3. 每次调整模型都同步补充目录校验、参数边界、System XML 编译和最小仿真测试。 +4. 跨域能力下一步聚焦 `PNRP17` 或等价实时气动-机械耦合;在事件和闭合能力完成前保持暂不公开。 diff --git a/docs/amesim-helium-peng-robinson.md b/docs/amesim-helium-peng-robinson.md new file mode 100644 index 0000000..ae46029 --- /dev/null +++ b/docs/amesim-helium-peng-robinson.md @@ -0,0 +1,76 @@ +# AMESim 氦气 Peng-Robinson 介质模型 + +## 本地资料依据 + +仓库中已经包含该模型所需的说明和算例数据: + +- `AmesimModels/help/pneumatic_library_manual/03_gas_properties.pdf` 第 3.3 节 + 给出 AMESim Pneumatic Library 的气体物性层级和 Peng-Robinson 状态方程。 +- `AmesimModels/help/pneumatic_library_manual/lib_pneumatic.pdf` 第 87--88 页 + 给出 293.15 K、1 barA 下的氦气参考物性。 +- `AmesimModels/test_mql.ame` 中的 `PNGD00` 实例明确选择 + `fluidType=12`(helium)和 `eosType=6`(Peng-Robinson)。 +- `app/simulation/core/peng_robinson.py` 已实现纯物质 Peng-Robinson 方程, + 并定义了 `HELIUM_PR`。 + +AMESim 手册中的 Peng-Robinson 形式为: + +```text +p = r T / (v - b) - a alpha(T) / (v^2 + 2 b v - b^2) +a = 0.457235583 r^2 Tc^2 / Pc +b = 0.07779607 r Tc / Pc +alpha(T) = [1 + m (1 - sqrt(T/Tc))]^2 +m = 0.37464 + 1.54226 omega - 0.26992 omega^2 +``` + +当前 Python EOS 使用等价的摩尔体积形式,并通过最大物理解选择气相根。 + +## 首版参数 + +| 参数 | 数值 | 本地依据 | +| --- | ---: | --- | +| 摩尔质量 | `0.004002602 kg/mol` | `HELIUM_PR` | +| 临界温度 | `5.1953 K` | `HELIUM_PR` | +| 临界压力 | `227460 Pa` | `HELIUM_PR` | +| 偏心因子 | `-0.385` | `HELIUM_PR` | +| 定压比热 | `5193 J/(kg*K)` | AMESim 手册表 11.1 | +| 定容比热 | `3116 J/(kg*K)` | AMESim 手册表 11.1 | +| 比气体常数 | 由 `R/M` 计算,约 `2077.26 J/(kg*K)` | EOS + 手册表 11.1 | +| 293.15 K 黏度 | `1.96e-5 Pa*s` | AMESim 手册表 11.2 | +| Sutherland 常数 | `79.4 K` | 已迁移 `test_mql` 管路近似 | + +手册参考点还给出 `rho=0.164 kg/m3`、`gamma=1.667`、`Z=1.0005`。 + +## 组件与索引映射 + +公开介质组件使用: + +```text +modelType = amesim_helium_medium +gi = 画布自动分配的 1..99 引用索引 +property_model = 0 (Peng-Robinson,本应用内的稳定编号) +fluidType = 12 (AMESim 元数据) +eosType = 6 (AMESim 元数据) +``` + +`property_model` 与 AMESim 的 `eosType` 有意分离:前者是单个介质组件内部的 +下拉选项编号,后者用于保留 AMESim 原模型语义。以后给氦气增加其他计算方法时, +只需扩展氦气的物性模型注册表。 + +源归档的 `gasName` 缓存文本与结构字段存在冲突,因此介质识别以 +`fluidType=12`、`PNGD_HELIUM` 和组件标签为准,不使用 `gasName` 猜测。 + +## 首版计算边界 + +首版公共组件实现以下闭合: + +- `rho(p,T)`、`p(rho,T)` 和控制容积压力使用 Peng-Robinson EOS; +- `u=cv*T`、`h=cp*T`,温度反解使用同一组定比热; +- 黏度使用以 293.15 K 为参考点的 Sutherland 近似; +- `gi` 注册、气动连通域一致性、XML/JSON 保存均复用现有介质机制。 + +这是一版可运行的“PR 压力--密度闭合 + 定比热热量学”,不是 AMESim 真实气体 +物性的完全复刻。AMESim 的 `Cp/Cv/h/mu/rho/Z` 接口均可随 `(p,T)` 变化,而当前 +`GasMedium` 的焓、内能和比热接口只有温度参数。后续若要接入完整残余焓、 +压力相关比热和真实气体临界流,应先扩展为状态相关物性接口,再调整气室、管路和 +孔口公式。 diff --git a/docs/component-library-spec-v1.md b/docs/component-library-spec-v1.md index e6d0df5..cf138fe 100644 --- a/docs/component-library-spec-v1.md +++ b/docs/component-library-spec-v1.md @@ -550,6 +550,30 @@ GET /api/components/catalog 6. 使用 `symbol` 选择图标渲染器。 7. 不在前端重新定义参数默认值、边界或端口语义。 +目录协议提供以下可选的 AMESim 介质扩展字段: + +- 参数的 `editor: "amesimGasReference"` 表示该数值不是普通连续量,而是 + 项目介质定义的 `gi` 引用。前端应保留索引 `0`,并从当前画布的介质定义 + 组件生成其余下拉项;索引下拉项只显示数值,不拼接介质名称或中文说明。 +- 参数的 `editor: "amesimGasPropertyModel"` 表示该参数选择介质定义内部的 + 物性计算模型。参数同时提供 `options: [{"value": 0, "label": "理想气体"}]` + 一类目录数据,前端据此生成下拉栏;后续增加算法时由介质模型注册新的选项, + 前端不硬编码算法名称。 +- 模型的 `role: "amesimGasMediumDefinition"` 表示该模型是项目级介质定义。 + 此类模型允许 `ports: []`,在 System XML v2 中仍按普通零端口 + `Component` 保存。 + +这些字段在目录对象中均为可选。宽松读取目录的消费者可以把未知编辑器参数 +退化为普通数值输入;按本仓库 JSON Schema 严格校验的消费者必须与后端成套 +升级,才能识别新增的 `editor` 值和 `options` 字段。正式前端必须依据目录字段 +生成控件,不能硬编码具体 AMESim 模型名。 + +`property_model` 是每种介质组件内部的稳定选项编号,不等同于 AMESim 原始 +`eosType`。例如空气组件的 `property_model=0` 表示理想气体,并映射到 +`fluidType=2/eosType=1`;氦气组件的 `property_model=0` 表示 +Peng-Robinson,并映射到 `fluidType=12/eosType=6`。编译层负责保存这种映射, +前端只使用目录选项。 + 当前前端保留内置兜底目录,用于后端未启动时继续打开工程。兜底只是一种开发期 容错机制,不能成为新增模型的正式注册方式;正式环境应明确提示目录加载失败。 @@ -603,6 +627,12 @@ System XML 中: - ``:必须存在于模型的 `PORTS`。 - ``:必须存在于模型的 `PARAMETERS`。 +介质定义组件不通过物理端口连接。编译器先收集目录角色为 +`amesimGasMediumDefinition` 的零端口组件,再解析带 +`editor="amesimGasReference"` 参数的组件引用;介质定义组件本身不进入数值 +仿真网络。System XML 语义校验会在编译前检查介质索引的整数范围、定义唯一 +性、正索引引用完整性,以及同一气动连通分量的引用一致性。 + XML 解析器只负责结构、引用和契约校验;模型注册中心负责选择 Python 类并创建实例; 模型自身负责方程和状态。三层职责不得混合。 diff --git a/docs/system-xml-v2.md b/docs/system-xml-v2.md index e518453..163ec4d 100644 --- a/docs/system-xml-v2.md +++ b/docs/system-xml-v2.md @@ -5,6 +5,8 @@ System XML v2 是 ReactFlow 建模前端与 `app.simulation` 仿真层之间的 ## 基本约定 - 根元素 `System` 的 `schemaVersion` 固定为 `2`,`unitSystem` 固定为 `SI`。 +- 新导出的介质引用语义使用可选根属性 + `mediumReferenceVersion="1"`。缺少该属性的文档按旧版介质索引规则读取。 - 子元素顺序固定为 `Simulation`、`Components`、`Connections`。 - `Component.id` 是稳定实例 ID,`name` 是用户可编辑名称。 - 参数以 SI 基准值保存;显示单位不改变 XML 中的数值含义。 @@ -16,7 +18,7 @@ System XML v2 是 ReactFlow 建模前端与 `app.simulation` 仿真层之间的 ```xml - + @@ -80,6 +82,71 @@ System XML v2 是 ReactFlow 建模前端与 `app.simulation` 仿真层之间的 XSD 负责结构和基础枚举校验,端口注册、拓扑完整性与可求解性由模型校验层负责。 +### AMESim 介质定义与索引兼容 + +AMESim 介质定义继续使用普通的零端口 `Component`,不增加新的 XML +层级。其介质索引和物性选择仍以数值 `Parameter` 保存,例如: + +```xml + + + + +``` + +氦气 Peng-Robinson 定义使用相同结构: + +```xml + + + + +``` + +这里的 `property_model=0` 是氦气组件内部的 Peng-Robinson 选项编号;编译时 +同时保留源 AMESim `fluidType=12/eosType=6` 元数据,不能把两个编号体系混用。 + +气动组件的 `gi=0` 表示内置的空气理想气体;正整数引用画布中的显式介质 +定义。介质定义的 `property_model` 是该介质内部的物性计算模型编号,当前 +空气定义中的 `0` 表示理想气体。界面中的 `gi` 下拉栏只显示索引数值, +`property_model` 下拉栏的名称和可选值则来自组件目录。 + +解析旧文档时,只有同时满足以下条件才执行旧 `gi` 兼容转换: + +- 根元素没有 `mediumReferenceVersion`; +- 所有组件类型均可由当前注册表识别; +- 画布中不存在目录角色为 `amesimGasMediumDefinition` 的组件。 + +在这种可证明没有显式介质定义的旧文档中,缺失的 `gi` 会补为 `0`,旧 +`gi=1` 会映射为 `0`,并返回语义层警告。带有 +`mediumReferenceVersion="1"` 的新文档不会重解释正整数索引。 + +在物性计算模型下拉接口加入之前保存的介质定义可能只有 `gi`。无论是否存在 +`mediumReferenceVersion`,解析器都会为缺失的 `property_model` 注入组件目录 +声明的默认值,并返回 `AMESIM_GAS_PROPERTY_MODEL_DEFAULTED` 警告;重新保存后 +该参数会显式写入 XML。 + +完成兼容转换后,解析器统一按介质引用版本 1 执行以下语义校验: + +- 介质定义组件的 `gi` 必须是 `1..99` 的整数,且工程内不得重复。 +- 气动组件的介质引用必须是 `0..99` 的整数。 +- `gi=0` 始终引用内置空气理想气体;所有正索引必须存在对应介质定义。 +- 同一个气动连通分量内只能使用一个 `gi`,不同的独立气动网络可以选择不同 + 介质。 + +相应错误码为 `AMESIM_GAS_MEDIUM_INDEX_INVALID`、 +`AMESIM_GAS_MEDIUM_INDEX_DUPLICATE`、 +`AMESIM_GAS_REFERENCE_INDEX_INVALID`、`AMESIM_GAS_REFERENCE_UNDEFINED` 和 +`AMESIM_GAS_REFERENCE_CONFLICT`。XML 解析得到的规范化工程 JSON 总是输出 +数值字段 `"mediumReferenceVersion": 1`;因此旧文档一经解析并重新保存,便 +不再依赖旧版启发式迁移。 + ## 仿真模型编译接口 `POST /api/reactflow/compile-model` 接收与工程保存、XML 导出相同的 ReactFlow 工程 JSON。它会执行以下操作: diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 898127a..409625d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -8,10 +8,12 @@ import { useRef, useState, } from "react"; +import { createPortal } from "react-dom"; import type { CSSProperties, KeyboardEvent as ReactKeyboardEvent, PointerEvent as ReactPointerEvent, + ReactNode, } from "react"; import { ChevronDown, @@ -26,6 +28,7 @@ import { FolderOpen, Grid2X2, Grip, + List, ListChecks, Magnet, Maximize2, @@ -54,6 +57,7 @@ import { ReactFlowProvider, SelectionMode, addEdge, + applyNodeChanges, type Connection, type Edge, type EdgeChange, @@ -73,6 +77,18 @@ import { type SimulationResultsSnapshot, } from "./SimulationResultsView"; import { AutoFitView } from "./AutoFitView"; +import { + ComponentSymbol, + componentSymbolPresentation, + hasComponentSymbol, +} from "./ComponentSymbol"; +import { + ParameterTable, + loadParameterColumnLayout, + saveParameterColumnLayout, + type ParameterColumnLayout, + type ParameterTableRow, +} from "./ParameterTable"; import { WorkspaceViewTabs, type WorkspaceView, @@ -103,12 +119,20 @@ type UnitOption = { type ParameterDefinition = { label: string; + description?: string; unit?: string; quantity?: UnitQuantity; default: ParameterValue; min?: number; max?: number; minExclusive?: boolean; + editor?: "amesimGasReference" | "amesimGasPropertyModel"; + options?: ParameterOption[]; +}; + +type ParameterOption = { + value: number; + label: string; }; type ComponentDefinition = { @@ -123,6 +147,7 @@ type ComponentDefinition = { label: string; order: number; }; + role?: "amesimGasMediumDefinition"; ports: PortDefinition[]; parameters: Record; }; @@ -140,12 +165,15 @@ type ComponentLibraryDefinition = { type ComponentCatalogParameterPayload = { name: string; label: string; + description?: string; quantity: string; unit: string; default: number; minimum?: number; maximum?: number; minimumExclusive: boolean; + editor?: "amesimGasReference" | "amesimGasPropertyModel"; + options?: ParameterOption[]; }; type ComponentCatalogComponentPayload = { @@ -160,6 +188,7 @@ type ComponentCatalogComponentPayload = { label: string; order: number; }; + role?: "amesimGasMediumDefinition"; ports: Array; parameters: ComponentCatalogParameterPayload[]; }; @@ -220,6 +249,7 @@ type ProjectEdgePayload = { type ReactFlowProjectPayload = { name: string; + mediumReferenceVersion?: 1; nodes: ProjectNodePayload[]; edges: ProjectEdgePayload[]; simulation: SimulationConfig; @@ -288,12 +318,24 @@ type CanvasGridVisibility = { dots: boolean; }; +type PaletteDisplayMode = "detailed" | "icons"; + +type PaletteIconPreview = { + definition: ComponentDefinition; + left: number; + top: number; +}; + const AUTOSAVE_KEY = "system-simulation-flow:autosave"; const PROJECT_KEY_PREFIX = "system-simulation-flow:project:"; const RESULT_SNAPSHOT_KEY = "system-simulation-flow:latest-result"; const CONSOLE_PLACEMENT_KEY = "system-simulation-flow:console-positions"; const MODELING_PANE_LAYOUT_KEY = "system-simulation-flow:modeling-pane-layout"; const CANVAS_GRID_VISIBILITY_KEY = "system-simulation-flow:canvas-grid-visibility"; +const PALETTE_DISPLAY_MODE_KEY = "system-simulation-flow:palette-display-mode"; +const PALETTE_COLLAPSED_SECTIONS_KEY = + "system-simulation-flow:palette-collapsed-sections"; +const PALETTE_ICON_PREVIEW_DELAY_MS = 500; const CONSOLE_VIEWPORT_MARGIN = 8; const SIMULATION_STREAM_IDLE_TIMEOUT_MS = 30_000; const SIMULATION_SOLVER_STALL_TIMEOUT_MS = 60_000; @@ -398,6 +440,53 @@ function loadCanvasGridVisibility(): CanvasGridVisibility { return { lines: false, dots: true }; } +function loadPaletteDisplayMode(): PaletteDisplayMode { + try { + const storedMode = localStorage.getItem(PALETTE_DISPLAY_MODE_KEY); + return storedMode === "icons" || storedMode === "detailed" + ? storedMode + : "detailed"; + } catch { + return "detailed"; + } +} + +function loadPaletteCollapsedSections() { + try { + const parsed = JSON.parse( + localStorage.getItem(PALETTE_COLLAPSED_SECTIONS_KEY) ?? "[]", + ) as unknown; + if (!Array.isArray(parsed)) { + return new Set(); + } + return new Set( + parsed.filter( + (value): value is string => + typeof value === "string" && + (value.startsWith("library:") || value.startsWith("category:")), + ), + ); + } catch { + return new Set(); + } +} + +function paletteLibraryCollapseKey(libraryId: string) { + return `library:${libraryId}`; +} + +function paletteCategoryCollapseKey(libraryId: string, categoryId: string) { + return `category:${libraryId}:${categoryId}`; +} + +function paletteLibraryContentId(libraryId: string) { + return `palette-library-content-${encodeURIComponent(libraryId)}`; +} + +function paletteCategoryItemsId(libraryId: string, categoryId: string) { + return `palette-category-items-${encodeURIComponent(libraryId)}-${encodeURIComponent(categoryId)}`; +} + const identityUnit = (value: number) => value; const scaleUnit = (factor: number) => ({ toBase: (value: number) => value * factor, @@ -725,14 +814,6 @@ const fallbackComponentLibraries: ComponentLibraryDefinition[] = [ }, ]; -const supportedComponentSymbols = new Set([ - "cylinder", - "tank", - "pipe", - "orifice", - "tee", -]); - function normalizeComponentCatalog(payload: unknown): ComponentLibraryDefinition[] { const catalog = payload as ComponentCatalogResponse | null; if ( @@ -781,6 +862,9 @@ function normalizeComponentCatalog(payload: unknown): ComponentLibraryDefinition label: component.category?.label || "其他元件", order: Number(component.category?.order) || 0, }, + ...(component.role === "amesimGasMediumDefinition" + ? { role: "amesimGasMediumDefinition" as const } + : {}), ports: component.ports.map((port) => ({ name: port.name, kind: port.kind, @@ -796,6 +880,10 @@ function normalizeComponentCatalog(payload: unknown): ComponentLibraryDefinition parameter.name, { label: parameter.label || parameter.name, + ...(typeof parameter.description === "string" && + parameter.description.trim() + ? { description: parameter.description.trim() } + : {}), ...(parameter.unit ? { unit: parameter.unit } : {}), ...(parameter.quantity && parameter.quantity !== "dimensionless" ? { quantity: parameter.quantity } @@ -810,6 +898,20 @@ function normalizeComponentCatalog(payload: unknown): ComponentLibraryDefinition ...(parameter.minimumExclusive ? { minExclusive: true } : {}), + ...(parameter.editor === "amesimGasReference" || + parameter.editor === "amesimGasPropertyModel" + ? { editor: parameter.editor } + : {}), + ...(Array.isArray(parameter.options) + ? { + options: parameter.options.flatMap((option) => + Number.isFinite(Number(option?.value)) && + typeof option?.label === "string" + ? [{ value: Number(option.value), label: option.label }] + : [], + ), + } + : {}), }, ]), ), @@ -837,10 +939,6 @@ function normalizeComponentCatalog(payload: unknown): ComponentLibraryDefinition ); } -function componentSymbolClass(symbol: string) { - return supportedComponentSymbols.has(symbol) ? symbol : "generic"; -} - function groupLibraryComponents(library: ComponentLibraryDefinition) { const categories = new Map< string, @@ -877,7 +975,7 @@ function defaultParameters(definition: ComponentDefinition) { return Object.fromEntries( Object.entries(definition.parameters).map(([key, parameter]) => [ key, - parameter.default, + parameter.editor === "amesimGasReference" ? 0 : parameter.default, ]), ); } @@ -890,6 +988,13 @@ function defaultParameterUnits(definition: ComponentDefinition) { ); } +function orderedParameterEntries(definition: ComponentDefinition) { + return Object.entries(definition.parameters).sort( + ([firstKey], [secondKey]) => + Number(secondKey === "gi") - Number(firstKey === "gi"), + ); +} + function buildNodeData(definition: ComponentDefinition): SimulationNodeData { return { label: definition.label, @@ -904,14 +1009,291 @@ function buildNodeData(definition: ComponentDefinition): SimulationNodeData { }; } +const AMESIM_BUILTIN_GAS_INDEX = 0; +const AMESIM_MIN_DEFINED_GAS_INDEX = 1; +const AMESIM_MAX_DEFINED_GAS_INDEX = 99; + +type AmesimGasMediumOption = { + value: number; +}; + +type AmesimGasReferenceRepair = { + nodes: SimulationNode[]; + repairedReferenceCount: number; +}; + +function componentDefinitionForNode( + node: SimulationNode, + componentDefinitions: ComponentDefinition[], +) { + return ( + componentDefinitions.find( + (definition) => definition.type === node.data.componentType, + ) ?? + componentDefinitions.find( + (definition) => definition.modelType === node.data.modelType, + ) + ); +} + +function isAmesimGasMediumDefinition( + definition: ComponentDefinition | undefined, +) { + return definition?.role === "amesimGasMediumDefinition"; +} + +function normalizeAmesimGasIndex( + value: ParameterValue | undefined, + minimum = AMESIM_BUILTIN_GAS_INDEX, +) { + if (typeof value === "string" && value.trim() === "") { + return null; + } + const numericValue = Number(value); + if ( + !Number.isInteger(numericValue) || + numericValue < minimum || + numericValue > AMESIM_MAX_DEFINED_GAS_INDEX + ) { + return null; + } + return numericValue; +} + +function definedAmesimGasIndices( + nodes: SimulationNode[], + componentDefinitions: ComponentDefinition[], +) { + const indices = new Set(); + nodes.forEach((node) => { + const definition = componentDefinitionForNode(node, componentDefinitions); + if (!isAmesimGasMediumDefinition(definition)) { + return; + } + const index = normalizeAmesimGasIndex( + node.data.parameters.gi, + AMESIM_MIN_DEFINED_GAS_INDEX, + ); + if (index !== null) { + indices.add(index); + } + }); + return indices; +} + +function nextAvailableAmesimGasIndex(usedIndices: ReadonlySet) { + for ( + let index = AMESIM_MIN_DEFINED_GAS_INDEX; + index <= AMESIM_MAX_DEFINED_GAS_INDEX; + index += 1 + ) { + if (!usedIndices.has(index)) { + return index; + } + } + return null; +} + +function buildAmesimGasMediumOptions( + nodes: SimulationNode[], + componentDefinitions: ComponentDefinition[], +): AmesimGasMediumOption[] { + const options: AmesimGasMediumOption[] = [ + { + value: AMESIM_BUILTIN_GAS_INDEX, + }, + ]; + const seenIndices = new Set(); + + nodes.forEach((node) => { + const definition = componentDefinitionForNode(node, componentDefinitions); + if (!isAmesimGasMediumDefinition(definition)) { + return; + } + const index = normalizeAmesimGasIndex( + node.data.parameters.gi, + AMESIM_MIN_DEFINED_GAS_INDEX, + ); + if (index === null || seenIndices.has(index)) { + return; + } + seenIndices.add(index); + options.push({ + value: index, + }); + }); + + return [ + options[0], + ...options.slice(1).sort((first, second) => first.value - second.value), + ]; +} + +function repairAmesimGasReferences( + nodes: SimulationNode[], + componentDefinitions: ComponentDefinition[], +): AmesimGasReferenceRepair { + const availableIndices = definedAmesimGasIndices(nodes, componentDefinitions); + availableIndices.add(AMESIM_BUILTIN_GAS_INDEX); + let repairedReferenceCount = 0; + let changed = false; + + const repairedNodes = nodes.map((node) => { + const definition = componentDefinitionForNode(node, componentDefinitions); + if (!definition || isAmesimGasMediumDefinition(definition)) { + return node; + } + const referenceKeys = Object.entries(definition.parameters) + .filter(([, parameter]) => parameter.editor === "amesimGasReference") + .map(([key]) => key); + if (referenceKeys.length === 0) { + return node; + } + + let parameters = node.data.parameters; + let nodeChanged = false; + referenceKeys.forEach((key) => { + const index = normalizeAmesimGasIndex(parameters[key]); + if (index !== null && availableIndices.has(index)) { + return; + } + if (!nodeChanged) { + parameters = { ...parameters }; + nodeChanged = true; + } + parameters[key] = AMESIM_BUILTIN_GAS_INDEX; + repairedReferenceCount += 1; + }); + if (!nodeChanged) { + return node; + } + changed = true; + return { + ...node, + data: { + ...node.data, + parameters, + }, + }; + }); + + return { + nodes: changed ? repairedNodes : nodes, + repairedReferenceCount, + }; +} + +function migrateLegacyDefaultAmesimGasReference( + nodes: SimulationNode[], + componentDefinitions: ComponentDefinition[], + mediumReferenceVersion: 1 | undefined, +): AmesimGasReferenceRepair { + const hasVersionMarker = mediumReferenceVersion !== undefined; + const hasMediumDefinition = nodes.some((node) => + isAmesimGasMediumDefinition( + componentDefinitionForNode(node, componentDefinitions), + ), + ); + if (hasVersionMarker || hasMediumDefinition) { + return { nodes, repairedReferenceCount: 0 }; + } + + let repairedReferenceCount = 0; + let changed = false; + const migratedNodes = nodes.map((node) => { + const definition = componentDefinitionForNode(node, componentDefinitions); + if (!definition) { + return node; + } + let parameters = node.data.parameters; + let nodeChanged = false; + Object.entries(definition.parameters).forEach(([key, parameter]) => { + if ( + parameter.editor !== "amesimGasReference" || + normalizeAmesimGasIndex(parameters[key]) !== 1 + ) { + return; + } + if (!nodeChanged) { + parameters = { ...parameters }; + nodeChanged = true; + } + parameters[key] = AMESIM_BUILTIN_GAS_INDEX; + repairedReferenceCount += 1; + }); + if (!nodeChanged) { + return node; + } + changed = true; + return { + ...node, + data: { + ...node.data, + parameters, + }, + }; + }); + + return { + nodes: changed ? migratedNodes : nodes, + repairedReferenceCount, + }; +} + +function assignMissingAmesimGasDefinitionIndices( + nodes: SimulationNode[], + componentDefinitions: ComponentDefinition[], +) { + const usedIndices = definedAmesimGasIndices(nodes, componentDefinitions); + let assignedDefinitionCount = 0; + let exhaustedDefinitionCount = 0; + + const indexedNodes = nodes.map((node) => { + const definition = componentDefinitionForNode(node, componentDefinitions); + if (!isAmesimGasMediumDefinition(definition)) { + return node; + } + const currentIndex = normalizeAmesimGasIndex( + node.data.parameters.gi, + AMESIM_MIN_DEFINED_GAS_INDEX, + ); + if (currentIndex !== null) { + return node; + } + const nextIndex = nextAvailableAmesimGasIndex(usedIndices); + if (nextIndex === null) { + exhaustedDefinitionCount += 1; + return node; + } + usedIndices.add(nextIndex); + assignedDefinitionCount += 1; + return { + ...node, + data: { + ...node.data, + parameters: { + ...node.data.parameters, + gi: nextIndex, + }, + }, + }; + }); + + return { + nodes: indexedNodes, + assignedDefinitionCount, + exhaustedDefinitionCount, + }; +} + function SimulationComponentNode({ id, data, selected }: NodeProps) { const updateNodeInternals = useUpdateNodeInternals(); const rotation = normalizeNodeRotation(data.rotation); const mirrored = Boolean(data.mirrored); const vertical = rotation === 90 || rotation === 270; - const hasDedicatedSymbol = supportedComponentSymbols.has( - data.symbol ?? data.componentType, - ); + const symbol = data.symbol ?? data.componentType; + const hasDedicatedSymbol = hasComponentSymbol(symbol); + const symbolPresentation = componentSymbolPresentation(symbol); const portPlacements = transformedPortPlacements(data.ports, rotation, mirrored); useEffect(() => { @@ -920,7 +1302,7 @@ function SimulationComponentNode({ id, data, selected }: NodeProps
- {symbolForComponent(data.symbol ?? data.componentType)} +
) : null} @@ -1060,25 +1442,6 @@ function portPosition( return Position.Bottom; } -function symbolForComponent(type: string) { - if (type === "cylinder") { - return
; - } - if (type === "tank") { - return
; - } - if (type === "pipe") { - return
; - } - if (type === "orifice") { - return
; - } - if (type === "tee") { - return
; - } - return null; -} - type ToolbarButtonProps = { label: string; icon: LucideIcon; @@ -1870,7 +2233,7 @@ function FlowWorkbench() { () => componentLibraries.flatMap((library) => library.components), [componentLibraries], ); - const [nodes, setNodes, onNodesChange] = useNodesState([]); + const [nodes, setNodes] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); const [selectedNodeId, setSelectedNodeId] = useState(null); const [selectedEdgeId, setSelectedEdgeId] = useState(null); @@ -1901,6 +2264,14 @@ function FlowWorkbench() { const [gridVisibility, setGridVisibility] = useState( loadCanvasGridVisibility, ); + const [paletteDisplayMode, setPaletteDisplayMode] = + useState(loadPaletteDisplayMode); + const [collapsedPaletteSections, setCollapsedPaletteSections] = + useState>(loadPaletteCollapsedSections); + const [paletteIconPreview, setPaletteIconPreview] = + useState(null); + const [parameterColumnLayout, setParameterColumnLayout] = + useState(loadParameterColumnLayout); const [modelingPaneLayout, setModelingPaneLayout] = useState(loadModelingPaneLayout); const [activeModelingPaneResize, setActiveModelingPaneResize] = @@ -1929,12 +2300,62 @@ function FlowWorkbench() { const consoleEntryIdRef = useRef(0); const activeSimulationIdRef = useRef(null); const previousActiveViewRef = useRef(activeView); + const paletteIconPreviewTimerRef = useRef(null); nodesRef.current = nodes; edgesRef.current = edges; projectNameRef.current = projectName; simulationConfigRef.current = simulationConfig; + const dismissPaletteIconPreview = useCallback(() => { + if (paletteIconPreviewTimerRef.current !== null) { + window.clearTimeout(paletteIconPreviewTimerRef.current); + paletteIconPreviewTimerRef.current = null; + } + setPaletteIconPreview(null); + }, []); + + const schedulePaletteIconPreview = useCallback( + (target: HTMLElement, definition: ComponentDefinition) => { + dismissPaletteIconPreview(); + if (paletteDisplayMode !== "icons") { + return; + } + const bounds = target.getBoundingClientRect(); + const estimatedPreviewWidth = 196; + const estimatedPreviewHeight = 112; + const left = Math.max( + 8, + Math.min(bounds.right + 12, window.innerWidth - estimatedPreviewWidth - 8), + ); + const top = Math.max( + 8, + Math.min(bounds.top, window.innerHeight - estimatedPreviewHeight - 8), + ); + paletteIconPreviewTimerRef.current = window.setTimeout(() => { + paletteIconPreviewTimerRef.current = null; + setPaletteIconPreview({ definition, left, top }); + }, PALETTE_ICON_PREVIEW_DELAY_MS); + }, + [dismissPaletteIconPreview, paletteDisplayMode], + ); + + const togglePaletteSection = useCallback( + (key: string) => { + dismissPaletteIconPreview(); + setCollapsedPaletteSections((current) => { + const next = new Set(current); + if (next.has(key)) { + next.delete(key); + } else { + next.add(key); + } + return next; + }); + }, + [dismissPaletteIconPreview], + ); + useEffect(() => { const controller = new AbortController(); @@ -1995,6 +2416,49 @@ function FlowWorkbench() { } }, [gridVisibility]); + useEffect(() => { + try { + localStorage.setItem(PALETTE_DISPLAY_MODE_KEY, paletteDisplayMode); + } catch { + // The display switch remains available when browser storage is unavailable. + } + }, [paletteDisplayMode]); + + useEffect(() => { + try { + localStorage.setItem( + PALETTE_COLLAPSED_SECTIONS_KEY, + JSON.stringify([...collapsedPaletteSections]), + ); + } catch { + // Section toggles remain available when browser storage is unavailable. + } + }, [collapsedPaletteSections]); + + useEffect(() => { + saveParameterColumnLayout(parameterColumnLayout); + }, [parameterColumnLayout]); + + useEffect(() => { + if (paletteDisplayMode !== "icons") { + dismissPaletteIconPreview(); + } + }, [dismissPaletteIconPreview, paletteDisplayMode]); + + useEffect(() => { + const dismissOnViewportChange = () => dismissPaletteIconPreview(); + window.addEventListener("scroll", dismissOnViewportChange, true); + window.addEventListener("resize", dismissOnViewportChange); + return () => { + window.removeEventListener("scroll", dismissOnViewportChange, true); + window.removeEventListener("resize", dismissOnViewportChange); + if (paletteIconPreviewTimerRef.current !== null) { + window.clearTimeout(paletteIconPreviewTimerRef.current); + paletteIconPreviewTimerRef.current = null; + } + }; + }, [dismissPaletteIconPreview]); + useLayoutEffect(() => { if (activeView !== "modeling") { return; @@ -2213,6 +2677,10 @@ function FlowWorkbench() { const selectedDefinition = selectedNode ? componentDefinitions.find((definition) => definition.type === selectedNode.data.componentType) : null; + const amesimGasMediumOptions = useMemo( + () => buildAmesimGasMediumOptions(nodes, componentDefinitions), + [componentDefinitions, nodes], + ); const buildCurrentProject = useCallback( () => buildProjectPayload(projectName, nodes, edges, simulationConfig), @@ -2397,12 +2865,28 @@ function FlowWorkbench() { const handleNodesChange = useCallback( (changes: NodeChange[]) => { - if (changes.some((change) => change.type === "remove")) { + const removesNodes = changes.some((change) => change.type === "remove"); + if (removesNodes) { recordHistory(); } - onNodesChange(changes); + const preview = repairAmesimGasReferences( + applyNodeChanges(changes, nodesRef.current), + componentDefinitions, + ); + setNodes((currentNodes) => { + const changedNodes = applyNodeChanges(changes, currentNodes); + return removesNodes + ? repairAmesimGasReferences(changedNodes, componentDefinitions).nodes + : changedNodes; + }); + if (removesNodes && preview.repairedReferenceCount > 0) { + appendConsoleEntry( + "warning", + `介质定义已删除,${preview.repairedReferenceCount} 个介质物性引用已切回 0 — 空气(理想气体,内置默认)`, + ); + } }, - [onNodesChange, recordHistory], + [appendConsoleEntry, componentDefinitions, recordHistory, setNodes], ); const handleEdgesChange = useCallback( @@ -2442,6 +2926,7 @@ function FlowWorkbench() { ); const onDragStart = (event: DragEvent, type: string) => { + dismissPaletteIconPreview(); event.dataTransfer.setData("application/system-component", type); event.dataTransfer.effectAllowed = "move"; const icon = event.currentTarget.querySelector(".palette-icon"); @@ -2477,12 +2962,31 @@ function FlowWorkbench() { x: event.clientX, y: event.clientY, }); + const mediumIndex = isAmesimGasMediumDefinition(definition) + ? nextAvailableAmesimGasIndex( + definedAmesimGasIndices(nodesRef.current, componentDefinitions), + ) + : null; + if (isAmesimGasMediumDefinition(definition) && mediumIndex === null) { + appendConsoleEntry( + "error", + "无法添加介质定义:可用介质定义索引 1..99 已全部占用", + ); + return; + } const id = takeNextNodeId(definition.type); + const data = { ...buildNodeData(definition), label: id }; + if (mediumIndex !== null) { + data.parameters = { + ...data.parameters, + gi: mediumIndex, + }; + } const node: SimulationNode = { id, type: "simulationComponent", position, - data: { ...buildNodeData(definition), label: id }, + data, }; recordHistory(); setNodes((currentNodes) => [...currentNodes, node]); @@ -2527,6 +3031,19 @@ function FlowWorkbench() { })); }; + const updateSelectedChoice = (key: string, value: number) => { + updateSelectedNode((node) => ({ + ...node, + data: { + ...node.data, + parameters: { + ...node.data.parameters, + [key]: value, + }, + }, + })); + }; + const updateSelectedParameterUnit = (key: string, unit: string) => { updateSelectedNode((node) => ({ ...node, @@ -2568,16 +3085,88 @@ function FlowWorkbench() { return; } + const existingMediumIndices = definedAmesimGasIndices( + nodesRef.current, + componentDefinitions, + ); + const reservedMediumIndices = new Set(existingMediumIndices); + const assignedIndexByCopiedNodeId = new Map(); + const copiedMediumNodeIdsByOldIndex = new Map(); + for (const node of clipboard.nodes) { + const definition = componentDefinitionForNode(node, componentDefinitions); + if (!isAmesimGasMediumDefinition(definition)) { + continue; + } + const nextIndex = nextAvailableAmesimGasIndex(reservedMediumIndices); + if (nextIndex === null) { + appendConsoleEntry( + "error", + "无法粘贴介质定义:可用介质定义索引 1..99 不足", + ); + return; + } + reservedMediumIndices.add(nextIndex); + assignedIndexByCopiedNodeId.set(node.id, nextIndex); + const oldIndex = normalizeAmesimGasIndex( + node.data.parameters.gi, + AMESIM_MIN_DEFINED_GAS_INDEX, + ); + if (oldIndex !== null) { + const nodeIds = copiedMediumNodeIdsByOldIndex.get(oldIndex) ?? []; + nodeIds.push(node.id); + copiedMediumNodeIdsByOldIndex.set(oldIndex, nodeIds); + } + } + const copiedIndexRemap = new Map(); + const ambiguousCopiedIndices = new Set(); + copiedMediumNodeIdsByOldIndex.forEach((nodeIds, oldIndex) => { + if (nodeIds.length !== 1) { + ambiguousCopiedIndices.add(oldIndex); + return; + } + const assignedIndex = assignedIndexByCopiedNodeId.get(nodeIds[0]); + if (assignedIndex !== undefined) { + copiedIndexRemap.set(oldIndex, assignedIndex); + } + }); + recordHistory(); pasteCountRef.current += 1; const offset = GRID_SIZE * 2 * pasteCountRef.current; const idMap = new Map(); const usedNames = new Set(nodesRef.current.map((node) => node.data.label.trim())); + let repairedPastedReferenceCount = 0; const pastedNodes = clipboard.nodes.map((node) => { const id = takeNextNodeId(node.data.componentType); idMap.set(node.id, id); const label = makeUniqueName(`${node.data.label}_副本`, usedNames); usedNames.add(label); + const definition = componentDefinitionForNode(node, componentDefinitions); + const parameters = { ...cloneValue(node.data.parameters) }; + if (isAmesimGasMediumDefinition(definition)) { + parameters.gi = assignedIndexByCopiedNodeId.get(node.id) as number; + } else if (definition) { + Object.entries(definition.parameters).forEach(([key, parameter]) => { + if (parameter.editor !== "amesimGasReference") { + return; + } + const oldIndex = normalizeAmesimGasIndex(parameters[key]); + if (oldIndex === AMESIM_BUILTIN_GAS_INDEX) { + parameters[key] = AMESIM_BUILTIN_GAS_INDEX; + } else if (oldIndex !== null && copiedIndexRemap.has(oldIndex)) { + parameters[key] = copiedIndexRemap.get(oldIndex) as number; + } else if ( + oldIndex !== null && + !ambiguousCopiedIndices.has(oldIndex) && + existingMediumIndices.has(oldIndex) + ) { + parameters[key] = oldIndex; + } else { + parameters[key] = AMESIM_BUILTIN_GAS_INDEX; + repairedPastedReferenceCount += 1; + } + }); + } return { ...cloneValue(node), id, @@ -2589,6 +3178,7 @@ function FlowWorkbench() { data: { ...cloneValue(node.data), label, + parameters, }, } satisfies SimulationNode; }); @@ -2611,7 +3201,19 @@ function FlowWorkbench() { setSelectedNodeId(pastedNodes.length === 1 ? pastedNodes[0].id : null); setSelectedEdgeId(null); appendConsoleEntry("info", `已粘贴 ${pastedNodes.length} 个组件`); - }, [appendConsoleEntry, recordHistory, setEdges, setNodes]); + if (repairedPastedReferenceCount > 0) { + appendConsoleEntry( + "warning", + `${repairedPastedReferenceCount} 个粘贴的介质物性引用没有对应介质定义,已切回 0 — 空气(理想气体,内置默认)`, + ); + } + }, [ + appendConsoleEntry, + componentDefinitions, + recordHistory, + setEdges, + setNodes, + ]); const updateSimulationConfig = (key: keyof SimulationConfig, value: string) => { setSimulationConfig((current) => ({ @@ -2650,9 +3252,19 @@ function FlowWorkbench() { if (addToHistory) { recordHistory(); } - const loadedNodes = project.nodes.map((node) => + const normalizedNodes = project.nodes.map((node) => normalizeLoadedNode(node, componentDefinitions), ); + const indexedMedia = assignMissingAmesimGasDefinitionIndices( + normalizedNodes, + componentDefinitions, + ); + const legacyGasMigration = migrateLegacyDefaultAmesimGasReference( + indexedMedia.nodes, + componentDefinitions, + project.mediumReferenceVersion, + ); + const loadedNodes = legacyGasMigration.nodes; setNodes(loadedNodes); setEdges(project.edges.map(normalizeLoadedEdge)); setProjectName(project.name || "untitled"); @@ -2663,6 +3275,24 @@ function FlowWorkbench() { setValidatedSignature(""); syncNextNodeNumbers(loadedNodes); appendConsoleEntry("success", message); + if (indexedMedia.assignedDefinitionCount > 0) { + appendConsoleEntry( + "info", + `已为 ${indexedMedia.assignedDefinitionCount} 个介质定义补充分配介质定义索引`, + ); + } + if (legacyGasMigration.repairedReferenceCount > 0) { + appendConsoleEntry( + "warning", + `检测到无版本且没有介质定义的旧工程:已将 ${legacyGasMigration.repairedReferenceCount} 个历史默认介质物性引用 1 迁移到 0 — 空气(理想气体,内置默认)`, + ); + } + if (indexedMedia.exhaustedDefinitionCount > 0) { + appendConsoleEntry( + "error", + `${indexedMedia.exhaustedDefinitionCount} 个介质定义无法分配 1..99 索引,请删除多余定义`, + ); + } requestModelingFitView(); }, [ @@ -3063,7 +3693,12 @@ function FlowWorkbench() { return; } recordHistory(); - setNodes((currentNodes) => currentNodes.filter((node) => !nodeIds.has(node.id))); + const remainingNodes = nodesRef.current.filter((node) => !nodeIds.has(node.id)); + const repairedGasReferences = repairAmesimGasReferences( + remainingNodes, + componentDefinitions, + ); + setNodes(repairedGasReferences.nodes); setEdges((currentEdges) => currentEdges.filter( (edge) => @@ -3078,8 +3713,15 @@ function FlowWorkbench() { "info", `已删除 ${nodeIds.size} 个组件和 ${edgeIds.size} 条连接`, ); + if (repairedGasReferences.repairedReferenceCount > 0) { + appendConsoleEntry( + "warning", + `介质定义已删除,${repairedGasReferences.repairedReferenceCount} 个介质物性引用已切回 0 — 空气(理想气体,内置默认)`, + ); + } }, [ appendConsoleEntry, + componentDefinitions, recordHistory, selectedEdgeId, selectedNodeId, @@ -3263,6 +3905,313 @@ function FlowWorkbench() { ); } + const componentNameError = selectedNode + ? !selectedNode.data.label.trim() + ? "组件名称不能为空" + : duplicateNames.has(selectedNode.data.label.trim()) + ? "组件名称不能重复" + : "" + : ""; + const componentParameterRows: ParameterTableRow[] = + selectedNode && selectedDefinition + ? [ + { + id: "component-name", + name: "组件名称", + value: ( + + updateSelectedNode((node) => ({ + ...node, + data: { ...node.data, label: event.target.value }, + })) + } + onFocus={recordHistory} + value={selectedNode.data.label} + /> + ), + error: componentNameError || undefined, + description: "画布中用于识别该组件的名称,必须非空且不能与其他组件重复。", + }, + { + id: "component-id", + name: "节点 ID", + value: ( + + {selectedNode.id} + + ), + readOnly: true, + description: "系统为画布节点分配的稳定标识,用于工程数据和连接关系。", + }, + ...orderedParameterEntries(selectedDefinition).map( + ([key, definition]): ParameterTableRow => { + const selectedUnit = + selectedNode.data.parameterUnits[key] ?? definition.unit ?? ""; + const units = parameterUnitOptions(definition); + const baseValue = selectedNode.data.parameters[key] ?? ""; + const displayValue = displayParameterValue( + baseValue, + definition, + selectedUnit, + ); + const mediumDefinitionIndex = + isAmesimGasMediumDefinition(selectedDefinition) && key === "gi"; + const gasReference = + !mediumDefinitionIndex && + definition.editor === "amesimGasReference"; + const gasPropertyModel = + definition.editor === "amesimGasPropertyModel"; + const parameterLabel = mediumDefinitionIndex + ? "介质定义索引" + : gasReference + ? "介质物性索引" + : definition.label; + const gasReferenceIndex = gasReference + ? normalizeAmesimGasIndex(baseValue) + : null; + const gasReferenceAvailable = + gasReferenceIndex !== null && + amesimGasMediumOptions.some( + (option) => option.value === gasReferenceIndex, + ); + const propertyModelValue = Number(baseValue); + const propertyModelAvailable = + gasPropertyModel && + Number.isFinite(propertyModelValue) && + (definition.options ?? []).some( + (option) => option.value === propertyModelValue, + ); + const error = + gasReference && !gasReferenceAvailable + ? "引用的介质定义不存在" + : parameterValidationMessage(definition, baseValue); + let valueControl: ReactNode; + if (mediumDefinitionIndex) { + valueControl = ( + + ); + } else if (gasReference) { + valueControl = ( + + ); + } else if (gasPropertyModel) { + valueControl = ( + + ); + } else { + valueControl = ( + + updateSelectedParameter(key, definition, value) + } + onEditEnd={finishHistoryEdit} + onEditStart={recordHistory} + value={displayValue} + /> + ); + } + const unitControl = + units.length > 0 ? ( + + ) : undefined; + return { + id: `component-parameter-${key}`, + name: parameterLabel, + value: valueControl, + unit: unitControl, + error: error || undefined, + readOnly: mediumDefinitionIndex, + help: mediumDefinitionIndex + ? "索引由画布自动维护;0 保留给内置空气。" + : undefined, + description: definition.description, + defaultValue: parameterDefaultMetadata( + definition, + selectedUnit, + ), + range: parameterRangeMetadata(definition, selectedUnit), + currentUnit: parameterCurrentUnitMetadata( + definition, + selectedUnit, + ), + }; + }, + ), + ] + : []; + + const simulationSettingRows: ParameterTableRow[] = [ + { + id: "simulation-t-start", + name: "起始时间", + value: ( + updateSimulationConfig("t_start", value)} + onEditEnd={finishHistoryEdit} + onEditStart={recordHistory} + /> + ), + unit: s, + description: "仿真时间区间的起点。", + defaultValue: `${defaultSimulationConfig.t_start} s`, + range: "有限数值", + currentUnit: "s", + }, + { + id: "simulation-t-stop", + name: "结束时间", + value: ( + updateSimulationConfig("t_stop", value)} + onEditEnd={finishHistoryEdit} + onEditStart={recordHistory} + /> + ), + unit: s, + description: "仿真时间区间的终点,必须大于起始时间。", + defaultValue: `${defaultSimulationConfig.t_stop} s`, + range: "> 起始时间", + currentUnit: "s", + }, + { + id: "simulation-step", + name: "采样步长", + value: ( + updateSimulationConfig("step", value)} + onEditEnd={finishHistoryEdit} + onEditStart={recordHistory} + /> + ), + unit: s, + description: "仿真结果输出与采样的时间间隔,必须大于 0。", + defaultValue: `${defaultSimulationConfig.step} s`, + range: "> 0", + currentUnit: "s", + }, + { + id: "simulation-max-step", + name: "最大积分步长", + value: ( + updateSimulationConfig("max_step", value)} + onEditEnd={finishHistoryEdit} + onEditStart={recordHistory} + /> + ), + unit: s, + description: "积分器允许采用的最大内部时间步长,必须大于 0。", + defaultValue: `${defaultSimulationConfig.max_step} s`, + range: "> 0", + currentUnit: "s", + }, + { + id: "simulation-method", + name: "求解器", + value: ( + + ), + description: "用于求解模型微分方程的数值积分方法。", + defaultValue: defaultSimulationConfig.method, + range: SUPPORTED_SOLVER_METHODS.join(" / "), + }, + ]; + const parameterTableAvailableWidth = Math.max( + 0, + modelingPaneLayout.propertiesWidth - 24, + ); + return ( <>
@@ -3429,7 +4378,7 @@ function FlowWorkbench() { } as CSSProperties } > -
{simulationConsole} + {paletteIconPreview && typeof document !== "undefined" + ? createPortal( + , + document.body, + ) + : null} ); } @@ -3793,6 +4776,7 @@ function buildProjectPayload( ): ReactFlowProjectPayload { return { name: name.trim() || "untitled", + mediumReferenceVersion: 1, nodes: nodes.map((node) => ({ id: node.id, type: "simulationComponent", @@ -3829,9 +4813,16 @@ function normalizeLoadedNode( const definition = componentDefinitions.find((item) => item.type === node.data.componentType) ?? componentDefinitions.find((item) => item.modelType === node.data.modelType); + const rawParameters = node.data.parameters ?? {}; const parameters = definition - ? { ...defaultParameters(definition), ...node.data.parameters } - : { ...node.data.parameters }; + ? { ...defaultParameters(definition), ...rawParameters } + : { ...rawParameters }; + if ( + isAmesimGasMediumDefinition(definition) && + !Object.prototype.hasOwnProperty.call(rawParameters, "gi") + ) { + parameters.gi = ""; + } const parameterUnits = definition ? { ...defaultParameterUnits(definition), ...(node.data.parameterUnits ?? {}) } : { ...(node.data.parameterUnits ?? {}) }; @@ -4071,6 +5062,66 @@ function formatEditableNumber(value: number) { return Number(value.toPrecision(12)).toString(); } +function parameterMetadataValue( + value: ParameterValue, + definition: ParameterDefinition, + selectedUnit: string, +) { + const displayValue = displayParameterValue(value, definition, selectedUnit); + const option = definition.options?.find( + (candidate) => candidate.value === Number(value), + ); + return option ? `${displayValue}(${option.label})` : displayValue; +} + +function parameterDefaultMetadata( + definition: ParameterDefinition, + selectedUnit: string, +) { + const defaultValue = + definition.editor === "amesimGasReference" ? 0 : definition.default; + return parameterMetadataValue(defaultValue, definition, selectedUnit); +} + +function parameterRangeMetadata( + definition: ParameterDefinition, + selectedUnit: string, +) { + const limits: string[] = []; + if (definition.min !== undefined) { + limits.push( + `${definition.minExclusive ? ">" : "≥"} ${parameterMetadataValue( + definition.min, + definition, + selectedUnit, + )}`, + ); + } + if (definition.max !== undefined) { + limits.push( + `≤ ${parameterMetadataValue(definition.max, definition, selectedUnit)}`, + ); + } + if (limits.length > 0) { + return limits.join(","); + } + if (definition.options?.length) { + return definition.options + .map((option) => `${option.value}(${option.label})`) + .join(" / "); + } + return undefined; +} + +function parameterCurrentUnitMetadata( + definition: ParameterDefinition, + selectedUnit: string, +) { + return parameterUnitOptions(definition).length > 0 + ? findUnitOption(definition, selectedUnit).label + : undefined; +} + function parameterValidationMessage( definition: ParameterDefinition, value: ParameterValue, @@ -4082,6 +5133,12 @@ function parameterValidationMessage( if (!Number.isFinite(numericValue)) { return "请输入有效数字"; } + if ( + definition.options && + !definition.options.some((option) => option.value === numericValue) + ) { + return "不在支持的可选项中"; + } if ( definition.min !== undefined && (definition.minExclusive ? numericValue <= definition.min : numericValue < definition.min) @@ -4132,10 +5189,43 @@ function validateModel( const nodeMap = new Map(nodes.map((node) => [node.id, node])); const duplicateNames = findDuplicateNames(nodes); + const gasMediumNodesByIndex = new Map(); nodes.forEach((node) => { - const definition = componentDefinitions.find( - (item) => item.type === node.data.componentType, + const definition = componentDefinitionForNode(node, componentDefinitions); + if (!isAmesimGasMediumDefinition(definition)) { + return; + } + const index = normalizeAmesimGasIndex( + node.data.parameters.gi, + AMESIM_MIN_DEFINED_GAS_INDEX, ); + if (index === null) { + addIssue( + "error", + `${node.data.label}:介质定义索引必须是 1..99 的整数`, + { nodeId: node.id }, + ); + return; + } + const definitionsAtIndex = gasMediumNodesByIndex.get(index) ?? []; + definitionsAtIndex.push(node); + gasMediumNodesByIndex.set(index, definitionsAtIndex); + }); + gasMediumNodesByIndex.forEach((mediumNodes, index) => { + if (mediumNodes.length <= 1) { + return; + } + mediumNodes.forEach((node) => { + addIssue( + "error", + `${node.data.label}:介质定义索引 ${index} 重复`, + { nodeId: node.id }, + ); + }); + }); + + nodes.forEach((node) => { + const definition = componentDefinitionForNode(node, componentDefinitions); if (!node.data.label.trim()) { addIssue("error", `${node.id} 的组件名称为空`, { nodeId: node.id }); } else if (duplicateNames.has(node.data.label.trim())) { @@ -4153,6 +5243,39 @@ function validateModel( }); } }); + if (!isAmesimGasMediumDefinition(definition)) { + Object.entries(definition.parameters).forEach(([key, parameter]) => { + if (parameter.editor !== "amesimGasReference") { + return; + } + const index = normalizeAmesimGasIndex(node.data.parameters[key]); + if (index === null) { + addIssue( + "error", + `${node.data.label}:介质物性索引必须是 0..99 的整数`, + { nodeId: node.id }, + ); + return; + } + if (index === AMESIM_BUILTIN_GAS_INDEX) { + return; + } + const providers = gasMediumNodesByIndex.get(index) ?? []; + if (providers.length === 0) { + addIssue( + "error", + `${node.data.label}:介质物性索引 ${index} 没有对应的介质定义`, + { nodeId: node.id }, + ); + } else if (providers.length > 1) { + addIssue( + "error", + `${node.data.label}:介质物性索引 ${index} 对应多个介质定义`, + { nodeId: node.id }, + ); + } + }); + } definition.ports.forEach((port) => { const connectionCount = edges.filter( (edge) => @@ -4201,6 +5324,98 @@ function validateModel( connectionKeys.add(key); }); + const pneumaticAdjacency = new Map>(); + nodes.forEach((node) => { + if ( + node.data.ports.some( + (port) => port.kind === "physical" && port.domain === "pneumatic", + ) + ) { + pneumaticAdjacency.set(node.id, new Set()); + } + }); + edges.forEach((edge) => { + const source = nodeMap.get(edge.source); + const target = nodeMap.get(edge.target); + const sourcePort = source?.data.ports.find( + (port) => port.name === edge.sourceHandle, + ); + const targetPort = target?.data.ports.find( + (port) => port.name === edge.targetHandle, + ); + if ( + !source || + !target || + sourcePort?.kind !== "physical" || + targetPort?.kind !== "physical" || + sourcePort.domain !== "pneumatic" || + targetPort.domain !== "pneumatic" + ) { + return; + } + pneumaticAdjacency.get(source.id)?.add(target.id); + pneumaticAdjacency.get(target.id)?.add(source.id); + }); + + const visitedPneumaticNodes = new Set(); + pneumaticAdjacency.forEach((_, startNodeId) => { + if (visitedPneumaticNodes.has(startNodeId)) { + return; + } + const pendingNodeIds = [startNodeId]; + const connectedNodeIds: string[] = []; + while (pendingNodeIds.length > 0) { + const nodeId = pendingNodeIds.pop() as string; + if (visitedPneumaticNodes.has(nodeId)) { + continue; + } + visitedPneumaticNodes.add(nodeId); + connectedNodeIds.push(nodeId); + pneumaticAdjacency.get(nodeId)?.forEach((neighborId) => { + if (!visitedPneumaticNodes.has(neighborId)) { + pendingNodeIds.push(neighborId); + } + }); + } + + const referencedIndices = new Set(); + const referencingNodes = new Map(); + connectedNodeIds.forEach((nodeId) => { + const node = nodeMap.get(nodeId); + if (!node) { + return; + } + const definition = componentDefinitionForNode(node, componentDefinitions); + if (!definition || isAmesimGasMediumDefinition(definition)) { + return; + } + Object.entries(definition.parameters).forEach(([key, parameter]) => { + if (parameter.editor !== "amesimGasReference") { + return; + } + const index = normalizeAmesimGasIndex(node.data.parameters[key]); + if (index === null) { + return; + } + referencedIndices.add(index); + referencingNodes.set(node.id, node); + }); + }); + if (referencedIndices.size <= 1) { + return; + } + const indexList = [...referencedIndices] + .sort((first, second) => first - second) + .join("、"); + referencingNodes.forEach((node) => { + addIssue( + "error", + `${node.data.label}:同一气路引用了不同的介质物性索引(${indexList})`, + { nodeId: node.id }, + ); + }); + }); + if (!Number.isFinite(simulation.t_start) || !Number.isFinite(simulation.t_stop)) { addIssue("error", "仿真起止时间必须是有效数字"); } else if (simulation.t_stop <= simulation.t_start) { @@ -4309,7 +5524,9 @@ function parseProjectPayload(raw: string | null): ReactFlowProjectPayload | null !Array.isArray(project.nodes) || !Array.isArray(project.edges) || !project.simulation || - typeof project.simulation !== "object" + typeof project.simulation !== "object" || + (project.mediumReferenceVersion !== undefined && + project.mediumReferenceVersion !== 1) ) { return null; } @@ -4336,6 +5553,7 @@ function buildSystemXml(project: ReactFlowProjectPayload) { const system = xmlDocument.documentElement; system.setAttribute("name", project.name); system.setAttribute("schemaVersion", "2"); + system.setAttribute("mediumReferenceVersion", "1"); system.setAttribute("unitSystem", "SI"); const simulation = xmlDocument.createElement("Simulation"); diff --git a/frontend/src/ComponentSymbol.tsx b/frontend/src/ComponentSymbol.tsx new file mode 100644 index 0000000..d6aa6da --- /dev/null +++ b/frontend/src/ComponentSymbol.tsx @@ -0,0 +1,235 @@ +import type { ReactNode } from "react"; + +export type ComponentSymbolSurface = "palette" | "canvas"; +export type ComponentSymbolPresentation = "framed" | "bare"; + +type ComponentSymbolProps = { + symbol: string; + surface: ComponentSymbolSurface; + className?: string; +}; + +type SymbolDefinition = { + presentation: ComponentSymbolPresentation; + render: () => ReactNode; +}; + +type SymbolSvgProps = { + children: ReactNode; + className?: string; +}; + +function SymbolSvg({ children, className }: SymbolSvgProps) { + return ( + + ); +} + +function CylinderSymbol() { + return ( + + + + + + ); +} + +function TankSymbol() { + return ( + + + + + + ); +} + +function PipeSymbol() { + return ( + + + + + ); +} + +function OrificeSymbol() { + return ( + + + + + ); +} + +function TeeSymbol() { + return ( + + + + + ); +} + +function GenericSymbol() { + return ( + + + + + + ); +} + +function SignalSymbol() { + return ( + + + + + + ); +} + +function JunctionSymbol() { + return ( + + + + + ); +} + +function MediumSymbol({ label }: { label: "Air" | "He" }) { + return ( + + + + {label} + + + ); +} + +const symbolRegistry = { + cylinder: { presentation: "framed", render: CylinderSymbol }, + tank: { presentation: "framed", render: TankSymbol }, + pipe: { presentation: "framed", render: PipeSymbol }, + orifice: { presentation: "framed", render: OrificeSymbol }, + tee: { presentation: "framed", render: TeeSymbol }, + generic: { presentation: "framed", render: GenericSymbol }, + signal: { presentation: "framed", render: SignalSymbol }, + junction: { presentation: "framed", render: JunctionSymbol }, + amesim_ideal_air_medium: { + presentation: "bare", + render: () => , + }, + amesim_helium_medium: { + presentation: "bare", + render: () => , + }, +} satisfies Record; + +type RegisteredComponentSymbol = keyof typeof symbolRegistry; + +function isRegisteredComponentSymbol(symbol: string): symbol is RegisteredComponentSymbol { + return Object.prototype.hasOwnProperty.call(symbolRegistry, symbol); +} + +export function hasComponentSymbol(symbol: string): boolean { + return isRegisteredComponentSymbol(symbol); +} + +export function componentSymbolPresentation( + symbol: string, +): ComponentSymbolPresentation { + return isRegisteredComponentSymbol(symbol) + ? symbolRegistry[symbol].presentation + : "framed"; +} + +export function ComponentSymbol({ + symbol, + surface, + className, +}: ComponentSymbolProps) { + const resolvedSymbol: RegisteredComponentSymbol = isRegisteredComponentSymbol(symbol) + ? symbol + : "generic"; + const definition = symbolRegistry[resolvedSymbol]; + const rootClassName = [ + "component-symbol", + `component-symbol-${surface}`, + `component-symbol-${resolvedSymbol}`, + className, + ] + .filter(Boolean) + .join(" "); + + return ( + + ); +} diff --git a/frontend/src/ParameterTable.tsx b/frontend/src/ParameterTable.tsx new file mode 100644 index 0000000..2407a74 --- /dev/null +++ b/frontend/src/ParameterTable.tsx @@ -0,0 +1,484 @@ +import { + Fragment, + useCallback, + useEffect, + useRef, + useState, +} from "react"; +import type { + CSSProperties, + FocusEvent as ReactFocusEvent, + KeyboardEvent as ReactKeyboardEvent, + MouseEvent as ReactMouseEvent, + PointerEvent as ReactPointerEvent, + ReactNode, +} from "react"; +import { createPortal } from "react-dom"; + +export type ParameterColumnLayout = { + nameWidth: number; + unitWidth: number; +}; + +export type ParameterTableRow = { + id: string; + name: string; + value: ReactNode; + unit?: ReactNode; + help?: string; + error?: string; + readOnly?: boolean; + description?: string; + defaultValue?: string; + range?: string; + currentUnit?: string; +}; + +type ParameterTableProps = { + ariaLabel: string; + rows: readonly ParameterTableRow[]; + columnLayout: ParameterColumnLayout; + availableWidth: number; + onColumnLayoutChange: (layout: ParameterColumnLayout) => void; +}; + +type ColumnName = "name" | "unit"; + +type ColumnDragState = { + column: ColumnName; + pointerId: number; + startX: number; + startLayout: ParameterColumnLayout; +}; + +type HelpPreviewState = { + rowId: string; + left: number; + top: number; + transform: string; +}; + +type HelpPreviewAnchor = { + rowId: string; + clientX: number; + clientY: number; +}; + +export const PARAMETER_COLUMN_LAYOUT_KEY = + "system-simulation-flow:parameter-column-layout"; + +export const DEFAULT_PARAMETER_COLUMN_LAYOUT: ParameterColumnLayout = { + nameWidth: 112, + unitWidth: 72, +}; + +const MIN_NAME_WIDTH = 80; +const MAX_NAME_WIDTH = 220; +const MIN_VALUE_WIDTH = 96; +const MIN_UNIT_WIDTH = 48; +const MAX_UNIT_WIDTH = 112; +const COLUMN_KEYBOARD_STEP = 8; +const HELP_PREVIEW_DELAY_MS = 500; +const HELP_PREVIEW_GAP = 12; +const HELP_PREVIEW_MARGIN = 8; +const HELP_PREVIEW_MAX_WIDTH = 300; +const HELP_PREVIEW_ESTIMATED_HEIGHT = 156; + +function boundedWidth(value: number, minimum: number, maximum: number) { + return Math.min(maximum, Math.max(minimum, value)); +} + +export function loadParameterColumnLayout(): ParameterColumnLayout { + try { + const parsed = JSON.parse( + localStorage.getItem(PARAMETER_COLUMN_LAYOUT_KEY) ?? "null", + ) as Partial | null; + if ( + !parsed || + !Number.isFinite(parsed.nameWidth) || + !Number.isFinite(parsed.unitWidth) + ) { + return { ...DEFAULT_PARAMETER_COLUMN_LAYOUT }; + } + return { + nameWidth: boundedWidth(Number(parsed.nameWidth), MIN_NAME_WIDTH, MAX_NAME_WIDTH), + unitWidth: boundedWidth(Number(parsed.unitWidth), MIN_UNIT_WIDTH, MAX_UNIT_WIDTH), + }; + } catch { + return { ...DEFAULT_PARAMETER_COLUMN_LAYOUT }; + } +} + +export function saveParameterColumnLayout(layout: ParameterColumnLayout) { + try { + localStorage.setItem(PARAMETER_COLUMN_LAYOUT_KEY, JSON.stringify(layout)); + } catch { + // Column resizing remains available when browser storage is unavailable. + } +} + +function effectiveColumnLayout( + preferred: ParameterColumnLayout, + availableWidth: number, +): ParameterColumnLayout { + const usableWidth = Number.isFinite(availableWidth) + ? Math.max(MIN_NAME_WIDTH + MIN_VALUE_WIDTH + MIN_UNIT_WIDTH, availableWidth) + : MIN_NAME_WIDTH + MIN_VALUE_WIDTH + MIN_UNIT_WIDTH; + let nameWidth = boundedWidth(preferred.nameWidth, MIN_NAME_WIDTH, MAX_NAME_WIDTH); + let unitWidth = boundedWidth(preferred.unitWidth, MIN_UNIT_WIDTH, MAX_UNIT_WIDTH); + let overflow = nameWidth + MIN_VALUE_WIDTH + unitWidth - usableWidth; + if (overflow > 0) { + const unitReduction = Math.min(overflow, unitWidth - MIN_UNIT_WIDTH); + unitWidth -= unitReduction; + overflow -= unitReduction; + } + if (overflow > 0) { + nameWidth -= Math.min(overflow, nameWidth - MIN_NAME_WIDTH); + } + return { nameWidth, unitWidth }; +} + +function rowHasHelpPreview(row: ParameterTableRow) { + return Boolean( + row.description || + (typeof row.help === "string" && row.help) || + row.defaultValue || + row.range || + row.currentUnit, + ); +} + +function helpPreviewPosition(clientX: number, clientY: number) { + const placeOnRight = + clientX + HELP_PREVIEW_GAP + HELP_PREVIEW_MAX_WIDTH <= + window.innerWidth - HELP_PREVIEW_MARGIN; + const placeBelow = + clientY + HELP_PREVIEW_GAP + HELP_PREVIEW_ESTIMATED_HEIGHT <= + window.innerHeight - HELP_PREVIEW_MARGIN; + return { + left: clientX + (placeOnRight ? HELP_PREVIEW_GAP : -HELP_PREVIEW_GAP), + top: clientY + (placeBelow ? HELP_PREVIEW_GAP : -HELP_PREVIEW_GAP), + transform: `translate(${placeOnRight ? "0" : "-100%"}, ${ + placeBelow ? "0" : "-100%" + })`, + }; +} + +export function ParameterTable({ + ariaLabel, + rows, + columnLayout, + availableWidth, + onColumnLayoutChange, +}: ParameterTableProps) { + const effectiveLayout = effectiveColumnLayout(columnLayout, availableWidth); + const columnDragRef = useRef(null); + const helpPreviewTimerRef = useRef(null); + const helpPreviewAnchorRef = useRef(null); + const [activeColumnResize, setActiveColumnResize] = + useState(null); + const [helpPreview, setHelpPreview] = useState(null); + + const dismissHelpPreview = useCallback(() => { + if (helpPreviewTimerRef.current !== null) { + window.clearTimeout(helpPreviewTimerRef.current); + helpPreviewTimerRef.current = null; + } + helpPreviewAnchorRef.current = null; + setHelpPreview(null); + }, []); + + const scheduleHelpPreview = useCallback( + ( + target: HTMLElement, + row: ParameterTableRow, + pointer?: { clientX: number; clientY: number }, + ) => { + dismissHelpPreview(); + if (!rowHasHelpPreview(row)) { + return; + } + const bounds = target.getBoundingClientRect(); + const fallbackAnchor = { + clientX: bounds.left + bounds.width / 2, + clientY: bounds.top + bounds.height / 2, + }; + helpPreviewAnchorRef.current = { + rowId: row.id, + ...(pointer ?? fallbackAnchor), + }; + helpPreviewTimerRef.current = window.setTimeout(() => { + helpPreviewTimerRef.current = null; + const anchor = + helpPreviewAnchorRef.current?.rowId === row.id + ? helpPreviewAnchorRef.current + : { rowId: row.id, ...fallbackAnchor }; + setHelpPreview({ + rowId: row.id, + ...helpPreviewPosition(anchor.clientX, anchor.clientY), + }); + }, HELP_PREVIEW_DELAY_MS); + }, + [dismissHelpPreview], + ); + + const trackHelpPointer = ( + event: ReactMouseEvent, + row: ParameterTableRow, + ) => { + helpPreviewAnchorRef.current = { + rowId: row.id, + clientX: event.clientX, + clientY: event.clientY, + }; + }; + + useEffect(() => { + const dismissOnViewportChange = () => dismissHelpPreview(); + window.addEventListener("scroll", dismissOnViewportChange, true); + window.addEventListener("resize", dismissOnViewportChange); + return () => { + window.removeEventListener("scroll", dismissOnViewportChange, true); + window.removeEventListener("resize", dismissOnViewportChange); + if (helpPreviewTimerRef.current !== null) { + window.clearTimeout(helpPreviewTimerRef.current); + helpPreviewTimerRef.current = null; + } + }; + }, [dismissHelpPreview]); + + const changeColumnLayout = ( + column: ColumnName, + boundaryDelta: number, + startLayout = effectiveLayout, + ) => { + const proposed = + column === "name" + ? { ...startLayout, nameWidth: startLayout.nameWidth + boundaryDelta } + : { ...startLayout, unitWidth: startLayout.unitWidth - boundaryDelta }; + onColumnLayoutChange(effectiveColumnLayout(proposed, availableWidth)); + }; + + const startColumnResize = ( + event: ReactPointerEvent, + column: ColumnName, + ) => { + dismissHelpPreview(); + event.preventDefault(); + event.currentTarget.setPointerCapture(event.pointerId); + columnDragRef.current = { + column, + pointerId: event.pointerId, + startX: event.clientX, + startLayout: effectiveLayout, + }; + setActiveColumnResize(column); + }; + + const moveColumnResize = (event: ReactPointerEvent) => { + const drag = columnDragRef.current; + if (!drag || drag.pointerId !== event.pointerId) { + return; + } + changeColumnLayout(drag.column, event.clientX - drag.startX, drag.startLayout); + }; + + const stopColumnResize = (event: ReactPointerEvent) => { + if (columnDragRef.current?.pointerId !== event.pointerId) { + return; + } + columnDragRef.current = null; + setActiveColumnResize(null); + if (event.currentTarget.hasPointerCapture(event.pointerId)) { + event.currentTarget.releasePointerCapture(event.pointerId); + } + }; + + const resizeColumnWithKeyboard = ( + event: ReactKeyboardEvent, + column: ColumnName, + ) => { + if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") { + return; + } + event.preventDefault(); + dismissHelpPreview(); + const step = event.shiftKey ? COLUMN_KEYBOARD_STEP * 3 : COLUMN_KEYBOARD_STEP; + changeColumnLayout(column, event.key === "ArrowRight" ? step : -step); + }; + + const nameColumnMaximum = Math.max( + MIN_NAME_WIDTH, + Math.min(MAX_NAME_WIDTH, availableWidth - MIN_VALUE_WIDTH - effectiveLayout.unitWidth), + ); + const unitColumnMaximum = Math.max( + MIN_UNIT_WIDTH, + Math.min(MAX_UNIT_WIDTH, availableWidth - MIN_VALUE_WIDTH - effectiveLayout.nameWidth), + ); + const tableStyle = { + "--parameter-name-width": `${effectiveLayout.nameWidth}px`, + "--parameter-unit-width": `${effectiveLayout.unitWidth}px`, + } as CSSProperties; + const previewRow = helpPreview + ? rows.find((row) => row.id === helpPreview.rowId) ?? null + : null; + const previewDescription = previewRow?.description || + (typeof previewRow?.help === "string" ? previewRow.help : ""); + + const renderColumnResizer = (column: ColumnName) => { + const currentWidth = + column === "name" ? effectiveLayout.nameWidth : effectiveLayout.unitWidth; + const minimum = column === "name" ? MIN_NAME_WIDTH : MIN_UNIT_WIDTH; + const maximum = column === "name" ? nameColumnMaximum : unitColumnMaximum; + const label = column === "name" ? "调整参数名列宽" : "调整单位列宽"; + return ( +
{ + dismissHelpPreview(); + onColumnLayoutChange({ ...DEFAULT_PARAMETER_COLUMN_LAYOUT }); + }} + onKeyDown={(event) => resizeColumnWithKeyboard(event, column)} + onLostPointerCapture={stopColumnResize} + onPointerCancel={stopColumnResize} + onPointerDown={(event) => startColumnResize(event, column)} + onPointerMove={moveColumnResize} + onPointerUp={stopColumnResize} + role="separator" + tabIndex={0} + /> + ); + }; + + const rowBlurred = (event: ReactFocusEvent) => { + if (!event.currentTarget.contains(event.relatedTarget as globalThis.Node | null)) { + dismissHelpPreview(); + } + }; + + return ( +
+ + + + + + + + + + + + + + + {rows.map((row) => { + const hasPreview = rowHasHelpPreview(row); + const hasUnit = row.unit !== undefined && row.unit !== null; + return ( + + + scheduleHelpPreview(event.currentTarget, row) + } + onMouseEnter={(event) => + scheduleHelpPreview(event.currentTarget, row, { + clientX: event.clientX, + clientY: event.clientY, + }) + } + onMouseLeave={dismissHelpPreview} + onMouseMove={(event) => trackHelpPointer(event, row)} + tabIndex={row.readOnly && hasPreview ? 0 : undefined} + > + + + + + {row.error ? ( + + + + ) : null} + + ); + })} + +
+ 参数 + {renderColumnResizer("name")} + + 值 + {renderColumnResizer("unit")} + + 单位 +
+ {row.name} + + {row.value} + + {hasUnit ? row.unit : —} +
+ {row.error} +
+ {helpPreview && previewRow && typeof document !== "undefined" + ? createPortal( + , + document.body, + ) + : null} +
+ ); +} diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 4b9e074..f337bfe 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -387,11 +387,385 @@ textarea { padding: 12px; } +.parameter-table-wrap { + width: 100%; + min-width: 0; + padding: 0 12px 12px; + overflow-x: hidden; +} + +.parameter-table-wrap.parameter-form { + display: block; + padding: 0 12px 12px; +} + +.parameter-table { + --parameter-name-width: 116px; + --parameter-unit-width: 72px; + width: 100%; + min-width: 0; + border: 1px solid #cbd5df; + border-spacing: 0; + border-radius: 4px; + color: #263442; + background: #ffffff; + table-layout: fixed; + font-size: 11px; +} + +.parameter-table-head .parameter-table-cell { + position: relative; + z-index: 1; + height: 30px; + padding: 0 7px; + overflow: visible; + border-bottom-color: #bfcbd7; + color: #475569; + background: #edf1f5; + font-size: 10px; + font-weight: 700; + line-height: 30px; +} + +.parameter-table-head .parameter-table-name-cell { + z-index: 3; +} + +.parameter-table-head .parameter-table-value-cell { + z-index: 2; +} + +.parameter-table-row { + height: 36px; +} + +.parameter-table-cell { + min-width: 0; + height: 36px; + padding: 2px 6px; + overflow: hidden; + border-right: 1px solid #d9e0e7; + border-bottom: 1px solid #d9e0e7; + vertical-align: middle; +} + +.parameter-table-cell:last-child { + border-right: 0; +} + +.parameter-table > tbody > .parameter-table-row:hover > .parameter-table-cell { + background: #f8fbfe; +} + +.parameter-table-row:focus-visible { + outline: 2px solid rgba(29, 111, 184, 0.5); + outline-offset: -2px; +} + +.parameter-name-cell, +.parameter-table-name-cell { + overflow: hidden; + color: #344256; + text-overflow: ellipsis; + white-space: nowrap; +} + +.parameter-value-cell, +.parameter-table-value-cell, +.parameter-unit-cell, +.parameter-table-unit-cell { + min-width: 0; + padding: 2px; +} + +.parameter-unit-cell, +.parameter-table-unit-cell { + color: #526172; + text-align: center; +} + +.parameter-table-cell input, +.parameter-table-cell select { + display: block; + width: 100%; + min-width: 0; + max-width: 100%; + height: 30px; + margin: 0; + padding: 0 6px; + border: 1px solid #d5dde6; + border-radius: 2px; + outline: 0; + color: #1f2933; + background: #ffffff; + font-size: 11px; +} + +.parameter-table-cell select { + padding-right: 4px; +} + +.parameter-table-cell input:hover:not(:disabled), +.parameter-table-cell select:hover:not(:disabled) { + border-color: #aebdcb; +} + +.parameter-table-cell input:focus, +.parameter-table-cell select:focus { + border-color: #1d6fb8; + outline: 0; + box-shadow: inset 0 0 0 1px #1d6fb8; +} + +.parameter-table-cell input:focus-visible, +.parameter-table-cell select:focus-visible { + outline: 2px solid rgba(29, 111, 184, 0.28); + outline-offset: -2px; +} + +.parameter-table-row.read-only .parameter-value-cell, +.parameter-table-row.read-only .parameter-table-value-cell, +.parameter-table-cell input:read-only { + background: #f4f6f8; +} + +.parameter-read-only-value, +.parameter-table-read-only-value { + display: flex; + align-items: center; + width: 100%; + min-width: 0; + height: 30px; + padding: 0 7px; + overflow: hidden; + border: 1px solid #d5dde6; + border-radius: 2px; + color: #526172; + text-overflow: ellipsis; + white-space: nowrap; + background: #f4f6f8; + font-variant-numeric: tabular-nums; +} + +.parameter-table-row.unitless .parameter-unit-cell, +.parameter-table-row.unitless .parameter-table-unit-cell { + color: #8a96a4; +} + +.parameter-table-row.unitless .parameter-unit-cell:empty::before, +.parameter-table-row.unitless .parameter-table-unit-cell:empty::before { + content: "—"; +} + +.parameter-table-row.invalid > .parameter-table-cell { + background: #fff8f7; +} + +.parameter-table-row.invalid input, +.parameter-table-row.invalid select, +.parameter-table-cell input[aria-invalid="true"], +.parameter-table-cell select[aria-invalid="true"] { + border-color: #c63b32; + box-shadow: inset 0 0 0 1px rgba(198, 59, 50, 0.18); +} + +.parameter-table-message-row { + height: auto; +} + +.parameter-table-message-row > .parameter-table-cell { + height: auto; + min-height: 24px; + padding: 4px 7px 5px; + overflow: visible; + white-space: normal; +} + +.parameter-table-message-row .field-error, +.parameter-table-message-row .field-hint { + display: block; + margin: 0; + overflow-wrap: anywhere; + line-height: 1.35; +} + +.parameter-table-row.invalid + .parameter-table-message-row > .parameter-table-cell { + color: #b42318; + background: #fff8f7; +} + +.parameter-table > tbody > tr:last-child > .parameter-table-cell { + border-bottom: 0; +} + +.parameter-column-resizer, +.parameter-table-column-resizer { + position: absolute; + z-index: 4; + top: 0; + right: -5px; + bottom: 0; + width: 9px; + padding: 0; + border: 0; + outline: 0; + background: transparent; + cursor: col-resize; + touch-action: none; + user-select: none; +} + +.parameter-column-resizer::after, +.parameter-table-column-resizer::after { + position: absolute; + top: 3px; + bottom: 3px; + left: 4px; + width: 1px; + background: #aebac7; + content: ""; +} + +.parameter-column-resizer:hover::after, +.parameter-column-resizer:focus-visible::after, +.parameter-column-resizer:active::after, +.parameter-table-column-resizer:hover::after, +.parameter-table-column-resizer:focus-visible::after, +.parameter-table-column-resizer:active::after { + width: 2px; + background: #1d6fb8; +} + +.parameter-column-resizer:focus-visible, +.parameter-table-column-resizer:focus-visible { + outline: 2px solid rgba(29, 111, 184, 0.42); + outline-offset: -2px; +} + +.parameter-table-wrap[data-resizing-column], +.parameter-table-wrap[data-resizing-column] * { + cursor: col-resize; + user-select: none; +} + +.parameter-table-wrap[data-resizing-column="name"] + .parameter-column-resizer[data-column="name"]::after, +.parameter-table-wrap[data-resizing-column="unit"] + .parameter-column-resizer[data-column="unit"]::after, +.parameter-table-wrap.resizing-name + .parameter-column-resizer[data-column="name"]::after, +.parameter-table-wrap.resizing-unit + .parameter-column-resizer[data-column="unit"]::after, +.parameter-table-wrap[data-resizing-column="name"] + .parameter-table-column-resizer[data-column="name"]::after, +.parameter-table-wrap[data-resizing-column="unit"] + .parameter-table-column-resizer[data-column="unit"]::after, +.parameter-table-wrap.resizing-name + .parameter-table-column-resizer[data-column="name"]::after, +.parameter-table-wrap.resizing-unit + .parameter-table-column-resizer[data-column="unit"]::after { + width: 2px; + background: #1d6fb8; +} + +.parameter-table .field, +.parameter-table .parameter-control { + display: contents; +} + +.parameter-help-preview { + position: fixed; + z-index: 10020; + width: max-content; + max-width: min(300px, calc(100vw - 16px)); + box-sizing: border-box; + padding: 10px 12px; + border: 1px solid #c7d1dc; + border-radius: 5px; + color: #263442; + background: #ffffff; + box-shadow: 0 8px 22px rgba(15, 23, 42, 0.2); + pointer-events: none; +} + +.parameter-help-preview .title, +.parameter-help-preview-title { + display: block; + overflow-wrap: anywhere; + color: #17212b; + font-size: 12px; + font-weight: 700; + line-height: 1.35; +} + +.parameter-help-preview .description, +.parameter-help-preview-description { + display: block; + margin-top: 6px; + overflow-wrap: anywhere; + color: #475569; + font-size: 11px; + line-height: 1.45; +} + +.parameter-help-preview .meta, +.parameter-help-preview-meta { + display: grid; + gap: 2px; + margin-top: 7px; + padding-top: 6px; + overflow-wrap: anywhere; + border-top: 1px solid #e2e8f0; + color: #64748b; + font-size: 10px; + line-height: 1.4; +} + .palette-panel-title { display: flex; align-items: center; justify-content: space-between; gap: 8px; + flex-wrap: wrap; +} + +.palette-display-mode { + display: inline-flex; + flex: 0 0 auto; + gap: 1px; + padding: 1px; + border: 1px solid #cad5e1; + border-radius: 5px; + background: #f4f7fa; +} + +.palette-display-mode-button { + display: grid; + width: 23px; + height: 22px; + padding: 0; + border: 0; + border-radius: 3px; + place-items: center; + color: #64748b; + background: transparent; + cursor: pointer; +} + +.palette-display-mode-button:hover { + color: #155a94; + background: #e6f0f8; +} + +.palette-display-mode-button:focus-visible { + outline: 2px solid #1d6fb8; + outline-offset: 1px; +} + +.palette-display-mode-button[aria-pressed="true"] { + color: #155a94; + background: #ffffff; + box-shadow: 0 1px 2px rgba(15, 23, 42, 0.16); } .catalog-source { @@ -455,11 +829,57 @@ textarea { font-weight: 700; } +.palette-collapse-toggle { + display: flex; + align-items: center; + gap: 4px; + width: 100%; + min-width: 0; + padding: 2px 0; + border: 0; + border-radius: 3px; + color: inherit; + text-align: left; + background: transparent; + font: inherit; + cursor: pointer; +} + +.palette-collapse-toggle:hover { + color: #155a94; +} + +.palette-collapse-toggle:focus-visible { + outline: 2px solid #1d6fb8; + outline-offset: 2px; +} + +.palette-collapse-chevron { + flex: 0 0 auto; + transform: rotate(0deg); + transition: transform 120ms ease; +} + +.palette-collapse-toggle[aria-expanded="false"] .palette-collapse-chevron { + transform: rotate(-90deg); +} + +.palette-library-toggle { + flex: 1 1 auto; + width: auto; +} + .palette-category-items { display: grid; gap: 8px; } +.palette[data-view-mode="icons"] .palette-category-items { + grid-template-columns: repeat(auto-fill, 76px); + gap: 6px; + justify-content: start; +} + .compact-form { grid-template-columns: repeat(2, minmax(0, 1fr)); } @@ -481,6 +901,37 @@ textarea { text-align: left; background: #ffffff; cursor: grab; + transition: border-color 120ms ease, box-shadow 120ms ease, + background 120ms ease, transform 80ms ease; +} + +.palette-item:hover { + border-color: #85add0; + background: #f5faff; + box-shadow: 0 2px 6px rgba(21, 90, 148, 0.1); +} + +.palette-item:focus-visible { + border-color: #1d6fb8; + outline: 2px solid rgba(29, 111, 184, 0.32); + outline-offset: 1px; +} + +.palette-item:active { + cursor: grabbing; + transform: translateY(1px); +} + +.palette-item-copy { + min-width: 0; + overflow: hidden; +} + +.palette-item-copy strong { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .palette-item small { @@ -494,11 +945,116 @@ textarea { } .palette-icon { + display: grid; + width: 36px; + height: 36px; + place-items: center; +} + +.component-symbol { + display: grid; + width: 100%; + height: 100%; + place-items: center; + pointer-events: none; +} + +.component-symbol-svg { display: block; - width: 32px; - height: 32px; - border: 2px solid #4b5563; - background: #f8fafc; + width: 100%; + height: 100%; + overflow: visible; +} + +.component-symbol-medium-label { + font-family: "Segoe UI", "Microsoft YaHei", sans-serif; + font-weight: 600; +} + +.palette[data-view-mode="icons"] .palette-list { + padding: 10px; +} + +.palette[data-view-mode="icons"] .palette-item { + grid-template-columns: minmax(0, 1fr); + grid-template-rows: 48px 16px; + gap: 4px; + width: 76px; + height: 82px; + min-height: 82px; + box-sizing: border-box; + padding: 6px 5px; + place-items: center; + text-align: center; +} + +.palette[data-view-mode="icons"] .palette-item-copy { + display: block; + width: 100%; + min-width: 0; + overflow: hidden; + line-height: 16px; + text-align: center; +} + +.palette[data-view-mode="icons"] .palette-item-copy strong { + width: 100%; + overflow: hidden; + font-size: 10px; + line-height: 16px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.palette[data-view-mode="icons"] .palette-item-copy small { + display: none; +} + +.palette[data-view-mode="icons"] .palette-icon { + flex: 0 0 48px; + width: 48px; + height: 48px; +} + +.palette-icon-preview { + position: fixed; + z-index: 10000; + display: grid; + grid-template-rows: 56px auto; + min-width: 104px; + max-width: 220px; + box-sizing: border-box; + padding: 9px 11px 10px; + border: 1px solid #cbd5e1; + border-radius: 6px; + place-items: center; + color: #1f2937; + background: #ffffff; + box-shadow: 0 7px 18px rgba(15, 23, 42, 0.2); + pointer-events: none; +} + +.palette-icon-preview-symbol { + display: grid; + flex: 0 0 56px; + width: 56px; + height: 56px; + place-items: center; +} + +.palette-icon-preview-symbol > .component-symbol { + width: 100%; + height: 100%; +} + +.palette-icon-preview-label { + max-width: 198px; + margin-top: 5px; + overflow-wrap: anywhere; + font-size: 12px; + font-weight: 700; + line-height: 1.35; + text-align: center; } .component-drag-preview { @@ -521,38 +1077,8 @@ textarea { } .component-drag-preview .palette-icon { - width: 28px; - height: 28px; - border-width: 1.5px; -} - -.component-drag-preview .icon-pipe { - width: 34px; - height: 10px; -} - -.component-drag-preview .icon-orifice { - width: 22px; - height: 22px; -} - -.component-drag-preview .icon-tee { width: 30px; - height: 28px; -} - -.component-drag-preview .icon-tee::before { - top: 10px; - width: 30px; - height: 8px; - border-width: 1.5px; -} - -.component-drag-preview .icon-tee::after { - left: 10px; - width: 8px; - height: 28px; - border-width: 1.5px; + height: 30px; } .component-drag-preview-label { @@ -565,82 +1091,6 @@ textarea { white-space: nowrap; } -.icon-cylinder { - border-radius: 13px; - background: #dbeafe; -} - -.icon-tank { - border-radius: 50%; - background: #dcfce7; -} - -.icon-pipe { - height: 14px; - border-radius: 8px; - background: #f8fafc; -} - -.icon-orifice { - transform: rotate(45deg); - background: #ffedd5; -} - -.icon-tee { - position: relative; - border: 0; -} - -.icon-generic { - position: relative; - border-radius: 4px; - background: #ffffff; -} - -.icon-generic::before, -.icon-generic::after { - position: absolute; - top: 50%; - width: 6px; - height: 6px; - box-sizing: border-box; - border: 1.5px solid #4b5563; - border-radius: 50%; - background: #ffffff; - content: ""; - transform: translateY(-50%); -} - -.icon-generic::before { - left: -4px; -} - -.icon-generic::after { - right: -4px; -} - -.icon-tee::before, -.icon-tee::after { - position: absolute; - content: ""; - border-radius: 6px; - background: #e5e7eb; - border: 2px solid #4b5563; -} - -.icon-tee::before { - top: 12px; - left: 0; - width: 32px; - height: 10px; -} - -.icon-tee::after { - top: 0; - left: 11px; - width: 10px; - height: 32px; -} .flow-canvas { position: relative; @@ -756,6 +1206,38 @@ textarea { transform-origin: center; } +.node-symbol > .component-symbol { + width: 100%; + height: 100%; +} + +.sim-node.symbol-presentation-bare .sim-node-frame, +.sim-node.symbol-presentation-bare.selected .sim-node-frame { + border-color: transparent; + background: transparent; + box-shadow: none; +} + +.sim-node.symbol-presentation-bare .node-label { + background: transparent; +} + +.sim-node.symbol-presentation-bare.selected .component-symbol-svg { + filter: drop-shadow(0 0 3px rgba(29, 111, 184, 0.62)); +} + +.react-flow__node.validation-error + .sim-node.symbol-presentation-bare + .component-symbol-svg { + filter: drop-shadow(0 0 3px rgba(198, 59, 50, 0.68)); +} + +.react-flow__node.validation-warning + .sim-node.symbol-presentation-bare + .component-symbol-svg { + filter: drop-shadow(0 0 3px rgba(192, 138, 22, 0.68)); +} + .node-label { position: absolute; z-index: 2; @@ -790,72 +1272,6 @@ textarea { -webkit-line-clamp: 2; } -.symbol-cylinder, -.symbol-tank, -.symbol-pipe, -.symbol-orifice, -.symbol-tee { - box-sizing: border-box; - width: 42px; - height: 22px; - border: 2px solid #4b5563; - background: #f8fafc; -} - -.symbol-cylinder { - height: 28px; - border-radius: 12px; - background: #dbeafe; -} - -.symbol-tank { - height: 28px; - border-radius: 50%; - background: #dcfce7; -} - -.symbol-pipe { - height: 12px; - border-radius: 8px; -} - -.symbol-orifice { - width: 26px; - height: 26px; - transform: rotate(45deg); - background: #ffedd5; -} - -.symbol-tee { - position: relative; - height: 32px; - border: 0; -} - -.symbol-tee::before, -.symbol-tee::after { - position: absolute; - box-sizing: border-box; - content: ""; - border: 2px solid #4b5563; - border-radius: 6px; - background: #f8fafc; -} - -.symbol-tee::before { - top: 10px; - left: 0; - width: 42px; - height: 12px; -} - -.symbol-tee::after { - top: 0; - left: 15px; - width: 12px; - height: 32px; -} - .port-handle { z-index: 3; width: 10px; @@ -941,6 +1357,13 @@ textarea { font-weight: 500; } +.field-hint { + color: #64748b; + font-size: 11px; + font-weight: 400; + line-height: 1.4; +} + .node-id { padding-bottom: 8px; border-bottom: 1px solid #e3e8ef; diff --git a/frontend/tests/e2e/amesim-medium.spec.ts b/frontend/tests/e2e/amesim-medium.spec.ts new file mode 100644 index 0000000..d84288c --- /dev/null +++ b/frontend/tests/e2e/amesim-medium.spec.ts @@ -0,0 +1,202 @@ +import { expect, test } from "@playwright/test"; +import { prepareApp } from "./fixtures"; + +test.beforeEach(async ({ page }) => { + await prepareApp(page); +}); + +test("拖入介质定义后可供气动元件选择,删除定义会回退到内置空气", async ({ + page, +}) => { + await page.goto("/"); + + const canvas = page.locator(".flow-canvas .react-flow__pane"); + await page + .getByRole("button", { name: /空气介质定义/ }) + .dragTo(canvas, { targetPosition: { x: 280, y: 220 } }); + + const mediumNode = page.locator( + '.flow-canvas .react-flow__node[data-id="amesim_ideal_air_medium_1"]', + ); + await expect(mediumNode).toBeVisible(); + await mediumNode.click(); + await expect(page.getByLabel("介质定义索引(自动分配)")).toHaveValue("1"); + const propertyModelSelect = page.getByLabel("物性计算模型"); + await expect(propertyModelSelect).toHaveValue("0"); + await expect(propertyModelSelect.locator('option[value="0"]')).toHaveText( + "理想气体", + ); + + await page + .getByRole("button", { name: /AMESim 气动测试元件/ }) + .dragTo(canvas, { targetPosition: { x: 650, y: 360 } }); + + const pneumaticNode = page.locator( + '.flow-canvas .react-flow__node[data-id="amesim_pneumatic_test_1"]', + ); + await expect(pneumaticNode).toBeVisible(); + await pneumaticNode.click(); + + const mediumSelect = page.getByLabel("介质物性索引"); + await expect(mediumSelect).toHaveValue("0"); + await expect(mediumSelect.locator('option[value="0"]')).toHaveText("0"); + await expect(mediumSelect.locator('option[value="1"]')).toHaveText("1"); + + await mediumSelect.selectOption("1"); + await expect(mediumSelect).toHaveValue("1"); + + await mediumNode.click(); + await page.getByRole("button", { name: "删除 (Delete)" }).click(); + await expect(mediumNode).toHaveCount(0); + + await pneumaticNode.click(); + await expect(page.getByLabel("介质物性索引")).toHaveValue("0"); + + const consolePanel = page.getByRole("complementary", { + name: "仿真控制台", + exact: true, + }); + await page.getByRole("button", { name: "展开仿真控制台" }).click(); + await expect(consolePanel).toContainText( + "介质定义已删除,1 个介质物性引用已切回 0 — 空气(理想气体,内置默认)", + ); + + await page.getByRole("button", { name: "生成系统 XML" }).click(); + const xmlBlock = consolePanel.getByLabel("生成的系统 XML"); + await expect(xmlBlock).toContainText('mediumReferenceVersion="1"'); + await expect(xmlBlock).toContainText(' { + await page.goto("/"); + + const canvas = page.locator(".flow-canvas .react-flow__pane"); + await page + .getByRole("button", { name: /空气介质定义/ }) + .dragTo(canvas, { targetPosition: { x: 280, y: 220 } }); + + const mediumNode = page.locator( + '.flow-canvas .react-flow__node[data-id="amesim_ideal_air_medium_1"]', + ); + await mediumNode.click(); + await expect(page.getByLabel("物性计算模型")).toHaveValue("0"); + + await page.getByRole("button", { name: "生成系统 XML" }).click(); + const consolePanel = page.getByRole("complementary", { + name: "仿真控制台", + exact: true, + }); + await expect(consolePanel.getByLabel("生成的系统 XML")).toContainText( + ' { + await page.goto("/"); + + const canvas = page.locator(".flow-canvas .react-flow__pane"); + await page + .getByRole("button", { name: /空气介质定义/ }) + .dragTo(canvas, { targetPosition: { x: 240, y: 180 } }); + await page + .getByRole("button", { name: /氦气介质定义/ }) + .dragTo(canvas, { targetPosition: { x: 520, y: 180 } }); + + const heliumNode = page.locator( + '.flow-canvas .react-flow__node[data-id="amesim_helium_medium_1"]', + ); + await expect(heliumNode).toBeVisible(); + await heliumNode.click(); + await expect(page.getByLabel("介质定义索引(自动分配)")).toHaveValue("2"); + + const propertyModelSelect = page.getByLabel("物性计算模型"); + await expect(propertyModelSelect).toHaveValue("0"); + await expect(propertyModelSelect.locator('option[value="0"]')).toHaveText( + "Peng–Robinson", + ); + + await page + .getByRole("button", { name: /AMESim 气动测试元件/ }) + .dragTo(canvas, { targetPosition: { x: 760, y: 360 } }); + + const pneumaticNode = page.locator( + '.flow-canvas .react-flow__node[data-id="amesim_pneumatic_test_1"]', + ); + await expect(pneumaticNode).toBeVisible(); + await pneumaticNode.click(); + + const mediumSelect = page.getByLabel("介质物性索引"); + await expect(mediumSelect.locator("option")).toHaveText(["0", "1", "2"]); + await mediumSelect.selectOption("2"); + await expect(mediumSelect).toHaveValue("2"); + + await page.getByRole("button", { name: "生成系统 XML" }).click(); + const consolePanel = page.getByRole("complementary", { + name: "仿真控制台", + exact: true, + }); + const xmlBlock = consolePanel.getByLabel("生成的系统 XML"); + await expect(xmlBlock).toContainText('type="amesim_helium_medium"'); + await expect(xmlBlock).toContainText(']+type="amesim_helium_medium"[\s\S]*?/, + ); + expect(xml).toMatch( + /]+type="amesim_pneumatic_test"[\s\S]*?/, + ); +}); + +test("导入工程中的未知物性计算模型会被检查出来", async ({ page }) => { + await page.goto("/"); + const project = { + name: "invalid-property-model", + mediumReferenceVersion: 1, + nodes: [ + { + id: "amesim_ideal_air_medium_1", + type: "simulationComponent", + position: { x: 280, y: 220 }, + data: { + label: "空气介质_1", + componentType: "amesim_ideal_air_medium", + modelType: "amesim_ideal_air_medium", + ports: [], + parameters: { gi: 1, property_model: 99 }, + parameterUnits: {}, + rotation: 0, + mirrored: false, + }, + }, + ], + edges: [], + simulation: { + t_start: 0, + t_stop: 1, + step: 0.1, + max_step: 0.05, + method: "RK45", + }, + }; + + await page.locator('input[type="file"]').setInputFiles({ + name: "invalid-property-model.json", + mimeType: "application/json", + buffer: Buffer.from(JSON.stringify(project)), + }); + await page.getByRole("button", { name: "检查模型" }).click(); + + const consolePanel = page.getByRole("complementary", { + name: "仿真控制台", + exact: true, + }); + await expect(consolePanel).toContainText( + "空气介质_1:物性计算模型不在支持的可选项中", + ); +}); diff --git a/frontend/tests/e2e/fixtures.ts b/frontend/tests/e2e/fixtures.ts index 37b7f01..52e11a8 100644 --- a/frontend/tests/e2e/fixtures.ts +++ b/frontend/tests/e2e/fixtures.ts @@ -25,13 +25,136 @@ export const componentCatalog = { temporary: true, order: 1, components: [ + { + type: "amesim_ideal_air_medium", + label: "空气介质定义", + modelType: "amesim_ideal_air_medium", + modelVersion: "0.2.0", + symbol: "amesim_ideal_air_medium", + order: 1, + category: { + id: "media", + label: "介质物性", + order: 1, + }, + role: "amesimGasMediumDefinition", + ports: [], + parameters: [ + { + name: "gi", + label: "介质定义索引(gi)", + quantity: "dimensionless", + unit: "", + default: 1, + minimum: 1, + maximum: 99, + minimumExclusive: false, + description: "介质定义索引由画布自动分配;索引 0 保留给内置理想空气。", + }, + { + name: "property_model", + label: "物性计算模型", + quantity: "dimensionless", + unit: "", + default: 0, + minimum: 0, + maximum: 0, + minimumExclusive: false, + editor: "amesimGasPropertyModel", + options: [{ value: 0, label: "理想气体" }], + description: "选择空气介质采用的物性计算模型。", + }, + ], + }, + { + type: "amesim_helium_medium", + label: "氦气介质定义", + modelType: "amesim_helium_medium", + modelVersion: "0.1.0", + symbol: "amesim_helium_medium", + order: 2, + category: { + id: "media", + label: "介质物性", + order: 1, + }, + role: "amesimGasMediumDefinition", + ports: [], + parameters: [ + { + name: "gi", + label: "介质定义索引(gi)", + quantity: "dimensionless", + unit: "", + default: 1, + minimum: 1, + maximum: 99, + minimumExclusive: false, + description: "介质定义索引由画布自动分配;索引 0 保留给内置理想空气。", + }, + { + name: "property_model", + label: "物性计算模型", + quantity: "dimensionless", + unit: "", + default: 0, + minimum: 0, + maximum: 0, + minimumExclusive: false, + editor: "amesimGasPropertyModel", + options: [{ value: 0, label: "Peng–Robinson" }], + description: "氦气当前使用 Peng–Robinson 状态方程计算物性。", + }, + ], + }, + { + type: "amesim_pneumatic_test", + label: "AMESim 气动测试元件", + modelType: "amesim_pneumatic_test", + modelVersion: "1.0.0", + symbol: "symbol-not-registered", + order: 3, + category: { + id: "pneumatic", + label: "气动元件", + order: 2, + }, + ports: [ + physicalPort("port_a", "left", "bidirectional"), + physicalPort("port_b", "right", "bidirectional"), + ], + parameters: [ + { + name: "gi", + label: "介质物性模型(gi)", + quantity: "dimensionless", + unit: "", + default: 0, + minimum: 0, + maximum: 99, + minimumExclusive: false, + editor: "amesimGasReference", + description: "选择该气动元件引用的画布介质定义索引;0 表示内置理想空气。", + }, + { + name: "reference_pressure", + label: "参考压力", + quantity: "pressure", + unit: "Pa", + default: 100000, + minimum: 0, + minimumExclusive: true, + description: "用于验证参数表单位换算和 XML 基础单位输出的参考压力。", + }, + ], + }, { type: "generic_sensor", label: "通用测试元件", modelType: "generic_sensor", modelVersion: "1.0.0", symbol: "symbol-not-registered", - order: 1, + order: 4, category: { id: "generic", label: "通用组件", @@ -49,6 +172,7 @@ export const componentCatalog = { unit: "", default: 1, minimumExclusive: false, + description: "传感器输入值的无量纲放大倍数。", }, ], }, @@ -158,8 +282,12 @@ export const resultSnapshot = { export async function prepareApp(page: Page) { await page.addInitScript(() => { - window.localStorage.clear(); - window.sessionStorage.clear(); + const preparedKey = "system-simulation-flow:e2e-storage-prepared"; + if (window.sessionStorage.getItem(preparedKey) !== "1") { + window.localStorage.clear(); + window.sessionStorage.clear(); + window.sessionStorage.setItem(preparedKey, "1"); + } window.location.hash = "#/modeling"; }); await page.route("**/api/components/catalog", async (route) => { diff --git a/frontend/tests/e2e/palette-display.spec.ts b/frontend/tests/e2e/palette-display.spec.ts new file mode 100644 index 0000000..5f4435e --- /dev/null +++ b/frontend/tests/e2e/palette-display.spec.ts @@ -0,0 +1,273 @@ +import { expect, test, type Locator } from "@playwright/test"; +import { prepareApp } from "./fixtures"; + +test.beforeEach(async ({ page }) => { + await prepareApp(page); + await page.addInitScript(() => { + window.localStorage.removeItem( + "system-simulation-flow:palette-collapsed-sections", + ); + }); +}); + +async function expectFixedIconCardSize( + item: Locator, +) { + const itemBox = await item.boundingBox(); + const iconBox = await item.locator(".palette-icon").boundingBox(); + + expect(itemBox).not.toBeNull(); + expect(iconBox).not.toBeNull(); + expect(itemBox!.width).toBeGreaterThanOrEqual(75); + expect(itemBox!.width).toBeLessThanOrEqual(77); + expect(itemBox!.height).toBeGreaterThanOrEqual(81); + expect(itemBox!.height).toBeLessThanOrEqual(83); + expect(iconBox!.width).toBeGreaterThanOrEqual(47); + expect(iconBox!.width).toBeLessThanOrEqual(49); + expect(iconBox!.height).toBeGreaterThanOrEqual(47); + expect(iconBox!.height).toBeLessThanOrEqual(49); +} + +test("组件库可切换详细与仅图标显示,并在刷新后保留选择", async ({ page }) => { + await page.goto("/"); + + const palette = page.locator("aside.palette"); + const detailedButton = page.getByRole("button", { + name: "详细显示", + exact: true, + }); + const iconsButton = page.getByRole("button", { + name: "仅图标显示", + exact: true, + }); + const airItem = palette.getByRole("button", { + name: "空气介质定义", + exact: true, + }); + const heliumItem = palette.getByRole("button", { + name: "氦气介质定义", + exact: true, + }); + + await expect(palette).toHaveAttribute("data-view-mode", "detailed"); + await expect(detailedButton).toHaveAttribute("aria-pressed", "true"); + await expect(iconsButton).toHaveAttribute("aria-pressed", "false"); + await expect(airItem.locator(".palette-item-copy")).toBeVisible(); + + const airSymbol = airItem.locator( + '[data-component-symbol="amesim_ideal_air_medium"]', + ); + const heliumSymbol = heliumItem.locator( + '[data-component-symbol="amesim_helium_medium"]', + ); + await expect(airSymbol.locator("svg")).toBeVisible(); + await expect( + airSymbol.locator('[data-symbol-label="Air"]'), + ).toHaveText("Air"); + await expect(airSymbol.locator(".component-symbol-medium-svg path")).toHaveCount(0); + await expect(heliumSymbol.locator("svg")).toBeVisible(); + await expect( + heliumSymbol.locator('[data-symbol-label="He"]'), + ).toHaveText("He"); + await expect(heliumSymbol.locator(".component-symbol-medium-svg path")).toHaveCount(0); + + const unknownSymbol = palette + .getByRole("button", { name: "通用测试元件", exact: true }) + .locator('[data-component-symbol="generic"]'); + await expect(unknownSymbol.locator("svg")).toBeVisible(); + + await iconsButton.click(); + await expect(palette).toHaveAttribute("data-view-mode", "icons"); + await expect(detailedButton).toHaveAttribute("aria-pressed", "false"); + await expect(iconsButton).toHaveAttribute("aria-pressed", "true"); + await expect(airItem).toBeVisible(); + await expect(airItem.locator(".palette-item-name")).toBeVisible(); + await expect(airItem.locator(".palette-item-name")).toHaveText("空气介质定义"); + await expect(airItem.locator(".palette-item-meta")).toBeHidden(); + await expect(airItem).not.toHaveAttribute("title"); + + await page.reload(); + + await expect(palette).toHaveAttribute("data-view-mode", "icons"); + await expect(iconsButton).toHaveAttribute("aria-pressed", "true"); + await expect(airItem.locator(".palette-item-name")).toBeVisible(); + await expect(airItem.locator(".palette-item-meta")).toBeHidden(); +}); + +test("组件库和分类支持独立折叠并同步可访问状态", async ({ page }) => { + await page.goto("/"); + + const palette = page.locator("aside.palette"); + const libraryToggle = palette.getByRole("button", { + name: "自动化测试组件库", + exact: true, + }); + const categoryToggle = palette.getByRole("button", { + name: "介质物性", + exact: true, + }); + + await expect(libraryToggle).toHaveAttribute("aria-expanded", "true"); + const libraryContentId = await libraryToggle.getAttribute("aria-controls"); + expect(libraryContentId).toBeTruthy(); + await expect(page.locator(`[id="${libraryContentId}"]`)).toBeVisible(); + + await expect(categoryToggle).toHaveAttribute("aria-expanded", "true"); + const categoryItemsId = await categoryToggle.getAttribute("aria-controls"); + expect(categoryItemsId).toBeTruthy(); + await expect(page.locator(`[id="${categoryItemsId}"]`)).toBeVisible(); + await expect( + palette.getByRole("button", { name: "空气介质定义", exact: true }), + ).toBeVisible(); + + await categoryToggle.click(); + await expect(categoryToggle).toHaveAttribute("aria-expanded", "false"); + await expect(page.locator(`[id="${categoryItemsId}"]`)).toHaveCount(0); + await expect( + palette.getByRole("button", { name: "空气介质定义", exact: true }), + ).toHaveCount(0); + + await categoryToggle.click(); + await expect(categoryToggle).toHaveAttribute("aria-expanded", "true"); + await expect(page.locator(`[id="${categoryItemsId}"]`)).toBeVisible(); + + await libraryToggle.click(); + await expect(libraryToggle).toHaveAttribute("aria-expanded", "false"); + await expect(page.locator(`[id="${libraryContentId}"]`)).toHaveCount(0); + await expect( + palette.getByRole("button", { name: "介质物性", exact: true }), + ).toHaveCount(0); + + await libraryToggle.click(); + await expect(libraryToggle).toHaveAttribute("aria-expanded", "true"); + await expect(page.locator(`[id="${libraryContentId}"]`)).toBeVisible(); + await expect( + palette.getByRole("button", { name: "介质物性", exact: true }), + ).toHaveAttribute("aria-expanded", "true"); +}); + +test("仅图标模式名称截断且卡片尺寸不随侧栏和视口改变", async ({ page }) => { + await page.goto("/"); + await page + .getByRole("button", { name: "仅图标显示", exact: true }) + .click(); + + const palette = page.locator("aside.palette"); + const longNameItem = palette.getByRole("button", { + name: "AMESim 气动测试元件", + exact: true, + }); + const name = longNameItem.locator(".palette-item-name"); + const meta = longNameItem.locator(".palette-item-meta"); + + await expect(name).toBeVisible(); + await expect(name).toHaveText("AMESim 气动测试元件"); + await expect(meta).toBeHidden(); + await expect(longNameItem).not.toHaveAttribute("title"); + const truncation = await name.evaluate((element) => { + const style = window.getComputedStyle(element); + return { + clipped: element.scrollWidth > element.clientWidth, + overflow: style.overflow, + textOverflow: style.textOverflow, + whiteSpace: style.whiteSpace, + }; + }); + expect(truncation).toEqual({ + clipped: true, + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + }); + + await expectFixedIconCardSize(longNameItem); + const initialPaletteBox = await palette.boundingBox(); + expect(initialPaletteBox).not.toBeNull(); + + const paletteSeparator = page.getByRole("separator", { + name: "调整组件库宽度", + }); + await paletteSeparator.focus(); + for (let index = 0; index < 5; index += 1) { + await page.keyboard.press("ArrowRight"); + } + await expect(paletteSeparator).toHaveAttribute("aria-valuenow", "330"); + const expandedPaletteBox = await palette.boundingBox(); + expect(expandedPaletteBox).not.toBeNull(); + expect(expandedPaletteBox!.width).toBeGreaterThan(initialPaletteBox!.width + 90); + await expectFixedIconCardSize(longNameItem); + + await page.setViewportSize({ width: 1000, height: 760 }); + await expect + .poll(async () => Number(await paletteSeparator.getAttribute("aria-valuenow"))) + .toBeLessThan(330); + await expectFixedIconCardSize(longNameItem); +}); + +test("仅图标模式悬停后显示完整名称和图标预览并在移开后关闭", async ({ page }) => { + await page.goto("/"); + await page + .getByRole("button", { name: "仅图标显示", exact: true }) + .click(); + + const item = page.locator("aside.palette").getByRole("button", { + name: "AMESim 气动测试元件", + exact: true, + }); + const preview = page.locator("body > .palette-icon-preview[role='tooltip']"); + + await item.hover(); + await page.waitForTimeout(600); + await expect(preview).toBeVisible(); + await expect(preview.locator(".palette-icon-preview-label")).toHaveText( + "AMESim 气动测试元件", + ); + await expect( + preview.locator( + '.palette-icon-preview-symbol [data-component-symbol="generic"] svg', + ), + ).toBeVisible(); + await expect(item).toHaveAttribute("aria-describedby", "palette-icon-preview"); + + await page.locator(".palette-panel-title").hover(); + await expect(preview).toHaveCount(0); + await expect(item).not.toHaveAttribute("aria-describedby", "palette-icon-preview"); +}); + +test("仅图标模式仍可拖入氦气介质并在画布显示专用图标", async ({ page }) => { + await page.goto("/"); + + await page + .getByRole("button", { name: "仅图标显示", exact: true }) + .click(); + + const palette = page.locator("aside.palette"); + const heliumItem = palette.getByRole("button", { + name: "氦气介质定义", + exact: true, + }); + const canvas = page.locator(".flow-canvas .react-flow__pane"); + await heliumItem.dragTo(canvas, { + targetPosition: { x: 420, y: 220 }, + }); + + const heliumNode = page.locator( + '.flow-canvas .react-flow__node[data-id="amesim_helium_medium_1"]', + ); + await expect(heliumNode).toBeVisible(); + await expect( + heliumNode.locator(".sim-node.symbol-presentation-bare"), + ).toBeVisible(); + + const heliumCanvasSymbol = heliumNode.locator( + '.node-symbol [data-component-symbol="amesim_helium_medium"]', + ); + await expect(heliumCanvasSymbol).toHaveAttribute( + "data-symbol-presentation", + "bare", + ); + await expect(heliumCanvasSymbol.locator("svg")).toBeVisible(); + await expect( + heliumCanvasSymbol.locator('[data-symbol-label="He"]'), + ).toHaveText("He"); +}); diff --git a/frontend/tests/e2e/parameter-table.spec.ts b/frontend/tests/e2e/parameter-table.spec.ts new file mode 100644 index 0000000..2d291c8 --- /dev/null +++ b/frontend/tests/e2e/parameter-table.spec.ts @@ -0,0 +1,203 @@ +import { expect, test, type Locator, type Page } from "@playwright/test"; + +import { prepareApp } from "./fixtures"; + +test.beforeEach(async ({ page }) => { + await prepareApp(page); +}); + +function parameterRow(table: Locator, name: string) { + return table.getByRole("rowheader", { name, exact: true }).locator(".."); +} + +async function addPneumaticTestComponent(page: Page) { + const canvas = page.locator(".flow-canvas .react-flow__pane"); + await page + .getByRole("button", { name: "AMESim 气动测试元件", exact: true }) + .dragTo(canvas, { targetPosition: { x: 620, y: 330 } }); + const node = page.locator( + '.flow-canvas .react-flow__node[data-id="amesim_pneumatic_test_1"]', + ); + await expect(node).toBeVisible(); + await node.click(); +} + +test("组件参数和仿真设置使用共享的三列表格", async ({ page }) => { + await page.goto("/"); + await addPneumaticTestComponent(page); + + const componentTable = page.getByRole("table", { name: "组件参数" }); + const simulationTable = page.getByRole("table", { name: "仿真设置" }); + await expect(componentTable).toBeVisible(); + await expect(simulationTable).toBeVisible(); + + await expect(componentTable.getByRole("columnheader")).toHaveText([ + "参数", + "值", + "单位", + ]); + await expect(componentTable.getByRole("rowheader")).toHaveText([ + "组件名称", + "节点 ID", + "介质物性索引", + "参考压力", + ]); + await expect(simulationTable.getByRole("rowheader")).toHaveText([ + "起始时间", + "结束时间", + "采样步长", + "最大积分步长", + "求解器", + ]); + + await expect(parameterRow(simulationTable, "起始时间").locator("td").last()).toHaveText( + "s", + ); + await expect(parameterRow(simulationTable, "求解器").locator("td").last()).toHaveText( + "—", + ); + + const componentNameWidth = await componentTable + .locator("thead .parameter-table-name-cell") + .evaluate((element) => element.getBoundingClientRect().width); + const simulationNameWidth = await simulationTable + .locator("thead .parameter-table-name-cell") + .evaluate((element) => element.getBoundingClientRect().width); + expect(Math.abs(componentNameWidth - simulationNameWidth)).toBeLessThanOrEqual(1); +}); + +test("参数说明延迟显示,单位切换不改变 XML 基础值", async ({ page }) => { + await page.goto("/"); + await addPneumaticTestComponent(page); + + const componentTable = page.getByRole("table", { name: "组件参数" }); + const pressureRow = parameterRow(componentTable, "参考压力"); + const preview = page.locator("body > #parameter-help-preview"); + + const pressureRowBox = await pressureRow.boundingBox(); + expect(pressureRowBox).not.toBeNull(); + const pointer = { + x: pressureRowBox!.x + pressureRowBox!.width / 2, + y: pressureRowBox!.y + pressureRowBox!.height / 2, + }; + await page.mouse.move(pointer.x, pointer.y); + await page.waitForTimeout(600); + await expect(preview).toBeVisible(); + await expect(preview.locator(".parameter-help-preview-title")).toHaveText("参考压力"); + await expect(preview.locator(".parameter-help-preview-description")).toContainText( + "单位换算和 XML 基础单位输出", + ); + await expect(preview.locator(".parameter-help-preview-meta")).toContainText( + "默认值:100000", + ); + await expect(preview.locator(".parameter-help-preview-meta")).toContainText( + "当前单位:Pa", + ); + const previewBox = await preview.boundingBox(); + expect(previewBox).not.toBeNull(); + const horizontalGap = + pointer.x < previewBox!.x + ? previewBox!.x - pointer.x + : pointer.x > previewBox!.x + previewBox!.width + ? pointer.x - (previewBox!.x + previewBox!.width) + : 0; + const verticalGap = + pointer.y < previewBox!.y + ? previewBox!.y - pointer.y + : pointer.y > previewBox!.y + previewBox!.height + ? pointer.y - (previewBox!.y + previewBox!.height) + : 0; + expect(Math.hypot(horizontalGap, verticalGap)).toBeLessThanOrEqual(24); + await page.mouse.move(pointer.x + 24, pointer.y); + await page.waitForTimeout(100); + const stationaryPreviewBox = await preview.boundingBox(); + expect(stationaryPreviewBox).not.toBeNull(); + expect(Math.abs(stationaryPreviewBox!.x - previewBox!.x)).toBeLessThanOrEqual(1); + expect(Math.abs(stationaryPreviewBox!.y - previewBox!.y)).toBeLessThanOrEqual(1); + + await page.locator(".panel-title").filter({ hasText: "组件参数" }).hover(); + await expect(preview).toHaveCount(0); + + await page.getByLabel("参考压力单位").selectOption("kPa"); + await expect(page.getByLabel("参考压力", { exact: true })).toHaveValue("100"); + + await pressureRow.hover(); + await page.waitForTimeout(600); + await expect(preview.locator(".parameter-help-preview-meta")).toContainText( + "默认值:100", + ); + await expect(preview.locator(".parameter-help-preview-meta")).toContainText( + "当前单位:kPa", + ); + + await page.getByRole("button", { name: "生成系统 XML", exact: true }).click(); + const consolePanel = page.getByRole("complementary", { + name: "仿真控制台", + exact: true, + }); + await expect(consolePanel.getByLabel("生成的系统 XML")).toContainText( + ' { + await page.goto("/"); + + const simulationTable = page.getByRole("table", { name: "仿真设置" }); + const nameHeader = simulationTable.locator("thead .parameter-table-name-cell"); + const separator = simulationTable.getByRole("separator", { + name: "调整参数名列宽", + exact: true, + }); + const initialWidth = await nameHeader.evaluate( + (element) => element.getBoundingClientRect().width, + ); + const initialValue = Number(await separator.getAttribute("aria-valuenow")); + const separatorBox = await separator.boundingBox(); + expect(separatorBox).not.toBeNull(); + await page.mouse.move( + separatorBox!.x + separatorBox!.width / 2, + separatorBox!.y + separatorBox!.height / 2, + ); + await page.mouse.down(); + await page.mouse.move( + separatorBox!.x + separatorBox!.width / 2 + 16, + separatorBox!.y + separatorBox!.height / 2, + { steps: 3 }, + ); + await page.mouse.up(); + + await expect(separator).toHaveAttribute( + "aria-valuenow", + String(initialValue + 16), + ); + const resizedWidth = await nameHeader.evaluate( + (element) => element.getBoundingClientRect().width, + ); + expect(resizedWidth).toBeGreaterThan(initialWidth + 14); + + await page.reload(); + const reloadedTable = page.getByRole("table", { name: "仿真设置" }); + await expect( + reloadedTable.getByRole("separator", { + name: "调整参数名列宽", + exact: true, + }), + ).toHaveAttribute("aria-valuenow", String(initialValue + 16)); + + const wrapOverflow = await reloadedTable.evaluate((table) => { + const wrap = table.closest(".parameter-table-wrap"); + return wrap ? wrap.scrollWidth - wrap.clientWidth : Number.POSITIVE_INFINITY; + }); + expect(wrapOverflow).toBeLessThanOrEqual(1); + + const reloadedSeparator = reloadedTable.getByRole("separator", { + name: "调整参数名列宽", + exact: true, + }); + await reloadedSeparator.dblclick(); + await expect(reloadedSeparator).toHaveAttribute("aria-valuenow", "112"); + await reloadedSeparator.focus(); + await page.keyboard.press("ArrowRight"); + await expect(reloadedSeparator).toHaveAttribute("aria-valuenow", "120"); +}); diff --git a/schemas/component-catalog-v1.schema.json b/schemas/component-catalog-v1.schema.json index d3a8300..76ce22c 100644 --- a/schemas/component-catalog-v1.schema.json +++ b/schemas/component-catalog-v1.schema.json @@ -160,6 +160,23 @@ } } }, + "parameterOption": { + "type": "object", + "additionalProperties": false, + "required": [ + "value", + "label" + ], + "properties": { + "value": { + "type": "number" + }, + "label": { + "type": "string", + "minLength": 1 + } + } + }, "parameter": { "type": "object", "additionalProperties": false, @@ -180,6 +197,10 @@ "type": "string", "minLength": 1 }, + "description": { + "type": "string", + "minLength": 1 + }, "quantity": { "type": "string", "minLength": 1 @@ -198,8 +219,57 @@ }, "minimumExclusive": { "type": "boolean" + }, + "editor": { + "enum": [ + "amesimGasReference", + "amesimGasPropertyModel" + ] + }, + "options": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/parameterOption" + } } - } + }, + "allOf": [ + { + "if": { + "required": [ + "editor" + ], + "properties": { + "editor": { + "const": "amesimGasPropertyModel" + } + } + }, + "then": { + "required": [ + "options" + ] + } + }, + { + "if": { + "required": [ + "options" + ] + }, + "then": { + "required": [ + "editor" + ], + "properties": { + "editor": { + "const": "amesimGasPropertyModel" + } + } + } + } + ] }, "component": { "type": "object", @@ -235,6 +305,11 @@ "order": { "type": "integer" }, + "role": { + "enum": [ + "amesimGasMediumDefinition" + ] + }, "category": { "$ref": "#/$defs/category" }, diff --git a/schemas/system-simulation-v2.xsd b/schemas/system-simulation-v2.xsd index 7442168..5fa8c6f 100644 --- a/schemas/system-simulation-v2.xsd +++ b/schemas/system-simulation-v2.xsd @@ -130,6 +130,7 @@ + diff --git a/tests/test_amesim_component_migration_matrix.py b/tests/test_amesim_component_migration_matrix.py index 76d2c08..2dd1e61 100644 --- a/tests/test_amesim_component_migration_matrix.py +++ b/tests/test_amesim_component_migration_matrix.py @@ -3,7 +3,7 @@ from __future__ import annotations import unittest from pathlib import Path -from PythonModels.systems import test_mql +from app.simulation.examples.test_mql import system as test_mql MATRIX_PATH = Path(__file__).resolve().parents[1] / "docs" / "amesim-component-migration-matrix.md" diff --git a/tests/test_amesim_gas_registry.py b/tests/test_amesim_gas_registry.py new file mode 100644 index 0000000..16be7a8 --- /dev/null +++ b/tests/test_amesim_gas_registry.py @@ -0,0 +1,472 @@ +from __future__ import annotations + +import unittest + +from app.main import ( + ReactFlowProjectPayload, + build_reactflow_system_xml, + compile_reactflow_network, +) +from app.system_xml import validate_system_xml_document +from app.simulation.components.amesim.gases import ( + AMESIM_BUILTIN_AIR_GAS_INDEX, + AMESIM_DEFAULT_GAS_INDEX, + AmesimGasDefinition, + AmesimGasRegistry, + default_amesim_gas_registry, + normalize_amesim_defined_gas_index, + normalize_amesim_gas_index, +) +from app.simulation.components.amesim.media import AmesimIdealAirMedium +from app.simulation.core.medium import IdealGasMedium +from app.simulation.registry import COMPONENT_MODEL_REGISTRY +from tests.test_generic_system_xml_simulation import component_node, physical_edge +from tests.test_system_xml_protocol import physical_port + + +GAS_INDEXED_MODEL_TYPES = ( + "amesim_pnch023", + "amesim_pnch012", + "amesim_pnor001", + "amesim_pnvo001_fixed", + "amesim_pnvo001", + "amesim_pnl00r", + "amesim_pnl0001", + "amesim_pnl0002", + "amesim_pnl0003", +) + + +def pneumatic_pipe_node( + component_id: str, + *, + gi: float, +) -> dict[str, object]: + return component_node( + component_id, + "amesim_pnl00r", + [ + physical_port("port_1", "bidirectional", "left"), + physical_port("port_2", "bidirectional", "right"), + ], + {"diam": 0.01, "le": 1.0, "rr": 1.0e-5, "gi": gi}, + ) + + +def pneumatic_chamber_node( + component_id: str, + *, + gi: float | None = None, + volume: float = 0.01, + pressure: float = 200000.0, + temperature: float = 300.0, +) -> dict[str, object]: + parameters = { + "cvol": volume, + "kth": 0.0, + "sth": 0.1, + "extemp": 293.15, + "p0": pressure, + "T0": temperature, + } + if gi is not None: + parameters["gi"] = gi + return component_node( + component_id, + "amesim_pnch023", + [ + physical_port("port_1", "bidirectional", "left"), + physical_port("port_2", "bidirectional", "right"), + ], + parameters, + ) + + +def ideal_air_medium_node( + component_id: str, + *, + gi: float, + property_model: float | None = None, +) -> dict[str, object]: + parameters = {"gi": gi} + if property_model is not None: + parameters["property_model"] = property_model + return component_node( + component_id, + "amesim_ideal_air_medium", + [], + parameters, + ) + + +def two_slot_registry() -> tuple[AmesimGasRegistry, IdealGasMedium, IdealGasMedium]: + future_helium = IdealGasMedium( + name="FutureHeliumStub", + R_gas=2077.26439404998, + cp_ref=5193.0, + viscosity_ref=1.96e-5, + sutherland_constant=79.4, + ) + registry = AmesimGasRegistry( + ( + AmesimGasDefinition( + gi=2, + label="Future helium", + medium=future_helium, + fluid_type=12, + eos_type=6, + ), + ) + ) + air = registry.resolve(AMESIM_BUILTIN_AIR_GAS_INDEX) + return registry, air, future_helium + + +class AmesimGasRegistryTests(unittest.TestCase): + def test_all_gas_sensitive_models_expose_the_same_gi_contract(self) -> None: + medium = IdealGasMedium() + + for model_type in GAS_INDEXED_MODEL_TYPES: + with self.subTest(model_type=model_type): + spec = COMPONENT_MODEL_REGISTRY[model_type] + parameter = spec.parameter_by_name["gi"] + + self.assertIs(spec.parameters[0], parameter) + self.assertEqual(parameter.default, 0.0) + self.assertEqual(parameter.minimum, 0.0) + self.assertEqual(parameter.maximum, 99.0) + self.assertEqual(parameter.editor, "amesimGasReference") + component = spec.create(f"{model_type}_1", medium, {}) + self.assertEqual(component.gi, AMESIM_DEFAULT_GAS_INDEX) + + with self.assertRaisesRegex(ValueError, "gi must be an integer"): + spec.create(f"{model_type}_fractional", medium, {"gi": 1.5}) + + def test_default_slot_resolves_to_air(self) -> None: + registry = default_amesim_gas_registry() + + medium = registry.resolve(AMESIM_DEFAULT_GAS_INDEX) + + self.assertIsInstance(medium, AmesimIdealAirMedium) + self.assertEqual(medium.name, "AMESimAirIdealGas") + self.assertEqual(medium.SUBSTANCE_ID, "air") + self.assertEqual(medium.PROPERTY_METHOD_ID, "ideal_gas") + self.assertEqual(medium.R_gas, 287.0) + self.assertEqual(medium.cp_ref, 1005.0) + self.assertEqual(medium.cv, 718.0) + + def test_gi_normalization_rejects_fractional_and_out_of_range_values(self) -> None: + self.assertEqual(normalize_amesim_gas_index(0.0), 0) + self.assertEqual(normalize_amesim_gas_index(1.0), 1) + with self.assertRaisesRegex(ValueError, "integer"): + normalize_amesim_gas_index(1.5) + with self.assertRaisesRegex(ValueError, "between 0 and 99"): + normalize_amesim_gas_index(-1) + with self.assertRaisesRegex(ValueError, "between 0 and 99"): + normalize_amesim_gas_index(100) + with self.assertRaisesRegex(ValueError, "reserved"): + normalize_amesim_defined_gas_index(0) + self.assertEqual(normalize_amesim_defined_gas_index(1), 1) + + def test_unknown_slot_is_not_silently_replaced_with_air(self) -> None: + registry = default_amesim_gas_registry() + + with self.assertRaisesRegex(ValueError, "gi=2.*not defined"): + registry.resolve(2, component_name="pipe_1") + + project = ReactFlowProjectPayload( + name="undefined-gas", + nodes=[pneumatic_pipe_node("pipe_1", gi=2.0)], + ) + with self.assertRaisesRegex(ValueError, "gi=2.*pipe_1.*not defined"): + compile_reactflow_network(project) + + def test_custom_registry_can_define_future_helium_slot(self) -> None: + registry, air, future_helium = two_slot_registry() + + self.assertIs(registry.resolve(0), air) + self.assertIs(registry.resolve(2), future_helium) + self.assertEqual(registry.definitions[2].fluid_type, 12) + self.assertEqual(registry.definitions[2].eos_type, 6) + + def test_builtin_air_slot_cannot_be_overridden(self) -> None: + registry = default_amesim_gas_registry() + + with self.assertRaisesRegex(ValueError, "gi=0.*reserved"): + registry.register( + AmesimGasDefinition( + gi=0, + label="Invalid override", + medium=IdealGasMedium(name="NotBuiltinAir"), + ) + ) + self.assertEqual(registry.resolve(0).name, "AMESimAirIdealGas") + + def test_air_medium_definition_is_a_zero_port_compile_time_component(self) -> None: + spec = COMPONENT_MODEL_REGISTRY["amesim_ideal_air_medium"] + + self.assertEqual(spec.display.category_id, "media") + self.assertEqual(spec.display.role, "amesimGasMediumDefinition") + self.assertEqual(spec.display.label, "空气介质定义") + self.assertEqual(spec.ports, ()) + self.assertEqual(spec.parameters[0].minimum, 1.0) + property_model = spec.parameter_by_name["property_model"] + self.assertIs(spec.parameters[1], property_model) + self.assertEqual(property_model.default, 0.0) + self.assertEqual(property_model.editor, "amesimGasPropertyModel") + self.assertEqual( + [(option.value, option.label) for option in property_model.options], + [(0, "理想气体")], + ) + + project = ReactFlowProjectPayload( + name="project-air-slot", + nodes=[ + pneumatic_pipe_node("pipe_1", gi=1.0), + ideal_air_medium_node("air_properties", gi=1.0), + ], + ) + network = compile_reactflow_network(project) + + self.assertNotIn("air_properties", network.components) + self.assertEqual( + network.components["pipe_1"].medium.name, + "AMESimAirIdealGas", + ) + + def test_air_definition_dispatches_the_selected_property_model(self) -> None: + spec = COMPONENT_MODEL_REGISTRY["amesim_ideal_air_medium"] + + definition = spec.create( + "air_properties", + IdealGasMedium(), + {"gi": 3.0, "property_model": 0.0}, + ) + gas_definition = definition.gas_definition() + + self.assertEqual( + definition.parameter_values, + {"gi": 3.0, "property_model": 0.0}, + ) + self.assertEqual(gas_definition.label, "空气(理想气体)") + self.assertEqual(gas_definition.fluid_type, 2) + self.assertEqual(gas_definition.eos_type, 1) + self.assertIsInstance(gas_definition.medium, AmesimIdealAirMedium) + self.assertEqual( + gas_definition.medium.PROPERTY_METHOD_ID, + "ideal_gas", + ) + + with self.assertRaisesRegex(ValueError, "property_model.*at most 0"): + spec.create( + "unsupported_air_properties", + IdealGasMedium(), + {"gi": 4.0, "property_model": 1.0}, + ) + + def test_project_medium_definition_may_appear_after_its_references(self) -> None: + project = ReactFlowProjectPayload( + name="two-phase-medium-resolution", + nodes=[ + pneumatic_chamber_node("chamber_1", gi=7.0), + ideal_air_medium_node("air_properties", gi=7.0), + ], + ) + + network = compile_reactflow_network(project) + + self.assertEqual( + network.components["chamber_1"].medium.name, + "AMESimAirIdealGas", + ) + + def test_medium_definition_and_reference_survive_system_xml_round_trip( + self, + ) -> None: + original = ReactFlowProjectPayload( + name="medium-xml-round-trip", + nodes=[ + ideal_air_medium_node( + "air_properties", + gi=4.0, + property_model=0.0, + ), + pneumatic_pipe_node("pipe_1", gi=4.0), + ], + ) + + xml_bytes = build_reactflow_system_xml(original) + report = validate_system_xml_document(xml_bytes) + + self.assertTrue(report.valid, report.issues) + self.assertIsNotNone(report.document) + assert report.document is not None + self.assertEqual(report.document.medium_reference_version, "1") + components = { + component.id: component for component in report.document.components + } + self.assertEqual( + components["air_properties"].model_type, + "amesim_ideal_air_medium", + ) + self.assertEqual( + {item.name: item.value for item in components["air_properties"].parameters}, + {"gi": 4.0, "property_model": 0.0}, + ) + self.assertEqual( + {item.name: item.value for item in components["pipe_1"].parameters}[ + "gi" + ], + 4.0, + ) + + parsed_project = ReactFlowProjectPayload( + **report.document.as_project_data() + ) + network = compile_reactflow_network(parsed_project) + + self.assertNotIn("air_properties", network.components) + self.assertIsInstance( + network.components["pipe_1"].medium, + AmesimIdealAirMedium, + ) + + def test_duplicate_project_medium_indices_are_rejected(self) -> None: + project = ReactFlowProjectPayload( + name="duplicate-medium-index", + nodes=[ + ideal_air_medium_node("air_properties_1", gi=1.0), + ideal_air_medium_node("air_properties_2", gi=1.0), + ], + ) + + with self.assertRaisesRegex( + ValueError, + "air_properties_2.*gi=1.*already defined", + ): + compile_reactflow_network(project) + + def test_medium_definition_cannot_claim_builtin_slot_zero(self) -> None: + project = ReactFlowProjectPayload( + name="reserved-medium-index", + nodes=[ideal_air_medium_node("air_properties", gi=0.0)], + ) + + with self.assertRaisesRegex(ValueError, "gi.*at least 1"): + compile_reactflow_network(project) + + def test_default_compile_uses_air_before_state_initialization(self) -> None: + project = ReactFlowProjectPayload( + name="default-air", + nodes=[pneumatic_chamber_node("chamber_1")], + ) + + network = compile_reactflow_network(project) + chamber = network.components["chamber_1"] + + self.assertEqual(chamber.medium.name, "AMESimAirIdealGas") + self.assertAlmostEqual( + chamber.state.m, + chamber.medium.density(200000.0, 300.0) * 0.01, + ) + + def test_gi_is_resolved_before_dynamic_state_initialization(self) -> None: + registry, _, future_helium = two_slot_registry() + project = ReactFlowProjectPayload( + name="selected-medium-initialization", + nodes=[pneumatic_chamber_node("chamber_1", gi=2.0)], + ) + + network = compile_reactflow_network( + project, + amesim_gas_registry=registry, + ) + chamber = network.components["chamber_1"] + + self.assertIs(chamber.medium, future_helium) + self.assertAlmostEqual( + chamber.state.m, + future_helium.density(200000.0, 300.0) * 0.01, + ) + self.assertNotAlmostEqual( + chamber.state.m, + IdealGasMedium().density(200000.0, 300.0) * 0.01, + ) + + def test_selected_medium_is_injected_into_the_whole_pneumatic_circuit( + self, + ) -> None: + registry, _, future_helium = two_slot_registry() + project = ReactFlowProjectPayload( + name="future-helium-circuit", + nodes=[ + pneumatic_pipe_node("pnl_1", gi=2.0), + component_node( + "tank_1", + "tank", + [physical_port("port_a", "inlet", "left")], + {"volume": 0.1, "p0": 100000.0, "T0": 300.0}, + ), + ], + edges=[ + physical_edge("edge-1", "pnl_1", "port_2", "tank_1", "port_a"), + ], + ) + + network = compile_reactflow_network( + project, + amesim_gas_registry=registry, + ) + + self.assertIs(network.components["pnl_1"].medium, future_helium) + self.assertIs(network.components["tank_1"].medium, future_helium) + + def test_connected_circuit_rejects_conflicting_gas_indices(self) -> None: + registry, _, _ = two_slot_registry() + project = ReactFlowProjectPayload( + name="conflicting-gases", + nodes=[ + pneumatic_pipe_node("air_pipe", gi=0.0), + pneumatic_pipe_node("helium_pipe", gi=2.0), + ], + edges=[ + physical_edge( + "edge-1", + "air_pipe", + "port_2", + "helium_pipe", + "port_1", + ), + ], + ) + + with self.assertRaisesRegex( + ValueError, + "conflicting AMESim gas definitions", + ): + compile_reactflow_network( + project, + amesim_gas_registry=registry, + ) + + def test_disconnected_circuits_may_use_different_gas_indices(self) -> None: + registry, air, future_helium = two_slot_registry() + project = ReactFlowProjectPayload( + name="separate-gases", + nodes=[ + pneumatic_pipe_node("air_pipe", gi=0.0), + pneumatic_pipe_node("helium_pipe", gi=2.0), + ], + ) + + network = compile_reactflow_network( + project, + amesim_gas_registry=registry, + ) + + self.assertIs(network.components["air_pipe"].medium, air) + self.assertIs(network.components["helium_pipe"].medium, future_helium) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_amesim_helium_medium.py b/tests/test_amesim_helium_medium.py new file mode 100644 index 0000000..ff8f45f --- /dev/null +++ b/tests/test_amesim_helium_medium.py @@ -0,0 +1,154 @@ +from __future__ import annotations + +import unittest + +from app.main import ReactFlowProjectPayload, compile_reactflow_network +from app.simulation.components.amesim.library import LIBRARY +from app.simulation.components.amesim.media.mediums import ( + AMESIM_AIR_PROPERTY_MODELS, + AMESIM_HELIUM_PENG_ROBINSON_PROPERTY_MODEL, + AMESIM_HELIUM_PROPERTY_MODELS, + AmesimHeliumPengRobinsonMedium, +) +from app.simulation.core.medium import IdealGasMedium +from app.simulation.core.peng_robinson import HELIUM_PR +from app.simulation.registry import COMPONENT_MODEL_REGISTRY +from tests.test_generic_system_xml_simulation import component_node +from tests.test_system_xml_protocol import physical_port + + +class AmesimHeliumPengRobinsonMediumTests(unittest.TestCase): + def test_uses_shared_peng_robinson_eos_and_committed_caloric_constants( + self, + ) -> None: + medium = AmesimHeliumPengRobinsonMedium() + pressure = 15.3e6 + temperature = 293.15 + + density = medium.density(pressure, temperature) + volume = 0.02 + + self.assertIs(medium.fluid, HELIUM_PR) + self.assertEqual(medium.SUBSTANCE_ID, "helium") + self.assertEqual(medium.PROPERTY_METHOD_ID, "peng_robinson") + self.assertAlmostEqual(medium.R_gas, HELIUM_PR.specific_gas_constant) + self.assertEqual(medium.cp_ref, 5193.0) + self.assertEqual(medium.cv, 3116.0) + self.assertEqual(medium.cp_at_temperature(400.0), 5193.0) + self.assertEqual(medium.cv_at_temperature(400.0), 3116.0) + self.assertAlmostEqual(medium.gamma, 5193.0 / 3116.0) + self.assertAlmostEqual(density, HELIUM_PR.density(pressure, temperature)) + self.assertAlmostEqual( + medium.pressure(density * volume, temperature, volume), + pressure, + delta=pressure * 1.0e-12, + ) + + def test_constant_caloric_model_and_sutherland_viscosity_complete_contract( + self, + ) -> None: + medium = AmesimHeliumPengRobinsonMedium() + temperature = 340.0 + + internal_energy = medium.specific_internal_energy(temperature) + enthalpy = medium.specific_enthalpy(temperature) + + self.assertEqual(internal_energy, 3116.0 * temperature) + self.assertEqual(enthalpy, 5193.0 * temperature) + self.assertEqual( + medium.temperature_from_internal_energy(internal_energy), + temperature, + ) + self.assertEqual( + medium.temperature_from_enthalpy(enthalpy), + temperature, + ) + self.assertAlmostEqual(medium.dynamic_viscosity(293.15), 1.96e-5) + self.assertEqual(medium.sutherland_constant, 79.4) + + def test_definition_maps_local_selector_to_amesim_fluid_and_eos_codes( + self, + ) -> None: + self.assertEqual(LIBRARY.version, "0.2.0") + self.assertEqual(AMESIM_HELIUM_PENG_ROBINSON_PROPERTY_MODEL, 0) + self.assertEqual(AMESIM_HELIUM_PROPERTY_MODELS[0].value, 0) + self.assertEqual(AMESIM_HELIUM_PROPERTY_MODELS[0].eos_type, 6) + self.assertEqual(AMESIM_AIR_PROPERTY_MODELS[0].eos_type, 1) + + spec = COMPONENT_MODEL_REGISTRY["amesim_helium_medium"] + property_model = spec.parameter_by_name["property_model"] + + self.assertEqual(spec.display.category_id, "media") + self.assertEqual(spec.display.role, "amesimGasMediumDefinition") + self.assertEqual(spec.display.label, "氦气介质定义") + self.assertEqual(spec.model_version, "0.1.0") + self.assertEqual(spec.ports, ()) + self.assertEqual(property_model.default, 0.0) + self.assertEqual(property_model.editor, "amesimGasPropertyModel") + self.assertEqual( + [(option.value, option.label) for option in property_model.options], + [(0, "Peng–Robinson")], + ) + + component = spec.create( + "helium_properties", + IdealGasMedium(), + {"gi": 2.0, "property_model": 0.0}, + ) + definition = component.gas_definition() + + self.assertEqual(component.parameter_values, {"gi": 2.0, "property_model": 0.0}) + self.assertEqual(definition.label, "氦气(Peng–Robinson)") + self.assertEqual(definition.fluid_type, 12) + self.assertEqual(definition.eos_type, 6) + self.assertIsInstance(definition.medium, AmesimHeliumPengRobinsonMedium) + + def test_project_compile_injects_helium_before_dynamic_state_initialization( + self, + ) -> None: + pressure = 13_839_965.0 + temperature = 287.7322 + volume = 0.01 + project = ReactFlowProjectPayload( + name="helium-peng-robinson-compile", + nodes=[ + component_node( + "helium_properties", + "amesim_helium_medium", + [], + {"gi": 2.0, "property_model": 0.0}, + ), + component_node( + "chamber_1", + "amesim_pnch023", + [ + physical_port("port_1", "bidirectional", "left"), + physical_port("port_2", "bidirectional", "right"), + ], + { + "gi": 2.0, + "cvol": volume, + "kth": 0.0, + "sth": 0.1, + "extemp": 293.15, + "p0": pressure, + "T0": temperature, + }, + ), + ], + ) + + network = compile_reactflow_network(project) + chamber = network.components["chamber_1"] + + self.assertNotIn("helium_properties", network.components) + self.assertIsInstance(chamber.medium, AmesimHeliumPengRobinsonMedium) + self.assertAlmostEqual( + chamber.state.m, + HELIUM_PR.density(pressure, temperature) * volume, + delta=1.0e-12, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_amesim_mechanical_components.py b/tests/test_amesim_mechanical_components.py index 1589672..da4f8ff 100644 --- a/tests/test_amesim_mechanical_components.py +++ b/tests/test_amesim_mechanical_components.py @@ -3,7 +3,7 @@ from __future__ import annotations import unittest from pathlib import Path -from PythonModels.components.amesim_mechanical import ( +from app.simulation.examples.test_mql.primitives.mechanical import ( AmesimElasticEndstop, AmesimMassFrictionEndstops, AmesimPistonGeometry, @@ -11,7 +11,7 @@ from PythonModels.components.amesim_mechanical import ( m_to_mm, mm_to_m, ) -from PythonModels.reporting.amesim_results import load_test_mql_amesim_results +from app.simulation.reporting.amesim_results import load_test_mql_amesim_results REPO_ROOT = Path(__file__).resolve().parents[1] diff --git a/tests/test_amesim_pnch012_xml.py b/tests/test_amesim_pnch012_xml.py index a6b55d8..02e5c46 100644 --- a/tests/test_amesim_pnch012_xml.py +++ b/tests/test_amesim_pnch012_xml.py @@ -40,7 +40,7 @@ def amesim_pnch012_project() -> ReactFlowProjectPayload: "kth": 0.0, "sth": 0.7, "extemp": 300.0, - "gi": 1.0, + "gi": 0.0, "p0": 200000.0, "T0": 300.0, "vol1": 0.001, diff --git a/tests/test_amesim_pnch023_component.py b/tests/test_amesim_pnch023_component.py index 673083c..ca3b4ab 100644 --- a/tests/test_amesim_pnch023_component.py +++ b/tests/test_amesim_pnch023_component.py @@ -25,7 +25,7 @@ class AmesimPnch023ComponentTests(unittest.TestCase): {"cvol", "kth", "sth", "extemp", "gi", "p0", "T0"}, ) self.assertEqual(chamber.parameter_values["cvol"], 0.057) - self.assertEqual(chamber.gi, 1) + self.assertEqual(chamber.gi, 0) def test_rejects_fractional_gas_index(self) -> None: with self.assertRaisesRegex(ValueError, "gi must be an integer"): diff --git a/tests/test_amesim_pnch023_xml.py b/tests/test_amesim_pnch023_xml.py index e4220d5..d452885 100644 --- a/tests/test_amesim_pnch023_xml.py +++ b/tests/test_amesim_pnch023_xml.py @@ -44,7 +44,7 @@ def amesim_pnch023_project() -> ReactFlowProjectPayload: "kth": 0.0, "sth": 0.1, "extemp": 293.15, - "gi": 1.0, + "gi": 0.0, "p0": 100000.0, "T0": 293.15, }, @@ -56,7 +56,7 @@ def amesim_pnch023_project() -> ReactFlowProjectPayload: physical_port("port_1", "bidirectional", "left"), physical_port("port_2", "bidirectional", "right"), ], - {"diam": 0.02, "le": 1.0, "rr": 1.0e-5, "gi": 1.0}, + {"diam": 0.02, "le": 1.0, "rr": 1.0e-5, "gi": 0.0}, ), component_node( "tank_1", @@ -91,6 +91,10 @@ class AmesimPnch023XmlTests(unittest.TestCase): def test_pnch023_system_xml_validates_and_simulates(self) -> None: xml = build_reactflow_system_xml(amesim_pnch023_project()) + xml_text = xml.decode("utf-8") + self.assertNotIn("description", xml_text) + self.assertIn(' ReactFlowProjectPayload: "k": 1.35, "kth": 0.0, "extemp": 300.0, - "gi": 1.0, + "gi": 0.0, "mode": 2.0, "p0": 300000.0, "T0": 300.0, @@ -50,7 +50,7 @@ def amesim_pnl0001_project() -> ReactFlowProjectPayload: physical_port("port_1", "bidirectional", "left"), physical_port("port_2", "bidirectional", "right"), ], - {"diam": 0.01, "le": 0.5, "rr": 1.0e-5, "gi": 1.0}, + {"diam": 0.01, "le": 0.5, "rr": 1.0e-5, "gi": 0.0}, ), component_node( "tank_1", diff --git a/tests/test_amesim_pnl0002_pnl0003_xml.py b/tests/test_amesim_pnl0002_pnl0003_xml.py index 32ba243..3501abc 100644 --- a/tests/test_amesim_pnl0002_pnl0003_xml.py +++ b/tests/test_amesim_pnl0002_pnl0003_xml.py @@ -38,7 +38,7 @@ def _dynamic_pipe_params() -> dict[str, float]: "k": 1.35, "kth": 0.0, "extemp": 300.0, - "gi": 1.0, + "gi": 0.0, "mode": 2.0, "p0": 300000.0, "T0": 300.0, @@ -53,7 +53,7 @@ def _resistance_node(name: str) -> object: physical_port("port_1", "bidirectional", "left"), physical_port("port_2", "bidirectional", "right"), ], - {"diam": 0.01, "le": 0.5, "rr": 1.0e-5, "gi": 1.0}, + {"diam": 0.01, "le": 0.5, "rr": 1.0e-5, "gi": 0.0}, ) diff --git a/tests/test_amesim_pnl00r_component.py b/tests/test_amesim_pnl00r_component.py index 620f297..a82c83a 100644 --- a/tests/test_amesim_pnl00r_component.py +++ b/tests/test_amesim_pnl00r_component.py @@ -22,7 +22,7 @@ class AmesimPnl00rComponentTests(unittest.TestCase): self.assertEqual(set(pipe.ports), {"port_1", "port_2"}) self.assertEqual(set(pipe.parameter_values), {"diam", "le", "rr", "gi"}) self.assertEqual(pipe.parameter_values["diam"], 0.01) - self.assertEqual(pipe.gi, 1) + self.assertEqual(pipe.gi, 0) def test_rejects_fractional_gas_index(self) -> None: with self.assertRaisesRegex(ValueError, "gi must be an integer"): diff --git a/tests/test_amesim_pnl00r_xml.py b/tests/test_amesim_pnl00r_xml.py index 6c4cae3..c4a817d 100644 --- a/tests/test_amesim_pnl00r_xml.py +++ b/tests/test_amesim_pnl00r_xml.py @@ -30,7 +30,7 @@ def amesim_pnl00r_project() -> ReactFlowProjectPayload: physical_port("port_1", "bidirectional", "left"), physical_port("port_2", "bidirectional", "right"), ], - {"diam": 0.01, "le": 1.0, "rr": 1.0e-5, "gi": 1.0}, + {"diam": 0.01, "le": 1.0, "rr": 1.0e-5, "gi": 0.0}, ), component_node( "tank_1", diff --git a/tests/test_amesim_pnor001_xml.py b/tests/test_amesim_pnor001_xml.py index 5a7004b..c086437 100644 --- a/tests/test_amesim_pnor001_xml.py +++ b/tests/test_amesim_pnor001_xml.py @@ -35,7 +35,7 @@ def amesim_pnor001_project() -> ReactFlowProjectPayload: "area": 5.0e-6, "Cv": 0.5, "Kv": 0.4, - "gi": 1.0, + "gi": 0.0, "flowset": 1.0, }, ), diff --git a/tests/test_amesim_pnpl01_xml.py b/tests/test_amesim_pnpl01_xml.py index 6469239..4a70e19 100644 --- a/tests/test_amesim_pnpl01_xml.py +++ b/tests/test_amesim_pnpl01_xml.py @@ -29,7 +29,7 @@ def amesim_pnpl01_project() -> ReactFlowProjectPayload: "kth": 0.0, "sth": 0.1, "extemp": 293.15, - "gi": 1.0, + "gi": 0.0, "p0": 100000.0, "T0": 293.15, }, diff --git a/tests/test_amesim_pnvo001_fixed_xml.py b/tests/test_amesim_pnvo001_fixed_xml.py index 11e8f14..a7db3d0 100644 --- a/tests/test_amesim_pnvo001_fixed_xml.py +++ b/tests/test_amesim_pnvo001_fixed_xml.py @@ -35,7 +35,7 @@ def amesim_pnvo001_fixed_project() -> ReactFlowProjectPayload: "area0": 5.0e-6, "Cv": 0.5, "Kv": 0.4, - "gi": 1.0, + "gi": 0.0, "flowset": 1.0, "opening": 0.5, }, @@ -47,7 +47,7 @@ def amesim_pnvo001_fixed_project() -> ReactFlowProjectPayload: physical_port("port_1", "bidirectional", "left"), physical_port("port_2", "bidirectional", "right"), ], - {"diam": 0.02, "le": 1.0, "rr": 1.0e-5, "gi": 1.0}, + {"diam": 0.02, "le": 1.0, "rr": 1.0e-5, "gi": 0.0}, ), component_node( "tank_1", diff --git a/tests/test_amesim_pnvo001_signal_xml.py b/tests/test_amesim_pnvo001_signal_xml.py index 451bbbe..509b699 100644 --- a/tests/test_amesim_pnvo001_signal_xml.py +++ b/tests/test_amesim_pnvo001_signal_xml.py @@ -63,7 +63,7 @@ def amesim_pnvo001_signal_project() -> ReactFlowProjectPayload: "area0": 5.0e-6, "Cv": 0.5, "Kv": 0.4, - "gi": 1.0, + "gi": 0.0, "flowset": 1.0, "opening0": 0.0, }, diff --git a/tests/test_amesim_results.py b/tests/test_amesim_results.py index 679c763..0ee5102 100644 --- a/tests/test_amesim_results.py +++ b/tests/test_amesim_results.py @@ -4,7 +4,7 @@ import unittest from pathlib import Path from unittest.mock import Mock -from PythonModels.reporting.amesim_results import ( +from app.simulation.reporting.amesim_results import ( _resolve_result_members, load_test_mql_amesim_results, ) diff --git a/tests/test_amesim_ud00_xml.py b/tests/test_amesim_ud00_xml.py index 51fa88b..1bbe1a0 100644 --- a/tests/test_amesim_ud00_xml.py +++ b/tests/test_amesim_ud00_xml.py @@ -71,7 +71,7 @@ def amesim_ud00_signal_project() -> ReactFlowProjectPayload: "area0": 5.0e-6, "Cv": 0.5, "Kv": 0.4, - "gi": 1.0, + "gi": 0.0, "flowset": 1.0, "opening0": 0.0, }, diff --git a/tests/test_component_catalog.py b/tests/test_component_catalog.py index ce85478..070d4a9 100644 --- a/tests/test_component_catalog.py +++ b/tests/test_component_catalog.py @@ -9,6 +9,7 @@ from app.simulation.registry import ( COMPONENT_MODEL_REGISTRY, validate_component_registries, ) +from app.simulation.core.metadata import ParameterDefinition class ComponentCatalogTests(unittest.TestCase): @@ -47,8 +48,58 @@ class ComponentCatalogTests(unittest.TestCase): } self.assertEqual(library["label"], "AMESim 组件库") - self.assertEqual([category["id"] for category in library["categories"]], ["storage", "flow", "junctions", "boundary", "signals", "mechanical"]) - self.assertEqual(set(components), {"amesim_pnpl01", "amesim_step0", "amesim_ud00", "amesim_f000", "amesim_forc", "amesim_mecmas21", "amesim_lstp00a", "amesim_lmechn1", "amesim_pnch023", "amesim_pnch012", "amesim_pnor001", "amesim_pnvo001_fixed", "amesim_pnvo001", "amesim_pnl00r", "amesim_pnl0001", "amesim_pnl0002", "amesim_pnl0003", "amesim_pn3node2", "amesim_p4node2"}) + self.assertEqual([category["id"] for category in library["categories"]], ["media", "storage", "flow", "junctions", "boundary", "signals", "mechanical"]) + self.assertEqual(set(components), {"amesim_ideal_air_medium", "amesim_helium_medium", "amesim_pnpl01", "amesim_step0", "amesim_ud00", "amesim_f000", "amesim_forc", "amesim_mecmas21", "amesim_lstp00a", "amesim_lmechn1", "amesim_pnch023", "amesim_pnch012", "amesim_pnor001", "amesim_pnvo001_fixed", "amesim_pnvo001", "amesim_pnl00r", "amesim_pnl0001", "amesim_pnl0002", "amesim_pnl0003", "amesim_pn3node2", "amesim_p4node2"}) + self.assertEqual( + components["amesim_ideal_air_medium"]["role"], + "amesimGasMediumDefinition", + ) + self.assertEqual( + components["amesim_ideal_air_medium"]["category"]["id"], + "media", + ) + self.assertEqual( + components["amesim_ideal_air_medium"]["label"], + "空气介质定义", + ) + self.assertEqual( + components["amesim_ideal_air_medium"]["modelVersion"], + "0.2.0", + ) + medium_parameters = { + parameter["name"]: parameter + for parameter in components["amesim_ideal_air_medium"]["parameters"] + } + self.assertEqual( + list(medium_parameters), + ["gi", "property_model"], + ) + self.assertEqual( + medium_parameters["property_model"]["editor"], + "amesimGasPropertyModel", + ) + self.assertEqual( + medium_parameters["property_model"]["options"], + [{"value": 0, "label": "理想气体"}], + ) + helium_medium = components["amesim_helium_medium"] + self.assertEqual(helium_medium["role"], "amesimGasMediumDefinition") + self.assertEqual(helium_medium["category"]["id"], "media") + self.assertEqual(helium_medium["label"], "氦气介质定义") + self.assertEqual(helium_medium["modelVersion"], "0.1.0") + helium_parameters = { + parameter["name"]: parameter + for parameter in helium_medium["parameters"] + } + self.assertEqual(list(helium_parameters), ["gi", "property_model"]) + self.assertEqual( + helium_parameters["property_model"]["editor"], + "amesimGasPropertyModel", + ) + self.assertEqual( + helium_parameters["property_model"]["options"], + [{"value": 0, "label": "Peng–Robinson"}], + ) self.assertEqual( [port["name"] for port in components["amesim_p4node2"]["ports"]], ["port_1", "port_2", "port_3", "port_4"], @@ -174,6 +225,61 @@ class ComponentCatalogTests(unittest.TestCase): self.assertEqual(parameters["p0"]["unit"], "Pa") self.assertTrue(parameters["p0"]["minimumExclusive"]) + def test_parameter_description_is_optional_interface_metadata(self) -> None: + described = ParameterDefinition( + "diam", + 0.01, + label="管径", + quantity="length", + unit="m", + minimum=0.0, + minimum_exclusive=True, + description="用于计算流通面积。", + ).as_interface_dict() + undescribed = ParameterDefinition( + "gain", + 1.0, + label="增益", + ).as_interface_dict() + + self.assertEqual(described["description"], "用于计算流通面积。") + self.assertEqual(described["default"], 0.01) + self.assertEqual(described["unit"], "m") + self.assertEqual(described["minimum"], 0.0) + self.assertTrue(described["minimumExclusive"]) + self.assertNotIn("description", undescribed) + + def test_catalog_exposes_descriptions_for_core_amesim_pneumatic_parameters( + self, + ) -> None: + medium_parameters = { + parameter["name"]: parameter + for parameter in self.components["amesim_helium_medium"]["parameters"] + } + chamber_parameters = { + parameter["name"]: parameter + for parameter in self.components["amesim_pnch023"]["parameters"] + } + pipe_parameters = { + parameter["name"]: parameter + for parameter in self.components["amesim_pnl00r"]["parameters"] + } + orifice_parameters = { + parameter["name"]: parameter + for parameter in self.components["amesim_pnor001"]["parameters"] + } + + self.assertIn("自动分配", medium_parameters["gi"]["description"]) + self.assertIn( + "Peng–Robinson", + medium_parameters["property_model"]["description"], + ) + self.assertIn("固定有效容积", chamber_parameters["cvol"]["description"]) + self.assertIn("Darcy", pipe_parameters["rr"]["description"]) + self.assertIn("有效孔口面积", orifice_parameters["area"]["description"]) + self.assertEqual(chamber_parameters["cvol"]["default"], 0.057) + self.assertEqual(pipe_parameters["diam"]["unit"], "m") + def test_catalog_adds_port_layout_without_changing_physical_contract(self) -> None: tee_ports = { port["name"]: port for port in self.components["tee"]["ports"] @@ -200,6 +306,14 @@ class ComponentCatalogTests(unittest.TestCase): self.assertEqual(schema["properties"]["schemaVersion"]["const"], 1) self.assertIn("modelVersion", schema["$defs"]["component"]["required"]) self.assertIn("version", schema["$defs"]["library"]["required"]) + parameter_schema = schema["$defs"]["parameter"] + self.assertEqual( + parameter_schema["properties"]["description"], + {"type": "string", "minLength": 1}, + ) + self.assertNotIn("description", parameter_schema["required"]) + self.assertFalse(parameter_schema["additionalProperties"]) + self.assertNotIn("unknownField", parameter_schema["properties"]) if __name__ == "__main__": diff --git a/tests/test_component_registry.py b/tests/test_component_registry.py index 88909e5..aa70597 100644 --- a/tests/test_component_registry.py +++ b/tests/test_component_registry.py @@ -6,6 +6,7 @@ from app.simulation.components.experimental.library import LIBRARY from app.simulation.components.amesim.library import LIBRARY as AMESIM_LIBRARY from app.simulation.core.base import AlgebraicComponent from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec +from app.simulation.core.metadata import ParameterDefinition, ParameterOption from app.simulation.core.medium import IdealGasMedium from app.simulation.core.ports import PortDefinition from app.simulation.registry import ( @@ -30,6 +31,8 @@ class ComponentRegistryTests(unittest.TestCase): "pipe", "orifice", "tee", + "amesim_ideal_air_medium", + "amesim_helium_medium", "amesim_pnpl01", "amesim_step0", "amesim_ud00", @@ -62,6 +65,11 @@ class ComponentRegistryTests(unittest.TestCase): self.assertIn("amesim", libraries) self.assertEqual(libraries["amesim"], AMESIM_LIBRARY) + self.assertEqual( + models["amesim_ideal_air_medium"].library.id, + "amesim", + ) + self.assertEqual(models["amesim_helium_medium"].library.id, "amesim") self.assertEqual(models["amesim_pnpl01"].library.id, "amesim") self.assertEqual(models["amesim_step0"].library.id, "amesim") self.assertEqual(models["amesim_ud00"].library.id, "amesim") @@ -134,6 +142,97 @@ class ComponentRegistryTests(unittest.TestCase): library=LIBRARY, ) + def test_property_model_parameter_options_are_strictly_validated(self) -> None: + def validate_parameter(parameter: ParameterDefinition) -> None: + class PropertyModelComponent(AlgebraicComponent): + MODEL_TYPE = "property_model_component" + MODEL_VERSION = "1.0.0" + PORTS = () + PARAMETERS = (parameter,) + RESULT_VARIABLES = () + DISPLAY = ComponentDisplaySpec( + label="物性模型测试组件", + library_id="experimental", + category_id="flow", + symbol="generic", + ports=(), + ) + + @classmethod + def create(cls, *, name, medium, parameters): + del medium, parameters + return cls(name) + + validate_component_model_class( + PropertyModelComponent, + library=LIBRARY, + ) + + def property_parameter( + *, + default: float = 0.0, + minimum: float | None = None, + maximum: float | None = None, + options: tuple[ParameterOption, ...] = (), + description: object = "", + ) -> ParameterDefinition: + return ParameterDefinition( + "property_model", + default, + label="物性计算模型", + quantity="dimensionless", + unit="", + minimum=minimum, + maximum=maximum, + editor="amesimGasPropertyModel", + options=options, + description=description, # type: ignore[arg-type] + ) + + with self.assertRaisesRegex(ValueError, "description must be a string"): + validate_parameter(property_parameter(description=None)) + with self.assertRaisesRegex(ValueError, "description must not be blank"): + validate_parameter(property_parameter(description=" ")) + with self.assertRaisesRegex(ValueError, "must declare options"): + validate_parameter(property_parameter()) + with self.assertRaisesRegex(ValueError, "duplicate option values"): + validate_parameter( + property_parameter( + options=( + ParameterOption(0, "理想气体"), + ParameterOption(0, "重复模型"), + ) + ) + ) + with self.assertRaisesRegex(ValueError, "finite numbers"): + validate_parameter( + property_parameter( + options=(ParameterOption(float("nan"), "非法模型"),) + ) + ) + with self.assertRaisesRegex(ValueError, "default value must be one of 0"): + validate_parameter( + property_parameter( + default=1.0, + options=(ParameterOption(0, "理想气体"),), + ) + ) + with self.assertRaisesRegex(ValueError, "option 1 must be at most 0"): + validate_parameter( + property_parameter( + minimum=0.0, + maximum=0.0, + options=(ParameterOption(1, "越界模型"),), + ) + ) + with self.assertRaisesRegex(ValueError, "option values must be integers"): + validate_parameter( + property_parameter( + default=0.5, + options=(ParameterOption(0.5, "非整数模型"),), + ) + ) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_core_solver.py b/tests/test_core_solver.py index 2fe9d8b..31903e7 100644 --- a/tests/test_core_solver.py +++ b/tests/test_core_solver.py @@ -3,10 +3,13 @@ import types import unittest from unittest.mock import patch -from PythonModels.core.solver import SolveIVPConfig, integrate_ode +from app.simulation.solvers.solver import SolveIVPConfig, integrate_ode class IntegrateOdeTests(unittest.TestCase): + def test_generic_solver_keeps_canonical_default_tolerance(self) -> None: + self.assertEqual(SolveIVPConfig().atol, 1.0e-8) + def test_scipy_solver_receives_step_size_controls(self) -> None: calls: list[dict[str, object]] = [] diff --git a/tests/test_medium_reference_contract.py b/tests/test_medium_reference_contract.py new file mode 100644 index 0000000..bc138b7 --- /dev/null +++ b/tests/test_medium_reference_contract.py @@ -0,0 +1,505 @@ +from __future__ import annotations + +import json +from pathlib import Path +import unittest +from unittest.mock import patch + +from app.simulation.components.experimental.library import LIBRARY +from app.simulation.core.base import AlgebraicComponent +from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec +from app.simulation.core.metadata import ParameterDefinition +from app.simulation.core.ports import PortDefinition +from app.simulation.registry import ( + COMPONENT_MODEL_REGISTRY, + ComponentModelSpec, + validate_component_model_class, +) +from app.system_xml import validate_system_xml_document + + +GAS_REFERENCE_PARAMETER = ParameterDefinition( + "gi", + 0.0, + label="气体类型索引", + minimum=0.0, + maximum=99.0, + editor="amesimGasReference", +) + + +class GasReferenceFixture(AlgebraicComponent): + MODEL_TYPE = "gas_reference_fixture" + MODEL_VERSION = "1.0.0" + PORTS = () + PARAMETERS = (GAS_REFERENCE_PARAMETER,) + RESULT_VARIABLES = () + DISPLAY = ComponentDisplaySpec( + label="气体引用测试组件", + library_id="experimental", + category_id="flow", + symbol="generic", + ports=(), + ) + + def __init__(self, name: str, gi: float) -> None: + super().__init__(name) + self.set_parameter_values({"gi": gi}) + + @classmethod + def create(cls, *, name, medium, parameters): + del medium + return cls(name, parameters["gi"]) + + +class GasMediumDefinitionFixture(AlgebraicComponent): + MODEL_TYPE = "gas_medium_definition_fixture" + MODEL_VERSION = "1.0.0" + PORTS = () + PARAMETERS = ( + ParameterDefinition( + "gi", + 1.0, + label="介质索引", + minimum=1.0, + maximum=99.0, + ), + ) + RESULT_VARIABLES = () + DISPLAY = ComponentDisplaySpec( + label="气体介质定义测试组件", + library_id="experimental", + category_id="flow", + symbol="generic", + ports=(), + role="amesimGasMediumDefinition", + ) + + def __init__(self, name: str, gi: float) -> None: + super().__init__(name) + self.set_parameter_values({"gi": gi}) + + @classmethod + def create(cls, *, name, medium, parameters): + del medium + return cls(name, parameters["gi"]) + + +class ConnectedGasReferenceFixture(AlgebraicComponent): + MODEL_TYPE = "connected_gas_reference_fixture" + MODEL_VERSION = "1.0.0" + PORTS = ( + PortDefinition.pneumatic("port_1", nominal_role="bidirectional"), + ) + PARAMETERS = (GAS_REFERENCE_PARAMETER,) + RESULT_VARIABLES = () + DISPLAY = ComponentDisplaySpec( + label="相连气体引用测试组件", + library_id="experimental", + category_id="flow", + symbol="generic", + ports=(PortDisplaySpec("port_1", "right"),), + ) + + def __init__(self, name: str, gi: float) -> None: + super().__init__(name) + self.register_declared_port("port_1") + self.set_parameter_values({"gi": gi}) + + @classmethod + def create(cls, *, name, medium, parameters): + del medium + return cls(name, parameters["gi"]) + + +GAS_REFERENCE_SPEC = ComponentModelSpec( + component_class=GasReferenceFixture, + library=LIBRARY, +) +GAS_MEDIUM_DEFINITION_SPEC = ComponentModelSpec( + component_class=GasMediumDefinitionFixture, + library=LIBRARY, +) +CONNECTED_GAS_REFERENCE_SPEC = ComponentModelSpec( + component_class=ConnectedGasReferenceFixture, + library=LIBRARY, +) + + +def system_xml( + *, + gi: float | None, + medium_reference_version: str | None = None, + medium_gas_indices: tuple[float, ...] = (), +) -> bytes: + version_attribute = ( + f' mediumReferenceVersion="{medium_reference_version}"' + if medium_reference_version is not None + else "" + ) + reference_parameter = ( + f'' if gi is not None else "" + ) + medium_components = "".join( + f""" + + + """ + for index, gas_index in enumerate(medium_gas_indices, start=1) + ) + return f""" + + + + + {reference_parameter} + {medium_components} + + + +""".encode() + + +def connected_system_xml( + *, + first_gi: float, + second_gi: float, + medium_gas_indices: tuple[float, ...], +) -> bytes: + medium_components = "".join( + f""" + + + """ + for index, gas_index in enumerate(medium_gas_indices, start=1) + ) + return f""" + + + + + + + + + + + {medium_components} + + + + + + + + +""".encode() + + +def air_medium_system_xml(*, property_model: float | None) -> bytes: + property_parameter = ( + f'' + if property_model is not None + else "" + ) + return f""" + + + + + + {property_parameter} + + + + +""".encode() + + +class MediumReferenceCatalogContractTests(unittest.TestCase): + def test_registry_emits_parameter_editor_and_component_role(self) -> None: + validate_component_model_class(GasReferenceFixture, library=LIBRARY) + validate_component_model_class(GasMediumDefinitionFixture, library=LIBRARY) + + reference_payload = GAS_REFERENCE_SPEC.as_catalog_dict() + definition_payload = GAS_MEDIUM_DEFINITION_SPEC.as_catalog_dict() + + self.assertEqual( + reference_payload["parameters"][0]["editor"], + "amesimGasReference", + ) + self.assertEqual( + definition_payload["role"], + "amesimGasMediumDefinition", + ) + + def test_committed_catalog_schema_constrains_the_extension_values(self) -> None: + schema_path = ( + Path(__file__).resolve().parent.parent + / "schemas" + / "component-catalog-v1.schema.json" + ) + schema = json.loads(schema_path.read_text(encoding="utf-8")) + + self.assertEqual( + schema["$defs"]["parameter"]["properties"]["editor"]["enum"], + ["amesimGasReference", "amesimGasPropertyModel"], + ) + self.assertEqual( + schema["$defs"]["parameter"]["properties"]["options"]["items"][ + "$ref" + ], + "#/$defs/parameterOption", + ) + self.assertEqual( + schema["$defs"]["component"]["properties"]["role"]["enum"], + ["amesimGasMediumDefinition"], + ) + + +class MediumReferenceSystemXmlContractTests(unittest.TestCase): + def setUp(self) -> None: + self.registry_patch = patch.dict( + COMPONENT_MODEL_REGISTRY, + { + GasReferenceFixture.MODEL_TYPE: GAS_REFERENCE_SPEC, + GasMediumDefinitionFixture.MODEL_TYPE: GAS_MEDIUM_DEFINITION_SPEC, + ConnectedGasReferenceFixture.MODEL_TYPE: CONNECTED_GAS_REFERENCE_SPEC, + }, + ) + self.registry_patch.start() + self.addCleanup(self.registry_patch.stop) + + def test_legacy_gi_one_without_definition_maps_to_builtin_air(self) -> None: + report = validate_system_xml_document(system_xml(gi=1.0)) + + self.assertTrue(report.valid) + self.assertEqual( + {issue.code for issue in report.issues}, + {"AMESIM_GI_LEGACY_AIR_MAPPED"}, + ) + assert report.document is not None + self.assertEqual(report.document.medium_reference_version, "1") + self.assertEqual( + report.document.as_project_data()["mediumReferenceVersion"], + 1, + ) + self.assertEqual( + report.document.as_project_data()["nodes"][0]["data"]["parameters"]["gi"], + 0.0, + ) + + def test_legacy_missing_gi_injects_builtin_air(self) -> None: + report = validate_system_xml_document(system_xml(gi=None)) + + self.assertTrue(report.valid) + self.assertEqual( + {issue.code for issue in report.issues}, + {"AMESIM_GI_DEFAULT_INJECTED"}, + ) + assert report.document is not None + self.assertEqual( + report.document.as_project_data()["nodes"][0]["data"]["parameters"]["gi"], + 0.0, + ) + + def test_versioned_gi_one_is_not_reinterpreted(self) -> None: + report = validate_system_xml_document( + system_xml(gi=1.0, medium_reference_version="1") + ) + + self.assertFalse(report.valid) + self.assertEqual( + {issue.code for issue in report.issues}, + {"AMESIM_GAS_REFERENCE_UNDEFINED"}, + ) + assert report.document is not None + self.assertEqual(report.document.medium_reference_version, "1") + self.assertEqual( + report.document.as_project_data()["nodes"][0]["data"]["parameters"]["gi"], + 1.0, + ) + + def test_legacy_document_with_definition_is_not_reinterpreted(self) -> None: + report = validate_system_xml_document( + system_xml(gi=1.0, medium_gas_indices=(1.0,)) + ) + + self.assertTrue(report.valid) + self.assertEqual(report.issues, ()) + assert report.document is not None + self.assertEqual( + report.document.as_project_data()["nodes"][0]["data"]["parameters"]["gi"], + 1.0, + ) + + def test_versioned_positive_reference_requires_matching_definition(self) -> None: + report = validate_system_xml_document( + system_xml( + gi=2.0, + medium_reference_version="1", + medium_gas_indices=(2.0,), + ) + ) + + self.assertTrue(report.valid) + self.assertEqual(report.issues, ()) + + def test_missing_property_model_is_defaulted_for_versioned_medium(self) -> None: + report = validate_system_xml_document( + air_medium_system_xml(property_model=None) + ) + + self.assertTrue(report.valid) + self.assertEqual( + {issue.code for issue in report.issues}, + {"AMESIM_GAS_PROPERTY_MODEL_DEFAULTED"}, + ) + assert report.document is not None + parameters = report.document.as_project_data()["nodes"][0]["data"][ + "parameters" + ] + self.assertEqual(parameters["gi"], 1.0) + self.assertEqual(parameters["property_model"], 0.0) + + def test_explicit_supported_property_model_round_trips_without_warning(self) -> None: + report = validate_system_xml_document( + air_medium_system_xml(property_model=0.0) + ) + + self.assertTrue(report.valid) + self.assertEqual(report.issues, ()) + + def test_unsupported_property_model_is_rejected_semantically(self) -> None: + for property_model in (1.0, 99.0): + with self.subTest(property_model=property_model): + report = validate_system_xml_document( + air_medium_system_xml(property_model=property_model) + ) + + self.assertFalse(report.valid) + self.assertEqual( + {issue.code for issue in report.issues}, + {"PARAMETER_VALUE_INVALID"}, + ) + + def test_medium_definition_index_must_be_integer_in_range(self) -> None: + fractional = validate_system_xml_document( + system_xml( + gi=0.0, + medium_reference_version="1", + medium_gas_indices=(1.5,), + ) + ) + zero = validate_system_xml_document( + system_xml( + gi=0.0, + medium_reference_version="1", + medium_gas_indices=(0.0,), + ) + ) + + self.assertIn( + "AMESIM_GAS_MEDIUM_INDEX_INVALID", + {issue.code for issue in fractional.issues}, + ) + self.assertIn( + "AMESIM_GAS_MEDIUM_INDEX_INVALID", + {issue.code for issue in zero.issues}, + ) + + def test_medium_definition_indices_must_be_unique(self) -> None: + report = validate_system_xml_document( + system_xml( + gi=1.0, + medium_reference_version="1", + medium_gas_indices=(1.0, 1.0), + ) + ) + + self.assertFalse(report.valid) + self.assertIn( + "AMESIM_GAS_MEDIUM_INDEX_DUPLICATE", + {issue.code for issue in report.issues}, + ) + + def test_gas_reference_index_must_be_integer_in_range(self) -> None: + fractional = validate_system_xml_document( + system_xml(gi=1.5, medium_reference_version="1") + ) + out_of_range = validate_system_xml_document( + system_xml(gi=100.0, medium_reference_version="1") + ) + + self.assertIn( + "AMESIM_GAS_REFERENCE_INDEX_INVALID", + {issue.code for issue in fractional.issues}, + ) + self.assertIn( + "AMESIM_GAS_REFERENCE_INDEX_INVALID", + {issue.code for issue in out_of_range.issues}, + ) + + def test_connected_pneumatic_references_cannot_conflict(self) -> None: + report = validate_system_xml_document( + connected_system_xml( + first_gi=1.0, + second_gi=2.0, + medium_gas_indices=(1.0, 2.0), + ) + ) + + self.assertFalse(report.valid) + self.assertEqual( + {issue.code for issue in report.issues}, + {"AMESIM_GAS_REFERENCE_CONFLICT"}, + ) + + def test_connected_pneumatic_references_may_share_one_medium(self) -> None: + report = validate_system_xml_document( + connected_system_xml( + first_gi=1.0, + second_gi=1.0, + medium_gas_indices=(1.0,), + ) + ) + + self.assertTrue(report.valid) + self.assertEqual(report.issues, ()) + + def test_unknown_medium_reference_version_is_rejected_by_xsd(self) -> None: + report = validate_system_xml_document( + system_xml(gi=0.0, medium_reference_version="2") + ) + + self.assertFalse(report.valid) + self.assertIsNone(report.document) + self.assertEqual({issue.code for issue in report.issues}, {"XSD_VALIDATION_ERROR"}) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_peng_robinson.py b/tests/test_peng_robinson.py index 080202b..3d85626 100644 --- a/tests/test_peng_robinson.py +++ b/tests/test_peng_robinson.py @@ -2,7 +2,7 @@ from __future__ import annotations import unittest -from PythonModels.core.peng_robinson import AIR_PR, HELIUM_PR, NITROGEN_PR, PengRobinsonFluid +from app.simulation.core.peng_robinson import AIR_PR, HELIUM_PR, NITROGEN_PR, PengRobinsonFluid class PengRobinsonTest(unittest.TestCase): diff --git a/tests/test_reactflow_project_medium_reference.py b/tests/test_reactflow_project_medium_reference.py new file mode 100644 index 0000000..6897410 --- /dev/null +++ b/tests/test_reactflow_project_medium_reference.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +import unittest + +from app.main import ( + ReactFlowProjectPayload, + build_reactflow_system_xml, + pydantic_to_jsonable, +) + + +class ReactFlowProjectMediumReferenceVersionTests(unittest.TestCase): + def test_version_one_is_accepted_and_serialized(self) -> None: + project = ReactFlowProjectPayload( + name="versioned-medium-references", + mediumReferenceVersion=1, + ) + + self.assertEqual(project.mediumReferenceVersion, 1) + self.assertEqual( + pydantic_to_jsonable(project)["mediumReferenceVersion"], + 1, + ) + + def test_other_medium_reference_versions_are_rejected(self) -> None: + for version in (0, 2, -1, "1"): + with self.subTest(version=version): + with self.assertRaises(ValueError): + ReactFlowProjectPayload( + name="unsupported-medium-references", + mediumReferenceVersion=version, + ) + + def test_legacy_project_without_version_remains_compatible(self) -> None: + project = ReactFlowProjectPayload(name="legacy-project") + + self.assertIsNone(project.mediumReferenceVersion) + + def test_xml_export_adds_default_property_model_to_legacy_medium_node(self) -> None: + project = ReactFlowProjectPayload( + name="legacy-medium-node", + mediumReferenceVersion=1, + nodes=[ + { + "id": "air_1", + "position": {"x": 10, "y": 20}, + "data": { + "label": "Air 1", + "componentType": "amesim_ideal_air_medium", + "modelType": "amesim_ideal_air_medium", + "ports": [], + "parameters": {"gi": 1}, + }, + } + ], + ) + + xml = build_reactflow_system_xml(project).decode("utf-8") + + self.assertIn(' None: - from PythonModels.components.amesim_pneumatic import HELIUM_PNEUMATIC_GAS + from app.simulation.examples.test_mql.primitives.pneumatic import HELIUM_PNEUMATIC_GAS gas = HELIUM_PNEUMATIC_GAS result = _ideal_pn2vol_reference_dtemp( diff --git a/tests/test_system_xml_protocol.py b/tests/test_system_xml_protocol.py index c3b8a43..7e14d9b 100644 --- a/tests/test_system_xml_protocol.py +++ b/tests/test_system_xml_protocol.py @@ -87,6 +87,7 @@ class SystemXmlProtocolTests(unittest.TestCase): self.assertEqual(root.attrib["name"], "protocol-test") self.assertEqual(root.attrib["schemaVersion"], "2") self.assertEqual(root.attrib["unitSystem"], "SI") + self.assertEqual(root.attrib["mediumReferenceVersion"], "1") self.assertEqual( [child.tag for child in root], ["Simulation", "Components", "Connections"], diff --git a/tests/test_test_mql_baseline.py b/tests/test_test_mql_baseline.py index cb82017..e181205 100644 --- a/tests/test_test_mql_baseline.py +++ b/tests/test_test_mql_baseline.py @@ -3,7 +3,7 @@ from __future__ import annotations import unittest from pathlib import Path -from PythonModels.systems.test_mql_baseline import run_test_mql_baseline_passthrough +from app.simulation.examples.test_mql.baseline import run_test_mql_baseline_passthrough REPO_ROOT = Path(__file__).resolve().parents[1] diff --git a/tests/test_test_mql_branch_simulation.py b/tests/test_test_mql_branch_simulation.py index 9869c22..64772d5 100644 --- a/tests/test_test_mql_branch_simulation.py +++ b/tests/test_test_mql_branch_simulation.py @@ -2,8 +2,8 @@ from __future__ import annotations import unittest -from PythonModels.core.solver import SolveIVPConfig -from PythonModels.systems.test_mql import TestMqlSystem +from app.simulation.examples.test_mql.solver import SolveIVPConfig +from app.simulation.examples.test_mql.system import TestMqlSystem class TestMqlPneumaticBranchSimulationTests(unittest.TestCase): diff --git a/tests/test_test_mql_chamber_observations.py b/tests/test_test_mql_chamber_observations.py index 7a2cc92..352b8c9 100644 --- a/tests/test_test_mql_chamber_observations.py +++ b/tests/test_test_mql_chamber_observations.py @@ -3,8 +3,8 @@ from __future__ import annotations import unittest from pathlib import Path -from PythonModels.reporting.amesim_results import load_test_mql_amesim_results -from PythonModels.reporting.test_mql_chamber_observations import ( +from app.simulation.reporting.amesim_results import load_test_mql_amesim_results +from app.simulation.reporting.test_mql_chamber_observations import ( build_test_mql_chamber_observation_catalog, ) diff --git a/tests/test_test_mql_chamber_segment.py b/tests/test_test_mql_chamber_segment.py index e722624..90b0284 100644 --- a/tests/test_test_mql_chamber_segment.py +++ b/tests/test_test_mql_chamber_segment.py @@ -2,8 +2,8 @@ from __future__ import annotations import unittest -from PythonModels.core.solver import SolveIVPConfig -from PythonModels.systems.test_mql import TestMqlSystem +from app.simulation.examples.test_mql.solver import SolveIVPConfig +from app.simulation.examples.test_mql.system import TestMqlSystem class TestMqlPneumaticChamberSegmentTests(unittest.TestCase): diff --git a/tests/test_test_mql_closure.py b/tests/test_test_mql_closure.py index dc698fe..177d40c 100644 --- a/tests/test_test_mql_closure.py +++ b/tests/test_test_mql_closure.py @@ -2,11 +2,11 @@ from __future__ import annotations import unittest -from PythonModels.components.amesim_pneumatic import ( +from app.simulation.examples.test_mql.primitives.pneumatic import ( AmesimPneumaticOrifice, AmesimPneumaticVolume, ) -from PythonModels.systems.test_mql_closure import ( +from app.simulation.examples.test_mql.closure import ( TestMqlPneumaticBranchComponents, TestMqlPneumaticClosure, ) diff --git a/tests/test_test_mql_comparison.py b/tests/test_test_mql_comparison.py index 65fcf54..bbb30ab 100644 --- a/tests/test_test_mql_comparison.py +++ b/tests/test_test_mql_comparison.py @@ -4,8 +4,8 @@ import tempfile import unittest from pathlib import Path -from PythonModels.reporting.amesim_results import load_test_mql_amesim_results -from PythonModels.reporting.test_mql_comparison import ( +from app.simulation.reporting.amesim_results import load_test_mql_amesim_results +from app.simulation.reporting.test_mql_comparison import ( DEFAULT_TEST_MQL_ALIGNMENT_PATHS, TestMqlComparisonError, compare_test_mql_series, diff --git a/tests/test_test_mql_computed.py b/tests/test_test_mql_computed.py index 96b5c18..7bc541c 100644 --- a/tests/test_test_mql_computed.py +++ b/tests/test_test_mql_computed.py @@ -3,7 +3,7 @@ from __future__ import annotations import unittest from pathlib import Path -from PythonModels.systems.test_mql_computed import ( +from app.simulation.examples.test_mql.computed import ( run_test_mql_computed_geometry, run_test_mql_computed_line_relations, run_test_mql_computed_mechanical_relations, diff --git a/tests/test_test_mql_config.py b/tests/test_test_mql_config.py index d16ab58..bf619d7 100644 --- a/tests/test_test_mql_config.py +++ b/tests/test_test_mql_config.py @@ -2,7 +2,7 @@ from __future__ import annotations import unittest -from PythonModels.systems.test_mql_config import TestMqlConfig, resolve_numeric_expression +from app.simulation.examples.test_mql.config import TestMqlConfig, resolve_numeric_expression class TestMqlConfigTest(unittest.TestCase): diff --git a/tests/test_test_mql_example_runner.py b/tests/test_test_mql_example_runner.py index 20f4648..ed6ce65 100644 --- a/tests/test_test_mql_example_runner.py +++ b/tests/test_test_mql_example_runner.py @@ -6,11 +6,15 @@ from pathlib import Path from app.main import ReactFlowProjectPayload, run_reactflow_test_mql from app.simulation.examples.test_mql.run import prepare_test_mql_run, run_test_mql +from app.simulation.examples.test_mql.solver import SolveIVPConfig +from app.simulation.examples.test_mql.system import TestMqlRunConfig from app.simulation.paths import PROJECT_ROOT, SIMULATION_RUNS_DIR -from PythonModels.systems.test_mql import TestMqlRunConfig class TestMqlExampleRunnerTests(unittest.TestCase): + def test_test_mql_keeps_calibrated_default_tolerance(self) -> None: + self.assertEqual(SolveIVPConfig().atol, 1.0e-10) + def test_default_run_paths_use_app_simulation_runs_dir(self) -> None: prepared = prepare_test_mql_run() diff --git a/tests/test_test_mql_line_observations.py b/tests/test_test_mql_line_observations.py index 97504ac..996193f 100644 --- a/tests/test_test_mql_line_observations.py +++ b/tests/test_test_mql_line_observations.py @@ -3,8 +3,8 @@ from __future__ import annotations import unittest from pathlib import Path -from PythonModels.reporting.amesim_results import load_test_mql_amesim_results -from PythonModels.reporting.test_mql_line_observations import ( +from app.simulation.reporting.amesim_results import load_test_mql_amesim_results +from app.simulation.reporting.test_mql_line_observations import ( G_PER_S_TO_KG_PER_S, build_test_mql_line_observation_catalog, ) diff --git a/tests/test_test_mql_lines.py b/tests/test_test_mql_lines.py index 069d0de..8c4ce89 100644 --- a/tests/test_test_mql_lines.py +++ b/tests/test_test_mql_lines.py @@ -3,9 +3,9 @@ from __future__ import annotations import unittest from pathlib import Path -from PythonModels.reporting.amesim_results import load_test_mql_amesim_results -from PythonModels.reporting.test_mql_variables import build_test_mql_variable_catalog -from PythonModels.systems.test_mql_lines import build_test_mql_line_assembly +from app.simulation.reporting.amesim_results import load_test_mql_amesim_results +from app.simulation.reporting.test_mql_variables import build_test_mql_variable_catalog +from app.simulation.examples.test_mql.lines import build_test_mql_line_assembly REPO_ROOT = Path(__file__).resolve().parents[1] diff --git a/tests/test_test_mql_mechanical.py b/tests/test_test_mql_mechanical.py index 3d74536..8a188e2 100644 --- a/tests/test_test_mql_mechanical.py +++ b/tests/test_test_mql_mechanical.py @@ -3,11 +3,11 @@ from __future__ import annotations import unittest from pathlib import Path -from PythonModels.reporting.amesim_results import load_test_mql_amesim_results -from PythonModels.core.solver import SolveIVPConfig -from PythonModels.reporting.test_mql_variables import build_test_mql_variable_catalog -from PythonModels.systems.test_mql import TestMqlSystem -from PythonModels.systems.test_mql_mechanical import ( +from app.simulation.reporting.amesim_results import load_test_mql_amesim_results +from app.simulation.examples.test_mql.solver import SolveIVPConfig +from app.simulation.reporting.test_mql_variables import build_test_mql_variable_catalog +from app.simulation.examples.test_mql.system import TestMqlSystem +from app.simulation.examples.test_mql.mechanical import ( TestMqlMechanicalMassClosure, build_test_mql_mechanical_assembly, circular_area, diff --git a/tests/test_test_mql_mechanical_observations.py b/tests/test_test_mql_mechanical_observations.py index 9cb83f1..21b2913 100644 --- a/tests/test_test_mql_mechanical_observations.py +++ b/tests/test_test_mql_mechanical_observations.py @@ -3,8 +3,8 @@ from __future__ import annotations import unittest from pathlib import Path -from PythonModels.reporting.amesim_results import load_test_mql_amesim_results -from PythonModels.reporting.test_mql_mechanical_observations import ( +from app.simulation.reporting.amesim_results import load_test_mql_amesim_results +from app.simulation.reporting.test_mql_mechanical_observations import ( build_test_mql_mechanical_observation_catalog, ) diff --git a/tests/test_test_mql_nodes.py b/tests/test_test_mql_nodes.py index f1e20b3..ad98b8a 100644 --- a/tests/test_test_mql_nodes.py +++ b/tests/test_test_mql_nodes.py @@ -3,9 +3,9 @@ from __future__ import annotations import unittest from pathlib import Path -from PythonModels.reporting.amesim_results import load_test_mql_amesim_results -from PythonModels.systems.test_mql import TestMqlSystem -from PythonModels.systems.test_mql_nodes import ( +from app.simulation.reporting.amesim_results import load_test_mql_amesim_results +from app.simulation.examples.test_mql.system import TestMqlSystem +from app.simulation.examples.test_mql.nodes import ( build_test_mql_node3_assembly, build_test_mql_node4_assembly, ) diff --git a/tests/test_test_mql_observations.py b/tests/test_test_mql_observations.py index 47b3dd5..abff8e7 100644 --- a/tests/test_test_mql_observations.py +++ b/tests/test_test_mql_observations.py @@ -3,9 +3,9 @@ from __future__ import annotations import unittest from pathlib import Path -from PythonModels.reporting.amesim_results import load_test_mql_amesim_results -from PythonModels.reporting.test_mql_comparison import compare_test_mql_series -from PythonModels.reporting.test_mql_observations import build_test_mql_observation_catalog +from app.simulation.reporting.amesim_results import load_test_mql_amesim_results +from app.simulation.reporting.test_mql_comparison import compare_test_mql_series +from app.simulation.reporting.test_mql_observations import build_test_mql_observation_catalog REPO_ROOT = Path(__file__).resolve().parents[1] diff --git a/tests/test_test_mql_orifice_observations.py b/tests/test_test_mql_orifice_observations.py index bb03c10..98f2611 100644 --- a/tests/test_test_mql_orifice_observations.py +++ b/tests/test_test_mql_orifice_observations.py @@ -3,12 +3,12 @@ from __future__ import annotations import unittest from pathlib import Path -from PythonModels.reporting.amesim_results import load_test_mql_amesim_results -from PythonModels.reporting.test_mql_orifice_observations import ( +from app.simulation.reporting.amesim_results import load_test_mql_amesim_results +from app.simulation.reporting.test_mql_orifice_observations import ( G_PER_S_TO_KG_PER_S, build_test_mql_orifice_observation_catalog, ) -from PythonModels.systems.test_mql_pneumatic import ( +from app.simulation.examples.test_mql.pneumatic import ( TEST_MQL_PNVO001_FLOW_COEFFICIENT_MULTIPLIER, ) diff --git a/tests/test_test_mql_output_schema.py b/tests/test_test_mql_output_schema.py index 6c1b8d9..e682bac 100644 --- a/tests/test_test_mql_output_schema.py +++ b/tests/test_test_mql_output_schema.py @@ -3,9 +3,9 @@ from __future__ import annotations import unittest from pathlib import Path -from PythonModels.reporting.amesim_results import load_test_mql_amesim_results -from PythonModels.reporting.test_mql_observations import build_test_mql_observation_catalog -from PythonModels.reporting.test_mql_output_schema import build_test_mql_output_schema +from app.simulation.reporting.amesim_results import load_test_mql_amesim_results +from app.simulation.reporting.test_mql_observations import build_test_mql_observation_catalog +from app.simulation.reporting.test_mql_output_schema import build_test_mql_output_schema REPO_ROOT = Path(__file__).resolve().parents[1] diff --git a/tests/test_test_mql_output_validation.py b/tests/test_test_mql_output_validation.py index e280371..eb42218 100644 --- a/tests/test_test_mql_output_validation.py +++ b/tests/test_test_mql_output_validation.py @@ -3,10 +3,10 @@ from __future__ import annotations import unittest from pathlib import Path -from PythonModels.reporting.amesim_results import load_test_mql_amesim_results -from PythonModels.reporting.test_mql_observations import build_test_mql_observation_catalog -from PythonModels.reporting.test_mql_output_schema import build_test_mql_output_schema -from PythonModels.reporting.test_mql_output_validation import ( +from app.simulation.reporting.amesim_results import load_test_mql_amesim_results +from app.simulation.reporting.test_mql_observations import build_test_mql_observation_catalog +from app.simulation.reporting.test_mql_output_schema import build_test_mql_output_schema +from app.simulation.reporting.test_mql_output_validation import ( TestMqlOutputValidationError, compare_validated_test_mql_output, validate_test_mql_output, diff --git a/tests/test_test_mql_pneumatic.py b/tests/test_test_mql_pneumatic.py index b840098..7b45067 100644 --- a/tests/test_test_mql_pneumatic.py +++ b/tests/test_test_mql_pneumatic.py @@ -3,9 +3,9 @@ from __future__ import annotations import unittest from pathlib import Path -from PythonModels.components.amesim_pneumatic import HELIUM_PNEUMATIC_GAS, cm3_to_m3 -from PythonModels.reporting.amesim_results import load_test_mql_amesim_results -from PythonModels.systems.test_mql_pneumatic import ( +from app.simulation.examples.test_mql.primitives.pneumatic import HELIUM_PNEUMATIC_GAS, cm3_to_m3 +from app.simulation.reporting.amesim_results import load_test_mql_amesim_results +from app.simulation.examples.test_mql.pneumatic import ( TEST_MQL_PNVO001_FLOW_COEFFICIENT_MULTIPLIER, absolute_pressure_from_amesim_bar_parameter, build_test_mql_pneumatic_assembly, diff --git a/tests/test_test_mql_pnl0001.py b/tests/test_test_mql_pnl0001.py index d8a2057..00bf86b 100644 --- a/tests/test_test_mql_pnl0001.py +++ b/tests/test_test_mql_pnl0001.py @@ -3,9 +3,9 @@ from __future__ import annotations import unittest from pathlib import Path -from PythonModels.reporting.amesim_results import load_test_mql_amesim_results -from PythonModels.systems.test_mql_line_parameters import load_test_mql_pnl0001_specs -from PythonModels.systems.test_mql_pneumatic_lines import ( +from app.simulation.reporting.amesim_results import load_test_mql_amesim_results +from app.simulation.examples.test_mql.line_parameters import load_test_mql_pnl0001_specs +from app.simulation.examples.test_mql.pneumatic_lines import ( TEST_MQL_PNL0001_D20_L1_LINEAR_CONDUCTANCE, build_test_mql_pnl0001_assembly, ) diff --git a/tests/test_test_mql_pnl0001_segment.py b/tests/test_test_mql_pnl0001_segment.py index 48376f3..5ab6595 100644 --- a/tests/test_test_mql_pnl0001_segment.py +++ b/tests/test_test_mql_pnl0001_segment.py @@ -3,11 +3,11 @@ from __future__ import annotations import unittest from pathlib import Path -from PythonModels.core.solver import SolveIVPConfig -from PythonModels.reporting.amesim_results import load_test_mql_amesim_results -from PythonModels.reporting.test_mql_output_schema import build_test_mql_output_schema -from PythonModels.reporting.test_mql_output_validation import compare_validated_test_mql_output -from PythonModels.systems.test_mql import TestMqlSystem +from app.simulation.examples.test_mql.solver import SolveIVPConfig +from app.simulation.reporting.amesim_results import load_test_mql_amesim_results +from app.simulation.reporting.test_mql_output_schema import build_test_mql_output_schema +from app.simulation.reporting.test_mql_output_validation import compare_validated_test_mql_output +from app.simulation.examples.test_mql.system import TestMqlSystem REPO_ROOT = Path(__file__).resolve().parents[1] diff --git a/tests/test_test_mql_pnl0002.py b/tests/test_test_mql_pnl0002.py index 4139cb3..24eab0a 100644 --- a/tests/test_test_mql_pnl0002.py +++ b/tests/test_test_mql_pnl0002.py @@ -3,10 +3,10 @@ from __future__ import annotations import unittest from pathlib import Path -from PythonModels.reporting.amesim_results import load_test_mql_amesim_results -from PythonModels.systems.test_mql import TestMqlSystem -from PythonModels.systems.test_mql_line_parameters import load_test_mql_pnl0002_specs -from PythonModels.systems.test_mql_pneumatic_lines import build_test_mql_pnl0002_assembly +from app.simulation.reporting.amesim_results import load_test_mql_amesim_results +from app.simulation.examples.test_mql.system import TestMqlSystem +from app.simulation.examples.test_mql.line_parameters import load_test_mql_pnl0002_specs +from app.simulation.examples.test_mql.pneumatic_lines import build_test_mql_pnl0002_assembly REPO_ROOT = Path(__file__).resolve().parents[1] diff --git a/tests/test_test_mql_pnl0003_pnl00r.py b/tests/test_test_mql_pnl0003_pnl00r.py index a47bb19..5560b9a 100644 --- a/tests/test_test_mql_pnl0003_pnl00r.py +++ b/tests/test_test_mql_pnl0003_pnl00r.py @@ -3,13 +3,13 @@ from __future__ import annotations import unittest from pathlib import Path -from PythonModels.reporting.amesim_results import load_test_mql_amesim_results -from PythonModels.systems.test_mql import TestMqlSystem -from PythonModels.systems.test_mql_line_parameters import ( +from app.simulation.reporting.amesim_results import load_test_mql_amesim_results +from app.simulation.examples.test_mql.system import TestMqlSystem +from app.simulation.examples.test_mql.line_parameters import ( load_test_mql_pnl0003_specs, load_test_mql_pnl00r_specs, ) -from PythonModels.systems.test_mql_pneumatic_lines import ( +from app.simulation.examples.test_mql.pneumatic_lines import ( build_test_mql_pnl0003_assembly, build_test_mql_pnl00r_assembly, ) diff --git a/tests/test_test_mql_structural.py b/tests/test_test_mql_structural.py index f85a504..b8568c8 100644 --- a/tests/test_test_mql_structural.py +++ b/tests/test_test_mql_structural.py @@ -2,12 +2,12 @@ from __future__ import annotations import unittest -from PythonModels.components.amesim_pneumatic import ( +from app.simulation.examples.test_mql.primitives.pneumatic import ( AmesimPneumaticOrifice, AmesimPneumaticVolume, AmesimVariablePneumaticVolume, ) -from PythonModels.systems.test_mql import GLOBAL_PARAMETERS, SUBMODEL_COUNTS, TestMqlSystem +from app.simulation.examples.test_mql.system import GLOBAL_PARAMETERS, SUBMODEL_COUNTS, TestMqlSystem class TestMqlStructuralTest(unittest.TestCase): diff --git a/tests/test_test_mql_topology.py b/tests/test_test_mql_topology.py index fe3947d..afcdb5a 100644 --- a/tests/test_test_mql_topology.py +++ b/tests/test_test_mql_topology.py @@ -3,7 +3,7 @@ from __future__ import annotations import unittest from pathlib import Path -from PythonModels.systems.test_mql_topology import load_test_mql_cir_topology +from app.simulation.examples.test_mql.topology import load_test_mql_cir_topology REPO_ROOT = Path(__file__).resolve().parents[1] diff --git a/tests/test_test_mql_variables.py b/tests/test_test_mql_variables.py index fc80290..d9ba682 100644 --- a/tests/test_test_mql_variables.py +++ b/tests/test_test_mql_variables.py @@ -3,8 +3,8 @@ from __future__ import annotations import unittest from pathlib import Path -from PythonModels.reporting.amesim_results import load_test_mql_amesim_results -from PythonModels.reporting.test_mql_variables import ( +from app.simulation.reporting.amesim_results import load_test_mql_amesim_results +from app.simulation.reporting.test_mql_variables import ( build_test_mql_variable_catalog, split_data_path, )