171 Commits
Author SHA1 Message Date
huojiarong 408b4ecb22 修复机械事件力曲线并对齐气动孔口端口显示 2026-08-20 05:59:04 +00:00
lujingze e18399c022 整合求解器活动监控与步长回归证据
同步远端 PNL0003 诊断和大采样网格能力,语义合并活动感知的 60 秒真停滞判定与旧后端 15 分钟兼容兜底。

纳管热路径优化、15 单元运行证据、浏览器与 API 报告,并补充北京时间更新日志和遗留问题。
2026-08-19 16:24:31 +00:00
huojiarong c19cf77aee docs: record August 19 updates 2026-08-19 11:35:59 +00:00
huojiarong eb6ea70e19 feat: extend stall timeout and remove sample cap 2026-08-19 11:34:31 +00:00
huojiarong 27f9f4add8 fix: align PNL0003 Reynolds diagnostic with AMESim 2026-08-19 10:40:57 +00:00
lujingze 60b743dd81 补记当前快照存档与推送结果
按北京时间记录 a8c7338 的远端存档、验证结果及仍待完成的 production 复跑和托管 workflow 检查。
2026-08-18 15:22:27 +00:00
lujingze a8c733883c 存档求解器回归基线与当前改动
纳管 AMESim 对齐基线、发布锁、回归测试及当前物理门禁调整。

更新日志仅记录已完成成果,并注明当前 HEAD 尚待真实 production 复跑与远端 workflow 验证。
2026-08-18 15:20:42 +00:00
huojiarong 53f8601fec 修正动态管与阀门诊断输出 2026-08-18 10:24:29 +00:00
huojiarong f725f038b6 完善 PNL00R 摩擦模型与八路回归 2026-08-18 09:30:29 +00:00
lujingze 684d28752a 更新八路仿真最新版本文件test-mql-8 2026-08-18 06:44:24 +00:00
lujingze b435daecf2 完善通用求解器回归与前端交互
- 引入因果坐标内核、热流体恢复和递进长时回归\n- 完善正交连线、线桥、视图保持与结果曲线缩放\n- 补充依赖约束、CI、测试基线和北京时间更新日志
2026-08-18 06:42:07 +00:00
huojiarong 143e8dd309 修复测试资源路径 2026-08-18 00:57:32 +00:00
huojiarong 0fa166c8e5 Fix pneumatic node zero-flow reversal 2026-08-17 09:24:35 +00:00
lujingze 16a7eb2d6c 完成求解器雅可比矩阵首轮优化,增加更新目录,整理了文档文件夹,增加了服务启动脚本 2026-08-17 07:33:31 +00:00
Codex 6bb0591d32 fix: align PNL0001 symbol causality 2026-08-16 11:37:02 +00:00
Codex cca9d1e883 fix: seed PNVO pipe series pressure 2026-08-16 11:34:35 +00:00
Codex 22b35b2945 perf: retain exact flow caches and compiled targets 2026-08-16 11:21:44 +00:00
Codex f09dfcf542 fix: bound reported pipe friction diagnostics 2026-08-16 11:03:21 +00:00
ljz 5332a788f3 优化仿真求解性能并修复流量闭合问题(初版) 2026-08-16 17:46:05 +08:00
ljz 57b459bc72 增加可选性能埋点并完成物性效率评估 2026-08-16 17:46:04 +08:00
huojiarong 4e0b9fd8cc fix: align mechanical dynamic port validation 2026-08-15 12:57:03 +00:00
huojiarong 6a064892e2 优化压力流量求解并达到四路性能门槛 2026-08-15 11:45:22 +00:00
ljz 6572defaa4 完善建模交互、组件图标与系统协议 2026-08-15 17:40:18 +08:00
huojiarong 456c29b3b6 验收四路模型并优化拓扑求解性能 2026-08-12 11:57:42 +00:00
huojiarong caca32a513 校准第二支路热流体能量与管路摩擦 2026-08-11 12:13:09 +00:00
huojiarong 0f73d5b568 对齐PNL0002上游温度并稳定热流体闭合 2026-08-11 07:12:52 +00:00
huojiarong 6abcc220de 对齐AMESim管阻孔口与储气耦合 2026-08-10 13:07:03 +00:00
ljz 7671418582 统一组件图标布局并完善选择吸附交互 2026-08-06 23:35:50 +08:00
huojiarong 40c72422ff 对齐PNVO近等压层流平滑 2026-08-05 12:59:05 +00:00
huojiarong e9fc855a1c 修复参数帮助浮层滚动触发 2026-08-05 12:58:08 +00:00
ljz 09778972a6 完善端口标号与MECMAS21动态图标 2026-08-04 12:55:50 +08:00
ljz df1d676131 完善AMESim可配置参数交互
补充气动孔口、动态管路、UD00 与弹性接触组件的离散选项、条件显示和折叠分组。

增加目录协议、非法选项和前端参数表交互回归测试,不修改现有求解公式。
2026-08-04 01:27:17 +08:00
ljz be54070855 完善MECMAS21参数配置与分组交互 2026-08-03 23:51:27 +08:00
ljz 32f21f08ff 优化参数表样式与科学计数法支持 2026-08-03 23:51:27 +08:00
huojiarong 046aa49814 对齐Amesim氦气PR物性与PNVO流量 2026-08-03 15:34:33 +00:00
huojiarong 18d9802f03 优化求解器重试并校正AMESim机械端口 2026-08-03 09:54:46 +00:00
ljz 971e8f2336 初版:实现 AMESim 机械因果化与事件求解
初步支持 MECMAS21 刚性质量状态归并、端止事件、恢复系数,以及 LSTP 接触和压力流量显式因果化。

已知问题:显式传播仍会重复扫描全网方程,长时刚性仿真性能待优化;自适应积分器遇到越出物理域的试探状态时,尚未实现恢复并缩步重试。
2026-08-03 15:45:48 +08:00
huojiarong de265cdde6 调整Playwright默认浏览器策略 2026-08-03 07:03:13 +00:00
ljz cf249d6c0d 优化组件选择交互与视图适配时机 2026-08-03 11:59:24 +08:00
ljz 11093613c5 完善 PNVO001 与 PNRP17 前端图标及端口交互
按 AMESim 参考样式重绘孔板与气动活塞图标,调整端口锚点和旋转后的连接方向,并补充组件图标及接触连接回归测试。
2026-08-03 09:49:55 +08:00
huojiarong 1f4027573f 公开PNRP17并接通实时气动机械耦合 2026-08-02 14:51:31 +00:00
ljz 503394f3ed 完善建模交互与结果曲线功能
支持元件接口接触吸附、旋转后兼容连接及无连线逻辑边。

完善 AMESim 图标显示、参数双栏、结果曲线待定窗口及相关端到端测试。
2026-08-02 17:31:28 +08:00
ljz 410ef535e8 完善AMESim组件界面与仿真求解稳定性 2026-08-02 00:57:48 +08:00
ljz e7177ab03e feat: integrate AMESim media models and editor UI 2026-07-31 23:36:58 +08:00
lujingze 3e4f466114 Merge pull request 'merge/model-development-into-main' (#2) from merge/model-development-into-main into main
Reviewed-on: #2
2026-07-31 09:52:44 +08:00
huojiarong 127ec36a55 Merge model-development into main 2026-07-30 10:53:35 +00:00
huojiarong e87df789b2 接入 AMESim 机械接触和节点组件 2026-07-30 10:34:29 +00:00
huojiarong 2ddaaeb56a 上传 AMESim 参考资料 2026-07-30 10:15:23 +00:00
huojiarong 3f51a124d6 公开更多 AMESim 组件并接入信号机械闭环 2026-07-30 10:11:08 +00:00
huojiarong db4bdb4b70 同步仿真框架并接入AMESim气动组件 2026-07-30 03:53:10 +00:00
huojiarong 420bafeb4e 补充test_mql pneumatic_69局部参考RHS候选 2026-07-29 09:23:05 +00:00
huojiarong e17a952bee 补充test_mql pneumatic_96候选短窗口对比 2026-07-29 09:02:55 +00:00
huojiarong a8a68f6dd4 添加test_mql pneumatic_96局部参考RHS候选 2026-07-29 08:38:37 +00:00
huojiarong 71da73ef70 补充test_mql pneumatic_96流量反事实诊断 2026-07-29 08:15:36 +00:00
huojiarong cd2fa800b9 补充test_mql pneumatic_96参考导数反事实诊断 2026-07-29 08:04:30 +00:00
huojiarong 532fdb4e6b 补充test_mql pneumatic_96导数分项诊断 2026-07-29 07:45:39 +00:00
huojiarong bd7c8c99e9 补充test_mql pneumatic_96入口支路诊断 2026-07-29 07:31:08 +00:00
huojiarong bf4dc0af3d 补充test_mql PNL0003质量导数链路诊断 2026-07-29 07:09:11 +00:00
huojiarong 200d4593e8 补充test_mql PNL0003压力导数分项诊断 2026-07-29 06:40:42 +00:00
huojiarong 890bf1d483 补充test_mql PNL0003双端温度导数诊断 2026-07-29 06:31:04 +00:00
huojiarong 137a4cf7d6 补充test_mql PNL0003温度导数分项诊断 2026-07-28 10:48:32 +00:00
huojiarong 920ee336e9 补充test_mql PNL0003储气质量诊断 2026-07-28 10:35:14 +00:00
huojiarong fc46c4272a 补充test_mql PNL0003中心流反算诊断 2026-07-28 10:07:13 +00:00
huojiarong 8b9f229b80 补充test_mql PNL0003温度反事实诊断 2026-07-28 09:37:34 +00:00
huojiarong 918952f130 补充test_mql PNL0003中心焓候选诊断 2026-07-28 09:16:33 +00:00
huojiarong d67fb251d2 补充test_mql P4节点真实气体焓诊断 2026-07-28 09:00:02 +00:00
huojiarong e473162d4c 校正test_mql PNVO事件流量系数 2026-07-28 04:16:45 +00:00
huojiarong 8a2d2ae190 修正test_mql PNL0003节点混合焓 2026-07-28 03:33:10 +00:00
huojiarong 7182f929e8 补充test_mql PNL0003储能诊断 2026-07-28 02:49:02 +00:00
huojiarong 021ea63a2b 接入test_mql pneumatic_69传输焓RHS 2026-07-28 02:20:33 +00:00
huojiarong f8af99ace1 接入test_mql高密度事件窗口诊断 2026-07-27 16:18:20 +00:00
huojiarong 01b606df30 修正test_mql事件窗口插值诊断 2026-07-26 14:48:38 +00:00
huojiarong 331435e78a 补充test_mql PNVO焓流诊断与系数校正 2026-07-24 10:58:10 +00:00
huojiarong a219cddc0b 修正test_mql节点焓流诊断口径 2026-07-23 08:40:56 +00:00
huojiarong 1602e0dc51 补充pn2pipefr候选流量诊断 2026-07-22 09:50:48 +00:00
huojiarong 84d1675292 接入AMESim help文档与PNVO流量诊断 2026-07-22 08:56:04 +00:00
huojiarong f0f8f40c7a 接入PNL0001事件窗口RHS诊断 2026-07-22 04:21:38 +00:00
huojiarong 2120901909 修复test_mql事件后积分步长控制 2026-07-22 02:58:59 +00:00
huojiarong a0855018d5 对齐test_mql保存时刻与PNVO事件边界 2026-07-21 07:56:25 +00:00
huojiarong 7c68caed27 接入test_mql PNVO阶跃控制与流量诊断 2026-07-21 07:28:28 +00:00
huojiarong 65f6d6dd87 更新test_mql迁移状态文档 2026-07-20 10:34:51 +00:00
huojiarong 2c2952cff1 拆解test_mql变容气室RHS 2026-07-20 10:12:07 +00:00
huojiarong a6e1faa2d8 诊断test_mql总闭包对比端点 2026-07-20 10:04:43 +00:00
huojiarong 3951bc372c 排序test_mql总闭包对比偏差 2026-07-20 09:59:09 +00:00
huojiarong eaafe5eafc 接入test_mql总闭包对比报告 2026-07-20 09:54:03 +00:00
huojiarong 6953c864c4 导出test_mql总闭包关键序列 2026-07-20 09:45:32 +00:00
huojiarong 958d813300 接入test_mql变容气室机械反馈 2026-07-20 09:37:40 +00:00
huojiarong d4edf1ccb3 导出test_mql活塞运动学 2026-07-20 09:26:01 +00:00
huojiarong 65cdd2284e 接入test_mql大质量块约束 2026-07-20 09:23:23 +00:00
huojiarong bb4a71c612 接入test_mql外力信号边界 2026-07-20 09:18:05 +00:00
huojiarong c8de46fac2 导出test_mql机械节点总力 2026-07-20 09:14:20 +00:00
huojiarong 8782f96baf 补充test_mql机械节点运动学 2026-07-20 09:09:49 +00:00
huojiarong 0035243361 接入test_mql端止动机械力 2026-07-20 09:07:00 +00:00
huojiarong 716a1c572a 组合test_mql 132状态总闭包 2026-07-20 09:01:17 +00:00
huojiarong 3d1de6d5d0 接入test_mql机械质量状态闭包 2026-07-20 08:54:04 +00:00
huojiarong 7cb050d235 抽象test_mql邻域与阻力流量计算 2026-07-20 03:56:50 +00:00
huojiarong 3d572767d2 抽象test_mql双状态孔口线流量计算 2026-07-20 03:43:43 +00:00
huojiarong be1eec1348 抽象test_mql固定气室段流量计算 2026-07-20 03:38:17 +00:00
huojiarong cdd17d29c8 抽象test_mql PN3节点平衡组装 2026-07-20 03:31:04 +00:00
huojiarong a52019c5e9 抽象test_mql P4节点平衡组装 2026-07-20 03:25:12 +00:00
huojiarong 777f943660 抽象test_mql代数元件端口写回 2026-07-20 03:18:24 +00:00
huojiarong 65623fb1dd 复用test_mql输出线闭包逻辑 2026-07-19 10:58:39 +00:00
huojiarong cf33d445a2 抽象test_mql双状态连接线闭包逻辑 2026-07-19 10:54:11 +00:00
huojiarong 13f37bfd0f 抽象test_mql相邻连接线闭包逻辑 2026-07-19 10:43:13 +00:00
huojiarong d02dacae0f 抽象test_mql主线气室闭包逻辑 2026-07-19 10:34:38 +00:00
huojiarong 7b9d28c0ee 抽象test_mql固定气室段闭包逻辑 2026-07-19 10:28:05 +00:00
huojiarong 2622bb8c92 补充test_mql pn_node3_8气室闭合断言 2026-07-19 10:17:53 +00:00
huojiarong dd2efff487 补充test_mql pneumatic_100闭合断言 2026-07-19 10:03:29 +00:00
huojiarong acee83fe53 接入test_mql pn_node3_10入口平衡 2026-07-19 09:55:25 +00:00
huojiarong bdbac6fbb5 接入test_mql pn_general_chamber_2出口支路 2026-07-19 09:48:08 +00:00
huojiarong 09fb3a5f72 接入test_mql pneumatic_107远端PN3子网 2026-07-19 09:34:05 +00:00
huojiarong 2afd5eeb11 接入test_mql pn_node3_12主压温支路 2026-07-19 09:22:00 +00:00
huojiarong 4242057bd8 接入test_mql pn_node3_13固定气室支路 2026-07-19 09:10:00 +00:00
huojiarong 492562a517 接入test_mql pneumatic_106远端PN3子网 2026-07-19 08:54:44 +00:00
huojiarong a1bddfcd98 接入test_mql pn_node3_14固定气室支路 2026-07-19 08:41:41 +00:00
huojiarong 8e1263833b 接入test_mql pneumatic_93直连P4端口 2026-07-19 08:27:14 +00:00
huojiarong b44bbbd110 接入test_mql pneumatic_94直连P4端口 2026-07-18 09:47:32 +00:00
huojiarong a09a8c8daf 接入test_mql P4桥接环网管路 2026-07-18 09:38:52 +00:00
huojiarong 8639b42574 接入test_mql pneumatic_105远端PN3子网 2026-07-18 09:24:36 +00:00
huojiarong f74b414b24 接入test_mql P4环网剩余管路 2026-07-18 09:10:31 +00:00
huojiarong 8c8b4f26e9 接入test_mql PNVO上游PN3子网 2026-07-18 08:56:23 +00:00
huojiarong 62c8aabeab 接入test_mql相邻P4远端主线 2026-07-18 08:35:42 +00:00
huojiarong 95ba5a5e81 接入test_mql相邻P4主压温边界 2026-07-17 10:20:43 +00:00
huojiarong 853ae3043d 复用test_mql P4邻接拓扑装配 2026-07-17 10:10:41 +00:00
huojiarong 43e238034a 补充test_mql P4邻接拓扑解析 2026-07-17 10:05:26 +00:00
huojiarong 8af0a27e4c 接入test_mql P4主线气室 2026-07-17 09:58:59 +00:00
huojiarong fafce0af87 接入test_mql P4节点局部闭合 2026-07-17 09:51:54 +00:00
huojiarong a071896834 实现test_mql PNL0002管路动态 2026-07-17 09:38:32 +00:00
huojiarong 7569917924 实现test_mql P4节点代数契约 2026-07-17 09:25:35 +00:00
huojiarong 5c3ad50090 接入test_mql PNVO孔口边界 2026-07-17 09:16:35 +00:00
huojiarong 78013b25c9 接入test_mql PN3节点管路闭合 2026-07-17 09:05:51 +00:00
huojiarong 568be2e632 实现test_mql PNL0003与PNL00R管路 2026-07-17 08:54:38 +00:00
huojiarong c4c1b35dee 接入test_mql双管路气室与PN3节点 2026-07-17 03:39:14 +00:00
huojiarong 1193b2dcee 实现test_mql PNL0001管路动态 2026-07-17 03:28:36 +00:00
huojiarong 2d33a8f4bc 接入test_mql真实固定气室拓扑 2026-07-17 02:49:06 +00:00
huojiarong c0f28520b4 补充test_mql气动拓扑候选发现 2026-07-16 09:42:25 +00:00
huojiarong 95ebca89f6 补充test_mql气动支路配置接口 2026-07-16 09:31:12 +00:00
huojiarong 2cea81739e 补充test_mql气动支路仿真入口 2026-07-16 09:22:51 +00:00
huojiarong 314b65872e 接入test_mql真实气动组件 2026-07-16 09:17:10 +00:00
huojiarong 94badfb56f 建立test_mql气动闭合骨架 2026-07-16 09:10:46 +00:00
huojiarong a1870f2789 补充test_mql管路反号观测输出 2026-07-16 08:54:59 +00:00
huojiarong 0cd4359964 补充test_mql气动反号观测输出 2026-07-16 08:51:59 +00:00
huojiarong bef9dce55e 补充test_mql机械零力计算输出 2026-07-16 08:44:34 +00:00
huojiarong 3ac83d9f00 补充test_mql质量端止动反号输出 2026-07-16 08:36:36 +00:00
huojiarong 999d840615 补充test_mql变容气室体积计算输出 2026-07-16 08:23:12 +00:00
huojiarong b3feb5c7e0 补充test_mql活塞几何计算输出 2026-07-16 02:52:26 +00:00
huojiarong 77e2f10cd5 补充test_mql基准透传脚本 2026-07-16 02:46:13 +00:00
huojiarong d35e85e066 补充test_mql基准透传运行器 2026-07-16 02:41:04 +00:00
huojiarong 6ccbf7eb5e 补充test_mql输出校验对比入口 2026-07-16 02:34:32 +00:00
huojiarong 9eb7d293f7 补充test_mql输出变量契约 2026-07-16 02:27:42 +00:00
huojiarong 143097dc28 补充test_mql统一观测目录 2026-07-16 02:21:49 +00:00
huojiarong c84a809f77 补齐test_mql机械观测覆盖 2026-07-16 02:15:47 +00:00
huojiarong f6871aefac 补充test_mql机械观测目录 2026-07-16 02:06:10 +00:00
huojiarong 6f9e7084dd 补充test_mql气室观测目录 2026-07-16 01:59:15 +00:00
huojiarong 7c820cbada 补充test_mql管路观测目录 2026-07-16 01:54:44 +00:00
huojiarong c7b92f99c6 补充test_mql孔口观测目录 2026-07-16 01:49:19 +00:00
huojiarong 2199f1c11e 修正test_mql气室压力约定 2026-07-15 10:25:50 +00:00
huojiarong f6dda509a2 补充test_mql质量端止动原语 2026-07-15 10:19:24 +00:00
huojiarong a63fda313b 校验test_mql机械观测量 2026-07-15 10:08:53 +00:00
huojiarong 6f34ac16bf 补充test_mql机械原语 2026-07-15 10:02:53 +00:00
huojiarong 8f14a3840f 装配test_mql机械组件 2026-07-15 09:56:57 +00:00
huojiarong 03a6db10ad 装配test_mql管路连接 2026-07-15 09:51:02 +00:00
huojiarong 3818b011d5 建立test_mql变量目录 2026-07-15 09:46:51 +00:00
huojiarong 33c8bfae3d 装配test_mql气动元件 2026-07-15 09:43:34 +00:00
huojiarong 1f47171d07 新增test_mql结果对齐工具 2026-07-15 09:40:07 +00:00
huojiarong 796433c275 解析AMESim二进制结果文件 2026-07-15 09:33:34 +00:00
huojiarong a598fcd5c3 新增test_mql气动组件原语 2026-07-15 09:21:03 +00:00
huojiarong cdead78977 补充氦气Peng-Robinson物性库 2026-07-15 09:16:11 +00:00
huojiarong 056b71fead 补充test_mql配置解析层 2026-07-15 09:05:53 +00:00
huojiarong c745fd2a5c Add test_mql structural Python model 2026-07-15 08:59:59 +00:00
huojiarong 2c38da16b5 Add AMESim test_mql model 2026-07-15 08:33:38 +00:00
362 changed files with 499209 additions and 3445 deletions

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
*.bat text eol=crlf
*.cmd text eol=crlf
*.sh text eol=lf
+180
View File
@@ -0,0 +1,180 @@
name: Solver regression
on:
push:
paths:
- "app/simulation/**"
- "tests/**"
- "requirements.txt"
- "constraints/**"
- ".python-version"
- "README.md"
- ".github/workflows/solver-regression.yml"
pull_request:
paths:
- "app/simulation/**"
- "tests/**"
- "requirements.txt"
- "constraints/**"
- ".python-version"
- "README.md"
- ".github/workflows/solver-regression.yml"
schedule:
- cron: "17 3 * * 1-6"
- cron: "17 3 * * 0"
workflow_dispatch:
inputs:
suite:
description: Regression tier
required: true
default: quick
type: choice
options:
- quick
- historical
- main-long
case:
description: Longest main-model horizon (predecessors run first)
required: true
default: 0.2s
type: choice
options:
- 0.2s
- 1s
- 5s
- 10s
lane:
description: Output sampling lane
required: true
default: production
type: choice
options:
- solver-only
- production
concurrency:
group: solver-regression-${{ github.ref }}-${{ github.event_name }}
cancel-in-progress: false
permissions:
contents: read
jobs:
quick:
if: >-
github.event_name == 'push' ||
github.event_name == 'pull_request' ||
(github.event_name == 'workflow_dispatch' && inputs.suite == 'quick')
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version-file: .python-version
cache: pip
cache-dependency-path: |
requirements.txt
constraints/python312-direct.txt
constraints/python312-linux-x86_64.lock
- name: Install hashed Linux release lock
run: |
python -m pip install \
--force-reinstall \
-r constraints/python312-linux-x86_64.lock
python -m pip check
- name: Run solver foundation tests
env:
SYSTEM_SIMULATION_VERIFY_LOCKED_ENV: "1"
run: |
python -W error::ResourceWarning -m unittest \
tests.test_dependency_constraints \
tests.test_benchmark_regression \
tests.test_physical_state_v21 \
tests.test_test_mql_ame_contract \
tests.test_test_mql_8_regression \
tests.test_mql_full_branches_regression \
tests.test_pressure_flow_causal_execution \
tests.test_stream_pressure_block_solver \
tests.test_core_solver \
tests.test_supported_piston_tangent \
tests.test_three_piston_tangent \
tests.test_sparse_secant_jacobian \
tests.test_generic_jacobian_sparsity \
tests.test_generic_system_xml_simulation
historical-nightly:
if: >-
(github.event_name == 'schedule' && github.event.schedule == '17 3 * * 1-6') ||
(github.event_name == 'workflow_dispatch' && inputs.suite == 'historical')
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version-file: .python-version
cache: pip
cache-dependency-path: |
requirements.txt
constraints/python312-direct.txt
constraints/python312-linux-x86_64.lock
- name: Install hashed Linux release lock
run: |
python -m pip install \
--force-reinstall \
-r constraints/python312-linux-x86_64.lock
python -m pip check
- name: Run 0.81 and 2.10 second historical regression
run: |
mkdir -p artifacts
python -m app.simulation.benchmark_regression \
--manifest tests/baselines/simulation/test_mql_full_branches/manifest.json \
--lane production \
--output artifacts/test-mql-full-branches.json
- if: always()
uses: actions/upload-artifact@v4
with:
name: historical-solver-regression
path: artifacts/*.json
if-no-files-found: warn
main-periodic:
if: >-
(github.event_name == 'schedule' && github.event.schedule == '17 3 * * 0') ||
(github.event_name == 'workflow_dispatch' && inputs.suite == 'main-long')
runs-on: ubuntu-24.04
timeout-minutes: 180
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version-file: .python-version
cache: pip
cache-dependency-path: |
requirements.txt
constraints/python312-direct.txt
constraints/python312-linux-x86_64.lock
- name: Install hashed Linux release lock
run: |
python -m pip install \
--force-reinstall \
-r constraints/python312-linux-x86_64.lock
python -m pip check
- name: Run bounded progressive main-model regression
env:
REQUESTED_CASE: ${{ github.event_name == 'workflow_dispatch' && inputs.case || '10s' }}
REQUESTED_LANE: ${{ github.event_name == 'workflow_dispatch' && inputs.lane || 'production' }}
run: |
mkdir -p artifacts
python -m app.simulation.benchmark_regression \
--manifest tests/baselines/simulation/test_mql_8/manifest.json \
--lane "$REQUESTED_LANE" \
--case "$REQUESTED_CASE" \
--output artifacts/test-mql-8-progressive.json
- if: always()
uses: actions/upload-artifact@v4
with:
name: main-model-progressive-regression
path: artifacts/*.json
if-no-files-found: warn
+4
View File
@@ -12,6 +12,10 @@ htmlcov/
# Local virtual environments # Local virtual environments
.venv/ .venv/
.venv-win/ .venv-win/
# Local Linux toolchain (downloaded for the startup scripts)
.tools/node-*-linux-x64/
app/data/ app/data/
frontend/node_modules/ frontend/node_modules/
frontend/dist/ frontend/dist/
+1
View File
@@ -0,0 +1 @@
3.12.3
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+388
View File
@@ -0,0 +1,388 @@
/* Submodel PNCH012 skeleton created by AME Submodel editing utility
mar. oct. 9 14:41:15 2018 */
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include "ameutils.h"
/* *******************************************************************************
TITLE : PNCH012
--------------------------------------------------------------------------------
DESCRIPTION :
This submodel represents a pneumatic chamber with a variable volume
and pressure dynamics.
Each port receives a mass flow rate and an enthalpy flow rate as
input and gives the pressure and the temperature of the chamber as
output. Each port receives also the volume and volume variation as
input. The total volume is calculated by summing the four volume
inputs and a dead volume which is a parameter of PNCH012.
The model takes into account heat exchange. It express the variation
of internal energy U using the first law of thermodynamics applied to
an open system. Therefore, this model should be preferred to the simple
polytropic chamber PNCH011.
The total volume of the chamber is limited to a lower value equal to
the dead volume divided by 100.
--------------------------------------------------------------------------------
USAGE :
Use this submodel to simulate a pneumatic chamber in a jack, spool
valve or any pneumatic chamber in which the volume can vary.
This submodel can be directly connected to any pneumatic PCD
component or standard pneumatic component.
The submodels PNGD001, PNGD002, PNGD003, PNGD004 or PNRGD00 should be
included in your circuit to define the characteristics of the gas.
--------------------------------------------------------------------------------
PARAMETER SETTINGS:
The dead volume is the volume of the pneumatic fluid when all the input
volumes are zero. It is essential that this volume must be greater
than zero.
--------------------------------------------------------------------------------
DATE OF CREATION / AUTHOR :
2002 FS from PNCH12
--------------------------------------------------------------------------------
INDEX OF REVISIONS :
2008 OBA - Real gas improvements : the mass and volume were considered as
internal state variable, they are now coded as internal basic
variable. The mass initialisation was removed as it was linked
to the perfect gas formulation.
--------------------------------------------------------------------------------
LIST OF FUNCTIONS USED :
pn2getatp : get atmospheric pressure
firstc_ : checks if this is the first call to this submodel
pn2vol_ : pneumatic chamber with heat exchange
stepdn_ : reduce simulation step
--------------------------------------------------------------------------------
SOURCE :
This material contains trade secrets or otherwise confidential
information owned by Siemens Industry Software Inc. or its
affiliates (collectively, "Siemens"), or its licensors. Access to
and use of this information is strictly limited as set forth in the
Customer's applicable agreements with Siemens.
Unpublished work. Copyright 2023 Siemens
******************************************************************************* */
#define _SUBMODELNAME_ "PNCH012"
/* >>>>>>>>>>>>Insert Private Code Here. */
/* <<<<<<<<<<<<End of Private Code. */
/* There are 4 real parameters:
cvol0 dead volume [L -> m**3]
kth thermal exchange coefficient [J/m**2/K/s -> W/m**2/K]
sth thermal exchange area [m**2]
extemp external temperature [K]
*/
/* There is 1 integer parameter:
gi gas type index
*/
void pnch012in_(int *n, double rp[4], int ip[1], double c[2]
, int ic[2], double *temp, double *press, double *dvol1
, double *vol1, double *dvol2, double *vol2, double *dvol3
, double *vol3, double *dvol4, double *vol4)
{
int loop, error;
/* >>>>>>>>>>>>Extra Initialization Function Declarations Here. */
/* <<<<<<<<<<<<End of Extra Initialization declarations. */
int gi;
double cvol0, kth, sth, extemp;
gi = ip[0];
cvol0 = rp[0];
kth = rp[1];
sth = rp[2];
extemp = rp[3];
loop = 0;
error = 0;
/* Assign default values to input(s) with default. */
*dvol1 = 0.00000000000000e+000;
*vol1 = 0.00000000000000e+000;
*dvol2 = 0.00000000000000e+000;
*vol2 = 0.00000000000000e+000;
*dvol3 = 0.00000000000000e+000;
*vol3 = 0.00000000000000e+000;
*dvol4 = 0.00000000000000e+000;
*vol4 = 0.00000000000000e+000;
/*
If necessary, check values of the following:
rp[0..3]
*temp
*press
*/
/* >>>>>>>>>>>>Initialization Function Check Statements. */
pn2_valid_gas_(&gi, &error);
if (cvol0 <= 0.0)
{
error = 2;
amefprintf(stderr, "\nVolume chamber must be strictly positive.\n");
}
if (kth < 0.0)
{
error = 2;
amefprintf(stderr, "\nthermal exchange coefficient must be positive.\n");
}
if (sth < 0.0)
{
error = 2;
amefprintf(stderr, "\nthermal exchange area must be positive.\n");
}
if (extemp <= 0.0)
{
error = 2;
amefprintf(stderr, "\nExternal temperature must be strictly positive.\n");
}
if (*temp <= 0.0)
{
error = 2;
amefprintf(stderr, "\nInitial temperature must be strictly positive.\n");
}
/* <<<<<<<<<<<<End of Initialization Check Statements. */
/* Integer parameter checking: */
if (gi < 1 || gi > 99)
{
amefprintf(stderr, "\ngas type index must be in range [1..99].\n");
error = 2;
}
if(error == 1)
{
amefprintf(stderr, "\nWarning in %s instance %d.\n", _SUBMODELNAME_, *n);
}
else if(error == 2)
{
amefprintf(stderr, "\nFatal error in %s instance %d.\n", _SUBMODELNAME_, *n);
amefprintf(stderr, "Terminating the program.\n");
AmeExit(1);
}
/* Common -> SI units conversions. */
rp[0] *= 1.00000000000000e-003;
cvol0 = rp[0];
/* >>>>>>>>>>>>Initialization Function Executable Statements. */
c[0] = cvol0 / 100;
/* Set initial value for the test of limited volume :
ic[1] = 1 when the chamber volume is limited to cvol0 / 100 else ic[1] = 0*/
ic[1] = 0;
/* set atmospheric pressure */
c[1] = pn2getatp_();
/* <<<<<<<<<<<<End of Initialization Executable Statements. */
}
/* There are 4 ports.
Port 1 has 6 variables:
1 temp temperature [K] explicit state (derivative `dtemp')
2 press pressure [Pa] explicit state (derivative `dpress')
3 dh1 enthalpy flow rate at port 1 [J/s -> W] basic variable input
4 dm1 mass flow rate at port 1 [g/s -> kg/s] basic variable input
5 dvol1 derivative of volume at port 1 [L/min -> m**3/s] basic variable input with default 0.000000e+000
6 vol1 volume at port 1 [cm**3 -> m**3] basic variable input with default 0.000000e+000
Port 2 has 6 variables:
1 temp2 duplicate of temp
2 press2 duplicate of press
3 dh2 enthalpy flow rate at port 2 [J/s -> W] basic variable input
4 dm2 mass flow rate at port 2 [g/s -> kg/s] basic variable input
5 dvol2 derivative of volume at port 2 [L/min -> m**3/s] basic variable input with default 0.000000e+000
6 vol2 volume at port 2 [cm**3 -> m**3] basic variable input with default 0.000000e+000
Port 3 has 6 variables:
1 temp3 duplicate of temp
2 press3 duplicate of press
3 dh3 enthalpy flow rate at port 3 [J/s -> W] basic variable input
4 dm3 mass flow rate at port 3 [g/s -> kg/s] basic variable input
5 dvol3 derivative of volume at port 3 [L/min -> m**3/s] basic variable input with default 0.000000e+000
6 vol3 volume at port 3 [cm**3 -> m**3] basic variable input with default 0.000000e+000
Port 4 has 6 variables:
1 temp4 duplicate of temp
2 press4 duplicate of press
3 dh4 enthalpy flow rate at port 4 [J/s -> W] basic variable input
4 dm4 mass flow rate at port 4 [g/s -> kg/s] basic variable input
5 dvol4 derivative of volume at port 4 [L/min -> m**3/s] basic variable input with default 0.000000e+000
6 vol4 volume at port 4 [cm**3 -> m**3] basic variable input with default 0.000000e+000
*/
/* There are 2 internal variables.
1 vol volume of pneumatic chamber [cm**3 -> m**3] basic variable
2 mgas1 mass of gas in chamber [g -> kg] basic variable
*/
void pnch012_(int *n, double *temp, double *dtemp, double *press
, double *dpress, double *dh1, double *dm1, double *dvol1
, double *vol1, double *dh2, double *dm2, double *dvol2
, double *vol2, double *dh3, double *dm3, double *dvol3
, double *vol3, double *dh4, double *dm4, double *dvol4
, double *vol4, double *vol, double *mgas1, double rp[4]
, int ip[1], double c[2], int ic[2])
{
int loop;
/* >>>>>>>>>>>>Extra Calculation Function Declarations Here. */
double dvol;
double sdm, sdh;
double dq;
double pressa;
/* <<<<<<<<<<<<End of Extra Calculation declarations. */
int gi;
double cvol0, kth, sth, extemp;
gi = ip[0];
cvol0 = rp[0];
kth = rp[1];
sth = rp[2];
extemp = rp[3];
loop = 0;
/* Common -> SI units conversions. */
*dm1 *= 1.00000000000000e-003;
*dvol1 *= 1.66666666666667e-005;
*vol1 *= 1.00000000000000e-006;
*dm2 *= 1.00000000000000e-003;
*dvol2 *= 1.66666666666667e-005;
*vol2 *= 1.00000000000000e-006;
*dm3 *= 1.00000000000000e-003;
*dvol3 *= 1.66666666666667e-005;
*vol3 *= 1.00000000000000e-006;
*dm4 *= 1.00000000000000e-003;
*dvol4 *= 1.66666666666667e-005;
*vol4 *= 1.00000000000000e-006;
/*
Set all submodel outputs below:
*dtemp = ??;
*dpress = ??;
*vol = ??;
*mgas1 = ??;
*/
/* >>>>>>>>>>>>Calculation Function Executable Statements. */
/* set absolute pressure */
pressa = *press + c[1];
/*** sum of the volume variation and volume ***/
dvol = *dvol1 + *dvol2 + *dvol3 + *dvol4;
/*** setup the initial mass of the gaz inside of the chamber ***/
*vol = *vol1 + *vol2 + *vol3 + *vol4 + cvol0;
/*** sum of the flows ***/
sdm = *dm1 + *dm2 + *dm3 + *dm4; /* mass flow */
sdh = *dh1 + *dh2 + *dh3 + *dh4; /* heat flow */
/*** V, M, T and P can not be lower than zero ***/
*vol = llimit_(vol, &c[0], &ic[0]);
if (ic[0] == -1)
{
dvol = 0.;
if (ic[1] == 0)
{
amefprintf(stderr, "\nWarning in %s instance %d chamber volume is limited by cvol0 / 100 = %g cm**3.\n", _SUBMODELNAME_, *n, c[0]*1E+6);
ic[1] = 1;
}
}
if (*vol < c[0]/10)
{
*vol = c[0]/10;
}
if ( (*mgas1 <= 1.0e-10) && (!firstc_()) )
{
/* panic step reduction */
stepdn_();
*mgas1 = 1.0e-10;
}
if (pressa <= 1.0e-10)
{
/* panic step reduction */
stepdn_();
*press = 1.0e-10 - c[1];
}
if (*temp <= 1.0e-10)
{
/* panic step reduction */
stepdn_();
*temp = 1.0e-10;
}
/*** temperature & pressure variation ***/
dq = kth*sth*(extemp-*temp);
pn2vol_(dtemp, dpress, mgas1, temp, &pressa,
&sdm, &sdh, vol, &dvol, &dq, &gi);
/* <<<<<<<<<<<<End of Calculation Executable Statements. */
/* SI -> Common units conversions. */
*dm1 /= 1.00000000000000e-003;
*dvol1 /= 1.66666666666667e-005;
*vol1 /= 1.00000000000000e-006;
*dm2 /= 1.00000000000000e-003;
*dvol2 /= 1.66666666666667e-005;
*vol2 /= 1.00000000000000e-006;
*dm3 /= 1.00000000000000e-003;
*dvol3 /= 1.66666666666667e-005;
*vol3 /= 1.00000000000000e-006;
*dm4 /= 1.00000000000000e-003;
*dvol4 /= 1.66666666666667e-005;
*vol4 /= 1.00000000000000e-006;
*vol /= 1.00000000000000e-006;
*mgas1 /= 1.00000000000000e-003;
}
+356
View File
@@ -0,0 +1,356 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE SPE>
<SPE DOC_VERSION="2" AME_VERSION="16.0.0 - 68387-65635 2017">
<SUBMODEL>
<SUB_TYPE>0</SUB_TYPE>
<SUB_ID_MAX>33</SUB_ID_MAX>
<DEFAULT_ICON>pn_c1</DEFAULT_ICON>
<SUB_LABEL>variable volume pneumatic chamber with heat exchange (preferred)</SUB_LABEL>
<SUB_UNIT>0</SUB_UNIT>
<R_STORES_NUMBER>2</R_STORES_NUMBER>
<I_STORES_NUMBER>2</I_STORES_NUMBER>
<OUTPUT_TYPE>1</OUTPUT_TYPE>
<RPARAMS_LIST>
<RPARAM>
<SUB_ID>27</SUB_ID>
<TITLE>dead volume</TITLE>
<VARNAME>cvol0</VARNAME>
<VISIBILITY>True</VISIBILITY>
<DEF_VALUE>1.00000000000000e+00</DEF_VALUE>
<VALUE>1.00000000000000e+00</VALUE>
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
<MAX_VALUE>1.00000000000000e+005</MAX_VALUE>
<UNITS>L</UNITS>
</RPARAM>
<RPARAM>
<SUB_ID>28</SUB_ID>
<TITLE>thermal exchange coefficient</TITLE>
<VARNAME>kth</VARNAME>
<VISIBILITY>True</VISIBILITY>
<DEF_VALUE>0.00000000000000e+00</DEF_VALUE>
<VALUE>0.00000000000000e+00</VALUE>
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
<MAX_VALUE>1.00000000000000e+006</MAX_VALUE>
<UNITS>J/m**2/K/s</UNITS>
</RPARAM>
<RPARAM>
<SUB_ID>29</SUB_ID>
<TITLE>thermal exchange area</TITLE>
<VARNAME>sth</VARNAME>
<VISIBILITY>True</VISIBILITY>
<DEF_VALUE>1.00000000000000e-01</DEF_VALUE>
<VALUE>1.00000000000000e-01</VALUE>
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
<MAX_VALUE>1.00000000000000e+002</MAX_VALUE>
<UNITS>m**2</UNITS>
</RPARAM>
<RPARAM>
<SUB_ID>30</SUB_ID>
<TITLE>external temperature</TITLE>
<VARNAME>extemp</VARNAME>
<VISIBILITY>True</VISIBILITY>
<DEF_VALUE>2.93150000000000e+02</DEF_VALUE>
<VALUE>2.93150000000000e+02</VALUE>
<MIN_VALUE>1.00000000000000e+000</MIN_VALUE>
<MAX_VALUE>1.00000000000000e+003</MAX_VALUE>
<UNITS>K</UNITS>
</RPARAM>
</RPARAMS_LIST>
<IPARAMS_LIST>
<IPARAM>
<SUB_ID>31</SUB_ID>
<TITLE>gas type index</TITLE>
<VARNAME>gi</VARNAME>
<VISIBILITY>True</VISIBILITY>
<DEF_VALUE>1</DEF_VALUE>
<VALUE>1</VALUE>
<MIN_VALUE>1</MIN_VALUE>
<MAX_VALUE>99</MAX_VALUE>
</IPARAM>
</IPARAMS_LIST>
<IVARS_LIST>
<IVAR>
<SUB_ID>32</SUB_ID>
<TITLE>volume of pneumatic chamber</TITLE>
<VARNAME>vol</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<UNITS>cm**3</UNITS>
</IVAR>
<IVAR>
<SUB_ID>33</SUB_ID>
<TITLE>mass of gas in chamber</TITLE>
<VARNAME>mgas1</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<UNITS>g</UNITS>
</IVAR>
</IVARS_LIST>
<EVARS_LIST>
<PORT>
<EVAR>
<SUB_ID>1</SUB_ID>
<TITLE>temperature</TITLE>
<VARNAME>temp</VARNAME>
<VARNAME2>dtemp</VARNAME2>
<VISIBILITY>True</VISIBILITY>
<TYPE>1</TYPE>
<DIMENSION>1</DIMENSION>
<IO>2</IO>
<UNITS>K</UNITS>
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
<MAX_VALUE>1.00000000000000e+003</MAX_VALUE>
<DEF_VALUE>2.93150000000000e+002</DEF_VALUE>
<VALUE>2.93150000000000e+002</VALUE>
</EVAR>
<EVAR>
<SUB_ID>2</SUB_ID>
<TITLE>pressure</TITLE>
<VARNAME>press</VARNAME>
<VARNAME2>dpress</VARNAME2>
<VISIBILITY>True</VISIBILITY>
<TYPE>1</TYPE>
<DIMENSION>1</DIMENSION>
<IO>2</IO>
<UNITS>Pa</UNITS>
<MIN_VALUE>-1.01300000000000e+005</MIN_VALUE>
<MAX_VALUE>1.00000000000000e+012</MAX_VALUE>
<DEF_VALUE>0.00000000000000e+000</DEF_VALUE>
<VALUE>0.00000000000000e+000</VALUE>
</EVAR>
<EVAR>
<SUB_ID>3</SUB_ID>
<TITLE>enthalpy flow rate at port 1</TITLE>
<VARNAME>dh1</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>1</IO>
<UNITS>J/s</UNITS>
</EVAR>
<EVAR>
<SUB_ID>4</SUB_ID>
<TITLE>mass flow rate at port 1</TITLE>
<VARNAME>dm1</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>1</IO>
<UNITS>g/s</UNITS>
</EVAR>
<EVAR>
<SUB_ID>5</SUB_ID>
<TITLE>derivative of volume at port 1</TITLE>
<VARNAME>dvol1</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>3</IO>
<UNITS>L/min</UNITS>
<DEF_VALUE>0</DEF_VALUE>
</EVAR>
<EVAR>
<SUB_ID>6</SUB_ID>
<TITLE>volume at port 1</TITLE>
<VARNAME>vol1</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>3</IO>
<UNITS>cm**3</UNITS>
<DEF_VALUE>0</DEF_VALUE>
</EVAR>
</PORT>
<PORT>
<EVAR>
<SUB_ID>7</SUB_ID>
<VARNAME>temp2</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>4</TYPE>
<PRIMARY_PORT>0</PRIMARY_PORT>
<PRIMARY_VAR>0</PRIMARY_VAR>
<DUP_TYPE>0</DUP_TYPE>
</EVAR>
<EVAR>
<SUB_ID>8</SUB_ID>
<VARNAME>press2</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>4</TYPE>
<PRIMARY_PORT>0</PRIMARY_PORT>
<PRIMARY_VAR>1</PRIMARY_VAR>
<DUP_TYPE>0</DUP_TYPE>
</EVAR>
<EVAR>
<SUB_ID>9</SUB_ID>
<TITLE>enthalpy flow rate at port 2</TITLE>
<VARNAME>dh2</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>1</IO>
<UNITS>J/s</UNITS>
</EVAR>
<EVAR>
<SUB_ID>10</SUB_ID>
<TITLE>mass flow rate at port 2</TITLE>
<VARNAME>dm2</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>1</IO>
<UNITS>g/s</UNITS>
</EVAR>
<EVAR>
<SUB_ID>11</SUB_ID>
<TITLE>derivative of volume at port 2</TITLE>
<VARNAME>dvol2</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>3</IO>
<UNITS>L/min</UNITS>
<DEF_VALUE>0</DEF_VALUE>
</EVAR>
<EVAR>
<SUB_ID>12</SUB_ID>
<TITLE>volume at port 2</TITLE>
<VARNAME>vol2</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>3</IO>
<UNITS>cm**3</UNITS>
<DEF_VALUE>0</DEF_VALUE>
</EVAR>
</PORT>
<PORT>
<EVAR>
<SUB_ID>13</SUB_ID>
<VARNAME>temp3</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>4</TYPE>
<PRIMARY_PORT>0</PRIMARY_PORT>
<PRIMARY_VAR>0</PRIMARY_VAR>
<DUP_TYPE>0</DUP_TYPE>
</EVAR>
<EVAR>
<SUB_ID>14</SUB_ID>
<VARNAME>press3</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>4</TYPE>
<PRIMARY_PORT>0</PRIMARY_PORT>
<PRIMARY_VAR>1</PRIMARY_VAR>
<DUP_TYPE>0</DUP_TYPE>
</EVAR>
<EVAR>
<SUB_ID>15</SUB_ID>
<TITLE>enthalpy flow rate at port 3</TITLE>
<VARNAME>dh3</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>1</IO>
<UNITS>J/s</UNITS>
</EVAR>
<EVAR>
<SUB_ID>16</SUB_ID>
<TITLE>mass flow rate at port 3</TITLE>
<VARNAME>dm3</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>1</IO>
<UNITS>g/s</UNITS>
</EVAR>
<EVAR>
<SUB_ID>17</SUB_ID>
<TITLE>derivative of volume at port 3</TITLE>
<VARNAME>dvol3</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>3</IO>
<UNITS>L/min</UNITS>
<DEF_VALUE>0</DEF_VALUE>
</EVAR>
<EVAR>
<SUB_ID>18</SUB_ID>
<TITLE>volume at port 3</TITLE>
<VARNAME>vol3</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>3</IO>
<UNITS>cm**3</UNITS>
<DEF_VALUE>0</DEF_VALUE>
</EVAR>
</PORT>
<PORT>
<EVAR>
<SUB_ID>19</SUB_ID>
<VARNAME>temp4</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>4</TYPE>
<PRIMARY_PORT>0</PRIMARY_PORT>
<PRIMARY_VAR>0</PRIMARY_VAR>
<DUP_TYPE>0</DUP_TYPE>
</EVAR>
<EVAR>
<SUB_ID>20</SUB_ID>
<VARNAME>press4</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>4</TYPE>
<PRIMARY_PORT>0</PRIMARY_PORT>
<PRIMARY_VAR>1</PRIMARY_VAR>
<DUP_TYPE>0</DUP_TYPE>
</EVAR>
<EVAR>
<SUB_ID>21</SUB_ID>
<TITLE>enthalpy flow rate at port 4</TITLE>
<VARNAME>dh4</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>1</IO>
<UNITS>J/s</UNITS>
</EVAR>
<EVAR>
<SUB_ID>22</SUB_ID>
<TITLE>mass flow rate at port 4</TITLE>
<VARNAME>dm4</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>1</IO>
<UNITS>g/s</UNITS>
</EVAR>
<EVAR>
<SUB_ID>23</SUB_ID>
<TITLE>derivative of volume at port 4</TITLE>
<VARNAME>dvol4</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>3</IO>
<UNITS>L/min</UNITS>
<DEF_VALUE>0</DEF_VALUE>
</EVAR>
<EVAR>
<SUB_ID>24</SUB_ID>
<TITLE>volume at port 4</TITLE>
<VARNAME>vol4</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>3</IO>
<UNITS>cm**3</UNITS>
<DEF_VALUE>0</DEF_VALUE>
</EVAR>
</PORT>
</EVARS_LIST>
<SUBIDS_RESET>0</SUBIDS_RESET>
</SUBMODEL>
</SPE>
+348
View File
@@ -0,0 +1,348 @@
/* Submodel PNL0001 skeleton created by AME Submodel editing utility
mer. juin 20 14:20:39 2018 */
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include "ameutils.h"
/* *******************************************************************************
TITLE : PNL0001 (C-R)
------------------------------------------------------------------------------
DESCRIPTION :
PNL0001 is a submodel of a pneumatic pipe with only compressibility
and friction effects taking into account heat exchange.
The compressibility of the gas is taken into account by using a
simple polytropic model or a more complex one taking into account
heat exchange.
The polytropic model is a simplified form of the general internal
energy model based on the first law of thermodynamics. The polytropic
approach is obtained by representing the thermal exchange phenomena
by a polytropic constant k. In that case, the temperature and
pressure are no more independent variables.
The reduction of the complexity of the model implies a lack of
accuracy. For general studies, you'd better use the heat exchange
approach.
Pipe friction is taken into account using a friction factor based on
the Reynolds number and the relative roughness.
The temperature and pressure in the volume are state variables.
------------------------------------------------------------------------------
USAGE :
Use this submodel to simulate a pneumatic pipe with compressibility
and friction effects, when the Mach number is low, ie gas velocity
< 0.3 * speed of sound .
The submodels PNGD001 or PNGD002 should be included in your circuit to
define the characteristics of the gas.
------------------------------------------------------------------------------
PARAMETER SETTINGS :
------------------------------------------------------------------------------
DATE OF CREATION / AUTHOR :
2002 FS from PNL01 SN.
------------------------------------------------------------------------------
REVISIONS :
------------------------------------------------------------------------------
LIST OF FUNCTIONS USED :
pn2getatp_() : get atmospheric pressure
pn2ri_() : get perfect gas constant
pn2vol1_() : polytropic model for chambers
pn2vol_() : heat exchange model for chambers
pn2pipefr_() : frictional coeffitient in pneumatic pipes
------------------------------------------------------------------------------
SOURCE :
This material contains trade secrets or otherwise confidential
information owned by Siemens Industry Software Inc. or its
affiliates (collectively, "Siemens"), or its licensors. Access to
and use of this information is strictly limited as set forth in the
Customer's applicable agreements with Siemens.
Unpublished work. Copyright 2023 Siemens
******************************************************************************* */
#define _SUBMODELNAME_ "PNL0001"
/* >>>>>>>>>>>>Insert Private Code Here. */
#define TABFR 0 /* real store 0, 1 & 2 are used by pn2pipefr */
#define PATM 3
#define AREA 4
#define VOL 5
#define AREAEX 6
#define SPL_FR 0
/* <<<<<<<<<<<<End of Private Code. */
/* There are 6 real parameters:
diam diameter of pipe [mm -> m]
le pipe length [m]
rr relative roughness [null]
k polytropic constant [null]
kth thermal exchange coefficient [J/m**2/K/s -> W/m**2/K]
extemp external temperature [K]
*/
/* There are 2 integer parameters:
gi gas type index
mode model
*/
void pnl0001in_(int *n, double rp[6], int ip[2], double c[7]
, int ic[1], double *t2, double *p2)
{
int loop, error;
/* >>>>>>>>>>>>Extra Initialization Function Declarations Here. */
/* <<<<<<<<<<<<End of Extra Initialization declarations. */
int gi, mode;
double diam, le, rr, k, kth, extemp;
gi = ip[0];
mode = ip[1];
diam = rp[0];
le = rp[1];
rr = rp[2];
k = rp[3];
kth = rp[4];
extemp = rp[5];
loop = 0;
error = 0;
/*
If necessary, check values of the following:
rp[0..5]
*t2
*p2
*/
/* >>>>>>>>>>>>Initialization Function Check Statements. */
pn2_valid_gas_(&gi, &error);
if (*p2 < -GPATMOS)
{
error = 2;
amefprintf(stderr, "\nInitial pressure at port 2 should be > 0 [barA].\n");
}
if (*t2 <= 0.0)
{
error = 2;
amefprintf(stderr, "\nInitial temperature at port 2 should be > 0 [K].\n");
}
if (diam <= 0.0)
{
error = 2;
amefprintf(stderr, "\nDiameter of pipe should be > 0 [mm].\n");
}
if (le <= 0.0)
{
error = 2;
amefprintf(stderr, "\nPipe length should be > 0 [m].\n");
}
if (rr < 0.0)
{
error = 2;
amefprintf(stderr, "\nRelative roughness should be >= 0.\n");
}
if (mode == 1)
{
if (k <= 0.)
{
error = 2;
amefprintf(stderr, "\nPolytropic constant should be > 0.\n");
}
}
else
{
if (kth < 0.)
{
error = 2;
amefprintf(stderr, "\nThermal exchange coefficient should be >= 0 [J/m**2/K/s].\n");
}
if (extemp <= 0.)
{
error = 2;
amefprintf(stderr, "\nExternal temperature should be > 0 [K].\n");
}
}
/* <<<<<<<<<<<<End of Initialization Check Statements. */
/* Integer parameter checking: */
if (gi < 1 || gi > 99)
{
amefprintf(stderr, "\ngas type index must be in range [1..99].\n");
error = 2;
}
if (mode < 1 || mode > 2)
{
amefprintf(stderr, "\nmodel must be in range [1..2].\n");
error = 2;
}
SUBMODEL_HANDLE_AND_RESET_ERROR(_SUBMODELNAME_, n, error)
/* Common -> SI units conversions. */
rp[0] *= 1.00000000000000e-003;
diam = rp[0];
/* >>>>>>>>>>>>Initialization Function Executable Statements. */
/* get atmospheric pressure */
c[PATM] = pn2getatp_();
/* Compute the cross-sectional area of pipe. */
c[AREA] = M_PI * (diam) * (diam) / 4.0;
/* Compute volume of pipe. */
c[VOL] = c[AREA] * le;
/* Compute exchange area of pipe. */
c[AREAEX] = M_PI * diam * le;
/* <<<<<<<<<<<<End of Initialization Executable Statements. */
}
/* There are 2 ports.
Port 1 has 4 variables:
1 dh1 enthalpy flow rate at port 1 [J/s -> W] basic variable output
2 dm1 mass flow rate at port 1 [g/s -> kg/s] basic variable output
3 t1 temperature at port 1 [K] basic variable input
4 p1 pressure at port 1 [Pa] basic variable input
Port 2 has 4 variables:
1 t2 temperature at port 2 [K] explicit state (derivative `dt2')
2 p2 pressure at port 2 [Pa] explicit state (derivative `dp2')
3 dh2 enthalpy flow rate at port 2 [J/s -> W] basic variable input
4 dm2 mass flow rate at port 2 [g/s -> kg/s] basic variable input
*/
/* There are 5 internal variables.
1 mgas mass of gas in pipe [g -> kg] basic variable
2 re Reynolds number [null] basic variable
3 cm mass flow parameter (cm) [(kg*K/J)**(1/2)] basic variable
4 v mean gas velocity [m/s] basic variable
5 ff friction factor [null] basic variable
*/
void pnl0001_(int *n, double *dh1, double *dm1, double *t1, double *p1
, double *t2, double *dt2, double *p2, double *dp2, double *dh2
, double *dm2, double *mgas, double *re, double *cm, double *v
, double *ff, double rp[6], int ip[2], double c[7], int ic[1])
{
int loop;
/* >>>>>>>>>>>>Extra Calculation Function Declarations Here. */
static double zero = 0.0;
double sdh;
double dh2i, dm2i;
double dq;
double pa1, pa2, dmgas;
double r;
int dummyreg;
/* <<<<<<<<<<<<End of Extra Calculation declarations. */
int gi, mode;
double diam, le, rr, k, kth, extemp;
gi = ip[0];
mode = ip[1];
diam = rp[0];
le = rp[1];
rr = rp[2];
k = rp[3];
kth = rp[4];
extemp = rp[5];
loop = 0;
/* Common -> SI units conversions. */
*dm2 *= 1.00000000000000e-003;
/*
Set all submodel outputs below:
*dh1 = ??;
*dm1 = ??;
*dt2 = ??;
*dp2 = ??;
*mgas = ??;
*re = ??;
*cm = ??;
*v = ??;
*ff = ??;
*/
/* >>>>>>>>>>>>Calculation Function Executable Statements. */
/* set absolute pressures */
pa1 = *p1 + c[PATM];
pa2 = *p2 + c[PATM];
/* Compute flows through the pipe */
pn2pipefr_(&pa1, t1, &pa2, t2, &diam, &rr, &le, &c[AREA], re, v,ff,
dh1, dm1, &dh2i, &dm2i, cm, &c[TABFR], &gi, &ic[SPL_FR], &dummyreg);
/* Compute mass variation */
dmgas = (*dm2) + dm2i;
/* sum of enthalpy flows */
sdh = (*dh2) + dh2i;
/*** temperature & pressure variation ***/
if (mode == 1) /* Polytropic model. */
{
r = pn2ri_(&gi);
/* Compute initial mass of gas inside the pipe */
*mgas = (pa2) * c[VOL] / ((*t2) * r);
pn2vol1_(dt2, dp2, t2, &pa2,
&dmgas, mgas, &zero, &c[VOL], &k, &gi);
}
else /* Heat exchange. */
{
dq = kth * c[AREAEX] * (extemp - *t2);
pn2vol_(dt2, dp2, mgas, t2, &pa2,
&dmgas, &sdh, &c[VOL], &zero, &dq, &gi);
}
/* <<<<<<<<<<<<End of Calculation Executable Statements. */
/* SI -> Common units conversions. */
*dm1 /= 1.00000000000000e-003;
*dm2 /= 1.00000000000000e-003;
*mgas /= 1.00000000000000e-003;
}
+257
View File
@@ -0,0 +1,257 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE SPE>
<SPE DOC_VERSION="2" AME_VERSION="16.0.0 - 68387-65635 2017">
<SUBMODEL>
<SUB_TYPE>0</SUB_TYPE>
<SUB_ID_MAX>22</SUB_ID_MAX>
<DEFAULT_ICON>p2port</DEFAULT_ICON>
<SUB_LABEL>Compressibility + friction submodel of pneumatic pipe (C-R)</SUB_LABEL>
<SUB_UNIT>0</SUB_UNIT>
<R_STORES_NUMBER>7</R_STORES_NUMBER>
<I_STORES_NUMBER>1</I_STORES_NUMBER>
<OUTPUT_TYPE>1</OUTPUT_TYPE>
<RPARAMS_LIST>
<RPARAM>
<SUB_ID>14</SUB_ID>
<TITLE>diameter of pipe</TITLE>
<VARNAME>diam</VARNAME>
<VISIBILITY>True</VISIBILITY>
<DEF_VALUE>1.00000000000000e+01</DEF_VALUE>
<VALUE>1.00000000000000e+01</VALUE>
<MIN_VALUE>1.00000000000000e-003</MIN_VALUE>
<MAX_VALUE>1.00000000000000e+007</MAX_VALUE>
<UNITS>mm</UNITS>
</RPARAM>
<RPARAM>
<SUB_ID>15</SUB_ID>
<TITLE>pipe length</TITLE>
<VARNAME>le</VARNAME>
<VISIBILITY>True</VISIBILITY>
<DEF_VALUE>1.00000000000000e+00</DEF_VALUE>
<VALUE>1.00000000000000e+00</VALUE>
<MIN_VALUE>1.00000000000000e-006</MIN_VALUE>
<MAX_VALUE>1.00000000000000e+004</MAX_VALUE>
<UNITS>m</UNITS>
</RPARAM>
<RPARAM>
<SUB_ID>16</SUB_ID>
<TITLE>relative roughness</TITLE>
<VARNAME>rr</VARNAME>
<VISIBILITY>True</VISIBILITY>
<DEF_VALUE>1.00000000000000e-05</DEF_VALUE>
<VALUE>1.00000000000000e-05</VALUE>
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
<MAX_VALUE>1.00000000000000e-001</MAX_VALUE>
<UNITS>null</UNITS>
</RPARAM>
<RPARAM>
<SUB_ID>17</SUB_ID>
<TITLE>polytropic constant</TITLE>
<VARNAME>k</VARNAME>
<VISIBILITY>(mode == 1)</VISIBILITY>
<DEF_VALUE>1.35000000000000e+00</DEF_VALUE>
<VALUE>1.35000000000000e+00</VALUE>
<MIN_VALUE>5.00000000000000e-001</MIN_VALUE>
<MAX_VALUE>2.00000000000000e+000</MAX_VALUE>
<UNITS>null</UNITS>
</RPARAM>
<RPARAM>
<SUB_ID>18</SUB_ID>
<TITLE>thermal exchange coefficient</TITLE>
<VARNAME>kth</VARNAME>
<VISIBILITY>(mode == 2)</VISIBILITY>
<DEF_VALUE>0.00000000000000e+00</DEF_VALUE>
<VALUE>0.00000000000000e+00</VALUE>
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
<MAX_VALUE>1.00000000000000e+006</MAX_VALUE>
<UNITS>J/m**2/K/s</UNITS>
</RPARAM>
<RPARAM>
<SUB_ID>19</SUB_ID>
<TITLE>external temperature</TITLE>
<VARNAME>extemp</VARNAME>
<VISIBILITY>(mode == 2)</VISIBILITY>
<DEF_VALUE>2.93150000000000e+02</DEF_VALUE>
<VALUE>2.93150000000000e+02</VALUE>
<MIN_VALUE>1.00000000000000e+000</MIN_VALUE>
<MAX_VALUE>1.00000000000000e+003</MAX_VALUE>
<UNITS>K</UNITS>
</RPARAM>
</RPARAMS_LIST>
<IPARAMS_LIST>
<IPARAM>
<SUB_ID>20</SUB_ID>
<TITLE>gas type index</TITLE>
<VARNAME>gi</VARNAME>
<VISIBILITY>True</VISIBILITY>
<DEF_VALUE>1</DEF_VALUE>
<VALUE>1</VALUE>
<MIN_VALUE>1</MIN_VALUE>
<MAX_VALUE>99</MAX_VALUE>
</IPARAM>
<IPARAM>
<SUB_ID>21</SUB_ID>
<TITLE>model</TITLE>
<VARNAME>mode</VARNAME>
<VISIBILITY>True</VISIBILITY>
<DEF_VALUE>2</DEF_VALUE>
<VALUE>2</VALUE>
<MIN_VALUE>1</MIN_VALUE>
<MAX_VALUE>2</MAX_VALUE>
<ENUM_LIST>
<ENUM>
<ENUM_STRING>polytropic</ENUM_STRING>
</ENUM>
<ENUM>
<ENUM_STRING>with thermal exchange</ENUM_STRING>
</ENUM>
</ENUM_LIST>
</IPARAM>
</IPARAMS_LIST>
<IVARS_LIST>
<IVAR>
<SUB_ID>22</SUB_ID>
<TITLE>mass of gas in pipe</TITLE>
<VARNAME>mgas</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<UNITS>g</UNITS>
</IVAR>
<IVAR>
<SUB_ID>10</SUB_ID>
<TITLE>Reynolds number</TITLE>
<VARNAME>re</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<UNITS>null</UNITS>
</IVAR>
<IVAR>
<SUB_ID>11</SUB_ID>
<TITLE>mass flow parameter (cm)</TITLE>
<VARNAME>cm</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<UNITS>(kg*K/J)**(1/2)</UNITS>
</IVAR>
<IVAR>
<SUB_ID>12</SUB_ID>
<TITLE>mean gas velocity</TITLE>
<VARNAME>v</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<UNITS>m/s</UNITS>
</IVAR>
<IVAR>
<SUB_ID>13</SUB_ID>
<TITLE>friction factor</TITLE>
<VARNAME>ff</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<UNITS>null</UNITS>
</IVAR>
</IVARS_LIST>
<EVARS_LIST>
<PORT>
<EVAR>
<SUB_ID>1</SUB_ID>
<TITLE>enthalpy flow rate at port 1</TITLE>
<VARNAME>dh1</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>2</IO>
<UNITS>J/s</UNITS>
</EVAR>
<EVAR>
<SUB_ID>2</SUB_ID>
<TITLE>mass flow rate at port 1</TITLE>
<VARNAME>dm1</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>2</IO>
<UNITS>g/s</UNITS>
</EVAR>
<EVAR>
<SUB_ID>3</SUB_ID>
<TITLE>temperature at port 1</TITLE>
<VARNAME>t1</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>1</IO>
<UNITS>K</UNITS>
</EVAR>
<EVAR>
<SUB_ID>4</SUB_ID>
<TITLE>pressure at port 1</TITLE>
<VARNAME>p1</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>1</IO>
<UNITS>Pa</UNITS>
</EVAR>
</PORT>
<PORT>
<EVAR>
<SUB_ID>5</SUB_ID>
<TITLE>temperature at port 2</TITLE>
<VARNAME>t2</VARNAME>
<VARNAME2>dt2</VARNAME2>
<VISIBILITY>True</VISIBILITY>
<TYPE>1</TYPE>
<DIMENSION>1</DIMENSION>
<IO>2</IO>
<UNITS>K</UNITS>
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
<MAX_VALUE>1.00000000000000e+004</MAX_VALUE>
<DEF_VALUE>2.93150000000000e+002</DEF_VALUE>
<VALUE>2.93150000000000e+002</VALUE>
</EVAR>
<EVAR>
<SUB_ID>6</SUB_ID>
<TITLE>pressure at port 2</TITLE>
<VARNAME>p2</VARNAME>
<VARNAME2>dp2</VARNAME2>
<VISIBILITY>True</VISIBILITY>
<TYPE>1</TYPE>
<DIMENSION>1</DIMENSION>
<IO>2</IO>
<UNITS>Pa</UNITS>
<MIN_VALUE>-1.01300000000000e+005</MIN_VALUE>
<MAX_VALUE>1.00000000000000e+012</MAX_VALUE>
<DEF_VALUE>0.00000000000000e+000</DEF_VALUE>
<VALUE>0.00000000000000e+000</VALUE>
</EVAR>
<EVAR>
<SUB_ID>7</SUB_ID>
<TITLE>enthalpy flow rate at port 2</TITLE>
<VARNAME>dh2</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>1</IO>
<UNITS>J/s</UNITS>
</EVAR>
<EVAR>
<SUB_ID>8</SUB_ID>
<TITLE>mass flow rate at port 2</TITLE>
<VARNAME>dm2</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>1</IO>
<UNITS>g/s</UNITS>
</EVAR>
</PORT>
</EVARS_LIST>
<SUBIDS_RESET>0</SUBIDS_RESET>
</SUBMODEL>
</SPE>
+368
View File
@@ -0,0 +1,368 @@
/* Submodel PNL0002 skeleton created by AME Submodel editing utility
mer. juin 20 14:35:13 2018 */
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include "ameutils.h"
/* *******************************************************************************
TITLE : PNL0002 (R-C-R)
------------------------------------------------------------------------------
DESCRIPTION :
PNL0002 is a submodel of a pneumatic pipe with only compressibility
and friction effects taking into account heat exchange.
The compressibility of the gas is taken into account by using a
simple polytropic model or a more complex one taking into account
heat exchange.
The polytropic model is a simplified form of the general internal
energy model based on the first law of thermodynamics. The polytropic
approach is obtained by representing the thermal exchange phenomena
by a polytropic constant k. In that case, the temperature and
pressure are no more independent variables.
The reduction of the complexity of the model implies a lack of
accuracy. For general studies, you'd better use the heat exchange
approach.
Pipe friction is taken into account using a friction factor based on
the Reynolds number and the relative roughness.
The temperature and pressure in the middle volume are state variables.
------------------------------------------------------------------------------
USAGE :
Use this submodel to simulate a pneumatic pipe with compressibility
and friction effects, when the Mach number is low, ie gas velocity
< 0.3 * speed of sound .
PNL0002 is basically similar to PNL0001 and PNL0003 differing only in the
input and output requirements.
The submodels PNGD01 or PNGD02 should be included in your circuit to
define the characteristics of the gas.
------------------------------------------------------------------------------
PARAMETER SETTINGS :
------------------------------------------------------------------------------
DATE OF CREATION / AUTHOR :
2002 FS from PNL02 SN.
------------------------------------------------------------------------------
REVISIONS :
------------------------------------------------------------------------------
LIST OF FUNCTIONS USED :
pn2getatp_() : get atmospheric pressure
pn2ri_() : get perfect gas constant
pn2vol1_() : polytropic model for chambers
pn2vol_() : heat exchange model for chambers
pn2pipefr_() : frictional coeffitient in pneumatic pipes
------------------------------------------------------------------------------
SOURCE :
This material contains trade secrets or otherwise confidential
information owned by Siemens Industry Software Inc. or its
affiliates (collectively, "Siemens"), or its licensors. Access to
and use of this information is strictly limited as set forth in the
Customer's applicable agreements with Siemens.
Unpublished work. Copyright 2023 Siemens
******************************************************************************* */
#define _SUBMODELNAME_ "PNL0002"
/* >>>>>>>>>>>>Insert Private Code Here. */
#define TABFR 0 /* real store 0, 1 & 2 are used by pn2pipefr */
#define PATM 3
#define AREA 4
#define VOL 5
#define HALFLE 6
#define AREAEX 7
#define SPL_FR 0
/* <<<<<<<<<<<<End of Private Code. */
/* There are 6 real parameters:
diam diameter of pipe [mm -> m]
le pipe length [m]
rr relative roughness [null]
k polytropic constant [null]
kth thermal exchange coefficient [J/m**2/K/s -> W/m**2/K]
extemp external temperature [K]
*/
/* There are 2 integer parameters:
gi gas type index
mode model
*/
void pnl0002in_(int *n, double rp[6], int ip[2], double c[8]
, int ic[1], double *tctr, double *pctr)
{
int loop, error;
/* >>>>>>>>>>>>Extra Initialization Function Declarations Here. */
/* <<<<<<<<<<<<End of Extra Initialization declarations. */
int gi, mode;
double diam, le, rr, k, kth, extemp;
gi = ip[0];
mode = ip[1];
diam = rp[0];
le = rp[1];
rr = rp[2];
k = rp[3];
kth = rp[4];
extemp = rp[5];
loop = 0;
error = 0;
/*
If necessary, check values of the following:
rp[0..5]
*tctr
*pctr
*/
/* >>>>>>>>>>>>Initialization Function Check Statements. */
pn2_valid_gas_(&gi, &error);
if (*pctr < -GPATMOS)
{
error = 2;
amefprintf(stderr, "\nInitial pressure at center of pipe should be > 0 [barA].\n");
}
if (*tctr <= 0.0)
{
error = 2;
amefprintf(stderr, "\nInitial temperature at center of pipe should be > 0 [K].\n");
}
if (diam <= 0.0)
{
error = 2;
amefprintf(stderr, "\nDiameter of pipe should be > 0 [mm].\n");
}
if (le <= 0.0)
{
error = 2;
amefprintf(stderr, "\nPipe length should be > 0 [m].\n");
}
if (rr < 0.0)
{
error = 2;
amefprintf(stderr, "\nRelative roughness should be >= 0.\n");
}
if (mode == 1)
{
if (k <= 0.)
{
error = 2;
amefprintf(stderr, "\nPolytropic constant should be > 0.\n");
}
}
else
{
if (kth < 0.)
{
error = 2;
amefprintf(stderr, "\nThermal exchange coefficient should be >= 0 [J/m**2/K/s].\n");
}
if (extemp <= 0.)
{
error = 2;
amefprintf(stderr, "\nExternal temperature should be > 0 [K].\n");
}
}
/* <<<<<<<<<<<<End of Initialization Check Statements. */
/* Integer parameter checking: */
if (gi < 1 || gi > 99)
{
amefprintf(stderr, "\ngas type index must be in range [1..99].\n");
error = 2;
}
if (mode < 1 || mode > 2)
{
amefprintf(stderr, "\nmodel must be in range [1..2].\n");
error = 2;
}
SUBMODEL_HANDLE_AND_RESET_ERROR(_SUBMODELNAME_, n, error)
/* Common -> SI units conversions. */
rp[0] *= 1.00000000000000e-003;
diam = rp[0];
/* >>>>>>>>>>>>Initialization Function Executable Statements. */
/* get atmospheric pressure */
c[PATM] = pn2getatp_();
/* Compute the cross-sectional area of pipe. */
c[AREA] = M_PI * (diam) * (diam) / 4.0;
/* Compute volume of pipe. */
c[VOL] = c[AREA] * le;
/* Divide the restriction in 2 identical restrictions */
c[HALFLE] = 0.5 * le;
/* Compute exchange area of pipe. */
c[AREAEX] = M_PI * diam * le;
/* <<<<<<<<<<<<End of Initialization Executable Statements. */
}
/* There are 2 ports.
Port 1 has 4 variables:
1 dh1 enthalpy flow rate at port 1 [J/s -> W] basic variable output
2 dm1 mass flow rate at port 1 [g/s -> kg/s] basic variable output
3 t1 temperature at port 1 [K] basic variable input
4 p1 pressure at port 1 [Pa] basic variable input
Port 2 has 4 variables:
1 dh2 enthalpy flow rate at port 2 [J/s -> W] basic variable output
2 dm2 mass flow rate at port 2 [g/s -> kg/s] basic variable output
3 t2 temperature at port 2 [K] basic variable input
4 p2 pressure at port 2 [Pa] basic variable input
*/
/* There are 7 internal variables.
1 tctr temperature at center of pipe [K] explicit state (derivative `dtctr')
2 pctr pressure at center of pipe [Pa] explicit state (derivative `dpctr')
3 mgas mass of gas in pipe [g -> kg] basic variable
4 re mean Reynolds number [null] basic variable
5 cm mean mass flow parameter [(kg*K/J)**(1/2)] basic variable
6 v mean gas velocity [m/s] basic variable
7 ff mean friction factor [null] basic variable
*/
void pnl0002_(int *n, double *dh1, double *dm1, double *t1, double *p1
, double *dh2, double *dm2, double *t2, double *p2, double *tctr
, double *dtctr, double *pctr, double *dpctr, double *mgas
, double *re, double *cm, double *v, double *ff, double rp[6]
, int ip[2], double c[8], int ic[1])
{
int loop;
/* >>>>>>>>>>>>Extra Calculation Function Declarations Here. */
static double zero = 0.0;
double sdh;
double dh1i, dm1i;
double dh2i, dm2i;
double ff1, ff2, re1, re2, cm1, cm2;
double dq;
double pa1, pa2, pactr;
double v1, v2;
double dmgas;
double r;
int dummyreg;
/* <<<<<<<<<<<<End of Extra Calculation declarations. */
int gi, mode;
double diam, le, rr, k, kth, extemp;
gi = ip[0];
mode = ip[1];
diam = rp[0];
le = rp[1];
rr = rp[2];
k = rp[3];
kth = rp[4];
extemp = rp[5];
loop = 0;
/*
Set all submodel outputs below:
*dh1 = ??;
*dm1 = ??;
*dh2 = ??;
*dm2 = ??;
*dtctr = ??;
*dpctr = ??;
*mgas = ??;
*re = ??;
*cm = ??;
*v = ??;
*ff = ??;
*/
/* >>>>>>>>>>>>Calculation Function Executable Statements. */
/* set absolute pressure */
pa1 = *p1 + c[PATM];
pa2 = *p2 + c[PATM];
pactr = *pctr + c[PATM];
/* Compute flows through the pipe */
pn2pipefr_(&pa1, t1, &pactr, tctr, &diam, &rr, &c[HALFLE], &c[AREA], &re1, &v1, &ff1,
dh1, dm1, &dh1i, &dm1i, &cm1, &c[TABFR], &gi, &ic[SPL_FR], &dummyreg);
pn2pipefr_(&pactr, tctr, &pa2, t2, &diam, &rr, &c[HALFLE], &c[AREA], &re2, &v2, &ff2,
&dh2i, &dm2i, dh2, dm2, &cm2, &c[TABFR], &gi, &ic[SPL_FR], &dummyreg);
/* Mean variables */
*ff = 0.5 * (ff1 + ff2);
*re = 0.5 * (re1 + re2);
*cm = 0.5 * (cm1 + cm2);
*v = 0.5 * (fabs(v1) + fabs(v2));
/* Compute mass variation */
dmgas = dm1i + dm2i;
/* sum of enthalpy flows */
sdh = dh1i + dh2i;
/*** temperature & pressure variation ***/
if (mode == 1) /* Polytropic model. */
{
r = pn2ri_(&gi);
*mgas = (pactr) * c[VOL] / ((*tctr) * r);
pn2vol1_(dtctr, dpctr, tctr, &pactr,
&dmgas, mgas, &zero, &c[VOL], &k, &gi);
}
else /* Heat exchange. */
{
dq = kth * c[AREAEX] * (extemp-*tctr);
pn2vol_(dtctr, dpctr, mgas, tctr, &pactr,
&dmgas, &sdh, &c[VOL], &zero, &dq, &gi);
}
/* <<<<<<<<<<<<End of Calculation Executable Statements. */
/* SI -> Common units conversions. */
*dm1 /= 1.00000000000000e-003;
*dm2 /= 1.00000000000000e-003;
*mgas /= 1.00000000000000e-003;
}
+275
View File
@@ -0,0 +1,275 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE SPE>
<SPE DOC_VERSION="2" AME_VERSION="16.0.0 - 68387-65635 2017">
<SUBMODEL>
<SUB_TYPE>0</SUB_TYPE>
<SUB_ID_MAX>24</SUB_ID_MAX>
<DEFAULT_ICON>p2port</DEFAULT_ICON>
<SUB_LABEL>Compressibility + friction submodel of pneumatic pipe (R-C-R)</SUB_LABEL>
<SUB_UNIT>0</SUB_UNIT>
<R_STORES_NUMBER>8</R_STORES_NUMBER>
<I_STORES_NUMBER>1</I_STORES_NUMBER>
<OUTPUT_TYPE>1</OUTPUT_TYPE>
<RPARAMS_LIST>
<RPARAM>
<SUB_ID>16</SUB_ID>
<TITLE>diameter of pipe</TITLE>
<VARNAME>diam</VARNAME>
<VISIBILITY>True</VISIBILITY>
<DEF_VALUE>1.00000000000000e+01</DEF_VALUE>
<VALUE>1.00000000000000e+01</VALUE>
<MIN_VALUE>1.00000000000000e-003</MIN_VALUE>
<MAX_VALUE>1.00000000000000e+007</MAX_VALUE>
<UNITS>mm</UNITS>
</RPARAM>
<RPARAM>
<SUB_ID>17</SUB_ID>
<TITLE>pipe length</TITLE>
<VARNAME>le</VARNAME>
<VISIBILITY>True</VISIBILITY>
<DEF_VALUE>1.00000000000000e+00</DEF_VALUE>
<VALUE>1.00000000000000e+00</VALUE>
<MIN_VALUE>1.00000000000000e-006</MIN_VALUE>
<MAX_VALUE>1.00000000000000e+004</MAX_VALUE>
<UNITS>m</UNITS>
</RPARAM>
<RPARAM>
<SUB_ID>18</SUB_ID>
<TITLE>relative roughness</TITLE>
<VARNAME>rr</VARNAME>
<VISIBILITY>True</VISIBILITY>
<DEF_VALUE>1.00000000000000e-05</DEF_VALUE>
<VALUE>1.00000000000000e-05</VALUE>
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
<MAX_VALUE>1.00000000000000e-001</MAX_VALUE>
<UNITS>null</UNITS>
</RPARAM>
<RPARAM>
<SUB_ID>19</SUB_ID>
<TITLE>polytropic constant</TITLE>
<VARNAME>k</VARNAME>
<VISIBILITY>(mode == 1)</VISIBILITY>
<DEF_VALUE>1.35000000000000e+00</DEF_VALUE>
<VALUE>1.35000000000000e+00</VALUE>
<MIN_VALUE>5.00000000000000e-001</MIN_VALUE>
<MAX_VALUE>2.00000000000000e+000</MAX_VALUE>
<UNITS>null</UNITS>
</RPARAM>
<RPARAM>
<SUB_ID>20</SUB_ID>
<TITLE>thermal exchange coefficient</TITLE>
<VARNAME>kth</VARNAME>
<VISIBILITY>(mode == 2)</VISIBILITY>
<DEF_VALUE>0.00000000000000e+00</DEF_VALUE>
<VALUE>0.00000000000000e+00</VALUE>
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
<MAX_VALUE>1.00000000000000e+006</MAX_VALUE>
<UNITS>J/m**2/K/s</UNITS>
</RPARAM>
<RPARAM>
<SUB_ID>21</SUB_ID>
<TITLE>external temperature</TITLE>
<VARNAME>extemp</VARNAME>
<VISIBILITY>(mode == 2)</VISIBILITY>
<DEF_VALUE>2.93150000000000e+02</DEF_VALUE>
<VALUE>2.93150000000000e+02</VALUE>
<MIN_VALUE>1.00000000000000e+000</MIN_VALUE>
<MAX_VALUE>1.00000000000000e+003</MAX_VALUE>
<UNITS>K</UNITS>
</RPARAM>
</RPARAMS_LIST>
<IPARAMS_LIST>
<IPARAM>
<SUB_ID>22</SUB_ID>
<TITLE>gas type index</TITLE>
<VARNAME>gi</VARNAME>
<VISIBILITY>True</VISIBILITY>
<DEF_VALUE>1</DEF_VALUE>
<VALUE>1</VALUE>
<MIN_VALUE>1</MIN_VALUE>
<MAX_VALUE>99</MAX_VALUE>
</IPARAM>
<IPARAM>
<SUB_ID>23</SUB_ID>
<TITLE>model</TITLE>
<VARNAME>mode</VARNAME>
<VISIBILITY>True</VISIBILITY>
<DEF_VALUE>2</DEF_VALUE>
<VALUE>2</VALUE>
<MIN_VALUE>1</MIN_VALUE>
<MAX_VALUE>2</MAX_VALUE>
<ENUM_LIST>
<ENUM>
<ENUM_STRING>polytropic</ENUM_STRING>
</ENUM>
<ENUM>
<ENUM_STRING>with thermal exchange</ENUM_STRING>
</ENUM>
</ENUM_LIST>
</IPARAM>
</IPARAMS_LIST>
<IVARS_LIST>
<IVAR>
<SUB_ID>9</SUB_ID>
<TITLE>temperature at center of pipe</TITLE>
<VARNAME>tctr</VARNAME>
<VARNAME2>dtctr</VARNAME2>
<VISIBILITY>True</VISIBILITY>
<TYPE>1</TYPE>
<DIMENSION>1</DIMENSION>
<UNITS>K</UNITS>
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
<MAX_VALUE>1.00000000000000e+006</MAX_VALUE>
<DEF_VALUE>2.931500e+02</DEF_VALUE>
<VALUE>2.931500e+02</VALUE>
</IVAR>
<IVAR>
<SUB_ID>10</SUB_ID>
<TITLE>pressure at center of pipe</TITLE>
<VARNAME>pctr</VARNAME>
<VARNAME2>dpctr</VARNAME2>
<VISIBILITY>True</VISIBILITY>
<TYPE>1</TYPE>
<DIMENSION>1</DIMENSION>
<UNITS>Pa</UNITS>
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
<MAX_VALUE>1.00000000000000e+006</MAX_VALUE>
<DEF_VALUE>1.013000e+00</DEF_VALUE>
<VALUE>1.013000e+00</VALUE>
</IVAR>
<IVAR>
<SUB_ID>24</SUB_ID>
<TITLE>mass of gas in pipe</TITLE>
<VARNAME>mgas</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<UNITS>g</UNITS>
</IVAR>
<IVAR>
<SUB_ID>12</SUB_ID>
<TITLE>mean Reynolds number</TITLE>
<VARNAME>re</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<UNITS>null</UNITS>
</IVAR>
<IVAR>
<SUB_ID>13</SUB_ID>
<TITLE>mean mass flow parameter</TITLE>
<VARNAME>cm</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<UNITS>(kg*K/J)**(1/2)</UNITS>
</IVAR>
<IVAR>
<SUB_ID>14</SUB_ID>
<TITLE>mean gas velocity</TITLE>
<VARNAME>v</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<UNITS>m/s</UNITS>
</IVAR>
<IVAR>
<SUB_ID>15</SUB_ID>
<TITLE>mean friction factor</TITLE>
<VARNAME>ff</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<UNITS>null</UNITS>
</IVAR>
</IVARS_LIST>
<EVARS_LIST>
<PORT>
<EVAR>
<SUB_ID>1</SUB_ID>
<TITLE>enthalpy flow rate at port 1</TITLE>
<VARNAME>dh1</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>2</IO>
<UNITS>J/s</UNITS>
</EVAR>
<EVAR>
<SUB_ID>2</SUB_ID>
<TITLE>mass flow rate at port 1</TITLE>
<VARNAME>dm1</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>2</IO>
<UNITS>g/s</UNITS>
</EVAR>
<EVAR>
<SUB_ID>3</SUB_ID>
<TITLE>temperature at port 1</TITLE>
<VARNAME>t1</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>1</IO>
<UNITS>K</UNITS>
</EVAR>
<EVAR>
<SUB_ID>4</SUB_ID>
<TITLE>pressure at port 1</TITLE>
<VARNAME>p1</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>1</IO>
<UNITS>Pa</UNITS>
</EVAR>
</PORT>
<PORT>
<EVAR>
<SUB_ID>5</SUB_ID>
<TITLE>enthalpy flow rate at port 2</TITLE>
<VARNAME>dh2</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>2</IO>
<UNITS>J/s</UNITS>
</EVAR>
<EVAR>
<SUB_ID>6</SUB_ID>
<TITLE>mass flow rate at port 2</TITLE>
<VARNAME>dm2</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>2</IO>
<UNITS>g/s</UNITS>
</EVAR>
<EVAR>
<SUB_ID>7</SUB_ID>
<TITLE>temperature at port 2</TITLE>
<VARNAME>t2</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>1</IO>
<UNITS>K</UNITS>
</EVAR>
<EVAR>
<SUB_ID>8</SUB_ID>
<TITLE>pressure at port 2</TITLE>
<VARNAME>p2</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>1</IO>
<UNITS>Pa</UNITS>
</EVAR>
</PORT>
</EVARS_LIST>
<SUBIDS_RESET>0</SUBIDS_RESET>
</SUBMODEL>
</SPE>
+399
View File
@@ -0,0 +1,399 @@
/* Submodel PNL0003 skeleton created by AME Submodel editing utility
mer. juin 20 14:17:28 2018 */
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include "ameutils.h"
/* *******************************************************************************
TITLE : PNL0003 (C-R-C)
------------------------------------------------------------------------------
DESCRIPTION :
PNL0003 is a submodel of a pneumatic pipe with only compressibility
and friction effects taking into account heat exchange.
The compressibility of the gas is taken into account by using a
simple polytropic model or a more complex one taking into account
heat exchange.
The polytropic model is a simplified form of the general internal
energy model based on the first law of thermodynamics. The polytropic
approach is obtained by representing the thermal exchange phenomena
by a polytropic constant k. In that case, the temperature and
pressure are no more independent variables.
The reduction of the complexity of the model implies a lack of
accuracy. For general studies, you'd better use the heat exchange
approach.
Pipe friction is taken into account using a friction factor based on
the Reynolds number and the relative roughness.
The temperature and pressure in each two volumes are state variables.
------------------------------------------------------------------------------
USAGE :
Use this submodel to simulate a pneumatic pipe with compressibility
and friction effects, when the Mach number is low, ie gas velocity
< 0.3 * speed of sound .
PNL0003 is basically similar to PNL0001 and PNL0002 differing only in the
input and output requirements.
The submodels PNGD01 or PNGD02 should be included in your circuit to
define the characteristics of the gas.
------------------------------------------------------------------------------
PARAMETER SETTINGS :
------------------------------------------------------------------------------
DATE OF CREATION / AUTHOR :
2002 FS from PNL03 SN.
------------------------------------------------------------------------------
REVISIONS :
------------------------------------------------------------------------------
LIST OF FUNCTIONS USED :
pn2getatp_() : get atmospheric pressure
pn2ri_() : get perfect gas constant
pn2vol1_() : polytropic model for chambers
pn2vol_() : heat exchange model for chambers
pn2pipefr_() : frictional coefficient in pneumatic pipes
------------------------------------------------------------------------------
SOURCE :
This material contains trade secrets or otherwise confidential
information owned by Siemens Industry Software Inc. or its
affiliates (collectively, "Siemens"), or its licensors. Access to
and use of this information is strictly limited as set forth in the
Customer's applicable agreements with Siemens.
Unpublished work. Copyright 2023 Siemens
******************************************************************************* */
#define _SUBMODELNAME_ "PNL0003"
/* >>>>>>>>>>>>Insert Private Code Here. */
#define TABFR 0 /* real store 0, 1 & 2 are used by pn2pipefr */
#define PATM 3
#define AREA 4
#define HALFVOL 5
#define HALFAREAEX 6
#define SPL_FR 0
/* <<<<<<<<<<<<End of Private Code. */
/* There are 6 real parameters:
diam diameter of pipe [mm -> m]
le pipe length [m]
rr relative roughness [null]
k polytropic constant [null]
kth thermal exchange coefficient [J/m**2/K/s -> W/m**2/K]
extemp external temperature [K]
*/
/* There are 2 integer parameters:
gi gas type index
mode model
*/
void pnl0003in_(int *n, double rp[6], int ip[2], double c[7]
, int ic[1], double *t1, double *p1, double *t2, double *p2)
{
int loop, error;
/* >>>>>>>>>>>>Extra Initialization Function Declarations Here. */
double vol, areaex;
/* <<<<<<<<<<<<End of Extra Initialization declarations. */
int gi, mode;
double diam, le, rr, k, kth, extemp;
gi = ip[0];
mode = ip[1];
diam = rp[0];
le = rp[1];
rr = rp[2];
k = rp[3];
kth = rp[4];
extemp = rp[5];
loop = 0;
error = 0;
/*
If necessary, check values of the following:
rp[0..5]
*t1
*p1
*t2
*p2
*/
/* >>>>>>>>>>>>Initialization Function Check Statements. */
pn2_valid_gas_(&gi, &error);
if (*p1 < -GPATMOS)
{
error = 2;
amefprintf(stderr, "\nInitial pressure at port 1 should be > 0 [barA].\n");
}
if (*t1 <= 0.0)
{
error = 2;
amefprintf(stderr, "\nInitial temperature at port 1 should be > 0 [K].\n");
}
if (*p2 < -GPATMOS)
{
error = 2;
amefprintf(stderr, "\nInitial pressure at port 2 should be > 0 [barA].\n");
}
if (*t2 <= 0.0)
{
error = 2;
amefprintf(stderr, "\nInitial temperature at port 2 should be > 0 [K].\n");
}
if (diam <= 0.0)
{
error = 2;
amefprintf(stderr, "\nDiameter of pipe should be > 0 [mm].\n");
}
if (le <= 0.0)
{
error = 2;
amefprintf(stderr, "\nPipe length should be > 0 [m].\n");
}
if (rr < 0.0)
{
error = 2;
amefprintf(stderr, "\nRelative roughness should be >= 0.\n");
}
if (mode == 1)
{
if (k <= 0.)
{
error = 2;
amefprintf(stderr, "\nPolytropic constant should be > 0.\n");
}
}
else
{
if (kth < 0.)
{
error = 2;
amefprintf(stderr, "\nThermal exchange coefficient should be >= 0 [J/m**2/K/s].\n");
}
if (extemp <= 0.)
{
error = 2;
amefprintf(stderr, "\nExternal temperature should be > 0 [K].\n");
}
}
/* <<<<<<<<<<<<End of Initialization Check Statements. */
/* Integer parameter checking: */
if (gi < 1 || gi > 99)
{
amefprintf(stderr, "\ngas type index must be in range [1..99].\n");
error = 2;
}
if (mode < 1 || mode > 2)
{
amefprintf(stderr, "\nmodel must be in range [1..2].\n");
error = 2;
}
SUBMODEL_HANDLE_AND_RESET_ERROR(_SUBMODELNAME_, n, error)
/* Common -> SI units conversions. */
rp[0] *= 1.00000000000000e-003;
diam = rp[0];
/* >>>>>>>>>>>>Initialization Function Executable Statements. */
/* set atmospheric pressure */
c[PATM] = pn2getatp_();
/* Compute the cross-sectional area of pipe. */
c[AREA] = M_PI * (diam) * (diam) / 4.0;
/* Compute volume of pipe. */
vol = c[AREA] * le;
/* Divide the volume in 2 identical volumes */
c[HALFVOL] = 0.5 * vol;
/* Compute exchange area of pipe. */
areaex = M_PI * diam * le;
/* Divide the exchange area of pipe in 2 identical areas */
c[HALFAREAEX] = 0.5 * areaex;
/* <<<<<<<<<<<<End of Initialization Executable Statements. */
}
/* There are 2 ports.
Port 1 has 4 variables:
1 t1 temperature at port 1 [K] explicit state (derivative `dt1')
2 p1 pressure at port 1 [Pa] explicit state (derivative `dp1')
3 dh1 enthalpy flow rate at port 1 [J/s -> W] basic variable input
4 dm1 mass flow rate at port 1 [g/s -> kg/s] basic variable input
Port 2 has 4 variables:
1 t2 temperature at port 2 [K] explicit state (derivative `dt2')
2 p2 pressure at port 2 [Pa] explicit state (derivative `dp2')
3 dh2 enthalpy flow rate at port 2 [J/s -> W] basic variable input
4 dm2 mass flow rate at port 2 [g/s -> kg/s] basic variable input
*/
/* There are 7 internal variables.
1 dhctr enthalpy flow at center of pipe [J/s -> W] basic variable
2 dmctr mass flow at center of pipe [g/s -> kg/s] basic variable
3 mgas mass of gas in pipe [g -> kg] basic variable
4 re Reynolds number [null] basic variable
5 cm mass flow parameter (cm) [(kg*K/J)**(1/2)] basic variable
6 v mean gas velocity [m/s] basic variable
7 ff friction factor [null] basic variable
*/
void pnl0003_(int *n, double *t1, double *dt1, double *p1, double *dp1
, double *dh1, double *dm1, double *t2, double *dt2, double *p2
, double *dp2, double *dh2, double *dm2, double *dhctr
, double *dmctr, double *mgas, double *re, double *cm, double *v
, double *ff, double rp[6], int ip[2], double c[7], int ic[1])
{
int loop;
/* >>>>>>>>>>>>Extra Calculation Function Declarations Here. */
static double zero = 0.0;
double dh1i, dm1i;
double dh2i, dm2i;
double sdh1, sdm1;
double sdh2, sdm2;
double m1, m2;
double dq1, dq2;
double pa1, pa2;
double dmgas;
double r;
int dummyreg;
/* <<<<<<<<<<<<End of Extra Calculation declarations. */
int gi, mode;
double diam, le, rr, k, kth, extemp;
gi = ip[0];
mode = ip[1];
diam = rp[0];
le = rp[1];
rr = rp[2];
k = rp[3];
kth = rp[4];
extemp = rp[5];
loop = 0;
/* Common -> SI units conversions. */
*dm1 *= 1.00000000000000e-003;
*dm2 *= 1.00000000000000e-003;
/*
Set all submodel outputs below:
*dt1 = ??;
*dp1 = ??;
*dt2 = ??;
*dp2 = ??;
*dhctr = ??;
*dmctr = ??;
*mgas = ??;
*re = ??;
*cm = ??;
*v = ??;
*ff = ??;
*/
/* >>>>>>>>>>>>Calculation Function Executable Statements. */
/* set absolute pressures */
pa1 = *p1 + c[PATM];
pa2 = *p2 + c[PATM];
/* Compute flow through the pipe */
pn2pipefr_(&pa1, t1, &pa2, t2, &diam, &rr, &le, &c[AREA], re, v, ff,
&dh1i, &dm1i, &dh2i, &dm2i, cm, &c[TABFR], &gi, &ic[SPL_FR], &dummyreg);
/* Enthalpy flow and mass flow at center of pipe */
*dhctr = dh1i; /* = -dh2i */
*dmctr = dm1i; /* = -dm2i */
/* Compute the sum of the flows inside each volume */
sdm1 = *dm1 + dm1i;
sdh1 = *dh1 + dh1i;
sdm2 = *dm2 + dm2i;
sdh2 = *dh2 + dh2i;
dmgas = sdm1 + sdm2;
/*** temperature & pressure variation ***/
if (mode == 1) /* Polytropic model. */
{
r = pn2ri_(&gi);
/* Current mass in each volume */
m1 = pa1 * c[HALFVOL] / (*t1 * r);
m2 = pa2 * c[HALFVOL] / (*t2 * r);
pn2vol1_(dt1, dp1, t1, &pa1,
&sdm1, &m1, &zero, &c[HALFVOL], &k,&gi);
pn2vol1_(dt2, dp2, t2, &pa2,
&sdm2, &m2, &zero, &c[HALFVOL], &k,&gi);
}
else /* Heat exchange. */
{
dq1 = kth * c[HALFAREAEX] * (extemp - *t1);
pn2vol_(dt1, dp1, &m1, t1, &pa1,
&sdm1, &sdh1, &c[HALFVOL], &zero, &dq1, &gi);
dq2 = kth * c[HALFAREAEX] * (extemp - *t2);
pn2vol_(dt2, dp2, &m2, t2, &pa2,
&sdm2, &sdh2, &c[HALFVOL], &zero, &dq2, &gi);
}
*mgas = m1 + m2;
/* <<<<<<<<<<<<End of Calculation Executable Statements. */
/* SI -> Common units conversions. */
*dm1 /= 1.00000000000000e-003;
*dm2 /= 1.00000000000000e-003;
*dmctr /= 1.00000000000000e-003;
*mgas /= 1.00000000000000e-003;
}
+285
View File
@@ -0,0 +1,285 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE SPE>
<SPE DOC_VERSION="2" AME_VERSION="16.0.0 - 68387-65635 2017">
<SUBMODEL>
<SUB_TYPE>0</SUB_TYPE>
<SUB_ID_MAX>24</SUB_ID_MAX>
<DEFAULT_ICON>p2port</DEFAULT_ICON>
<SUB_LABEL>Compressibility + friction submodel of pneumatic pipe (C-R-C)</SUB_LABEL>
<SUB_UNIT>0</SUB_UNIT>
<R_STORES_NUMBER>7</R_STORES_NUMBER>
<I_STORES_NUMBER>1</I_STORES_NUMBER>
<OUTPUT_TYPE>1</OUTPUT_TYPE>
<RPARAMS_LIST>
<RPARAM>
<SUB_ID>16</SUB_ID>
<TITLE>diameter of pipe</TITLE>
<VARNAME>diam</VARNAME>
<VISIBILITY>True</VISIBILITY>
<DEF_VALUE>1.00000000000000e+01</DEF_VALUE>
<VALUE>1.00000000000000e+01</VALUE>
<MIN_VALUE>1.00000000000000e-003</MIN_VALUE>
<MAX_VALUE>1.00000000000000e+007</MAX_VALUE>
<UNITS>mm</UNITS>
</RPARAM>
<RPARAM>
<SUB_ID>17</SUB_ID>
<TITLE>pipe length</TITLE>
<VARNAME>le</VARNAME>
<VISIBILITY>True</VISIBILITY>
<DEF_VALUE>1.00000000000000e+00</DEF_VALUE>
<VALUE>1.00000000000000e+00</VALUE>
<MIN_VALUE>1.00000000000000e-006</MIN_VALUE>
<MAX_VALUE>1.00000000000000e+004</MAX_VALUE>
<UNITS>m</UNITS>
</RPARAM>
<RPARAM>
<SUB_ID>18</SUB_ID>
<TITLE>relative roughness</TITLE>
<VARNAME>rr</VARNAME>
<VISIBILITY>True</VISIBILITY>
<DEF_VALUE>1.00000000000000e-05</DEF_VALUE>
<VALUE>1.00000000000000e-05</VALUE>
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
<MAX_VALUE>1.00000000000000e-001</MAX_VALUE>
<UNITS>null</UNITS>
</RPARAM>
<RPARAM>
<SUB_ID>19</SUB_ID>
<TITLE>polytropic constant</TITLE>
<VARNAME>k</VARNAME>
<VISIBILITY>(mode == 1)</VISIBILITY>
<DEF_VALUE>1.35000000000000e+00</DEF_VALUE>
<VALUE>1.35000000000000e+00</VALUE>
<MIN_VALUE>5.00000000000000e-001</MIN_VALUE>
<MAX_VALUE>2.00000000000000e+000</MAX_VALUE>
<UNITS>null</UNITS>
</RPARAM>
<RPARAM>
<SUB_ID>20</SUB_ID>
<TITLE>thermal exchange coefficient</TITLE>
<VARNAME>kth</VARNAME>
<VISIBILITY>(mode == 2)</VISIBILITY>
<DEF_VALUE>0.00000000000000e+00</DEF_VALUE>
<VALUE>0.00000000000000e+00</VALUE>
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
<MAX_VALUE>1.00000000000000e+006</MAX_VALUE>
<UNITS>J/m**2/K/s</UNITS>
</RPARAM>
<RPARAM>
<SUB_ID>21</SUB_ID>
<TITLE>external temperature</TITLE>
<VARNAME>extemp</VARNAME>
<VISIBILITY>(mode == 2)</VISIBILITY>
<DEF_VALUE>2.93150000000000e+02</DEF_VALUE>
<VALUE>2.93150000000000e+02</VALUE>
<MIN_VALUE>1.00000000000000e+000</MIN_VALUE>
<MAX_VALUE>1.00000000000000e+003</MAX_VALUE>
<UNITS>K</UNITS>
</RPARAM>
</RPARAMS_LIST>
<IPARAMS_LIST>
<IPARAM>
<SUB_ID>22</SUB_ID>
<TITLE>gas type index</TITLE>
<VARNAME>gi</VARNAME>
<VISIBILITY>True</VISIBILITY>
<DEF_VALUE>1</DEF_VALUE>
<VALUE>1</VALUE>
<MIN_VALUE>1</MIN_VALUE>
<MAX_VALUE>99</MAX_VALUE>
</IPARAM>
<IPARAM>
<SUB_ID>23</SUB_ID>
<TITLE>model</TITLE>
<VARNAME>mode</VARNAME>
<VISIBILITY>True</VISIBILITY>
<DEF_VALUE>2</DEF_VALUE>
<VALUE>2</VALUE>
<MIN_VALUE>1</MIN_VALUE>
<MAX_VALUE>2</MAX_VALUE>
<ENUM_LIST>
<ENUM>
<ENUM_STRING>polytropic</ENUM_STRING>
</ENUM>
<ENUM>
<ENUM_STRING>with thermal exchange</ENUM_STRING>
</ENUM>
</ENUM_LIST>
</IPARAM>
</IPARAMS_LIST>
<IVARS_LIST>
<IVAR>
<SUB_ID>9</SUB_ID>
<TITLE>enthalpy flow at center of pipe</TITLE>
<VARNAME>dhctr</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<UNITS>J/s</UNITS>
</IVAR>
<IVAR>
<SUB_ID>10</SUB_ID>
<TITLE>mass flow at center of pipe</TITLE>
<VARNAME>dmctr</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<UNITS>g/s</UNITS>
</IVAR>
<IVAR>
<SUB_ID>24</SUB_ID>
<TITLE>mass of gas in pipe</TITLE>
<VARNAME>mgas</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<UNITS>g</UNITS>
</IVAR>
<IVAR>
<SUB_ID>12</SUB_ID>
<TITLE>Reynolds number</TITLE>
<VARNAME>re</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<UNITS>null</UNITS>
</IVAR>
<IVAR>
<SUB_ID>13</SUB_ID>
<TITLE>mass flow parameter (cm)</TITLE>
<VARNAME>cm</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<UNITS>(kg*K/J)**(1/2)</UNITS>
</IVAR>
<IVAR>
<SUB_ID>14</SUB_ID>
<TITLE>mean gas velocity</TITLE>
<VARNAME>v</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<UNITS>m/s</UNITS>
</IVAR>
<IVAR>
<SUB_ID>15</SUB_ID>
<TITLE>friction factor</TITLE>
<VARNAME>ff</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<UNITS>null</UNITS>
</IVAR>
</IVARS_LIST>
<EVARS_LIST>
<PORT>
<EVAR>
<SUB_ID>1</SUB_ID>
<TITLE>temperature at port 1</TITLE>
<VARNAME>t1</VARNAME>
<VARNAME2>dt1</VARNAME2>
<VISIBILITY>True</VISIBILITY>
<TYPE>1</TYPE>
<DIMENSION>1</DIMENSION>
<IO>2</IO>
<UNITS>K</UNITS>
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
<MAX_VALUE>1.00000000000000e+006</MAX_VALUE>
<DEF_VALUE>2.93150000000000e+002</DEF_VALUE>
<VALUE>2.93150000000000e+002</VALUE>
</EVAR>
<EVAR>
<SUB_ID>2</SUB_ID>
<TITLE>pressure at port 1</TITLE>
<VARNAME>p1</VARNAME>
<VARNAME2>dp1</VARNAME2>
<VISIBILITY>True</VISIBILITY>
<TYPE>1</TYPE>
<DIMENSION>1</DIMENSION>
<IO>2</IO>
<UNITS>Pa</UNITS>
<MIN_VALUE>-1.01300000000000e+005</MIN_VALUE>
<MAX_VALUE>1.00000000000000e+012</MAX_VALUE>
<DEF_VALUE>0.00000000000000e+000</DEF_VALUE>
<VALUE>0.00000000000000e+000</VALUE>
</EVAR>
<EVAR>
<SUB_ID>3</SUB_ID>
<TITLE>enthalpy flow rate at port 1</TITLE>
<VARNAME>dh1</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>1</IO>
<UNITS>J/s</UNITS>
</EVAR>
<EVAR>
<SUB_ID>4</SUB_ID>
<TITLE>mass flow rate at port 1</TITLE>
<VARNAME>dm1</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>1</IO>
<UNITS>g/s</UNITS>
</EVAR>
</PORT>
<PORT>
<EVAR>
<SUB_ID>5</SUB_ID>
<TITLE>temperature at port 2</TITLE>
<VARNAME>t2</VARNAME>
<VARNAME2>dt2</VARNAME2>
<VISIBILITY>True</VISIBILITY>
<TYPE>1</TYPE>
<DIMENSION>1</DIMENSION>
<IO>2</IO>
<UNITS>K</UNITS>
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
<MAX_VALUE>1.00000000000000e+004</MAX_VALUE>
<DEF_VALUE>2.93150000000000e+002</DEF_VALUE>
<VALUE>2.93150000000000e+002</VALUE>
</EVAR>
<EVAR>
<SUB_ID>6</SUB_ID>
<TITLE>pressure at port 2</TITLE>
<VARNAME>p2</VARNAME>
<VARNAME2>dp2</VARNAME2>
<VISIBILITY>True</VISIBILITY>
<TYPE>1</TYPE>
<DIMENSION>1</DIMENSION>
<IO>2</IO>
<UNITS>Pa</UNITS>
<MIN_VALUE>-1.01300000000000e+005</MIN_VALUE>
<MAX_VALUE>1.00000000000000e+012</MAX_VALUE>
<DEF_VALUE>0.00000000000000e+000</DEF_VALUE>
<VALUE>0.00000000000000e+000</VALUE>
</EVAR>
<EVAR>
<SUB_ID>7</SUB_ID>
<TITLE>enthalpy flow rate at port 2</TITLE>
<VARNAME>dh2</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>1</IO>
<UNITS>J/s</UNITS>
</EVAR>
<EVAR>
<SUB_ID>8</SUB_ID>
<TITLE>mass flow rate at port 2</TITLE>
<VARNAME>dm2</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>1</IO>
<UNITS>g/s</UNITS>
</EVAR>
</PORT>
</EVARS_LIST>
<SUBIDS_RESET>0</SUBIDS_RESET>
</SUBMODEL>
</SPE>
+224
View File
@@ -0,0 +1,224 @@
/* Submodel PNL00R skeleton created by AME Submodel editing utility
ven. 5. août 14:34:41 2016 */
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include "ameutils.h"
/* *******************************************************************************
TITLE : PNL00R
------------------------------------------------------------------------------
DESCRIPTION :
PNL00R is a submodel of a pneumatic pipe with only friction effects.
Pipe friction is taken into account using a friction factor based on
the Reynolds number and the relative roughness.
------------------------------------------------------------------------------
USAGE :
Use this submodel to simulate a pneumatic pipe with friction effects,
when the Mach number is low, ie gas velocity < 0.3 * speed of sound .
The submodels PNGD001 or PNGD002 should be included in your circuit to
define the characteristics of the gas.
------------------------------------------------------------------------------
PARAMETER SETTINGS :
------------------------------------------------------------------------------
DATE OF CREATION / AUTHOR :
2002 FS from PNL0R SN
------------------------------------------------------------------------------
REVISIONS :
------------------------------------------------------------------------------
LIST OF FUNCTIONS USED :
pn2pipefr_() : frictional coefficient in pneumatic pipes
pn2getatp_() : get atmospheric pressure
------------------------------------------------------------------------------
SOURCE :
This material contains trade secrets or otherwise confidential
information owned by Siemens Industry Software Inc. or its
affiliates (collectively, "Siemens"), or its licensors. Access to
and use of this information is strictly limited as set forth in the
Customer's applicable agreements with Siemens.
Unpublished work. Copyright 2023 Siemens
******************************************************************************* */
#define _SUBMODELNAME_ "PNL00R"
/* >>>>>>>>>>>>Insert Private Code Here. */
#define TABFR 0 /* real store 0, 1 & 2 are used by pn2pipefr */
#define PATM 3
#define AREA 4
#define SPL_FR 0
/* <<<<<<<<<<<<End of Private Code. */
/* There are 3 real parameters:
diam diameter of pipe [mm -> m]
le pipe length [m]
rr relative roughness [null]
*/
/* There is 1 integer parameter:
gi gas type index
*/
void pnl00rin_(int *n, double rp[3], int ip[1], double c[5], int ic[1])
{
int loop, error;
/* >>>>>>>>>>>>Extra Initialization Function Declarations Here. */
/* <<<<<<<<<<<<End of Extra Initialization declarations. */
int gi;
double diam, le, rr;
gi = ip[0];
diam = rp[0];
le = rp[1];
rr = rp[2];
loop = 0;
error = 0;
/*
If necessary, check values of the following:
rp[0..2]
*/
/* >>>>>>>>>>>>Initialization Function Check Statements. */
pn2_valid_gas_(&gi, &error);
if (diam <= 0.0)
{
error = 2;
amefprintf(stderr, "\nDiameter of pipe should be > 0 [mm].\n");
}
if (le <= 0.0)
{
error = 2;
amefprintf(stderr, "\nPipe length should be > 0 [m].\n");
}
if (rr < 0.0)
{
error = 2;
amefprintf(stderr, "\nRelative roughness should be >= 0.\n");
}
/* <<<<<<<<<<<<End of Initialization Check Statements. */
/* Integer parameter checking: */
if (gi < 1 || gi > 99)
{
amefprintf(stderr, "\ngas type index must be in range [1..99].\n");
error = 2;
}
SUBMODEL_HANDLE_AND_RESET_ERROR(_SUBMODELNAME_, n, error)
/* Common -> SI units conversions. */
rp[0] *= 1.00000000000000e-003;
diam = rp[0];
/* >>>>>>>>>>>>Initialization Function Executable Statements. */
c[PATM] = pn2getatp_();
/* Compute the cross-sectional area of pipe. */
c[AREA] = M_PI * (diam) * (diam) / 4.0;
/* <<<<<<<<<<<<End of Initialization Executable Statements. */
}
/* There are 2 ports.
Port 1 has 4 variables:
1 dh1 duplicate of dh2 (sign reversed)
2 dm1 duplicate of dm2 (sign reversed)
3 t1 temperature at port 1 [K] basic variable input
4 p1 pressure at port 1 [Pa] basic variable input
Port 2 has 4 variables:
1 dh2 enthalpy flow rate at port 2 [J/s -> W] basic variable output
2 dm2 mass flow rate at port 2 [g/s -> kg/s] basic variable output
3 t2 temperature at port 2 [K] basic variable input
4 p2 pressure at port 2 [Pa] basic variable input
*/
/* There are 4 internal variables.
1 re Reynolds number [null] basic variable
2 cm mass flow parameter (cm) [(kg*K/J)**(1/2)] basic variable
3 v mean gas velocity [m/s] basic variable
4 ff friction factor [null] basic variable
*/
void pnl00r_(int *n, double *t1, double *p1, double *dh2, double *dm2
, double *t2, double *p2, double *re, double *cm, double *v
, double *ff, double rp[3], int ip[1], double c[5], int ic[1])
{
int loop;
/* >>>>>>>>>>>>Extra Calculation Function Declarations Here. */
double pa1, pa2;
double dh1loc, dm1loc;
int dummyreg;
/* <<<<<<<<<<<<End of Extra Calculation declarations. */
int gi;
double diam, le, rr;
gi = ip[0];
diam = rp[0];
le = rp[1];
rr = rp[2];
loop = 0;
/*
Set all submodel outputs below:
*dh2 = ??;
*dm2 = ??;
*re = ??;
*cm = ??;
*v = ??;
*ff = ??;
*/
/* >>>>>>>>>>>>Calculation Function Executable Statements. */
/* set absolute pressure */
pa1 = *p1 + c[PATM];
pa2 = *p2 + c[PATM];
/* Compute flow through the pipe */
pn2pipefr_(&pa2, t2, &pa1, t1, &diam, &rr, &le, &c[AREA], re, v, ff,
dh2, dm2, &dh1loc, &dm1loc, cm, &c[TABFR], &gi, &ic[SPL_FR], &dummyreg);
/* <<<<<<<<<<<<End of Calculation Executable Statements. */
/* SI -> Common units conversions. */
*dm2 /= 1.00000000000000e-003;
}
+185
View File
@@ -0,0 +1,185 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE SPE>
<SPE DOC_VERSION="2" AME_VERSION="14.0.0 - 42489-40361 2015">
<SUBMODEL>
<SUB_TYPE>0</SUB_TYPE>
<SUB_ID_MAX>18</SUB_ID_MAX>
<DEFAULT_ICON>p2port</DEFAULT_ICON>
<SUB_LABEL>Friction submodel of pneumatic pipe (R)</SUB_LABEL>
<SUB_UNIT>0</SUB_UNIT>
<R_STORES_NUMBER>5</R_STORES_NUMBER>
<I_STORES_NUMBER>1</I_STORES_NUMBER>
<OUTPUT_TYPE>1</OUTPUT_TYPE>
<RPARAMS_LIST>
<RPARAM>
<SUB_ID>13</SUB_ID>
<TITLE>diameter of pipe</TITLE>
<VARNAME>diam</VARNAME>
<VISIBILITY>True</VISIBILITY>
<DEF_VALUE>1.00000000000000e+01</DEF_VALUE>
<VALUE>1.00000000000000e+01</VALUE>
<MIN_VALUE>1.00000000000000e-003</MIN_VALUE>
<MAX_VALUE>1.00000000000000e+007</MAX_VALUE>
<UNITS>mm</UNITS>
</RPARAM>
<RPARAM>
<SUB_ID>14</SUB_ID>
<TITLE>pipe length</TITLE>
<VARNAME>le</VARNAME>
<VISIBILITY>True</VISIBILITY>
<DEF_VALUE>1.00000000000000e+00</DEF_VALUE>
<VALUE>1.00000000000000e+00</VALUE>
<MIN_VALUE>1.00000000000000e-006</MIN_VALUE>
<MAX_VALUE>1.00000000000000e+004</MAX_VALUE>
<UNITS>m</UNITS>
</RPARAM>
<RPARAM>
<SUB_ID>15</SUB_ID>
<TITLE>relative roughness</TITLE>
<VARNAME>rr</VARNAME>
<VISIBILITY>True</VISIBILITY>
<DEF_VALUE>1.00000000000000e-05</DEF_VALUE>
<VALUE>1.00000000000000e-05</VALUE>
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
<MAX_VALUE>1.00000000000000e-001</MAX_VALUE>
<UNITS>null</UNITS>
</RPARAM>
</RPARAMS_LIST>
<IPARAMS_LIST>
<IPARAM>
<SUB_ID>16</SUB_ID>
<TITLE>gas type index</TITLE>
<VARNAME>gi</VARNAME>
<VISIBILITY>True</VISIBILITY>
<DEF_VALUE>1</DEF_VALUE>
<VALUE>1</VALUE>
<MIN_VALUE>1</MIN_VALUE>
<MAX_VALUE>99</MAX_VALUE>
</IPARAM>
</IPARAMS_LIST>
<IVARS_LIST>
<IVAR>
<SUB_ID>9</SUB_ID>
<TITLE>Reynolds number</TITLE>
<VARNAME>re</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<UNITS>null</UNITS>
</IVAR>
<IVAR>
<SUB_ID>10</SUB_ID>
<TITLE>mass flow parameter (cm)</TITLE>
<VARNAME>cm</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<UNITS>(kg*K/J)**(1/2)</UNITS>
</IVAR>
<IVAR>
<SUB_ID>11</SUB_ID>
<TITLE>mean gas velocity</TITLE>
<VARNAME>v</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<UNITS>m/s</UNITS>
</IVAR>
<IVAR>
<SUB_ID>12</SUB_ID>
<TITLE>friction factor</TITLE>
<VARNAME>ff</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<UNITS>null</UNITS>
</IVAR>
</IVARS_LIST>
<EVARS_LIST>
<PORT>
<EVAR>
<SUB_ID>17</SUB_ID>
<VARNAME>dh1</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>4</TYPE>
<PRIMARY_PORT>1</PRIMARY_PORT>
<PRIMARY_VAR>0</PRIMARY_VAR>
<DUP_TYPE>1</DUP_TYPE>
</EVAR>
<EVAR>
<SUB_ID>18</SUB_ID>
<VARNAME>dm1</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>4</TYPE>
<PRIMARY_PORT>1</PRIMARY_PORT>
<PRIMARY_VAR>1</PRIMARY_VAR>
<DUP_TYPE>1</DUP_TYPE>
</EVAR>
<EVAR>
<SUB_ID>3</SUB_ID>
<TITLE>temperature at port 1</TITLE>
<VARNAME>t1</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>1</IO>
<UNITS>K</UNITS>
</EVAR>
<EVAR>
<SUB_ID>4</SUB_ID>
<TITLE>pressure at port 1</TITLE>
<VARNAME>p1</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>1</IO>
<UNITS>Pa</UNITS>
</EVAR>
</PORT>
<PORT>
<EVAR>
<SUB_ID>5</SUB_ID>
<TITLE>enthalpy flow rate at port 2</TITLE>
<VARNAME>dh2</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>2</IO>
<UNITS>J/s</UNITS>
</EVAR>
<EVAR>
<SUB_ID>6</SUB_ID>
<TITLE>mass flow rate at port 2</TITLE>
<VARNAME>dm2</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>2</IO>
<UNITS>g/s</UNITS>
</EVAR>
<EVAR>
<SUB_ID>7</SUB_ID>
<TITLE>temperature at port 2</TITLE>
<VARNAME>t2</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>1</IO>
<UNITS>K</UNITS>
</EVAR>
<EVAR>
<SUB_ID>8</SUB_ID>
<TITLE>pressure at port 2</TITLE>
<VARNAME>p2</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>1</IO>
<UNITS>Pa</UNITS>
</EVAR>
</PORT>
</EVARS_LIST>
<SUBIDS_RESET>0</SUBIDS_RESET>
</SUBMODEL>
</SPE>
+240
View File
@@ -0,0 +1,240 @@
/* Submodel PNOR001 skeleton created by AME Submodel editing utility
lun. 10. juil. 17:22:57 2017 */
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include "ameutils.h"
/* *******************************************************************************
TITLE : PNOR001
------------------------------------------------------------------------------
DATE OF CREATION / AUTHOR :
2002 : Created by FS from PNOR01
------------------------------------------------------------------------------
SOURCE :
This material contains trade secrets or otherwise confidential
information owned by Siemens Industry Software Inc. or its
affiliates (collectively, "Siemens"), or its licensors. Access to
and use of this information is strictly limited as set forth in the
Customer's applicable agreements with Siemens.
Unpublished work. Copyright 2023 Siemens
******************************************************************************* */
#define _SUBMODELNAME_ "PNOR001"
/* >>>>>>>>>>>>Insert Private Code Here. */
/* real stores */
#define PATM 0
#define AREA 1
#define CQ 2
/* integer stores */
#define DISC_ORIF 0
/* <<<<<<<<<<<<End of Private Code. */
/* There are 4 real parameters:
cq flow coefficient (Cq) [null]
area orifice area [mm**2 -> m**2]
Cv flow coefficient (Cv) [null]
Kv flow coefficient (Kv) [null]
*/
/* There are 2 integer parameters:
gi gas type index
flowset flow coefficient setting
*/
void pnor001in_(int *n, double rp[4], int ip[2], double c[3]
, int ic[1])
{
int loop, error;
/* >>>>>>>>>>>>Extra Initialization Function Declarations Here. */
/* <<<<<<<<<<<<End of Extra Initialization declarations. */
int gi, flowset;
double cq, area, Cv, Kv;
gi = ip[0];
flowset = ip[1];
cq = rp[0];
area = rp[1];
Cv = rp[2];
Kv = rp[3];
loop = 0;
error = 0;
/*
If necessary, check values of the following:
rp[0..3]
*/
/* >>>>>>>>>>>>Initialization Function Check Statements. */
pn2_valid_gas_(&gi, &error);
if (flowset == 1)
{
if (area < 0.0)
{
error = 2;
amefprintf(stderr, "\nOrifice area should be positive.\n");
}
if (cq <= 0.0)
{
error = 2;
amefprintf(stderr, "\nFlow coefficient should be strictly positive.\n");
}
}
else if (flowset == 2)
{
if (Cv < 0.0)
{
error = 2;
amefprintf(stderr, "\nFlow coefficient (Cv) should be positive (value is %g).\n", Cv);
}
}
else
{
if (Kv < 0.0)
{
error = 2;
amefprintf(stderr, "\nFlow coefficient (Kv) should be positive (value is %g).\n", Kv);
}
}
/* <<<<<<<<<<<<End of Initialization Check Statements. */
/* Integer parameter checking: */
if (gi < 1 || gi > 99)
{
amefprintf(stderr, "\ngas type index must be in range [1..99].\n");
error = 2;
}
if (flowset < 1 || flowset > 3)
{
amefprintf(stderr, "\nflow coefficient setting must be in range [1..3].\n");
error = 2;
}
SUBMODEL_HANDLE_AND_RESET_ERROR(_SUBMODELNAME_, n, error)
/* Common -> SI units conversions. */
rp[1] *= 1.00000000000000e-006;
area = rp[1];
/* >>>>>>>>>>>>Initialization Function Executable Statements. */
/* get atmospheric pressure */
c[PATM] = pn2getatp_();
if (flowset == 1)
{
c[CQ] = cq;
c[AREA] = area;
}
else
{
/* calculation of equivalent area with Cv or Kv.
Default value of cq; the same value will be used in pn2rcqfix. */
c[CQ] = 0.72;
if (flowset == 2) /* Cv */
orif_areafromcv_(&Cv, &c[CQ], &c[AREA]);
else
orif_areafromkv_(&Kv, &c[CQ], &c[AREA]);
}
/* <<<<<<<<<<<<End of Initialization Executable Statements. */
}
/* There are 2 ports.
Port 1 has 4 variables:
1 dh1 enthalpy flow rate at port 1 [J/s -> W] basic variable output
2 dm1 mass flow rate at port 1 [g/s -> kg/s] basic variable output
3 temp1 temperature at port 1 [K] basic variable input
4 press1 pressure at port 1 [Pa] basic variable input
Port 2 has 4 variables:
1 dh2 duplicate of dh1 (sign reversed)
2 dm2 duplicate of dm1 (sign reversed)
3 temp2 temperature at port 2 [K] basic variable input
4 press2 pressure at port 2 [Pa] basic variable input
*/
/* There are 2 internal variables.
1 cm mass flow parameter (cm) [(kg*K/J)**(1/2)] basic variable
2 gasvel vena contracta gas velocity [m/s] basic variable
*/
void pnor001_(int *n, double *dh1, double *dm1, double *temp1
, double *press1, double *temp2, double *press2, double *cm
, double *gasvel, double rp[4], int ip[2], double c[3]
, int ic[1])
{
int loop;
/* >>>>>>>>>>>>Extra Calculation Function Declarations Here. */
double pressa1, pressa2;
/* <<<<<<<<<<<<End of Extra Calculation declarations. */
int gi, flowset;
double cq, area, Cv, Kv;
gi = ip[0];
flowset = ip[1];
cq = rp[0];
area = rp[1];
Cv = rp[2];
Kv = rp[3];
loop = 0;
/*
Set all submodel outputs below:
*dh1 = ??;
*dm1 = ??;
*cm = ??;
*gasvel = ??;
*/
/* >>>>>>>>>>>>Calculation Function Executable Statements. */
/* set absolute pressures */
pressa1 = *press1 + c[PATM];
pressa2 = *press2 + c[PATM];
/* calculation of the flows */
pn2rcqfix_( dh1, dm1, temp1, &pressa1, temp2, &pressa2, &c[AREA], &c[CQ], &gi,
cm, gasvel, &ic[DISC_ORIF]);
/* <<<<<<<<<<<<End of Calculation Executable Statements. */
/* SI -> Common units conversions. */
*dm1 /= 1.00000000000000e-003;
}
+199
View File
@@ -0,0 +1,199 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE SPE>
<SPE DOC_VERSION="2" AME_VERSION="14.0.0 - 42489-40361 2015">
<SUBMODEL>
<SUB_TYPE>0</SUB_TYPE>
<SUB_ID_MAX>18</SUB_ID_MAX>
<DEFAULT_ICON>pn_orifice</DEFAULT_ICON>
<SUB_LABEL>pneumatic orifice (constant flow coefficient)</SUB_LABEL>
<SUB_UNIT>0</SUB_UNIT>
<R_STORES_NUMBER>3</R_STORES_NUMBER>
<I_STORES_NUMBER>1</I_STORES_NUMBER>
<OUTPUT_TYPE>1</OUTPUT_TYPE>
<RPARAMS_LIST>
<RPARAM>
<SUB_ID>12</SUB_ID>
<TITLE>flow coefficient (Cq)</TITLE>
<VARNAME>cq</VARNAME>
<VISIBILITY>flowset==1</VISIBILITY>
<DEF_VALUE>7.20000000000000e-01</DEF_VALUE>
<VALUE>7.20000000000000e-01</VALUE>
<MIN_VALUE>1.00000000000000e-010</MIN_VALUE>
<MAX_VALUE>1.00000000000000e+000</MAX_VALUE>
<UNITS>null</UNITS>
</RPARAM>
<RPARAM>
<SUB_ID>11</SUB_ID>
<TITLE>orifice area</TITLE>
<VARNAME>area</VARNAME>
<VISIBILITY>flowset==1</VISIBILITY>
<DEF_VALUE>5.00000000000000e+00</DEF_VALUE>
<VALUE>5.00000000000000e+00</VALUE>
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
<MAX_VALUE>1.00000000000000e+006</MAX_VALUE>
<UNITS>mm**2</UNITS>
</RPARAM>
<RPARAM>
<SUB_ID>17</SUB_ID>
<TITLE>flow coefficient (Cv)</TITLE>
<VARNAME>Cv</VARNAME>
<VISIBILITY>flowset==2</VISIBILITY>
<DEF_VALUE>5.00000000000000e-01</DEF_VALUE>
<VALUE>5.00000000000000e-01</VALUE>
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
<MAX_VALUE>1.00000000000000e+030</MAX_VALUE>
<UNITS>null</UNITS>
</RPARAM>
<RPARAM>
<SUB_ID>18</SUB_ID>
<TITLE>flow coefficient (Kv)</TITLE>
<VARNAME>Kv</VARNAME>
<VISIBILITY>flowset==3</VISIBILITY>
<DEF_VALUE>4.00000000000000e-01</DEF_VALUE>
<VALUE>4.00000000000000e-01</VALUE>
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
<MAX_VALUE>1.00000000000000e+030</MAX_VALUE>
<UNITS>null</UNITS>
</RPARAM>
</RPARAMS_LIST>
<IPARAMS_LIST>
<IPARAM>
<SUB_ID>13</SUB_ID>
<TITLE>gas type index</TITLE>
<VARNAME>gi</VARNAME>
<VISIBILITY>True</VISIBILITY>
<DEF_VALUE>1</DEF_VALUE>
<VALUE>1</VALUE>
<MIN_VALUE>1</MIN_VALUE>
<MAX_VALUE>99</MAX_VALUE>
</IPARAM>
<IPARAM>
<SUB_ID>14</SUB_ID>
<TITLE>flow coefficient setting</TITLE>
<VARNAME>flowset</VARNAME>
<VISIBILITY>True</VISIBILITY>
<DEF_VALUE>1</DEF_VALUE>
<VALUE>1</VALUE>
<MIN_VALUE>1</MIN_VALUE>
<MAX_VALUE>3</MAX_VALUE>
<ENUM_LIST>
<ENUM>
<ENUM_STRING>Cq</ENUM_STRING>
</ENUM>
<ENUM>
<ENUM_STRING>Cv</ENUM_STRING>
</ENUM>
<ENUM>
<ENUM_STRING>Kv</ENUM_STRING>
</ENUM>
</ENUM_LIST>
</IPARAM>
</IPARAMS_LIST>
<IVARS_LIST>
<IVAR>
<SUB_ID>9</SUB_ID>
<TITLE>mass flow parameter (cm)</TITLE>
<VARNAME>cm</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<UNITS>(kg*K/J)**(1/2)</UNITS>
</IVAR>
<IVAR>
<SUB_ID>10</SUB_ID>
<TITLE>vena contracta gas velocity</TITLE>
<VARNAME>gasvel</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<UNITS>m/s</UNITS>
</IVAR>
</IVARS_LIST>
<EVARS_LIST>
<PORT>
<EVAR>
<SUB_ID>1</SUB_ID>
<TITLE>enthalpy flow rate at port 1</TITLE>
<VARNAME>dh1</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>2</IO>
<UNITS>J/s</UNITS>
</EVAR>
<EVAR>
<SUB_ID>2</SUB_ID>
<TITLE>mass flow rate at port 1</TITLE>
<VARNAME>dm1</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>2</IO>
<UNITS>g/s</UNITS>
</EVAR>
<EVAR>
<SUB_ID>3</SUB_ID>
<TITLE>temperature at port 1</TITLE>
<VARNAME>temp1</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>1</IO>
<UNITS>K</UNITS>
</EVAR>
<EVAR>
<SUB_ID>4</SUB_ID>
<TITLE>pressure at port 1</TITLE>
<VARNAME>press1</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>1</IO>
<UNITS>Pa</UNITS>
</EVAR>
</PORT>
<PORT>
<EVAR>
<SUB_ID>5</SUB_ID>
<VARNAME>dh2</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>4</TYPE>
<PRIMARY_PORT>0</PRIMARY_PORT>
<PRIMARY_VAR>0</PRIMARY_VAR>
<DUP_TYPE>1</DUP_TYPE>
</EVAR>
<EVAR>
<SUB_ID>6</SUB_ID>
<VARNAME>dm2</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>4</TYPE>
<PRIMARY_PORT>0</PRIMARY_PORT>
<PRIMARY_VAR>1</PRIMARY_VAR>
<DUP_TYPE>1</DUP_TYPE>
</EVAR>
<EVAR>
<SUB_ID>7</SUB_ID>
<TITLE>temperature at port 2</TITLE>
<VARNAME>temp2</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>1</IO>
<UNITS>K</UNITS>
</EVAR>
<EVAR>
<SUB_ID>8</SUB_ID>
<TITLE>pressure at port 2</TITLE>
<VARNAME>press2</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>1</IO>
<UNITS>Pa</UNITS>
</EVAR>
</PORT>
</EVARS_LIST>
<SUBIDS_RESET>0</SUBIDS_RESET>
</SUBMODEL>
</SPE>
+254
View File
@@ -0,0 +1,254 @@
/* Submodel PNVO001 skeleton created by AME Submodel editing utility
ven. 6. oct. 11:10:58 2017 */
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include "ameutils.h"
/* *******************************************************************************
TITLE : PNVO001
------------------------------------------------------------------------------
DATE OF CREATION / AUTHOR :
2002 : Created by FS from PNVO01
------------------------------------------------------------------------------
SOURCE :
This material contains trade secrets or otherwise confidential
information owned by Siemens Industry Software Inc. or its
affiliates (collectively, "Siemens"), or its licensors. Access to
and use of this information is strictly limited as set forth in the
Customer's applicable agreements with Siemens.
Unpublished work. Copyright 2023 Siemens
******************************************************************************* */
#define _SUBMODELNAME_ "PNVO001"
/* >>>>>>>>>>>>Insert Private Code Here. */
/* real stores */
#define PATM 0
#define AREAMAX 1
#define CQ 2
/* integer stores */
#define DISC_LIMIT 0
#define DISC_ORIF 1
/* <<<<<<<<<<<<End of Private Code. */
/* There are 4 real parameters:
cq flow coefficient (Cq) [null]
area0 orifice area at maximum opening [mm**2 -> m**2]
Cv maximum flow coefficient (Cv) [null]
Kv maximum flow coefficient (Kv) [null]
*/
/* There are 2 integer parameters:
gi gas type index
flowset flow coefficient setting
*/
void pnvo001in_(int *n, double rp[4], int ip[2], double c[3]
, int ic[2])
{
int loop, error;
/* >>>>>>>>>>>>Extra Initialization Function Declarations Here. */
/* <<<<<<<<<<<<End of Extra Initialization declarations. */
int gi, flowset;
double cq, area0, Cv, Kv;
gi = ip[0];
flowset = ip[1];
cq = rp[0];
area0 = rp[1];
Cv = rp[2];
Kv = rp[3];
loop = 0;
error = 0;
/*
If necessary, check values of the following:
rp[0..3]
*/
/* >>>>>>>>>>>>Initialization Function Check Statements. */
pn2_valid_gas_(&gi, &error);
if (flowset == 1)
{
if (area0 < 0.0)
{
error = 2;
amefprintf(stderr, "\nOrifice area at maximum opening should be positive.\n");
}
if (cq <= 0.0 )
{
error = 2;
amefprintf(stderr, "\nFlow coefficient should be strictly positive.\n");
}
}
else if (flowset == 2)
{
if (Cv < 0.0)
{
error = 2;
amefprintf(stderr, "\nMaximum flow coefficient (Cv) should be positive (value is %g).\n", Cv);
}
}
else
{
if (Kv < 0.0)
{
error = 2;
amefprintf(stderr, "\nMaximum flow coefficient (Kv) should be positive (value is %g).\n", Kv);
}
}
/* <<<<<<<<<<<<End of Initialization Check Statements. */
/* Integer parameter checking: */
if (gi < 1 || gi > 99)
{
amefprintf(stderr, "\ngas type index must be in range [1..99].\n");
error = 2;
}
if (flowset < 1 || flowset > 3)
{
amefprintf(stderr, "\nflow coefficient setting must be in range [1..3].\n");
error = 2;
}
SUBMODEL_HANDLE_AND_RESET_ERROR(_SUBMODELNAME_, n, error)
/* Common -> SI units conversions. */
rp[1] *= 1.00000000000000e-006;
area0 = rp[1];
/* >>>>>>>>>>>>Initialization Function Executable Statements. */
/* get atmospheric pressure */
c[PATM] = pn2getatp_();
if (flowset == 1)
{
c[CQ] = cq;
c[AREAMAX] = area0;
}
else
{
/* calculation of equivalent maximal area with Cv or Kv.
Default value of cq; the same value will be used in pn2rcqfix. */
c[CQ] = 0.72;
if (flowset == 2) /* Cv */
orif_areafromcv_(&Cv, &c[CQ], &c[AREAMAX]);
else
orif_areafromkv_(&Kv, &c[CQ], &c[AREAMAX]);
}
/* <<<<<<<<<<<<End of Initialization Executable Statements. */
}
/* There are 3 ports.
Port 1 has 1 variable:
1 res input signal [null] basic variable input
Port 2 has 4 variables:
1 dh2 enthalpy flow rate at port 2 [J/s -> W] basic variable output
2 dm2 mass flow rate at port 2 [g/s -> kg/s] basic variable output
3 temp2 temperature at port 2 [K] basic variable input
4 press2 pressure at port 2 [Pa] basic variable input
Port 3 has 4 variables:
1 dh3 duplicate of dh2 (sign reversed)
2 dm3 duplicate of dm2 (sign reversed)
3 temp3 temperature at port 3 [K] basic variable input
4 press3 pressure at port 3 [Pa] basic variable input
*/
/* There are 3 internal variables.
1 xv fractional opening [null] basic variable
2 cm mass flow parameter (cm) [(kg*K/J)**(1/2)] basic variable
3 gasvel vena contracta gas velocity [m/s] basic variable
*/
void pnvo001_(int *n, double *res, double *dh2, double *dm2
, double *temp2, double *press2, double *temp3, double *press3
, double *xv, double *cm, double *gasvel, double rp[4]
, int ip[2], double c[3], int ic[2])
{
int loop;
/* >>>>>>>>>>>>Extra Calculation Function Declarations Here. */
double marea; /* modulated area */
double pressa2, pressa3;
static double zero = 0.0, one = 1.0;
/* <<<<<<<<<<<<End of Extra Calculation declarations. */
int gi, flowset;
double cq, area0, Cv, Kv;
gi = ip[0];
flowset = ip[1];
cq = rp[0];
area0 = rp[1];
Cv = rp[2];
Kv = rp[3];
loop = 0;
/*
Set all submodel outputs below:
*dh2 = ??;
*dm2 = ??;
*xv = ??;
*cm = ??;
*gasvel = ??;
*/
/* >>>>>>>>>>>>Calculation Function Executable Statements. */
/* set absolute pressure */
pressa2 = *press2 + c[PATM];
pressa3 = *press3 + c[PATM];
*xv = dlimit_(res, &zero, &one, &ic[DISC_LIMIT]);
/* limitation of the modulated area */
marea = *xv * c[AREAMAX];
/*** calculation of the flows ***/
pn2rcqfix_( dh2, dm2, temp2, &pressa2, temp3, &pressa3, &marea, &c[CQ], &gi,
cm, gasvel, &ic[DISC_ORIF] );
/* <<<<<<<<<<<<End of Calculation Executable Statements. */
/* SI -> Common units conversions. */
*dm2 /= 1.00000000000000e-003;
}
+220
View File
@@ -0,0 +1,220 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE SPE>
<SPE DOC_VERSION="2" AME_VERSION="14.0.0 - 42489-40361 2015">
<SUBMODEL>
<SUB_TYPE>0</SUB_TYPE>
<SUB_ID_MAX>20</SUB_ID_MAX>
<DEFAULT_ICON>pn_morifice</DEFAULT_ICON>
<SUB_LABEL>modulated pneumatic orifice (constant flow coefficient)</SUB_LABEL>
<SUB_UNIT>0</SUB_UNIT>
<R_STORES_NUMBER>3</R_STORES_NUMBER>
<I_STORES_NUMBER>2</I_STORES_NUMBER>
<OUTPUT_TYPE>1</OUTPUT_TYPE>
<RPARAMS_LIST>
<RPARAM>
<SUB_ID>12</SUB_ID>
<TITLE>flow coefficient (Cq)</TITLE>
<VARNAME>cq</VARNAME>
<VISIBILITY>flowset==1</VISIBILITY>
<DEF_VALUE>7.20000000000000e-01</DEF_VALUE>
<VALUE>7.20000000000000e-01</VALUE>
<MIN_VALUE>1.00000000000000e-010</MIN_VALUE>
<MAX_VALUE>1.00000000000000e+000</MAX_VALUE>
<UNITS>null</UNITS>
</RPARAM>
<RPARAM>
<SUB_ID>13</SUB_ID>
<TITLE>orifice area at maximum opening</TITLE>
<VARNAME>area0</VARNAME>
<VISIBILITY>flowset==1</VISIBILITY>
<DEF_VALUE>5.00000000000000e+00</DEF_VALUE>
<VALUE>5.00000000000000e+00</VALUE>
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
<MAX_VALUE>1.00000000000000e+006</MAX_VALUE>
<UNITS>mm**2</UNITS>
</RPARAM>
<RPARAM>
<SUB_ID>18</SUB_ID>
<TITLE>maximum flow coefficient (Cv)</TITLE>
<VARNAME>Cv</VARNAME>
<VISIBILITY>flowset==2</VISIBILITY>
<DEF_VALUE>5.00000000000000e-01</DEF_VALUE>
<VALUE>5.00000000000000e-01</VALUE>
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
<MAX_VALUE>1.00000000000000e+030</MAX_VALUE>
<UNITS>null</UNITS>
</RPARAM>
<RPARAM>
<SUB_ID>19</SUB_ID>
<TITLE>maximum flow coefficient (Kv)</TITLE>
<VARNAME>Kv</VARNAME>
<VISIBILITY>flowset==3</VISIBILITY>
<DEF_VALUE>4.00000000000000e-01</DEF_VALUE>
<VALUE>4.00000000000000e-01</VALUE>
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
<MAX_VALUE>1.00000000000000e+030</MAX_VALUE>
<UNITS>null</UNITS>
</RPARAM>
</RPARAMS_LIST>
<IPARAMS_LIST>
<IPARAM>
<SUB_ID>14</SUB_ID>
<TITLE>gas type index</TITLE>
<VARNAME>gi</VARNAME>
<VISIBILITY>True</VISIBILITY>
<DEF_VALUE>1</DEF_VALUE>
<VALUE>1</VALUE>
<MIN_VALUE>1</MIN_VALUE>
<MAX_VALUE>99</MAX_VALUE>
</IPARAM>
<IPARAM>
<SUB_ID>15</SUB_ID>
<TITLE>flow coefficient setting</TITLE>
<VARNAME>flowset</VARNAME>
<VISIBILITY>True</VISIBILITY>
<DEF_VALUE>1</DEF_VALUE>
<VALUE>1</VALUE>
<MIN_VALUE>1</MIN_VALUE>
<MAX_VALUE>3</MAX_VALUE>
<ENUM_LIST>
<ENUM>
<ENUM_STRING>Cq</ENUM_STRING>
</ENUM>
<ENUM>
<ENUM_STRING>Cv</ENUM_STRING>
</ENUM>
<ENUM>
<ENUM_STRING>Kv</ENUM_STRING>
</ENUM>
</ENUM_LIST>
</IPARAM>
</IPARAMS_LIST>
<IVARS_LIST>
<IVAR>
<SUB_ID>20</SUB_ID>
<TITLE>fractional opening</TITLE>
<VARNAME>xv</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<UNITS>null</UNITS>
</IVAR>
<IVAR>
<SUB_ID>10</SUB_ID>
<TITLE>mass flow parameter (cm)</TITLE>
<VARNAME>cm</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<UNITS>(kg*K/J)**(1/2)</UNITS>
</IVAR>
<IVAR>
<SUB_ID>11</SUB_ID>
<TITLE>vena contracta gas velocity</TITLE>
<VARNAME>gasvel</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<UNITS>m/s</UNITS>
</IVAR>
</IVARS_LIST>
<EVARS_LIST>
<PORT>
<EVAR>
<SUB_ID>1</SUB_ID>
<TITLE>input signal</TITLE>
<VARNAME>res</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>1</IO>
<UNITS>null</UNITS>
</EVAR>
</PORT>
<PORT>
<EVAR>
<SUB_ID>2</SUB_ID>
<TITLE>enthalpy flow rate at port 2</TITLE>
<VARNAME>dh2</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>2</IO>
<UNITS>J/s</UNITS>
</EVAR>
<EVAR>
<SUB_ID>3</SUB_ID>
<TITLE>mass flow rate at port 2</TITLE>
<VARNAME>dm2</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>2</IO>
<UNITS>g/s</UNITS>
</EVAR>
<EVAR>
<SUB_ID>4</SUB_ID>
<TITLE>temperature at port 2</TITLE>
<VARNAME>temp2</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>1</IO>
<UNITS>K</UNITS>
</EVAR>
<EVAR>
<SUB_ID>5</SUB_ID>
<TITLE>pressure at port 2</TITLE>
<VARNAME>press2</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>1</IO>
<UNITS>Pa</UNITS>
</EVAR>
</PORT>
<PORT>
<EVAR>
<SUB_ID>6</SUB_ID>
<VARNAME>dh3</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>4</TYPE>
<PRIMARY_PORT>1</PRIMARY_PORT>
<PRIMARY_VAR>0</PRIMARY_VAR>
<DUP_TYPE>1</DUP_TYPE>
</EVAR>
<EVAR>
<SUB_ID>7</SUB_ID>
<VARNAME>dm3</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>4</TYPE>
<PRIMARY_PORT>1</PRIMARY_PORT>
<PRIMARY_VAR>1</PRIMARY_VAR>
<DUP_TYPE>1</DUP_TYPE>
</EVAR>
<EVAR>
<SUB_ID>8</SUB_ID>
<TITLE>temperature at port 3</TITLE>
<VARNAME>temp3</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>1</IO>
<UNITS>K</UNITS>
</EVAR>
<EVAR>
<SUB_ID>9</SUB_ID>
<TITLE>pressure at port 3</TITLE>
<VARNAME>press3</VARNAME>
<VISIBILITY>True</VISIBILITY>
<TYPE>0</TYPE>
<DIMENSION>1</DIMENSION>
<IO>1</IO>
<UNITS>Pa</UNITS>
</EVAR>
</PORT>
</EVARS_LIST>
<SUBIDS_RESET>0</SUBIDS_RESET>
</SUBMODEL>
</SPE>
File diff suppressed because it is too large. Load diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+140
View File
@@ -0,0 +1,140 @@
# test_mql
本目录记录 AMESim 模型 `test_mql.ame` 向 `app.simulation` 迁移时使用的源模型信息、结果对齐约定和当前进度。
## 目标
迁移目标不是复制 AMESim `.results` 中的观测值,而是建立真实的 Python 仿真链路:
`组件方程 -> SimulationNetwork/系统装配 -> closure -> snapshot/端口写回 -> RHS -> solver -> reporting/comparison`
Python 输出只有通过 AMESim baseline 对比后才能作为数值一致性依据。duplicate、反号或派生观测可以用于验证端口方向,但不能替代组件方程和网络闭合。
## 模型与源数据
- AMESim 源模型:`AmesimModels/test_mql.ame`
- 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
- 离散状态数:24
- 全局参数:`D1=20`、`D2=20`、`D3=14`、`P0=153`、`Pdq=1`、`V=15`、`cf=0.45`
`.ame` 文件是 tar 包,迁移和校验主要使用其中的:
- `test_mql_.cir`:组件、连接、参数表达式和生成代码线索。
- `test_mql_.param` / `test_mql_.data`:参数和结果配套数据。
- `test_mql_.modelinfo` / `test_mql_.sim`:状态数和仿真设置。
- `test_mql_.var` / `test_mql_.results`:AMESim `Data_Path` 目录和 baseline 时序。
当前结果解析器可读取 1002 个时间点和 1116 个保存变量,无需先转换成 CSV。
## 当前实现
当前已经完成:
- AMESim 组件、LINE 连接、直接组件接触、全局参数和变量目录解析。
- 氦气 Peng-Robinson 物性;内部使用绝对压力,AMESim `press` 按相对 `101300 Pa` 的表压输出。
- `PNCH023 / PNCH012 / PNOR001 / PNVO001` 气动组件。
- `PNL0001 / PNL0002 / PNL0003 / PNL00R` 管路动态或阻性关系。
- `PN3NODE2 / P4NODE2` 代数节点、真实邻接拓扑、canonical flow 和端口写回。
- `PNRP17 / MECMAS21 / LSTP00A / LMECHN1 / UD00 / FORC` 当前工况可确认的机械行为。
- 112 个气动状态和 20 个机械状态组成的 132 状态总闭包。
- 活塞运动学、变容气室机械反馈、气动力、外力、端止动和质量约束耦合。
- 关键 `Data_Path` 序列导出、schema validation、AMESim 插值 comparison、误差排序、端点诊断和变容气室 RHS 拆解。
主要实现位置:
- `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`
## 当前对比结果
默认 comparison 已从区间内插值的 `t=1e-5 s` 改为与 AMESim
首个保存时刻精确对齐的 `t=0.01 s`,当前比较 13 个关键信号,并
新增 `xv@pn_morifice_1`、`dm2@pn_morifice_1` 的结构化 PNVO 诊断。
事件前 `t=0.01 s` 对比:
- `press@pn_c1_8`:Python `-1286.601221 Pa`,AMESim
`-1288.253185 Pa`,绝对误差约 `1.651964 Pa`。
- `dm1@pneumatic_69`:Python `-0.002485112 g/s`,AMESim
`-0.002054255 g/s`;换算后的 canonical 流量绝对误差约
`4.30857e-7 kg/s`。
- `xv@pn_morifice_1` 和 `dm2@pn_morifice_1` 在两侧均为 0,确认
STEP0 事件前 PNVO 保持关闭。
- `vol1@pn_brp2_8` 绝对误差约 `8.76e-5`,机械位移、速度和加速度
仍保持较小误差;当前较明显的累计差异集中在 PNL0001 流量和
PNCH012 压力/能量链路。
新增 `--pnvo-event-boundary` 诊断:先积分到 `0.04 s` 的左极限,
再按 STEP0 的右连续语义在事件时刻读取开度和流量。结果为:
- `xv@pn_morifice_1`:Python/AMESim 均为 `1`。
- `dm2@pn_morifice_1`:Python `502.945005 g/s`,AMESim
`497.823823 g/s`,绝对误差约 `5.121182 g/s`,相对约 `1.03%`。
- `press@pn_c1_8` 绝对误差约 `55.4815 Pa`;
`dm1@pneumatic_69` 绝对误差约 `0.00281880 g/s`。
这说明 PNVO 开启瞬间的开度语义和主流量量级已经对齐,但事件前
累积压力/支路流量仍有偏差。常规积分直接跨越事件到 `0.05 s` 时,
当前进程会被系统终止,尚未形成可信的事件后结果;不能据此声明
完整 `0.04 -> 0.05 s` 窗口已经可运行。
## 下一步
1. 优先定位 `0 -> 0.04 s` 累积的 `pneumatic_69` 流量与
`pn_c1_8` 压力偏差,区分 PNL0001 阻力和 PNCH012 能量方程。
2. 采用显式事件分段或针对事件后的局部数值策略,解决跨越
`t=0.04 s` 后积分进程被终止的问题,再验证 `t=0.05 s` 保存点。
3. 保留 `press@pn_c1_8`、`dm1@pneumatic_69`、PNVO `xv/dm2`
和 chamber RHS breakdown 作为同一条诊断链。
4. 在完整开启窗口稳定后,再判断是否需要校准 PNVO 流量系数。
`pn_c1_8` 的直接主线是 `pneumatic_69`;`pneumatic_96`
属于另一条固定气室支路,不是该诊断对象。
## 运行与验证
运行默认短时域 comparison:
```bash
python3 -m app.simulation.examples.test_mql.run_full_state_comparison
```
运行 PNVO 事件边界诊断:
```bash
python3 -m app.simulation.examples.test_mql.run_full_state_comparison --pnvo-event-boundary
```
运行相关测试:
```bash
python3 -m unittest tests.test_run_test_mql_full_state_comparison tests.test_test_mql_pnl0001_segment
```
运行全量测试:
```bash
python3 -m unittest discover -s tests -t .
```
## 对齐约定
- 组件 alias 和输出名优先保持 AMESim 原名及 `Data_Path`。
- `PortState.m_flow > 0` 表示流入当前组件。
- closure 先定义 canonical flow,再按各组件端口方向写回 `m_flow`。
- AMESim 管路质量流量通常以 `g/s` 保存,Python 内部统一使用 `kg/s`。
- CSV 是人工检查和交换格式,不是读取 AMESim baseline 的前置条件。
- 未完成真实 Python 输出对比前,不使用“与 AMESim 完全一致”之类结论。
+94 -17
View File
@@ -2,36 +2,113 @@
ReactFlow 系统建模与 `app.simulation` 仿真后端。 ReactFlow 系统建模与 `app.simulation` 仿真后端。
## 开发环境准备
后端统一使用 Python 3.12;仓库根目录的 `.python-version` 记录本轮参考补丁版本
`3.12.3`。`requirements.txt` 保留支持范围,
`constraints/python312-direct.txt` 固定跨平台开发环境的直接依赖参考版本;
`constraints/python312-linux-x86_64.lock` 则完整固定发布与 CI 所用的 Linux x86_64
wheel、全部传递依赖及其 SHA-256。
Windows:
```powershell
py -3.12 -m venv .venv-win
.\.venv-win\Scripts\python.exe -m pip install `
-r requirements.txt `
-c constraints/python312-direct.txt
.\.venv-win\Scripts\python.exe -m pip check
```
Linux:
```bash
python3.12 -m venv .venv
./.venv/bin/python -m pip install \
-r constraints/python312-linux-x86_64.lock
./.venv/bin/python -m pip check
```
Linux 发布锁仅适用于兼容 manylinux_2_28 的 Linux x86_64 和 CPython 3.12。它启用
`--only-binary=:all:` 与 `--require-hashes`,因此不会静默改用源码包或未审计 wheel;
CI 和正式性能复测必须直接以 `-r` 安装该文件。Windows 或其他平台的开发环境继续
使用 `requirements.txt` 加 `constraints/python312-direct.txt`。若要测试
`requirements.txt` 声明的兼容范围,可显式省略约束,但这类结果不应与锁定环境的
性能数据直接比较。
升级参考版本时,应在干净的 Python 3.12 Linux x86_64 虚拟环境中解析范围文件,
仅下载兼容 wheel,逐个记录 wheel 的 SHA-256,再从空环境安装发布锁并运行
`pip check`、依赖契约测试与后端测试。不能只复制 `pip freeze`,因为它既不证明
依赖来源,也不校验安装产物。
前端使用 Vite 8,需要 Node.js `20.19+` 或 `22.12+`。首次启动前安装前端依赖。
Windows(PowerShell,使用仓库内的便携 Node.js):
```powershell
$nodeDir = Get-ChildItem .tools -Directory -Filter "node-*-win-x64" |
Where-Object { (Test-Path "$($_.FullName)\node.exe") -and (Test-Path "$($_.FullName)\npm.cmd") } |
Select-Object -First 1
& "$($nodeDir.FullName)\npm.cmd" --prefix frontend ci
```
Linux:
```bash
cd frontend
npm ci
cd ..
```
Windows 启动脚本会自动使用 `.tools/node-*-win-x64` 下兼容的便携 Node.js;Linux 启动脚本优先使用 `.tools/node-*-linux-x64` 下兼容的运行时(如果存在),否则使用 `PATH` 中的 `node` 和 `npm`。`start-all.sh` 需要 Bash 4.3 或更高版本。
## 启动项目
脚本统一存放在 `bat/` 目录。三个入口分别用于同时启动、只启动后端、只启动前端。
Windows:
```bat
bat\start-all.bat
bat\start-backend.bat
bat\start-reactflow.bat
```
Linux:
```bash
./bat/start-all.sh
./bat/start-backend.sh
./bat/start-reactflow.sh
```
后端地址为 `http://127.0.0.1:8000`,前端地址为 `http://127.0.0.1:5173`。Windows 的 `start-all.bat` 会分别打开两个命令行窗口;Linux 的 `start-all.sh` 会在同一终端管理两个进程,按 `Ctrl+C` 会同时停止它们。
## 后端接口 ## 后端接口
- `GET /api/components/catalog`:返回组件库与模型版本、分类、图标键、端口布局和参数契约,供 ReactFlow 启动时自动加载。 - `GET /api/components/catalog`:返回组件库与模型版本、分类、图标键、端口布局和参数契约,供 ReactFlow 启动时自动加载。
- `POST /api/reactflow/system-xml`:导出 System XML v2。 - `POST /api/reactflow/system-xml`:导出精简的 System XML v3。
- `POST /api/reactflow/compile-model`:将 ReactFlow 节点、参数和连线编译为仿真网络,并返回组件端口、无方向物理连接、压力-流量方程结构及未连接端口。 - `POST /api/reactflow/compile-model`:将 ReactFlow 节点、参数和连线编译为仿真网络,并返回组件端口、无方向物理连接、压力-流量方程结构及未连接端口。
- `POST /api/reactflow/simulate-testmodel`:运行现有固定拓扑 TestModel;该接口暂时不是任意拓扑求解器。 - `POST /api/reactflow/simulate-testmodel`:运行现有固定拓扑 TestModel;该接口暂时不是任意拓扑求解器。
- `POST /api/system-xml/validate`:接收原始 System XML v2,返回 XML、XSD 和模型语义三层诊断。 - `POST /api/reactflow/simulate-test-mql`:返回固定拓扑 AMESim `test_mql` 的结构与采样摘要;132 状态数值对比使用独立 comparison 入口。AMESim 子模型已有 19 个第一版公开模型,但该接口本身不是任意拖拽拓扑求解器。
- `POST /api/system-xml/parse`:校验 XML 并返回规范化的 ReactFlow 工程对象。 - `POST /api/system-xml/validate`:接收原始 System XML v3,返回 XML、XSD 和模型语义三层诊断。
- `POST /api/system-xml/parse`:校验 XML,并返回可直接编译、求解的规范化模型数据;它不还原 ReactFlow 画布布局。
- `POST /api/system-xml/compile-model`:校验并解析 XML,然后创建 `app.simulation` 组件网络。 - `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 文件。 - `POST /api/simulation-results/csv`:校验结构化结果快照并导出 UTF-8 CSV 文件。
气动端口的后端契约采用 `p` 势变量相等、`m_flow` 流变量代数和为零、`h_outflow` 按 stream 规则混合。所有组件统一规定 `m_flow > 0` 表示流入组件,物理连接的端点顺序不表示流向。 气动端口的后端契约采用 `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 环境时使用: XML 解析依赖 `lxml` 执行本地 XSD 校验,该依赖已包含在 `requirements.txt` 中。
```powershell
.\.venv-win\Scripts\python.exe -m pip install -r requirements.txt
```
## 文档 ## 文档
- [开发文档索引](docs/README.md) - [开发文档索引](docs/README.md)
- [组件模型建模规范 v1](docs/component-model-authoring-spec-v1.md) - [后端接口版本与定义规范 v1](docs/standard/backend-interface-version-spec-v1.md)
- [组件库分类、发现与读取规范 v1](docs/component-library-spec-v1.md) - [组件模型建模规范 v1](docs/standard/component-model-authoring-spec-v1.md)
- [组件库分类、发现与读取规范 v1](docs/standard/component-library-spec-v1.md)
- [组件目录 JSON Schema v1](schemas/component-catalog-v1.schema.json) - [组件目录 JSON Schema v1](schemas/component-catalog-v1.schema.json)
- [System XML v2 协议](docs/system-xml-v2.md) - [System XML v3 协议(当前规范)](docs/standard/system-xml-v3.md)
- [System XML v2 XSD](schemas/system-simulation-v2.xsd) - [System XML v3 XSD(当前 Schema)](schemas/system-simulation-v3.xsd)
- [System XML v1 协议(旧版)](docs/system-xml-v1.md)
- [System XML v1 XSD(旧版)](schemas/system-simulation-v1.xsd)
+603 -204
View File
File diff suppressed because it is too large. Load diff
+73 -18
View File
@@ -2,9 +2,9 @@
`app.simulation` 是 SystemSimulationApp 的仿真子包,用于承接模型定义、系统装配、数值求解和结果导出。 `app.simulation` 是 SystemSimulationApp 的仿真子包,用于承接模型定义、系统装配、数值求解和结果导出。
目标不是把 `.mo` 文件逐行翻译成 Python,而是建立一个可运行、可对比、可逐步逼近 `OpenModelica` 行为的 Python 仿真框架。 目标不是逐行翻译源模型,而是建立可运行、可测试、可导出,并能与 OpenModelica 或 AMESim baseline 对比的 Python 仿真框架。
当前状态不是“只有骨架”,而是“`Testmodel` 已有一版可运行的 ODE 近似实现,并具备基础结果导出与对比能力”。 当前包含两条模型线:`Testmodel` 已有可运行的 ODE 近似和 OpenModelica 对比能力;`test_mql` 已形成 132 状态气动机械总闭包,正在按 AMESim baseline 做数值校准。
## 当前目录 ## 当前目录
@@ -14,9 +14,11 @@
- `components/experimental/storage/`: 气瓶和贮箱等储能元件。 - `components/experimental/storage/`: 气瓶和贮箱等储能元件。
- `components/experimental/flow/`: 对外注册的阻性管道和孔板等流动元件。 - `components/experimental/flow/`: 对外注册的阻性管道和孔板等流动元件。
- `components/experimental/junctions/`: 三通等连接节点。 - `components/experimental/junctions/`: 三通等连接节点。
- `components/amesim/`: AMESim 气动、信号和机械组件原语。
- `systems/`: 通用仿真网络与 XML 驱动系统装配。 - `systems/`: 通用仿真网络与 XML 驱动系统装配。
- `examples/testmodel/`: 固定 TestModel、专用闭合逻辑和运行入口。 - `examples/testmodel/`: 固定 TestModel、专用闭合逻辑和基线运行入口。
- `reporting/`: CSV、SVG、运行报告和 Modelica 对比结果导出。 - `examples/test_mql/`: AMESim `test_mql` 的系统装配、校准原语、诊断和运行入口。
- `reporting/`: CSV、SVG、运行报告、Modelica 对比结果、AMESim 结果读取和诊断报告导出。
- `registry.py`: 从已启用库清单受控发现、校验和实例化组件。 - `registry.py`: 从已启用库清单受控发现、校验和实例化组件。
- `paths.py`: 项目、运行产物、基准和 Modelica 参考结果路径。 - `paths.py`: 项目、运行产物、基准和 Modelica 参考结果路径。
@@ -27,25 +29,66 @@
FastAPI 的 `GET /api/components/catalog` 会把注册表转换成前端组件目录。ReactFlow 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 / 模型类中声明 `MODEL_TYPE / MODEL_VERSION / PORTS / PARAMETERS /
RESULT_VARIABLES / DISPLAY / create()`,再把类路径加入库清单。完整规范参见 RESULT_VARIABLES / DISPLAY / create()`,再把类路径加入库清单。完整规范参见
[`组件模型建模规范 v1`](../../docs/component-model-authoring-spec-v1.md)和 [`组件模型建模规范 v1`](../../docs/standard/component-model-authoring-spec-v1.md)和
[`组件库分类、发现与读取规范 v1`](../../docs/component-library-spec-v1.md)。 [`组件库分类、发现与读取规范 v1`](../../docs/standard/component-library-spec-v1.md)。
当前关键文件: 当前关键文件:
- `core/medium.py`: 理想气体近似介质 `IdealGasMedium` - `core/medium.py`: 气体介质协议 `GasMedium` 与通用理想气体实现 `IdealGasMedium`
- `core/medium.py`: 温度相关的空气近似介质 `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 状态方程
- `performance.py`: 默认关闭、按单次仿真隔离的阶段与物性性能埋点
- `benchmark_performance.py`: System XML 主求解路径的可重复命令行基准工具
- `systems/network.py`: `SimulationNetwork`,负责组件注册、连接拓扑和状态向量拼装 - `systems/network.py`: `SimulationNetwork`,负责组件注册、连接拓扑和状态向量拼装
- `solvers/solver.py`: `integrate_ode()`,优先走 `SciPy solve_ivp`,缺依赖时回退到内置 RK4,并支持 `t_start == t_stop` 的零时长返回 - `solvers/solver.py`: `integrate_ode()`,优先走 `SciPy solve_ivp`,缺依赖时回退到内置 RK4,并支持 `t_start == t_stop` 的零时长返回
- `examples/testmodel/dynamic_pipe.py`: TestModel 专用单阻容管道近似,入口压降 + 出口直连内容腔 - `examples/testmodel/dynamic_pipe.py`: TestModel 专用单阻容管道近似,入口压降 + 出口直连内容腔
- `components/experimental/junctions/tee.py`: 三通的最小 stream 混合 helper - `components/experimental/junctions/tee.py`: 三通的最小 stream 混合 helper
- `examples/testmodel/system.py`: `Testmodel` 的系统装配壳与外部运行入口 - `examples/testmodel/system.py`: `Testmodel` 的系统装配壳与外部运行入口
- `examples/testmodel/closure.py`: `Testmodel` 当前专用的闭合、初始化投影、分支求解与端口回写 - `examples/testmodel/closure.py`: `Testmodel` 当前专用的闭合、初始化投影、分支求解与端口回写
- `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/testmodel_outputs.py`: `Testmodel` 的 CSV/SVG/对比摘要导出
- `examples/testmodel/run.py`: 基线运行与程序化执行入口 - `reporting/amesim_results.py`: AMESim 结果读取入口
- `tests/`: 当前组件契约、XML、通用系统和结果导出测试 - `examples/test_mql/run_full_state_comparison.py`: `test_mql` 短时域 AMESim comparison 和诊断入口
- `examples/test_mql/run.py`: `test_mql` 结构运行与程序化执行入口
- `tests/`: 当前组件契约、XML、通用系统、AMESim 迁移和结果导出测试
## 可选性能诊断
`SIMULATIONAPP_PROFILE` 支持 `off`(默认)、`standard` 和 `audit`。`standard`
只统计低频的大阶段;`audit` 才展开 RHS、代数闭合、stream 和物性调用,开销也
明显更高。最终优化收益必须在 `off` 下复测。
Peng–Robinson 氦气的高开销物性默认使用一次仿真内独立的精确 LRU 缓存;不同
仿真任务不会共享条目,仿真结束后自动释放。可在启动进程前设置
`SIMULATIONAPP_PROPERTY_CACHE=off` 做数值和性能 A/B,正常运行保持默认 `on`。
缓存只复用完全相同的输入,不做四舍五入或容差匹配。
FastAPI worker 默认在 lifespan 启动阶段预热 SciPy 积分、非线性求解、稀疏
Jacobian 和 System XML XSD,完成后才开始接收请求。它不会运行业务模型,也不
写入文件;如需诊断冷启动,可设置 `SIMULATIONAPP_WARMUP=off`。每个 worker 都会
独立暖机一次。
```powershell
.venv-win\Scripts\python.exe -m app.simulation.benchmark_performance `
--mode audit --warmups 1 --runs 3 `
--factory "helium_step=tests.test_amesim_pnvo001_signal_xml:high_pressure_helium_step_project" `
--output app/data/performance-evaluations/helium-step.json
```
缓存关闭对照可在同一命令中增加 `--disable-property-cache`。缓存容量、命中、
未命中和驱逐数会在 audit 响应的
`diagnostics.performance.propertyCache` 中返回。
基准原始 JSON 默认放到已忽略的 `app/data/` 下。指标字段、实测结果和使用边界见
[`仿真性能评估 2026-08-15`](../../docs/other/仿真性能评估-2026-08-15.md)。
## 当前阶段进度 ## 当前阶段进度
@@ -175,7 +218,7 @@ RESULT_VARIABLES / DISPLAY / create()`,再把类路径加入库清单。完整
`testmodel_tank_temperature.svg` `testmodel_tank_temperature.svg`
11. 基于 `ModelicaModels/Simulation/Testmodel_res.csv` 的逐时刻对比与误差摘要导出。 11. 基于 `ModelicaModels/Simulation/Testmodel_res.csv` 的逐时刻对比与误差摘要导出。
12. 基于 `unittest` 的自动回归测试,当前已覆盖初始化守恒、主变量基线、运行接口、内部闭合诊断、通用分支兼容层、通用结果键与旧键别名一致性,以及部分中间闭合过程行为。 12. 基于 `unittest` 的自动回归测试,当前已覆盖初始化守恒、主变量基线、运行接口、内部闭合诊断、通用分支兼容层、通用结果键与旧键别名一致性,以及部分中间闭合过程行为。
13. 面向 System XML v2 的拓扑驱动仿真 MVP:压力-流量非线性闭合、stream 焓传播、动态状态自动拼装和端口结果序列。 13. 面向 System XML v3 的拓扑驱动仿真 MVP:压力-流量非线性闭合、stream 焓传播、动态状态自动拼装和端口结果序列。
当前没有实现: 当前没有实现:
@@ -268,18 +311,30 @@ print(result.used_modelica_reference)
## 基线结果 ## 基线结果
当前基线对比摘要来自: 当前基线对比摘要来自:
[`testmodel_modelica_comparison_summary.txt`](../../tests/baselines/simulation/testmodel/testmodel_modelica_comparison_summary.txt) [`testmodel_modelica_comparison_summary.txt`](../../tests/data/testmodel/testmodel_modelica_comparison_summary.txt)
当前四个主变量的最大误差为: 当前四个主变量的最大误差为:
- `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%` - `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.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%` - `mycylinder.T`: `max_abs_error = 0.009069 K`, `max_rel_error = 0.003870%`
这说明在当前基线工况下,Python 版主变量已经能较好贴近 OpenModelica 结果。 这说明在当前基线工况下,Python 版主变量已经能较好贴近 OpenModelica 结果。
## 当前架构判断 ## AMESim test_mql 当前进度
`test_mql` 从 `AmesimModels/test_mql.ame` 迁移,并与旧 `testmodel` 保持独立。
固定算例实现统一位于 `examples/test_mql/`,结果读取和比较能力位于
`reporting/`;公开拖拽组件由 `components/amesim/library.py` 单独登记。
当前已形成 112 个气动状态和 20 个机械状态的总闭包、AMESim 原生结果读取、
`Data_Path` 输出校验及短时域 comparison。这里不再复制易过期的数值进度;
最新对比结果、运行命令、限制和下一步校准路径以
[`AmesimModels/test_mql/README.md`](../../AmesimModels/test_mql/README.md)
为唯一说明。当前仍不能宣称 Python 时域仿真与 AMESim 全局一致。
## Testmodel 当前架构判断
如果按“组件正确 -> 网络闭合 -> 积分可跑 -> 结果对齐 -> 去近似”来看,当前大致处于: 如果按“组件正确 -> 网络闭合 -> 积分可跑 -> 结果对齐 -> 去近似”来看,当前大致处于:
@@ -293,7 +348,7 @@ print(result.used_modelica_reference)
`Testmodel` 已有一版可运行、可导出、可对比的 Python 近似实现。 `Testmodel` 已有一版可运行、可导出、可对比的 Python 近似实现。
## 已知限制 ## Testmodel 已知限制
当前最主要的限制可以直接理解成下面几条: 当前最主要的限制可以直接理解成下面几条:
@@ -319,7 +374,7 @@ print(result.used_modelica_reference)
- 作为最终工程结论的唯一依据 - 作为最终工程结论的唯一依据
- 直接扩展到更复杂拓扑而不补通用连接器语义 - 直接扩展到更复杂拓扑而不补通用连接器语义
## 文件级现状 ## Testmodel 文件级现状
按代码现状逐项看: 按代码现状逐项看:
@@ -342,7 +397,7 @@ print(result.used_modelica_reference)
- `tests/baselines/simulation/`: 是当前稳定基线,不应该随着日常运行频繁改动。 - `tests/baselines/simulation/`: 是当前稳定基线,不应该随着日常运行频繁改动。
- `app/data/simulation-runs/`: 是默认运行产物目录,不是手写源代码,也不应该提交。 - `app/data/simulation-runs/`: 是默认运行产物目录,不是手写源代码,也不应该提交。
## 当前主技术债 ## Testmodel 当前主技术债
目前最主要的技术债,可以直接理解成下面 4 件事: 目前最主要的技术债,可以直接理解成下面 4 件事:
+244
View File
@@ -0,0 +1,244 @@
from __future__ import annotations
import argparse
import hashlib
import importlib
import json
import os
import platform
import statistics
import sys
from datetime import UTC, datetime
from math import ceil
from pathlib import Path
from time import perf_counter_ns, process_time_ns
from typing import Any
def _named_value(value: str, *, option: str) -> tuple[str, str]:
name, separator, target = value.partition("=")
if not separator or not name.strip() or not target.strip():
raise ValueError(
f"{option} must use NAME=VALUE syntax, received {value!r}."
)
return name.strip(), target.strip()
def _percentile(values: list[float], percentile: float) -> float:
ordered = sorted(values)
index = max(0, min(len(ordered) - 1, ceil(percentile * len(ordered)) - 1))
return ordered[index]
def _duration_summary(values: list[float]) -> dict[str, object]:
return {
"samplesMs": values,
"minimumMs": min(values),
"medianMs": statistics.median(values),
"p95Ms": _percentile(values, 0.95),
"maximumMs": max(values),
}
def _load_factory_xml(specification: str) -> bytes:
module_name, separator, member_name = specification.partition(":")
if not separator or not module_name or not member_name:
raise ValueError(
"Factory specifications must use module.path:callable syntax."
)
factory = getattr(importlib.import_module(module_name), member_name)
value = factory()
if isinstance(value, bytes):
return value
if isinstance(value, str):
return value.encode("utf-8")
from app.main import build_reactflow_system_xml
return build_reactflow_system_xml(value)
def _serialize_result_event(result: dict[str, object]) -> bytes:
"""Render the final NDJSON payload shape used by the streaming endpoint."""
status = str(result.get("status", "completed"))
event = {
"event": "result",
"progress": 100 if status == "completed" else 0,
"phase": status,
"message": "仿真完成" if status == "completed" else "仿真任务结束",
"simulatedTime": result.get("simulatedUntil"),
"totalTime": result.get("requestedStopTime"),
"result": result,
}
return (
json.dumps(event, ensure_ascii=False, separators=(",", ":")) + "\n"
).encode("utf-8")
def _run_case(
name: str,
xml_bytes: bytes,
*,
warmups: int,
runs: int,
cancellable_path: bool,
allow_failures: bool,
) -> dict[str, object]:
from app.main import run_system_xml_simulation
cancel_check = (lambda: False) if cancellable_path else None
for _ in range(warmups):
result = run_system_xml_simulation(xml_bytes, cancel_check=cancel_check)
if not bool(result.get("success")) and not allow_failures:
raise RuntimeError(f"Warmup for {name!r} failed: {result.get('message')}")
wall_samples_ms: list[float] = []
cpu_samples_ms: list[float] = []
serialization_samples_ms: list[float] = []
serialized_sizes: list[int] = []
profiles: list[dict[str, object]] = []
final_result: dict[str, object] | None = None
for _ in range(runs):
wall_start = perf_counter_ns()
cpu_start = process_time_ns()
result = run_system_xml_simulation(xml_bytes, cancel_check=cancel_check)
cpu_samples_ms.append((process_time_ns() - cpu_start) / 1_000_000.0)
wall_samples_ms.append((perf_counter_ns() - wall_start) / 1_000_000.0)
if not bool(result.get("success")) and not allow_failures:
raise RuntimeError(f"Benchmark for {name!r} failed: {result.get('message')}")
diagnostics = result.get("diagnostics")
if isinstance(diagnostics, dict):
performance = diagnostics.get("performance")
if isinstance(performance, dict):
profiles.append(performance)
serialization_start = perf_counter_ns()
serialized_event = _serialize_result_event(result)
serialization_samples_ms.append(
(perf_counter_ns() - serialization_start) / 1_000_000.0
)
serialized_sizes.append(len(serialized_event))
final_result = result
assert final_result is not None
return {
"name": name,
"success": bool(final_result.get("success")),
"message": final_result.get("message"),
"inputBytes": len(xml_bytes),
"inputSha256": hashlib.sha256(xml_bytes).hexdigest(),
"status": final_result.get("status"),
"simulatedUntil": final_result.get("simulatedUntil"),
"requestedStopTime": final_result.get("requestedStopTime"),
"wall": _duration_summary(wall_samples_ms),
"cpu": _duration_summary(cpu_samples_ms),
"resultSerialization": _duration_summary(serialization_samples_ms),
"resultEventBytes": serialized_sizes,
"performanceRuns": profiles,
}
def _parse_arguments(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Benchmark the real System XML simulation path with optional profiling."
)
parser.add_argument(
"--mode",
choices=("off", "standard", "audit"),
default="audit",
help="Instrumentation depth selected before importing the simulation modules.",
)
parser.add_argument("--warmups", type=int, default=1)
parser.add_argument("--runs", type=int, default=5)
parser.add_argument(
"--xml",
action="append",
default=[],
metavar="NAME=PATH",
help="Add an XML file benchmark case.",
)
parser.add_argument(
"--factory",
action="append",
default=[],
metavar="NAME=MODULE:CALLABLE",
help="Add a zero-argument factory returning XML or ReactFlowProjectPayload.",
)
parser.add_argument(
"--direct-path",
action="store_true",
help="Do not pass a cancel callback; use the one-shot SciPy path when eligible.",
)
parser.add_argument(
"--disable-property-cache",
action="store_true",
help="Disable the run-local exact property cache for an A/B comparison.",
)
parser.add_argument(
"--allow-failures",
action="store_true",
help="Record failed simulation runs instead of aborting the benchmark.",
)
parser.add_argument("--output", type=Path)
arguments = parser.parse_args(argv)
if arguments.warmups < 0:
parser.error("--warmups must not be negative.")
if arguments.runs <= 0:
parser.error("--runs must be positive.")
if not arguments.xml and not arguments.factory:
parser.error("At least one --xml or --factory case is required.")
return arguments
def main(argv: list[str] | None = None) -> int:
arguments = _parse_arguments(argv)
os.environ["SIMULATIONAPP_PROFILE"] = arguments.mode
os.environ["SIMULATIONAPP_PROPERTY_CACHE"] = (
"off" if arguments.disable_property_cache else "on"
)
cases: list[tuple[str, bytes]] = []
for raw_case in arguments.xml:
name, raw_path = _named_value(raw_case, option="--xml")
cases.append((name, Path(raw_path).read_bytes()))
for raw_case in arguments.factory:
name, specification = _named_value(raw_case, option="--factory")
cases.append((name, _load_factory_xml(specification)))
report: dict[str, Any] = {
"generatedAt": datetime.now(UTC).isoformat(),
"profileMode": arguments.mode,
"cancellableSolverPath": not arguments.direct_path,
"propertyCacheEnabled": not arguments.disable_property_cache,
"allowFailures": bool(arguments.allow_failures),
"warmups": arguments.warmups,
"runs": arguments.runs,
"runtime": {
"python": sys.version,
"platform": platform.platform(),
"processor": platform.processor(),
},
"cases": [
_run_case(
name,
xml_bytes,
warmups=arguments.warmups,
runs=arguments.runs,
cancellable_path=not arguments.direct_path,
allow_failures=arguments.allow_failures,
)
for name, xml_bytes in cases
],
}
text = json.dumps(report, ensure_ascii=False, indent=2)
if arguments.output is not None:
arguments.output.parent.mkdir(parents=True, exist_ok=True)
arguments.output.write_text(text + "\n", encoding="utf-8")
print(f"Performance report written to {arguments.output.resolve()}")
else:
print(text)
return 0
if __name__ == "__main__":
raise SystemExit(main())
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,3 @@
from __future__ import annotations
__all__: list[str] = []
@@ -0,0 +1 @@
"""AMESim pneumatic boundary components."""
@@ -0,0 +1,68 @@
from __future__ import annotations
from collections.abc import Mapping
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.medium import IdealGasMedium
from app.simulation.core.ports import PortDefinition
class AmesimPnpl01(AlgebraicComponent):
"""AMESim PNPL01 zero pneumatic flow source.
The component behaves as a sealed pneumatic boundary in the current acausal
solver: it does not prescribe pressure, and only constrains its port mass
flow to zero.
"""
MODEL_TYPE = "amesim_pnpl01"
MODEL_VERSION = "0.1.0"
PRESSURE_FLOW_DEPENDS_ON_STREAM = False
PORTS = (PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),)
PARAMETERS = ()
RESULT_VARIABLES = ()
DISPLAY = ComponentDisplaySpec(
label="PNPL01 零气动流边界",
library_id="amesim",
category_id="boundary",
symbol="amesim_pnpl01",
ports=(PortDisplaySpec("port_1", "left", order=10),),
order=10,
)
def __init__(self, name: str) -> None:
super().__init__(name=name)
self.set_parameter_values({})
self.port_1 = self.register_declared_port("port_1")
@classmethod
def create(
cls,
*,
name: str,
medium: IdealGasMedium,
parameters: Mapping[str, float],
) -> AmesimPnpl01:
return cls(name=name)
def pressure_flow_equation_values(self) -> tuple[float, ...]:
return (self.port_1.m_flow,)
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
return (
EquationResidual(
id=f"{self.name}:zero_mass_flow",
owner="component",
owner_id=self.name,
relation="constitutive",
variables=(f"{self.name}.port_1.m_flow",),
role="flow",
value=self.port_1.m_flow,
),
)
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
if "port_1" in connected_h:
self.port_1.h_outflow = connected_h["port_1"]
@@ -0,0 +1 @@
"""AMESim pneumatic flow components."""
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+264
View File
@@ -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()
@@ -0,0 +1,5 @@
from __future__ import annotations
from app.simulation.components.amesim.junctions.nodes import AmesimP4Node2, AmesimPn3Node2
__all__ = ["AmesimP4Node2", "AmesimPn3Node2"]
@@ -0,0 +1,239 @@
from __future__ import annotations
from collections.abc import Mapping
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.medium import IdealGasMedium
from app.simulation.core.ports import PortDefinition, PortState
_REFERENCE_OUTFLOW_REGULARIZATION_RATIO = 0.05
def _regularized_inverse_outflow(flow: float, transition_flow: float) -> float:
"""Return a C1 inverse that tends to zero as a negative flow vanishes."""
if flow >= 0.0:
return 0.0
transition_flow = max(float(transition_flow), 1.0e-12)
if -flow >= transition_flow:
return 1.0 / flow
return (
flow
* (2.0 * transition_flow * transition_flow - flow * flow)
/ transition_flow**4
)
class _AmesimPneumaticNode(AlgebraicComponent):
"""Shared implementation for AMESim pneumatic junction submodels.
PN3NODE2/P4NODE2 use port 2 as their pressure and temperature reference.
Non-reference outlet ports use that reference temperature. When port 2 is
an outlet, its enthalpy is the residual that closes the junction energy
balance, matching the AMESim dh2 causality.
"""
PRESSURE_FLOW_DEPENDS_ON_STREAM = False
REFERENCE_PORT = "port_2"
def __init__(self, name: str) -> None:
super().__init__(name=name)
self.set_parameter_values({})
self.temperature_reference_h = 0.0
for definition in self.PORTS:
setattr(self, definition.name, self.register_declared_port(definition.name))
def pressure_flow_equation_values(self) -> tuple[float, ...]:
reference = self.get_port(self.REFERENCE_PORT)
return tuple(
self.get_port(definition.name).p - reference.p
for definition in self.PORTS
if definition.name != self.REFERENCE_PORT
) + (
sum(
self.get_port(definition.name).m_flow
for definition in self.PORTS
),
)
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
reference = self.get_port(self.REFERENCE_PORT)
residuals: list[EquationResidual] = []
for definition in self.PORTS:
if definition.name == self.REFERENCE_PORT:
continue
port = self.get_port(definition.name)
residuals.append(
EquationResidual(
id=f"{self.name}:{definition.name}_pressure_reference",
owner="component",
owner_id=self.name,
relation="equal",
variables=(
f"{self.name}.{definition.name}.p",
f"{self.name}.{self.REFERENCE_PORT}.p",
),
role="effort",
value=port.p - reference.p,
)
)
residuals.append(
EquationResidual(
id=f"{self.name}:mass_flow_balance",
owner="component",
owner_id=self.name,
relation="sumToZero",
variables=tuple(
f"{self.name}.{definition.name}.m_flow"
for definition in self.PORTS
),
role="flow",
value=sum(self.get_port(definition.name).m_flow for definition in self.PORTS),
)
)
return tuple(residuals)
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
self.temperature_reference_h = connected_h.get(
self.REFERENCE_PORT,
sum(connected_h.values()) / len(connected_h) if connected_h else 0.0,
)
incoming = [
(port.m_flow, connected_h[name])
for name, port in self.ports.items()
if port.m_flow > 1e-12
]
total_flow = sum(m_flow for m_flow, _ in incoming)
if total_flow > 1e-12:
mixed_h = sum(m_flow * h for m_flow, h in incoming) / total_flow
else:
mixed_h = self.temperature_reference_h
reference_port = self.get_port(self.REFERENCE_PORT)
for name, port in self.ports.items():
port.h_outflow = (
mixed_h
if name == self.REFERENCE_PORT
else self.temperature_reference_h
)
if reference_port.m_flow < 0.0:
energy_without_reference = sum(
port.m_flow
* (
connected_h[name]
if port.m_flow > 1e-12
else self.temperature_reference_h
)
for name, port in self.ports.items()
if name != self.REFERENCE_PORT
)
non_reference_flow_scale = sum(
abs(port.m_flow)
for name, port in self.ports.items()
if name != self.REFERENCE_PORT
)
transition_flow = (
_REFERENCE_OUTFLOW_REGULARIZATION_RATIO
* non_reference_flow_scale
)
# Port 2 carries AMESim's residual-energy causality. Exact
# division is singular when its outflow reverses through zero, so
# use a C1 band that matches the exact balance at its boundary and
# tends to the mixed enthalpy at zero flow.
inverse_flow = _regularized_inverse_outflow(
reference_port.m_flow,
transition_flow,
)
energy_residual_at_mixed_h = (
energy_without_reference
+ reference_port.m_flow * mixed_h
)
reference_port.h_outflow = (
mixed_h - energy_residual_at_mixed_h * inverse_flow
)
class AmesimPn3Node2(_AmesimPneumaticNode):
"""AMESim PN3NODE2 pneumatic three-port junction."""
MODEL_TYPE = "amesim_pn3node2"
MODEL_VERSION = "0.3.0"
PRESSURE_FLOW_DEPENDS_ON_STREAM = False
PRESSURE_FLOW_EXACT_SUM_TO_ZERO_EQUATION_SUFFIXES = frozenset(
("mass_flow_balance",)
)
PORTS = (
PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),
PortDefinition.pneumatic("port_2", nominal_role="bidirectional"),
PortDefinition.pneumatic("port_3", nominal_role="bidirectional"),
)
PARAMETERS = ()
RESULT_VARIABLES = ()
DISPLAY = ComponentDisplaySpec(
label="PN3NODE2 三端气动节点",
library_id="amesim",
category_id="junctions",
symbol="amesim_pn3node2",
ports=(
PortDisplaySpec("port_1", "left", order=10),
PortDisplaySpec("port_2", "right", order=20),
PortDisplaySpec("port_3", "right", order=30),
),
order=10,
)
@classmethod
def create(
cls,
*,
name: str,
medium: IdealGasMedium,
parameters: Mapping[str, float],
) -> AmesimPn3Node2:
return cls(name=name)
class AmesimP4Node2(_AmesimPneumaticNode):
"""AMESim P4NODE2 pneumatic four-port junction."""
MODEL_TYPE = "amesim_p4node2"
MODEL_VERSION = "0.3.0"
PRESSURE_FLOW_DEPENDS_ON_STREAM = False
PRESSURE_FLOW_EXACT_SUM_TO_ZERO_EQUATION_SUFFIXES = frozenset(
("mass_flow_balance",)
)
PORTS = (
PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),
PortDefinition.pneumatic("port_2", nominal_role="bidirectional"),
PortDefinition.pneumatic("port_3", nominal_role="bidirectional"),
PortDefinition.pneumatic("port_4", nominal_role="bidirectional"),
)
PARAMETERS = ()
RESULT_VARIABLES = ()
DISPLAY = ComponentDisplaySpec(
label="P4NODE2 四端气动节点",
library_id="amesim",
category_id="junctions",
symbol="amesim_p4node2",
ports=(
PortDisplaySpec("port_1", "left", order=10),
PortDisplaySpec("port_2", "right", order=20),
PortDisplaySpec("port_3", "right", order=30),
PortDisplaySpec("port_4", "right", order=40),
),
order=20,
)
@classmethod
def create(
cls,
*,
name: str,
medium: IdealGasMedium,
parameters: Mapping[str, float],
) -> AmesimP4Node2:
return cls(name=name)
@@ -0,0 +1,49 @@
"""AMESim-compatible public component library."""
from app.simulation.core.catalog import (
ComponentCategorySpec,
ComponentLibrarySpec,
)
LIBRARY = ComponentLibrarySpec(
id="amesim",
label="AMESim 组件库",
version="0.3.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),
ComponentCategorySpec(id="boundary", label="边界元件", order=40),
ComponentCategorySpec(id="signals", label="信号元件", order=50),
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",
"app.simulation.components.amesim.mechanical.translational:AmesimF000",
"app.simulation.components.amesim.mechanical.translational:AmesimForc",
"app.simulation.components.amesim.mechanical.translational:AmesimMecmas21",
"app.simulation.components.amesim.mechanical.translational:AmesimLstp00a",
"app.simulation.components.amesim.mechanical.translational:AmesimLmechn1",
"app.simulation.components.amesim.mechanical.pistons:AmesimPnrp17",
"app.simulation.components.amesim.storage.chambers:AmesimPnch023",
"app.simulation.components.amesim.storage.chambers:AmesimPnch012",
"app.simulation.components.amesim.flow.orifices:AmesimPnor001",
"app.simulation.components.amesim.flow.orifices:AmesimPnvo001FixedOpening",
"app.simulation.components.amesim.flow.orifices:AmesimPnvo001SignalOpening",
"app.simulation.components.amesim.flow.pipes:AmesimPnl00r",
"app.simulation.components.amesim.flow.pipes:AmesimPnl0001",
"app.simulation.components.amesim.flow.pipes:AmesimPnl0002",
"app.simulation.components.amesim.flow.pipes:AmesimPnl0003",
"app.simulation.components.amesim.junctions.nodes:AmesimPn3Node2",
"app.simulation.components.amesim.junctions.nodes:AmesimP4Node2",
),
)
@@ -0,0 +1 @@
"""AMESim mechanical components."""
@@ -0,0 +1,305 @@
from __future__ import annotations
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from math import isfinite, pi
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
from app.simulation.core.metadata import ParameterDefinition, ResultVariableDefinition
from app.simulation.core.medium import GasMedium
from app.simulation.core.ports import PortDefinition
AMESIM_REFERENCE_PRESSURE_PA = 101300.0
@dataclass(frozen=True)
class Pnrp17Linearization:
volume: float
volume_flow: float
pressure_force: float
volume_tangent: tuple[float, ...]
volume_flow_tangent: tuple[float, ...]
pressure_force_tangent: tuple[float, ...]
valid: bool = True
reason: str | None = None
class AmesimPnrp17(AlgebraicComponent):
"""AMESim PNRP17 pneumatic piston with two mechanical faces.
Mechanical ports 2/5 share the piston-side motion and ports 3/4 share the
cylinder-side motion. The pneumatic port contributes its swept volume and
volume rate to the connected variable-volume chamber.
"""
MODEL_TYPE = "amesim_pnrp17"
MODEL_VERSION = "0.1.0"
PRESSURE_FLOW_DEPENDS_ON_STREAM = False
PORTS = (
PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),
PortDefinition.mechanical_translational("port_2"),
PortDefinition.mechanical_translational("port_3"),
PortDefinition.mechanical_translational("port_4"),
PortDefinition.mechanical_translational("port_5"),
)
PARAMETERS = (
AMESIM_GAS_INDEX_PARAMETER,
ParameterDefinition(
"dp",
0.2,
label="活塞直径",
quantity="length",
unit="m",
minimum=0.0,
minimum_exclusive=True,
description="活塞外径;与活塞杆直径共同确定有效受压面积。",
),
ParameterDefinition(
"dr",
0.001,
label="活塞杆直径",
quantity="length",
unit="m",
minimum=0.0,
description="穿过气室一侧的活塞杆直径,必须不大于活塞直径。",
),
ParameterDefinition(
"x0",
0.0,
label="初始腔长",
quantity="length",
unit="m",
description="机械端位移均为零时的气动腔长度。",
),
)
RESULT_VARIABLES = (
ResultVariableDefinition("volume", "扫掠容积", "volume", "m3", "derived", 10),
ResultVariableDefinition(
"volume_flow",
"扫掠容积变化率",
"volume_flow",
"m3/s",
"derived",
20,
),
ResultVariableDefinition("length", "气动腔长度", "length", "m", "derived", 30),
ResultVariableDefinition(
"pressure_force",
"气压力",
"force",
"N",
"derived",
40,
),
)
DISPLAY = ComponentDisplaySpec(
label="PNRP17 气动活塞",
library_id="amesim",
category_id="mechanical",
symbol="amesim_pnrp17",
ports=(
PortDisplaySpec("port_1", "left", order=10),
PortDisplaySpec("port_3", "left", order=20),
PortDisplaySpec("port_2", "left", order=30),
PortDisplaySpec("port_4", "right", order=40),
PortDisplaySpec("port_5", "right", order=50),
),
order=60,
)
def __init__(
self,
name: str,
medium: GasMedium,
*,
gi: float = 0.0,
dp: float = 0.2,
dr: float = 0.001,
x0: float = 0.0,
) -> None:
super().__init__(name=name)
self.set_parameter_values({"gi": gi, "dp": dp, "dr": dr, "x0": x0})
self.medium = medium
self.gi = normalize_amesim_gas_index(gi)
self.dp = float(dp)
self.dr = float(dr)
self.x0 = float(x0)
if self.dr > self.dp:
raise ValueError("PNRP17 rod diameter dr must not exceed piston diameter dp.")
for definition in self.PORTS:
port = self.register_declared_port(definition.name)
setattr(self, definition.name, port)
self.port_1.h_outflow = medium.specific_enthalpy(medium.T_ref)
@classmethod
def create(
cls,
*,
name: str,
medium: GasMedium,
parameters: Mapping[str, float],
) -> "AmesimPnrp17":
return cls(name=name, medium=medium, **dict(parameters))
@property
def effective_area(self) -> float:
return pi * (self.dp * self.dp - self.dr * self.dr) / 4.0
@property
def chamber_length(self) -> float:
return self.x0 + self.port_5.x - self.port_4.x
@property
def chamber_volume(self) -> float:
return self.effective_area * self.chamber_length
@property
def chamber_volume_flow(self) -> float:
return self.effective_area * (self.port_5.v - self.port_4.v)
@property
def pressure_force(self) -> float:
return (self.port_1.p - AMESIM_REFERENCE_PRESSURE_PA) * self.effective_area
def pressure_flow_equation_values(self) -> tuple[float, ...]:
values = [self.port_1.m_flow]
effort_pairs = (("port_2", "port_5"), ("port_3", "port_4"))
for first_name, second_name in effort_pairs:
first = self.get_port(first_name)
second = self.get_port(second_name)
values.extend((first.x - second.x, first.v - second.v))
force = self.pressure_force
values.extend(
(
self.port_2.f + self.port_5.f + force,
self.port_3.f + self.port_4.f - force,
)
)
return tuple(values)
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
effort_pairs = (("port_2", "port_5"), ("port_3", "port_4"))
residuals: list[EquationResidual] = [
EquationResidual(
id=f"{self.name}:pneumatic_zero_mass_flow",
owner="component",
owner_id=self.name,
relation="constitutive",
variables=(f"{self.name}.port_1.m_flow",),
role="flow",
value=self.port_1.m_flow,
)
]
for first_name, second_name in effort_pairs:
first = self.get_port(first_name)
second = self.get_port(second_name)
for variable in ("x", "v"):
residuals.append(
EquationResidual(
id=f"{self.name}:{first_name}_{second_name}_{variable}_equal",
owner="component",
owner_id=self.name,
relation="equal",
variables=(
f"{self.name}.{first_name}.{variable}",
f"{self.name}.{second_name}.{variable}",
),
role="effort",
value=getattr(first, variable) - getattr(second, variable),
)
)
force = self.pressure_force
residuals.extend(
(
EquationResidual(
id=f"{self.name}:piston_side_force_balance",
owner="component",
owner_id=self.name,
relation="constitutive",
variables=(f"{self.name}.port_2.f", f"{self.name}.port_5.f", f"{self.name}.port_1.p"),
role="flow",
value=self.port_2.f + self.port_5.f + force,
),
EquationResidual(
id=f"{self.name}:cylinder_side_force_balance",
owner="component",
owner_id=self.name,
relation="constitutive",
variables=(f"{self.name}.port_3.f", f"{self.name}.port_4.f", f"{self.name}.port_1.p"),
role="flow",
value=self.port_3.f + self.port_4.f - force,
),
)
)
return tuple(residuals)
def pneumatic_volume_outputs(self) -> Mapping[str, tuple[float, float]]:
return {"port_1": (self.chamber_volume, self.chamber_volume_flow)}
def linearize_geometry_and_force(
self,
port_4_x_tangent: Sequence[float],
port_5_x_tangent: Sequence[float],
port_4_v_tangent: Sequence[float],
port_5_v_tangent: Sequence[float],
port_1_pressure_tangent: Sequence[float],
) -> Pnrp17Linearization:
"""Return exact piston geometry and pressure-force tangents."""
vectors = tuple(
tuple(float(value) for value in values)
for values in (
port_4_x_tangent,
port_5_x_tangent,
port_4_v_tangent,
port_5_v_tangent,
port_1_pressure_tangent,
)
)
widths = {len(values) for values in vectors}
if len(widths) != 1:
raise ValueError("PNRP17 tangent vectors must have equal lengths.")
valid = all(isfinite(value) for values in vectors for value in values)
area = self.effective_area
volume_tangent = tuple(
area * (right - left)
for left, right in zip(vectors[0], vectors[1], strict=True)
)
volume_flow_tangent = tuple(
area * (right - left)
for left, right in zip(vectors[2], vectors[3], strict=True)
)
pressure_force_tangent = tuple(
area * value for value in vectors[4]
)
return Pnrp17Linearization(
volume=self.chamber_volume,
volume_flow=self.chamber_volume_flow,
pressure_force=self.pressure_force,
volume_tangent=volume_tangent,
volume_flow_tangent=volume_flow_tangent,
pressure_force_tangent=pressure_force_tangent,
valid=valid,
reason=None if valid else "non_finite_tangent_input",
)
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
self.port_1.h_outflow = connected_h.get(
"port_1",
self.medium.specific_enthalpy(self.medium.T_ref),
)
def component_result_values(self) -> Mapping[str, float]:
return {
"volume": self.chamber_volume,
"volume_flow": self.chamber_volume_flow,
"length": self.chamber_length,
"pressure_force": self.pressure_force,
}
File diff suppressed because it is too large. Load diff
@@ -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",
)
@@ -0,0 +1,517 @@
from __future__ import annotations
from collections.abc import Callable, Sequence
from dataclasses import dataclass
from math import exp, isfinite, log
from typing import ClassVar
from app.simulation.core.errors import RecoverableTrialStateError
from app.simulation.core.medium import (
GasMedium,
IdealGasMedium,
ThermodynamicProperties,
ThermodynamicPropertiesLinearization,
ThermodynamicPropertyTangents,
)
from app.simulation.core.peng_robinson import HELIUM_PR, PengRobinsonFluid
from app.simulation.performance import profile_property, record_property_iterations
from app.simulation.property_cache import cache_property_calculation
@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 caloric reference follows the constant NASA
polynomial from Simcenter Amesim 2404 ``helium_cp_h_s.data``.
"""
SUBSTANCE_ID: ClassVar[str] = "helium"
PROPERTY_METHOD_ID: ClassVar[str] = "peng_robinson"
fluid: ClassVar[PengRobinsonFluid] = HELIUM_PR
nasa_cp_over_R: ClassVar[float] = 2.5
nasa_enthalpy_constant_K: ClassVar[float] = -745.375
nasa_viscosity_coefficients: ClassVar[tuple[float, float, float, float]] = (
0.7501594,
35.76324,
-2212.129,
0.9212635,
)
name: str = "AMESimHeliumPengRobinson"
R_gas: float = HELIUM_PR.specific_gas_constant
cp_ref: float = nasa_cp_over_R * HELIUM_PR.specific_gas_constant
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 (self.nasa_cp_over_R - 1.0) * self.R_gas
def cv_at_temperature(self, T: float) -> float:
del T
return self.cv
def diagnostic_dynamic_viscosity(self, T: float) -> float:
"""Return the AMESim NASA-table viscosity used by pipe diagnostics.
pn2pipefr reports Reynolds number with sagum viscosity. Keep this
separate from dynamic_viscosity so matching that diagnostic cannot
alter the already-validated pipe flow or friction dynamics.
"""
if T <= 0.0:
raise ValueError("Temperature must be positive.")
a, b, c, d = self.nasa_viscosity_coefficients
return 1.0e-7 * exp(a * log(T) + b / T + c / (T * T) + d)
@profile_property("density")
@cache_property_calculation("density")
def density(self, p: float, T: float) -> float:
return self.fluid.density(p, T)
def _real_heat_capacities(
self,
p: float,
T: float,
) -> tuple[float, float, float, float, float]:
density = self.density(p, T)
pressure_density_derivative = (
self.fluid.pressure_density_derivative_at_temperature(
T,
density,
)
)
pressure_temperature_derivative = (
self.fluid.pressure_temperature_derivative_at_density(
T,
density,
)
)
cv = (
self.cv_at_temperature(T)
+ self.fluid.residual_isochoric_heat_capacity_at_density(T, density)
)
cp = (
cv
+ T
* pressure_temperature_derivative
* pressure_temperature_derivative
/ (density * density * pressure_density_derivative)
)
if cp <= 0.0 or cv <= 0.0:
raise ValueError("Real-gas heat capacities must be positive.")
return (
cp,
cv,
density,
pressure_density_derivative,
pressure_temperature_derivative,
)
def _local_isentropic_density_pressure_factor(
self,
p: float,
T: float,
) -> tuple[float, float]:
cp, cv, density, pressure_density_derivative, pressure_temperature_derivative = (
self._real_heat_capacities(p, T)
)
heat_capacity_ratio = cp / cv
factor = p / (
density * pressure_density_derivative * heat_capacity_ratio
)
exponent = (
p
* (heat_capacity_ratio - 1.0)
/ (
heat_capacity_ratio
* T
* pressure_temperature_derivative
)
)
return factor, exponent
@profile_property("isentropic_density_pressure_factor")
@cache_property_calculation("isentropic_density_pressure_factor")
def isentropic_density_pressure_factor(
self,
p: float,
T: float,
downstream_pressure: float | None = None,
) -> float:
upstream_factor, isentropic_temperature_exponent = (
self._local_isentropic_density_pressure_factor(p, T)
)
if downstream_pressure is None or downstream_pressure >= p:
return upstream_factor
pressure_ratio = max(downstream_pressure / p, 1.0e-12)
isentropic_temperature = max(
T * pressure_ratio**isentropic_temperature_exponent,
2.2,
)
downstream_factor, _unused_exponent = (
self._local_isentropic_density_pressure_factor(
max(downstream_pressure, 1.0),
isentropic_temperature,
)
)
# AMESim 2404 saggs_ evaluates the local factor at the upstream
# state and at an approximate isentropic downstream state.
return 0.5 * (upstream_factor + downstream_factor)
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)
@profile_property("specific_internal_energy")
def specific_internal_energy(self, T: float) -> float:
return self.R_gas * (
(self.nasa_cp_over_R - 1.0) * T
+ self.nasa_enthalpy_constant_K
)
@profile_property("specific_internal_energy_at_pressure")
def specific_internal_energy_at_pressure(self, p: float, T: float) -> float:
density = self.density(p, T)
return (
self.specific_internal_energy(T)
+ self.fluid.residual_specific_internal_energy_at_density(T, density)
)
@profile_property("specific_enthalpy")
def specific_enthalpy(self, T: float) -> float:
return self.R_gas * (
self.nasa_cp_over_R * T
+ self.nasa_enthalpy_constant_K
)
@profile_property("specific_enthalpy_at_pressure")
def specific_enthalpy_at_pressure(self, p: float, T: float) -> float:
return self.specific_enthalpy(T) + self.fluid.residual_specific_enthalpy(p, T)
def temperature_from_internal_energy(self, u: float) -> float:
return (
u / self.R_gas - self.nasa_enthalpy_constant_K
) / (self.nasa_cp_over_R - 1.0)
def temperature_from_enthalpy(self, h: float) -> float:
return (
h / self.R_gas - self.nasa_enthalpy_constant_K
) / self.nasa_cp_over_R
@profile_property("temperature_from_pressure_enthalpy")
@cache_property_calculation("temperature_from_pressure_enthalpy")
def temperature_from_pressure_enthalpy(self, p: float, h: float) -> float:
temperature = max(self.temperature_from_enthalpy(h), 2.2)
for _iteration in range(16):
residual_enthalpy = self.fluid.residual_specific_enthalpy(p, temperature)
next_temperature = max(
self.temperature_from_enthalpy(h - residual_enthalpy),
2.2,
)
if abs(next_temperature - temperature) <= 1.0e-10 * max(
temperature,
1.0,
):
record_property_iterations(
"temperature_from_pressure_enthalpy",
_iteration + 1,
True,
)
return next_temperature
temperature = next_temperature
record_property_iterations(
"temperature_from_pressure_enthalpy",
16,
False,
)
return temperature
def temperature_from_mass_internal_energy(self, m: float, U: float) -> float:
if m <= 0.0:
raise RecoverableTrialStateError(
"Mass must stay positive when recovering temperature."
)
return self.temperature_from_internal_energy(U / m)
@profile_property("properties_from_mU")
@cache_property_calculation("properties_from_mU")
def properties_from_mU(
self,
m: float,
U: float,
V: float,
) -> ThermodynamicProperties:
"""Recover a real-gas state, reusing exact repeated evaluations.
Implicit integration asks several component interfaces for the same
``(m, U, V)`` state while closing one RHS evaluation and while building
finite-difference Jacobians. The calculation is pure and its result is
immutable, so an exact-key bounded cache avoids repeating the
Peng-Robinson temperature iteration without changing model semantics.
"""
if m <= 0.0:
raise RecoverableTrialStateError(
"Mass must stay positive when recovering temperature."
)
if V <= 0.0:
raise ValueError("Volume must stay positive.")
density = m / V
target_internal_energy = U / m
temperature = max(
self.temperature_from_internal_energy(target_internal_energy),
2.2,
)
converged = False
for _iteration in range(16):
residual_internal_energy = (
self.fluid.residual_specific_internal_energy_at_density(
temperature,
density,
)
)
next_temperature = max(
self.temperature_from_internal_energy(
target_internal_energy - residual_internal_energy
),
2.2,
)
if abs(next_temperature - temperature) <= 1.0e-10 * max(
temperature,
1.0,
):
temperature = next_temperature
converged = True
break
temperature = next_temperature
record_property_iterations(
"properties_from_mU",
_iteration + 1,
converged,
)
pressure = self.fluid.pressure_from_density(temperature, density)
return ThermodynamicProperties(
p=pressure,
T=temperature,
rho=density,
u=target_internal_energy,
h=self.specific_enthalpy_at_pressure(
pressure,
temperature,
),
)
def linearize_properties_from_mU(
self,
m: float,
U: float,
V: float,
dm: Sequence[float],
dU: Sequence[float],
dV: Sequence[float],
*,
properties: ThermodynamicProperties | None = None,
) -> ThermodynamicPropertiesLinearization:
"""Implicitly differentiate the Peng-Robinson m/U/V recovery."""
dm_values = tuple(float(value) for value in dm)
dU_values = tuple(float(value) for value in dU)
dV_values = tuple(float(value) for value in dV)
if not (len(dm_values) == len(dU_values) == len(dV_values)):
raise ValueError("Thermodynamic tangent vectors must have equal lengths.")
props = properties or self.properties_from_mU(m, U, V)
width = len(dm_values)
def invalid(reason: str) -> ThermodynamicPropertiesLinearization:
return ThermodynamicPropertiesLinearization(
properties=props,
tangents=ThermodynamicPropertyTangents.zeros(width),
valid=False,
reason=reason,
)
expected_density = m / V
expected_internal_energy = U / m
if (
abs(props.rho - expected_density)
> 1.0e-12 * max(abs(expected_density), 1.0)
or abs(props.u - expected_internal_energy)
> 1.0e-12 * max(abs(expected_internal_energy), 1.0)
):
return invalid("properties_primal_mismatch")
if not all(
isfinite(value)
for values in (dm_values, dU_values, dV_values)
for value in values
):
return invalid("non_finite_tangent_input")
if props.T <= 2.2 * (1.0 + 1.0e-10):
return invalid("temperature_floor_boundary")
pressure_temperature_derivative = (
self.fluid.pressure_temperature_derivative_at_density(
props.T,
props.rho,
)
)
pressure_density_derivative = (
self.fluid.pressure_density_derivative_at_temperature(
props.T,
props.rho,
)
)
cv = (
self.cv_at_temperature(props.T)
+ self.fluid.residual_isochoric_heat_capacity_at_density(
props.T,
props.rho,
)
)
recovered_internal_energy = (
self.specific_internal_energy(props.T)
+ self.fluid.residual_specific_internal_energy_at_density(
props.T,
props.rho,
)
)
recovery_scale = max(
abs(props.u),
abs(cv * props.T) if isfinite(cv) else 0.0,
1.0,
)
if (
not all(
isfinite(value)
for value in (
pressure_temperature_derivative,
pressure_density_derivative,
cv,
recovered_internal_energy,
)
)
or cv <= 0.0
):
return invalid("invalid_peng_robinson_derivative")
if abs(recovered_internal_energy - props.u) > 1.0e-8 * recovery_scale:
return invalid("properties_recovery_not_converged")
internal_energy_density_derivative = (
props.p - props.T * pressure_temperature_derivative
) / (props.rho * props.rho)
drho: list[float] = []
du: list[float] = []
dT: list[float] = []
dp: list[float] = []
dh: list[float] = []
for mass_tangent, energy_tangent, volume_tangent in zip(
dm_values,
dU_values,
dV_values,
strict=True,
):
density_tangent = (
mass_tangent / V - m * volume_tangent / (V * V)
)
internal_energy_tangent = (
energy_tangent / m - U * mass_tangent / (m * m)
)
temperature_tangent = (
internal_energy_tangent
- internal_energy_density_derivative * density_tangent
) / cv
pressure_tangent = (
pressure_temperature_derivative * temperature_tangent
+ pressure_density_derivative * density_tangent
)
enthalpy_tangent = (
internal_energy_tangent
+ pressure_tangent / props.rho
- props.p * density_tangent / (props.rho * props.rho)
)
drho.append(density_tangent)
du.append(internal_energy_tangent)
dT.append(temperature_tangent)
dp.append(pressure_tangent)
dh.append(enthalpy_tangent)
tangent_values = (*drho, *du, *dT, *dp, *dh)
if not all(isfinite(value) for value in tangent_values):
return invalid("non_finite_property_tangent")
return ThermodynamicPropertiesLinearization(
properties=props,
tangents=ThermodynamicPropertyTangents(
p=tuple(dp),
T=tuple(dT),
rho=tuple(drho),
u=tuple(du),
h=tuple(dh),
),
)
@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,
),
)
@@ -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"],
)
Whitespace-only changes.
@@ -0,0 +1,355 @@
from __future__ import annotations
from collections.abc import Mapping
from math import floor
from app.simulation.core.base import AlgebraicComponent
from app.simulation.core.catalog import (
ComponentDisplaySpec,
ParameterGroupDisplaySpec,
PortDisplaySpec,
)
from app.simulation.core.metadata import (
ParameterCondition,
ParameterDefinition,
ParameterOption,
ResultVariableDefinition,
)
from app.simulation.core.medium import IdealGasMedium
from app.simulation.core.ports import PortDefinition
def _ud00_stage_parameters(index: int) -> tuple[ParameterDefinition, ...]:
visible_when = (
()
if index == 1
else (
ParameterCondition(
"nstages",
tuple(float(stage_count) for stage_count in range(index, 9)),
),
)
)
return (
ParameterDefinition(
f"start{index}",
0.0 if index == 1 else 1.0,
label=f"第 {index} 段起点",
quantity="dimensionless",
unit="",
description=f"第 {index} 段开始时的输出值。",
visible_when=visible_when,
),
ParameterDefinition(
f"end{index}",
1.0,
label=f"第 {index} 段终点",
quantity="dimensionless",
unit="",
description=f"第 {index} 段结束时的输出值。",
visible_when=visible_when,
),
ParameterDefinition(
f"t{index}",
1.0 if index == 1 else 0.0,
label=f"第 {index} 段时长",
quantity="time",
unit="s",
minimum=0.0,
description=f"第 {index} 段的持续时间。",
visible_when=visible_when,
),
)
_UD00_STAGE_PARAMETERS = tuple(
parameter
for stage_index in range(1, 9)
for parameter in _ud00_stage_parameters(stage_index)
)
class AmesimStep0(AlgebraicComponent):
"""AMESim STEP0 scalar step signal source."""
MODEL_TYPE = "amesim_step0"
MODEL_VERSION = "0.1.0"
PORTS = (PortDefinition.signal("out", nominal_role="output"),)
PARAMETERS = (
ParameterDefinition("initial", 0.0, label="初始值", quantity="dimensionless", unit=""),
ParameterDefinition("final", 1.0, label="阶跃后值", quantity="dimensionless", unit=""),
ParameterDefinition("time", 0.0, label="阶跃时间", quantity="time", unit="s"),
)
RESULT_VARIABLES = (
ResultVariableDefinition("y", "输出", "dimensionless", "", "signal", 10),
)
DISPLAY = ComponentDisplaySpec(
label="STEP0 阶跃信号",
library_id="amesim",
category_id="signals",
symbol="amesim_step0",
ports=(PortDisplaySpec("out", "right", order=10),),
order=10,
)
def __init__(
self,
name: str,
medium: IdealGasMedium,
*,
initial: float = 0.0,
final: float = 1.0,
time: float = 0.0,
) -> None:
super().__init__(name=name)
self.set_parameter_values({"initial": initial, "final": final, "time": time})
self.initial = float(initial)
self.final = float(final)
self.time = float(time)
self.out = self.register_declared_port("out")
self.out.signal = self.output_at(0.0)
@classmethod
def create(
cls,
*,
name: str,
medium: IdealGasMedium,
parameters: Mapping[str, float],
) -> "AmesimStep0":
return cls(
name=name,
medium=medium,
initial=parameters["initial"],
final=parameters["final"],
time=parameters["time"],
)
def output_at(self, time: float) -> float:
return self.final if time >= self.time else self.initial
def signal_output_values(self, time: float) -> dict[str, float]:
return {"out": self.output_at(time)}
def signal_event_times(
self,
start_time: float,
stop_time: float,
) -> tuple[float, ...]:
"""Expose the exact STEP0 switch time as an integration split point."""
return (self.time,) if start_time < self.time < stop_time else ()
def component_result_values(self) -> Mapping[str, float]:
return {"y": self.out.signal}
class AmesimUd00(AlgebraicComponent):
"""AMESim UD00 piecewise-linear scalar signal source."""
MODEL_TYPE = "amesim_ud00"
MODEL_VERSION = "0.2.0"
PORTS = (PortDefinition.signal("out", nominal_role="output"),)
PARAMETERS = (
ParameterDefinition(
"tstart",
0.0,
label="启动时间",
quantity="time",
unit="s",
description="分段信号开始输出第一段之前的等待时间。",
),
*_UD00_STAGE_PARAMETERS,
ParameterDefinition(
"nstages",
1.0,
label="段数",
quantity="dimensionless",
unit="",
minimum=1.0,
maximum=8.0,
editor="choice",
options=tuple(
ParameterOption(float(stage_count), str(stage_count))
for stage_count in range(1, 9)
),
description="参与输出计算的有效线性分段数量。",
),
ParameterDefinition(
"iscyclic",
0.0,
label="循环",
quantity="dimensionless",
unit="",
minimum=0.0,
maximum=1.0,
editor="choice",
options=(
ParameterOption(0.0, "否"),
ParameterOption(1.0, "是"),
),
description="当前公共协议编码:0 表示单次输出,1 表示循环输出。",
),
)
RESULT_VARIABLES = (
ResultVariableDefinition("y", "输出", "dimensionless", "", "signal", 10),
)
DISPLAY = ComponentDisplaySpec(
label="UD00 分段线性信号",
library_id="amesim",
category_id="signals",
symbol="amesim_ud00",
ports=(PortDisplaySpec("out", "right", order=10),),
order=20,
parameter_groups=(
ParameterGroupDisplaySpec(
id="stages",
label="分段参数",
parameters=tuple(
parameter.name for parameter in _UD00_STAGE_PARAMETERS
),
order=10,
),
),
)
def __init__(
self,
name: str,
medium: IdealGasMedium,
*,
tstart: float = 0.0,
starts: tuple[float, ...] = (0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0),
ends: tuple[float, ...] = (1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0),
durations: tuple[float, ...] = (1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
nstages: int = 1,
iscyclic: bool = False,
) -> None:
super().__init__(name=name)
if len(starts) != 8 or len(ends) != 8 or len(durations) != 8:
raise ValueError("UD00 requires exactly eight start, end, and duration values.")
if nstages < 1 or nstages > 8:
raise ValueError("UD00 nstages must be between 1 and 8.")
self.tstart = float(tstart)
self.starts = tuple(float(value) for value in starts)
self.ends = tuple(float(value) for value in ends)
self.durations = tuple(float(value) for value in durations)
self.nstages = int(nstages)
self.iscyclic = bool(iscyclic)
values: dict[str, float] = {"tstart": self.tstart, "nstages": float(self.nstages), "iscyclic": float(int(self.iscyclic))}
for index in range(1, 9):
values[f"start{index}"] = self.starts[index - 1]
values[f"end{index}"] = self.ends[index - 1]
values[f"t{index}"] = self.durations[index - 1]
self.set_parameter_values(values)
self.out = self.register_declared_port("out")
self.out.signal = self.output_at(0.0)
@classmethod
def create(
cls,
*,
name: str,
medium: IdealGasMedium,
parameters: Mapping[str, float],
) -> "AmesimUd00":
nstages = parameters["nstages"]
iscyclic = parameters["iscyclic"]
definitions = {definition.name: definition for definition in cls.PARAMETERS}
for parameter_name, value in (
("nstages", nstages),
("iscyclic", iscyclic),
):
numeric_value = float(value)
if not numeric_value.is_integer():
raise ValueError(f"UD00 {parameter_name} must be an integer.")
message = definitions[parameter_name].validation_message(numeric_value)
if message is not None:
raise ValueError(f"UD00 {parameter_name} {message}.")
return cls(
name=name,
medium=medium,
tstart=parameters["tstart"],
starts=tuple(parameters[f"start{index}"] for index in range(1, 9)),
ends=tuple(parameters[f"end{index}"] for index in range(1, 9)),
durations=tuple(parameters[f"t{index}"] for index in range(1, 9)),
nstages=int(nstages),
iscyclic=bool(int(iscyclic)),
)
def output_at(self, time: float) -> float:
elapsed = max(float(time) - self.tstart, 0.0)
active_durations = self.durations[: self.nstages]
total_duration = sum(active_durations)
if self.iscyclic 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.nstages - 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.nstages - 1]
def signal_output_values(self, time: float) -> dict[str, float]:
return {"out": self.output_at(time)}
def signal_event_times(
self,
start_time: float,
stop_time: float,
) -> tuple[float, ...]:
"""Return UD00 start, stage, and repeated cycle boundaries.
The final non-cyclic stage is intentionally not given an end event:
``output_at`` continues that stage's slope after its configured duration.
"""
if stop_time <= start_time:
return ()
active_durations = self.durations[: self.nstages]
stage_offsets = [0.0]
elapsed = 0.0
for duration in active_durations[:-1]:
elapsed += duration
stage_offsets.append(elapsed)
if not self.iscyclic:
return tuple(
sorted(
{
event_time
for offset in stage_offsets
if start_time
< (event_time := self.tstart + offset)
< stop_time
}
)
)
cycle_duration = sum(active_durations)
if cycle_duration <= 0.0:
return ()
events: set[float] = set()
for offset in stage_offsets:
first_boundary = self.tstart + offset
cycle_index = max(
0,
floor((start_time - first_boundary) / cycle_duration) + 1,
)
event_time = first_boundary + cycle_index * cycle_duration
while event_time < stop_time:
if event_time > start_time:
events.add(event_time)
cycle_index += 1
event_time = first_boundary + cycle_index * cycle_duration
return tuple(sorted(events))
def component_result_values(self) -> Mapping[str, float]:
return {"y": self.out.signal}
@@ -0,0 +1 @@
"""AMESim pneumatic storage components."""
@@ -0,0 +1,690 @@
from __future__ import annotations
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from math import isfinite
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
from app.simulation.core.metadata import (
ParameterDefinition,
ResultVariableDefinition,
THERMODYNAMIC_VOLUME_RESULT_VARIABLES,
)
from app.simulation.core.medium import (
GasMedium,
ThermodynamicProperties,
ThermodynamicPropertiesLinearization,
)
from app.simulation.core.ports import PortDefinition
from app.simulation.core.state import VolumeState
@dataclass(frozen=True)
class Pnch012DerivativeLinearization:
derivative: tuple[float, float]
tangents: tuple[tuple[float, ...], tuple[float, ...]]
properties: ThermodynamicPropertiesLinearization
valid: bool = True
reason: str | None = None
class AmesimPnch023(ThermodynamicVolumeComponent):
"""AMESim PNCH023 simple pneumatic chamber with heat exchange.
The AMESim submodel owns pressure and temperature states and exposes two
pneumatic flow ports. This public component maps those states onto the
framework's mass/internal-energy volume state and keeps the AMESim
heat-transfer contract `kth * sth * (extemp - T)`.
"""
MODEL_TYPE = "amesim_pnch023"
MODEL_VERSION = "0.1.0"
PORTS = (
PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),
PortDefinition.pneumatic("port_2", nominal_role="bidirectional"),
)
PARAMETERS = (
AMESIM_GAS_INDEX_PARAMETER,
ParameterDefinition(
"cvol",
0.057,
label="气室容积",
quantity="volume",
unit="m3",
minimum=0.0,
minimum_exclusive=True,
description="气室内部用于储存气体的固定有效容积。",
),
ParameterDefinition(
"kth",
0.0,
label="换热系数",
quantity="heat_transfer_coefficient",
unit="W/(m2*K)",
minimum=0.0,
description="气室与环境之间的对流换热系数,与换热面积共同决定换热功率。",
),
ParameterDefinition(
"sth",
0.1,
label="换热面积",
quantity="area",
unit="m2",
minimum=0.0,
description="气室与环境进行热交换的有效表面积。",
),
ParameterDefinition(
"extemp",
293.15,
label="外部温度",
quantity="temperature",
unit="K",
minimum=0.0,
minimum_exclusive=True,
description="气室外部环境的绝对温度,用于计算气体与环境之间的换热。",
),
ParameterDefinition(
"p0",
100000.0,
label="初始压力",
quantity="pressure",
unit="Pa",
minimum=0.0,
minimum_exclusive=True,
description="仿真开始时气室内气体的绝对压力。",
),
ParameterDefinition(
"T0",
293.15,
label="初始温度",
quantity="temperature",
unit="K",
minimum=0.0,
minimum_exclusive=True,
description="仿真开始时气室内气体的绝对温度。",
),
)
RESULT_VARIABLES = THERMODYNAMIC_VOLUME_RESULT_VARIABLES
DISPLAY = ComponentDisplaySpec(
label="PNCH023 固定容积气室",
library_id="amesim",
category_id="storage",
symbol="amesim_pnch023",
ports=(
PortDisplaySpec("port_1", "left", order=10),
PortDisplaySpec("port_2", "right", order=20),
),
order=10,
)
def __init__(
self,
name: str,
medium: GasMedium,
*,
cvol: float = 0.057,
kth: float = 0.0,
sth: float = 0.1,
extemp: float = 293.15,
gi: float = 1.0,
p0: float = 100000.0,
T0: float = 293.15,
) -> None:
super().__init__(name=name)
self.set_parameter_values(
{
"cvol": cvol,
"kth": kth,
"sth": sth,
"extemp": extemp,
"gi": gi,
"p0": p0,
"T0": T0,
}
)
self.medium = medium
self.cvol = float(cvol)
self.kth = float(kth)
self.sth = float(sth)
self.extemp = float(extemp)
self.gi = normalize_amesim_gas_index(gi)
self.p0 = float(p0)
self.T0 = float(T0)
m0 = medium.density(self.p0, self.T0) * self.cvol
U0 = m0 * medium.specific_internal_energy_at_pressure(self.p0, self.T0)
self.state = VolumeState(m=m0, U=U0)
initial_h = medium.specific_enthalpy_at_pressure(self.p0, self.T0)
self.port_1 = self.register_declared_port("port_1")
self.port_1.p = self.p0
self.port_1.h_outflow = initial_h
self.port_2 = self.register_declared_port("port_2")
self.port_2.p = self.p0
self.port_2.h_outflow = initial_h
@classmethod
def create(
cls,
*,
name: str,
medium: GasMedium,
parameters: Mapping[str, float],
) -> AmesimPnch023:
return cls(
name=name,
medium=medium,
cvol=parameters["cvol"],
kth=parameters["kth"],
sth=parameters["sth"],
extemp=parameters["extemp"],
gi=parameters["gi"],
p0=parameters["p0"],
T0=parameters["T0"],
)
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.cvol)
self.port_1.p = props.p
self.port_1.h_outflow = props.h
self.port_2.p = props.p
self.port_2.h_outflow = props.h
return props
def refresh_thermodynamic_ports(self) -> ThermodynamicProperties:
return self.properties()
def thermal_energy_flow_w(self, temperature: float) -> float:
return self.kth * self.sth * (self.extemp - temperature)
def state_derivative_from_ports(
self,
connected_h: Mapping[str, float],
) -> list[float]:
props = self.properties()
inlet_h_1 = self.connection_inlet_enthalpy(
port_m_flow=self.port_1.m_flow,
connected_h=connected_h["port_1"],
internal_h=props.h,
)
inlet_h_2 = self.connection_inlet_enthalpy(
port_m_flow=self.port_2.m_flow,
connected_h=connected_h["port_2"],
internal_h=props.h,
)
derivative = VolumeState(
m=self.port_1.m_flow + self.port_2.m_flow,
U=(
self.port_1.m_flow * inlet_h_1
+ self.port_2.m_flow * inlet_h_2
+ self.thermal_energy_flow_w(props.T)
),
)
return derivative.as_vector()
def pressure_flow_equation_values(self) -> tuple[float, ...]:
pressure = self.medium.properties_from_mU(
self.state.m,
self.state.U,
self.cvol,
).p
return (
self.port_1.p - pressure,
self.port_2.p - pressure,
)
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
pressure = self.medium.properties_from_mU(
self.state.m,
self.state.U,
self.cvol,
).p
return (
EquationResidual(
id=f"{self.name}:port_1_pressure_state",
owner="component",
owner_id=self.name,
relation="state",
variables=(f"{self.name}.port_1.p", f"{self.name}.state"),
role="effort",
value=self.port_1.p - pressure,
),
EquationResidual(
id=f"{self.name}:port_2_pressure_state",
owner="component",
owner_id=self.name,
relation="state",
variables=(f"{self.name}.port_2.p", f"{self.name}.state"),
role="effort",
value=self.port_2.p - pressure,
),
)
class AmesimPnch012(ThermodynamicVolumeComponent):
"""AMESim PNCH012 variable-volume pneumatic chamber.
AMESim supplies four external volume and volume-rate inputs through the
chamber ports. Fixed/prescribed contributions remain available as SI
parameters, while connected moving-boundary components can now add live
volume and volume-rate values through the pneumatic connector contract.
"""
MODEL_TYPE = "amesim_pnch012"
MODEL_VERSION = "0.1.0"
PORTS = (
PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),
PortDefinition.pneumatic("port_2", nominal_role="bidirectional"),
PortDefinition.pneumatic("port_3", nominal_role="bidirectional"),
PortDefinition.pneumatic("port_4", nominal_role="bidirectional"),
)
PARAMETERS = (
AMESIM_GAS_INDEX_PARAMETER,
ParameterDefinition(
"cvol0",
0.015,
label="死容积",
quantity="volume",
unit="m3",
minimum=0.0,
minimum_exclusive=True,
description="变容气室在所有外部容积为零时仍保留的基础容积。",
),
ParameterDefinition(
"kth",
0.0,
label="换热系数",
quantity="heat_transfer_coefficient",
unit="W/(m2*K)",
minimum=0.0,
description="气室与环境之间的对流换热系数,与换热面积共同决定换热功率。",
),
ParameterDefinition(
"sth",
0.1,
label="换热面积",
quantity="area",
unit="m2",
minimum=0.0,
description="气室与环境进行热交换的有效表面积。",
),
ParameterDefinition(
"extemp",
293.15,
label="外部温度",
quantity="temperature",
unit="K",
minimum=0.0,
minimum_exclusive=True,
description="气室外部环境的绝对温度,用于计算气体与环境之间的换热。",
),
ParameterDefinition(
"p0",
100000.0,
label="初始压力",
quantity="pressure",
unit="Pa",
minimum=0.0,
minimum_exclusive=True,
description="仿真开始时气室内气体的绝对压力。",
),
ParameterDefinition(
"T0",
293.15,
label="初始温度",
quantity="temperature",
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"),
ParameterDefinition("vol3", 0.0, label="端口 3 外部容积", quantity="volume", unit="m3"),
ParameterDefinition("vol4", 0.0, label="端口 4 外部容积", quantity="volume", unit="m3"),
ParameterDefinition("dvol1", 0.0, label="端口 1 容积变化率", quantity="volume_flow", unit="m3/s"),
ParameterDefinition("dvol2", 0.0, label="端口 2 容积变化率", quantity="volume_flow", unit="m3/s"),
ParameterDefinition("dvol3", 0.0, label="端口 3 容积变化率", quantity="volume_flow", unit="m3/s"),
ParameterDefinition("dvol4", 0.0, label="端口 4 容积变化率", quantity="volume_flow", unit="m3/s"),
)
RESULT_VARIABLES = THERMODYNAMIC_VOLUME_RESULT_VARIABLES + (
ResultVariableDefinition("vol", "气室总容积", "volume", "m3", "derived", 100),
ResultVariableDefinition("dvol", "总容积变化率", "volume_flow", "m3/s", "derived", 110),
)
DISPLAY = ComponentDisplaySpec(
label="PNCH012 变容气室",
library_id="amesim",
category_id="storage",
symbol="amesim_pnch012",
ports=(
PortDisplaySpec("port_1", "left", order=10),
PortDisplaySpec("port_2", "right", order=20),
PortDisplaySpec("port_3", "left", order=30),
PortDisplaySpec("port_4", "right", order=40),
),
order=20,
)
def __init__(
self,
name: str,
medium: GasMedium,
*,
cvol0: float = 0.015,
kth: float = 0.0,
sth: float = 0.1,
extemp: float = 293.15,
gi: float = 1.0,
p0: float = 100000.0,
T0: float = 293.15,
vol1: float = 0.0,
vol2: float = 0.0,
vol3: float = 0.0,
vol4: float = 0.0,
dvol1: float = 0.0,
dvol2: float = 0.0,
dvol3: float = 0.0,
dvol4: float = 0.0,
) -> None:
super().__init__(name=name)
self.set_parameter_values(
{
"cvol0": cvol0,
"kth": kth,
"sth": sth,
"extemp": extemp,
"gi": gi,
"p0": p0,
"T0": T0,
"vol1": vol1,
"vol2": vol2,
"vol3": vol3,
"vol4": vol4,
"dvol1": dvol1,
"dvol2": dvol2,
"dvol3": dvol3,
"dvol4": dvol4,
}
)
self.medium = medium
self.cvol0 = float(cvol0)
self.kth = float(kth)
self.sth = float(sth)
self.extemp = float(extemp)
self.gi = normalize_amesim_gas_index(gi)
self.p0 = float(p0)
self.T0 = float(T0)
self.external_volumes = {
"port_1": float(vol1),
"port_2": float(vol2),
"port_3": float(vol3),
"port_4": float(vol4),
}
self.external_volume_rates = {
"port_1": float(dvol1),
"port_2": float(dvol2),
"port_3": float(dvol3),
"port_4": float(dvol4),
}
if self.total_volume() <= 0.0:
raise ValueError("PNCH012 total volume must be positive.")
m0 = medium.density(self.p0, self.T0) * self.total_volume()
U0 = m0 * medium.specific_internal_energy_at_pressure(self.p0, self.T0)
self.state = VolumeState(m=m0, U=U0)
initial_h = medium.specific_enthalpy_at_pressure(self.p0, self.T0)
for port_name in ("port_1", "port_2", "port_3", "port_4"):
port = self.register_declared_port(port_name)
port.p = self.p0
port.h_outflow = initial_h
setattr(self, port_name, port)
@classmethod
def create(
cls,
*,
name: str,
medium: GasMedium,
parameters: Mapping[str, float],
) -> "AmesimPnch012":
return cls(name=name, medium=medium, **dict(parameters))
def connected_external_volume(self) -> float:
return sum(
getattr(getattr(self, port_name, None), "volume", 0.0)
for port_name in self.external_volumes
)
def connected_external_volume_rate(self) -> float:
return sum(
getattr(getattr(self, port_name, None), "volume_flow", 0.0)
for port_name in self.external_volume_rates
)
def total_volume(self) -> float:
minimum_volume = self.cvol0 / 100.0
return max(
self.cvol0 + sum(self.external_volumes.values()) + self.connected_external_volume(),
minimum_volume,
)
def total_volume_rate(self) -> float:
if self.total_volume() <= self.cvol0 / 100.0:
return 0.0
return sum(self.external_volume_rates.values()) + self.connected_external_volume_rate()
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.total_volume())
for port_name in ("port_1", "port_2", "port_3", "port_4"):
port = self.get_port(port_name)
port.p = props.p
port.h_outflow = props.h
return props
def refresh_thermodynamic_ports(self) -> ThermodynamicProperties:
return self.properties()
def thermal_energy_flow_w(self, temperature: float) -> float:
return self.kth * self.sth * (self.extemp - temperature)
def component_result_values(self) -> Mapping[str, float]:
props = self.properties()
return {
"m": self.state.m,
"U": self.state.U,
"p": props.p,
"T": props.T,
"rho": props.rho,
"u": props.u,
"h": props.h,
"vol": self.total_volume(),
"dvol": self.total_volume_rate(),
}
def state_derivative_from_ports(self, connected_h: Mapping[str, float]) -> list[float]:
props = self.properties()
mass_derivative = 0.0
energy_derivative = 0.0
for port_name in ("port_1", "port_2", "port_3", "port_4"):
port = self.get_port(port_name)
inlet_h = self.connection_inlet_enthalpy(
port_m_flow=port.m_flow,
connected_h=connected_h[port_name],
internal_h=props.h,
)
mass_derivative += port.m_flow
energy_derivative += port.m_flow * inlet_h
energy_derivative += self.thermal_energy_flow_w(props.T)
energy_derivative -= props.p * self.total_volume_rate()
return VolumeState(m=mass_derivative, U=energy_derivative).as_vector()
def linearize_state_derivative(
self,
connected_h: Mapping[str, float],
*,
state_mass_tangent: Sequence[float],
state_energy_tangent: Sequence[float],
external_volume_tangent: Sequence[float],
external_volume_rate_tangent: Sequence[float],
port_mass_flow_tangents: Mapping[str, Sequence[float]],
connected_h_tangents: Mapping[str, Sequence[float]],
property_linearization: ThermodynamicPropertiesLinearization | None = None,
flow_boundary_tolerance: float = 1.0e-12,
) -> Pnch012DerivativeLinearization:
"""Linearize the chamber balance while keeping stream modes fixed."""
port_names = ("port_1", "port_2", "port_3", "port_4")
vectors = {
"state_mass": tuple(float(value) for value in state_mass_tangent),
"state_energy": tuple(float(value) for value in state_energy_tangent),
"volume": tuple(float(value) for value in external_volume_tangent),
"volume_rate": tuple(
float(value) for value in external_volume_rate_tangent
),
}
for port_name in port_names:
vectors[f"flow:{port_name}"] = tuple(
float(value) for value in port_mass_flow_tangents[port_name]
)
vectors[f"enthalpy:{port_name}"] = tuple(
float(value) for value in connected_h_tangents[port_name]
)
widths = {len(values) for values in vectors.values()}
if len(widths) != 1:
raise ValueError("PNCH012 tangent vectors must have equal lengths.")
width = len(vectors["state_mass"])
invalid_reason: str | None = None
if not all(isfinite(value) for values in vectors.values() for value in values):
invalid_reason = "non_finite_tangent_input"
raw_volume = (
self.cvol0
+ sum(self.external_volumes.values())
+ self.connected_external_volume()
)
minimum_volume = self.cvol0 / 100.0
volume_scale = max(abs(raw_volume), abs(minimum_volume), 1.0e-18)
on_volume_boundary = (
abs(raw_volume - minimum_volume) <= 1.0e-12 * volume_scale
)
supplied_volume_tangent = vectors["volume"]
if raw_volume < minimum_volume or on_volume_boundary:
used_volume_tangent = (0.0,) * width
used_volume_rate_tangent = (0.0,) * width
if on_volume_boundary and any(
value != 0.0
for value in (
*supplied_volume_tangent,
*vectors["volume_rate"],
)
):
invalid_reason = invalid_reason or "volume_floor_boundary"
else:
used_volume_tangent = supplied_volume_tangent
used_volume_rate_tangent = vectors["volume_rate"]
properties = property_linearization or self.medium.linearize_properties_from_mU(
self.state.m,
self.state.U,
self.total_volume(),
vectors["state_mass"],
vectors["state_energy"],
used_volume_tangent,
)
if properties.tangents.width != width:
raise ValueError(
"PNCH012 property tangent width must match balance tangents."
)
props = properties.properties
if not properties.valid:
invalid_reason = invalid_reason or properties.reason
mass_derivative = sum(
self.get_port(port_name).m_flow for port_name in port_names
)
volume_rate = self.total_volume_rate()
energy_derivative = self.thermal_energy_flow_w(props.T) - props.p * volume_rate
mass_tangent = [0.0] * width
energy_tangent = [
-self.kth * self.sth * properties.tangents.T[index]
- volume_rate * properties.tangents.p[index]
- props.p * used_volume_rate_tangent[index]
for index in range(width)
]
for port_name in port_names:
port = self.get_port(port_name)
flow_tangent = vectors[f"flow:{port_name}"]
if (
abs(port.m_flow) <= flow_boundary_tolerance
and any(value != 0.0 for value in flow_tangent)
):
invalid_reason = invalid_reason or (
f"flow_direction_boundary:{port_name}"
)
if port.m_flow > 0.0:
inlet_h = connected_h[port_name]
inlet_h_tangent = vectors[f"enthalpy:{port_name}"]
else:
inlet_h = props.h
inlet_h_tangent = properties.tangents.h
energy_derivative += port.m_flow * inlet_h
for index in range(width):
mass_tangent[index] += flow_tangent[index]
energy_tangent[index] += (
inlet_h * flow_tangent[index]
+ port.m_flow * inlet_h_tangent[index]
)
return Pnch012DerivativeLinearization(
derivative=(mass_derivative, energy_derivative),
tangents=(tuple(mass_tangent), tuple(energy_tangent)),
properties=properties,
valid=invalid_reason is None,
reason=invalid_reason,
)
def pressure_flow_equation_values(self) -> tuple[float, ...]:
pressure = self.medium.properties_from_mU(
self.state.m,
self.state.U,
self.total_volume(),
).p
return tuple(
self.get_port(port_name).p - pressure
for port_name in ("port_1", "port_2", "port_3", "port_4")
)
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
pressure = self.medium.properties_from_mU(
self.state.m,
self.state.U,
self.total_volume(),
).p
return tuple(
EquationResidual(
id=f"{self.name}:{port_name}_pressure_state",
owner="component",
owner_id=self.name,
relation="state",
variables=(f"{self.name}.{port_name}.p", f"{self.name}.state"),
role="effort",
value=self.get_port(port_name).p - pressure,
)
for port_name in ("port_1", "port_2", "port_3", "port_4")
)
+2 -2
View File
@@ -1,7 +1,7 @@
# 元件建模规范与示例 # 元件建模规范与示例
规范的权威版本位于 规范的权威版本位于
[`docs/component-model-authoring-spec-v1.md`](../../../docs/component-model-authoring-spec-v1.md)。 [`docs/standard/component-model-authoring-spec-v1.md`](../../../docs/standard/component-model-authoring-spec-v1.md)。
本文档保留在组件目录中,作为离模型源码最近的完整示例;若两者不一致,应在同一次 本文档保留在组件目录中,作为离模型源码最近的完整示例;若两者不一致,应在同一次
修改中同步,不能让示例形成另一套规则。 修改中同步,不能让示例形成另一套规则。
@@ -279,4 +279,4 @@ models=(
10. 是否补充参数边界、端口契约、目录输出、结果元数据和最小仿真的自动测试。 10. 是否补充参数边界、端口契约、目录输出、结果元数据和最小仿真的自动测试。
组件库、分类和自动发现的完整规则参见 组件库、分类和自动发现的完整规则参见
[`组件库分类、发现与读取规范 v1`](../../../docs/component-library-spec-v1.md)。 [`组件库分类、发现与读取规范 v1`](../../../docs/standard/component-library-spec-v1.md)。
@@ -1,12 +1,3 @@
"""Temporary component library used to validate the model authoring contract.""" """Temporary component library used to validate the model authoring contract."""
from app.simulation.components.experimental.library import LIBRARY from app.simulation.components.experimental.library import LIBRARY
# Compatibility aliases for code written before the v1 library manifest.
LIBRARY_ID = LIBRARY.id
LIBRARY_LABEL = LIBRARY.label
LIBRARY_VERSION = LIBRARY.version
LIBRARY_ORDER = LIBRARY.order
LIBRARY_SOURCE_PACKAGE = LIBRARY.source_package
LIBRARY_TEMPORARY = LIBRARY.temporary
@@ -16,6 +16,10 @@ class Orifice(AlgebraicComponent):
MODEL_TYPE = "orifice" MODEL_TYPE = "orifice"
MODEL_VERSION = "1.0.0" MODEL_VERSION = "1.0.0"
PRESSURE_FLOW_DEPENDS_ON_STREAM = False
PRESSURE_FLOW_EXACT_SUM_TO_ZERO_EQUATION_SUFFIXES = frozenset(
("mass_flow_balance",)
)
PORTS = ( PORTS = (
PortDefinition.pneumatic("port_a", nominal_role="inlet"), PortDefinition.pneumatic("port_a", nominal_role="inlet"),
PortDefinition.pneumatic("port_b", nominal_role="outlet"), PortDefinition.pneumatic("port_b", nominal_role="outlet"),
@@ -115,4 +119,3 @@ class Orifice(AlgebraicComponent):
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None: def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
self.port_a.h_outflow = connected_h["port_b"] self.port_a.h_outflow = connected_h["port_b"]
self.port_b.h_outflow = connected_h["port_a"] self.port_b.h_outflow = connected_h["port_a"]
@@ -16,6 +16,10 @@ class ResistivePipe(AlgebraicComponent):
MODEL_TYPE = "pipe" MODEL_TYPE = "pipe"
MODEL_VERSION = "1.0.0" MODEL_VERSION = "1.0.0"
PRESSURE_FLOW_DEPENDS_ON_STREAM = False
PRESSURE_FLOW_EXACT_SUM_TO_ZERO_EQUATION_SUFFIXES = frozenset(
("mass_flow_balance",)
)
PORTS = ( PORTS = (
PortDefinition.pneumatic("port_a", nominal_role="inlet"), PortDefinition.pneumatic("port_a", nominal_role="inlet"),
PortDefinition.pneumatic("port_b", nominal_role="outlet"), PortDefinition.pneumatic("port_b", nominal_role="outlet"),
@@ -14,6 +14,10 @@ class Tee(AlgebraicComponent):
MODEL_TYPE = "tee" MODEL_TYPE = "tee"
MODEL_VERSION = "1.0.0" MODEL_VERSION = "1.0.0"
PRESSURE_FLOW_DEPENDS_ON_STREAM = False
PRESSURE_FLOW_EXACT_SUM_TO_ZERO_EQUATION_SUFFIXES = frozenset(
("mass_flow_balance",)
)
PORTS = ( PORTS = (
PortDefinition.pneumatic("port_in", nominal_role="bidirectional"), PortDefinition.pneumatic("port_in", nominal_role="bidirectional"),
PortDefinition.pneumatic("port_out1", nominal_role="bidirectional"), PortDefinition.pneumatic("port_out1", nominal_role="bidirectional"),
+93 -5
View File
@@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections.abc import Mapping from collections.abc import Callable, Mapping
from typing import TYPE_CHECKING, Any, ClassVar from typing import TYPE_CHECKING, Any, ClassVar
from app.simulation.core.catalog import ComponentDisplaySpec from app.simulation.core.catalog import ComponentDisplaySpec
@@ -15,12 +15,26 @@ from app.simulation.core.metadata import (
from app.simulation.core.ports import PortDefinition, PortState from app.simulation.core.ports import PortDefinition, PortState
if TYPE_CHECKING: if TYPE_CHECKING:
from app.simulation.core.medium import IdealGasMedium from app.simulation.core.medium import GasMedium
class Component(ABC): class Component(ABC):
MODEL_TYPE: ClassVar[str | None] = None MODEL_TYPE: ClassVar[str | None] = None
MODEL_VERSION: ClassVar[str | None] = None MODEL_VERSION: ClassVar[str | None] = None
# ``True`` means that pressure/flow residuals read values written by
# ``update_stream_outflows`` or ``update_flow_temperature_references``.
# ``False`` is an explicit promise that those residuals are independent of
# stream propagation. ``None`` keeps custom components conservative: when
# they override either stream hook, the closure planner retains the legacy
# full-network thermofluid fixed point.
PRESSURE_FLOW_DEPENDS_ON_STREAM: ClassVar[bool | None] = None
# Exact residual suffixes whose declared variables are summed, in order,
# to form a ``sumToZero`` flow equation. The causal solver deliberately
# reads this capability from the concrete class ``__dict__``: subclasses
# must repeat the promise after changing any equation semantics.
PRESSURE_FLOW_EXACT_SUM_TO_ZERO_EQUATION_SUFFIXES: ClassVar[
frozenset[str]
] = frozenset()
PORTS: ClassVar[tuple[PortDefinition, ...]] = () PORTS: ClassVar[tuple[PortDefinition, ...]] = ()
PARAMETERS: ClassVar[tuple[ParameterDefinition, ...]] = () PARAMETERS: ClassVar[tuple[ParameterDefinition, ...]] = ()
RESULT_VARIABLES: ClassVar[tuple[ResultVariableDefinition, ...]] = () RESULT_VARIABLES: ClassVar[tuple[ResultVariableDefinition, ...]] = ()
@@ -44,6 +58,31 @@ class Component(ABC):
if port.definition is not None if port.definition is not None
) )
@classmethod
def active_port_definitions_for_parameters(
cls,
parameters: Mapping[str, float],
) -> tuple[PortDefinition, ...]:
"""Declared ports enabled by one normalized parameter set."""
return cls.PORTS
@property
def active_port_definitions(self) -> tuple[PortDefinition, ...]:
"""Instance ports that participate in execution and result reporting."""
return self.port_definitions
@property
def required_connection_ports(self) -> tuple[str, ...]:
"""Physical ports that must have an external connection before simulation."""
return tuple(
definition.name
for definition in self.active_port_definitions
if definition.kind == "physical"
)
def register_port(self, port: PortState) -> PortState: def register_port(self, port: PortState) -> PortState:
definition = port.definition definition = port.definition
if definition is None: if definition is None:
@@ -124,7 +163,7 @@ class Component(ABC):
) )
values[name] = float(component_values[name]) values[name] = float(component_values[name])
for port_definition in self.port_definitions: for port_definition in self.active_port_definitions:
port = self.get_port(port_definition.name) port = self.get_port(port_definition.name)
for variable in port_definition.variables: for variable in port_definition.variables:
if not variable.result_visible: if not variable.result_visible:
@@ -151,7 +190,7 @@ class Component(ABC):
for definition in self.RESULT_VARIABLES for definition in self.RESULT_VARIABLES
if definition.visible if definition.visible
] ]
for port_definition in self.port_definitions: for port_definition in self.active_port_definitions:
for variable in port_definition.variables: for variable in port_definition.variables:
if not variable.result_visible: if not variable.result_visible:
continue continue
@@ -185,7 +224,7 @@ class Component(ABC):
cls, cls,
*, *,
name: str, name: str,
medium: IdealGasMedium, medium: GasMedium,
parameters: Mapping[str, float], parameters: Mapping[str, float],
) -> Component: ) -> Component:
"""Create a catalog model from normalized SI parameters.""" """Create a catalog model from normalized SI parameters."""
@@ -199,11 +238,60 @@ class Component(ABC):
return () return ()
def pressure_flow_equation_values(self) -> tuple[float, ...]:
"""Return live residual values in the declared equation order.
Components with frequently evaluated equations can override this
method to avoid rebuilding immutable equation metadata during closure.
The default keeps third-party components compatible with the public
residual API.
"""
return tuple(
float(equation.value)
for equation in self.pressure_flow_equation_residuals()
)
def pressure_flow_equation_value_readers(
self,
) -> Mapping[str, Callable[[], float]]:
"""Return explicitly separable scalar residual readers.
The solver consumes this optional capability only when the concrete
component class declares the method itself. Subclasses therefore
cannot accidentally inherit an equation-purity promise.
"""
return {}
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None: def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
"""Update connector outflow properties from current flow directions.""" """Update connector outflow properties from current flow directions."""
return None return None
def update_flow_temperature_references(
self,
connected_h: Mapping[str, float],
) -> None:
"""Update enthalpy references used only by pressure-flow laws.
Most components use the normal stream enthalpy for both energy
transport and upstream-property evaluation. AMESim node submodels can
expose a distinct temperature reference, so the default is a no-op.
"""
return None
def pneumatic_volume_outputs(self) -> Mapping[str, tuple[float, float]]:
"""Return directed ``volume``/``volume_flow`` values by pneumatic port.
Most pneumatic components contribute no external chamber volume. Moving
boundaries such as PNRP17 override this hook; the network resolver then
propagates the pair to the component connected at the same physical port.
"""
return {}
class DynamicComponent(Component): class DynamicComponent(Component):
state_size = 2 state_size = 2
+23
View File
@@ -5,6 +5,7 @@ from typing import Literal
PortDisplaySide = Literal["left", "right"] PortDisplaySide = Literal["left", "right"]
ComponentCatalogRole = Literal["amesimGasMediumDefinition"]
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -32,6 +33,26 @@ class PortDisplaySpec:
order: int = 0 order: int = 0
@dataclass(frozen=True)
class ParameterGroupDisplaySpec:
"""Ordered, collapsible presentation group for component parameters."""
id: str
label: str
parameters: tuple[str, ...]
order: int = 0
default_expanded: bool = False
def as_catalog_dict(self) -> dict[str, object]:
return {
"id": self.id,
"label": self.label,
"parameters": list(self.parameters),
"order": self.order,
"defaultExpanded": self.default_expanded,
}
@dataclass(frozen=True) @dataclass(frozen=True)
class ComponentDisplaySpec: class ComponentDisplaySpec:
"""Frontend metadata co-located with a component implementation.""" """Frontend metadata co-located with a component implementation."""
@@ -42,6 +63,8 @@ class ComponentDisplaySpec:
symbol: str symbol: str
ports: tuple[PortDisplaySpec, ...] ports: tuple[PortDisplaySpec, ...]
order: int = 0 order: int = 0
role: ComponentCatalogRole | None = None
parameter_groups: tuple[ParameterGroupDisplaySpec, ...] = ()
@property @property
def port_by_name(self) -> dict[str, PortDisplaySpec]: def port_by_name(self) -> dict[str, PortDisplaySpec]:
+1 -1
View File
@@ -10,7 +10,7 @@ EquationOwner = Literal["connection", "component"]
EquationRelation = Literal["equal", "sumToZero", "constitutive", "state"] EquationRelation = Literal["equal", "sumToZero", "constitutive", "state"]
@dataclass(frozen=True) @dataclass(frozen=True, slots=True)
class EquationResidual: class EquationResidual:
"""One executable scalar equation in the pressure-flow subsystem.""" """One executable scalar equation in the pressure-flow subsystem."""
+5
View File
@@ -0,0 +1,5 @@
from __future__ import annotations
class RecoverableTrialStateError(ValueError):
"""A physical-domain failure caused by an integrator trial state."""
+283 -1
View File
@@ -1,6 +1,11 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from math import isfinite
from typing import Protocol, Sequence
from app.simulation.core.errors import RecoverableTrialStateError
from app.simulation.performance import profile_property
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -12,6 +17,110 @@ class ThermodynamicProperties:
h: float h: float
@dataclass(frozen=True)
class ThermodynamicPropertyTangents:
"""Directional derivatives of a recovered thermodynamic state."""
p: tuple[float, ...]
T: tuple[float, ...]
rho: tuple[float, ...]
u: tuple[float, ...]
h: tuple[float, ...]
@property
def width(self) -> int:
return len(self.p)
@classmethod
def zeros(cls, width: int) -> "ThermodynamicPropertyTangents":
values = (0.0,) * width
return cls(p=values, T=values, rho=values, u=values, h=values)
@dataclass(frozen=True)
class ThermodynamicPropertiesLinearization:
"""Primal properties and a validity-checked directional linearization."""
properties: ThermodynamicProperties
tangents: ThermodynamicPropertyTangents
valid: bool = True
reason: str | None = None
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 isentropic_density_pressure_factor(
self,
p: float,
T: float,
downstream_pressure: float | None = None,
) -> float: ...
def dynamic_viscosity(self, T: float) -> float: ...
def diagnostic_dynamic_viscosity(self, T: float) -> float: ...
def specific_internal_energy(self, T: float) -> float: ...
def specific_internal_energy_at_pressure(self, p: float, T: float) -> float: ...
def specific_enthalpy(self, T: float) -> float: ...
def specific_enthalpy_at_pressure(self, p: float, T: float) -> float: ...
def temperature_from_internal_energy(self, u: float) -> float: ...
def temperature_from_enthalpy(self, h: float) -> float: ...
def temperature_from_pressure_enthalpy(self, p: float, 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: ...
def linearize_properties_from_mU(
self,
m: float,
U: float,
V: float,
dm: Sequence[float],
dU: Sequence[float],
dV: Sequence[float],
*,
properties: ThermodynamicProperties | None = None,
) -> ThermodynamicPropertiesLinearization: ...
@dataclass(frozen=True) @dataclass(frozen=True)
class IdealGasMedium: class IdealGasMedium:
"""Temperature-dependent ideal-gas air approximation. """Temperature-dependent ideal-gas air approximation.
@@ -27,6 +136,9 @@ class IdealGasMedium:
cp_ref: float = 1005.0 cp_ref: float = 1005.0
T_ref: float = 300.0 T_ref: float = 300.0
cp_slope: float = 0.0 cp_slope: float = 0.0
viscosity_ref: float = 1.82e-5
viscosity_T_ref: float = 293.15
sutherland_constant: float = 110.4
@property @property
def cv(self) -> float: def cv(self) -> float:
@@ -42,9 +154,47 @@ class IdealGasMedium:
def cv_at_temperature(self, T: float) -> float: def cv_at_temperature(self, T: float) -> float:
return self.cp_at_temperature(T) - self.R_gas return self.cp_at_temperature(T) - self.R_gas
@profile_property("density")
def density(self, p: float, T: float) -> float: def density(self, p: float, T: float) -> float:
return p / (self.R_gas * T) return p / (self.R_gas * T)
@profile_property("isentropic_density_pressure_factor")
def isentropic_density_pressure_factor(
self,
p: float,
T: float,
downstream_pressure: float | None = None,
) -> float:
del p
del downstream_pressure
cp = self.cp_at_temperature(T)
cv = self.cv_at_temperature(T)
return cv / cp
@profile_property("dynamic_viscosity")
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 diagnostic_dynamic_viscosity(self, T: float) -> float:
"""Return the viscosity convention used by derived diagnostics.
Most media use the same transport property for dynamics and reported
diagnostics. Reference-library media may override this without
changing a calibrated constitutive flow relation.
"""
return self.dynamic_viscosity(T)
@profile_property("specific_internal_energy")
def specific_internal_energy(self, T: float) -> float: def specific_internal_energy(self, T: float) -> float:
delta_T = T - self.T_ref delta_T = T - self.T_ref
return ( return (
@@ -53,6 +203,12 @@ class IdealGasMedium:
+ 0.5 * self.cp_slope * delta_T * delta_T + 0.5 * self.cp_slope * delta_T * delta_T
) )
@profile_property("specific_internal_energy_at_pressure")
def specific_internal_energy_at_pressure(self, p: float, T: float) -> float:
del p
return self.specific_internal_energy(T)
@profile_property("specific_enthalpy")
def specific_enthalpy(self, T: float) -> float: def specific_enthalpy(self, T: float) -> float:
delta_T = T - self.T_ref delta_T = T - self.T_ref
return ( return (
@@ -61,6 +217,11 @@ class IdealGasMedium:
+ 0.5 * self.cp_slope * delta_T * delta_T + 0.5 * self.cp_slope * delta_T * delta_T
) )
@profile_property("specific_enthalpy_at_pressure")
def specific_enthalpy_at_pressure(self, p: float, T: float) -> float:
del p
return self.specific_enthalpy(T)
def temperature_from_internal_energy(self, u: float) -> float: def temperature_from_internal_energy(self, u: float) -> float:
reference_internal_energy = self.cv * self.T_ref reference_internal_energy = self.cv * self.T_ref
delta_u = u - reference_internal_energy delta_u = u - reference_internal_energy
@@ -77,9 +238,32 @@ class IdealGasMedium:
delta_T = positive_root if abs(positive_root) <= abs(negative_root) else negative_root delta_T = positive_root if abs(positive_root) <= abs(negative_root) else negative_root
return self.T_ref + delta_T 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
@profile_property("temperature_from_pressure_enthalpy")
def temperature_from_pressure_enthalpy(self, p: float, h: float) -> float:
del p
return self.temperature_from_enthalpy(h)
def temperature_from_mass_internal_energy(self, m: float, U: float) -> float: def temperature_from_mass_internal_energy(self, m: float, U: float) -> float:
if m <= 0.0: if m <= 0.0:
raise ValueError("Mass must stay positive when recovering temperature.") raise RecoverableTrialStateError(
"Mass must stay positive when recovering temperature."
)
return self.temperature_from_internal_energy(U / m) return self.temperature_from_internal_energy(U / m)
def pressure(self, m: float, T: float, V: float) -> float: def pressure(self, m: float, T: float, V: float) -> float:
@@ -87,6 +271,7 @@ class IdealGasMedium:
raise ValueError("Volume must stay positive.") raise ValueError("Volume must stay positive.")
return m * self.R_gas * T / V return m * self.R_gas * T / V
@profile_property("properties_from_mU")
def properties_from_mU(self, m: float, U: float, V: float) -> ThermodynamicProperties: def properties_from_mU(self, m: float, U: float, V: float) -> ThermodynamicProperties:
T = self.temperature_from_mass_internal_energy(m, U) T = self.temperature_from_mass_internal_energy(m, U)
p = self.pressure(m, T, V) p = self.pressure(m, T, V)
@@ -94,3 +279,100 @@ class IdealGasMedium:
u = U / m u = U / m
h = self.specific_enthalpy(T) h = self.specific_enthalpy(T)
return ThermodynamicProperties(p=p, T=T, rho=rho, u=u, h=h) return ThermodynamicProperties(p=p, T=T, rho=rho, u=u, h=h)
def linearize_properties_from_mU(
self,
m: float,
U: float,
V: float,
dm: Sequence[float],
dU: Sequence[float],
dV: Sequence[float],
*,
properties: ThermodynamicProperties | None = None,
) -> ThermodynamicPropertiesLinearization:
"""Linearize properties_from_mU for several seed directions."""
dm_values = tuple(float(value) for value in dm)
dU_values = tuple(float(value) for value in dU)
dV_values = tuple(float(value) for value in dV)
if not (len(dm_values) == len(dU_values) == len(dV_values)):
raise ValueError("Thermodynamic tangent vectors must have equal lengths.")
props = properties or self.properties_from_mU(m, U, V)
width = len(dm_values)
expected_density = m / V
expected_internal_energy = U / m
if (
abs(props.rho - expected_density)
> 1.0e-12 * max(abs(expected_density), 1.0)
or abs(props.u - expected_internal_energy)
> 1.0e-12 * max(abs(expected_internal_energy), 1.0)
):
return ThermodynamicPropertiesLinearization(
properties=props,
tangents=ThermodynamicPropertyTangents.zeros(width),
valid=False,
reason="properties_primal_mismatch",
)
if not all(
isfinite(value)
for values in (dm_values, dU_values, dV_values)
for value in values
):
return ThermodynamicPropertiesLinearization(
properties=props,
tangents=ThermodynamicPropertyTangents.zeros(width),
valid=False,
reason="non_finite_tangent_input",
)
cv = self.cv_at_temperature(props.T)
cp = self.cp_at_temperature(props.T)
if not isfinite(cv) or not isfinite(cp) or cv <= 0.0 or cp <= 0.0:
return ThermodynamicPropertiesLinearization(
properties=props,
tangents=ThermodynamicPropertyTangents.zeros(width),
valid=False,
reason="non_positive_heat_capacity",
)
drho: list[float] = []
du: list[float] = []
dT: list[float] = []
dp: list[float] = []
dh: list[float] = []
for mass_tangent, energy_tangent, volume_tangent in zip(
dm_values,
dU_values,
dV_values,
strict=True,
):
density_tangent = mass_tangent / V - m * volume_tangent / (V * V)
internal_energy_tangent = (
energy_tangent / m - U * mass_tangent / (m * m)
)
temperature_tangent = internal_energy_tangent / cv
pressure_tangent = self.R_gas * (
props.T * density_tangent + props.rho * temperature_tangent
)
enthalpy_tangent = cp * temperature_tangent
drho.append(density_tangent)
du.append(internal_energy_tangent)
dT.append(temperature_tangent)
dp.append(pressure_tangent)
dh.append(enthalpy_tangent)
tangent_values = (*drho, *du, *dT, *dp, *dh)
valid = all(isfinite(value) for value in tangent_values)
return ThermodynamicPropertiesLinearization(
properties=props,
tangents=ThermodynamicPropertyTangents(
p=tuple(dp),
T=tuple(dT),
rho=tuple(drho),
u=tuple(du),
h=tuple(dh),
),
valid=valid,
reason=None if valid else "non_finite_property_tangent",
)
+69 -1
View File
@@ -6,12 +6,21 @@ from typing import Literal
ResultVariableScope = Literal["component", "port"] ResultVariableScope = Literal["component", "port"]
ParameterEditor = Literal[
"amesimGasReference",
"amesimGasPropertyModel",
"choice",
]
SI_UNIT_BY_QUANTITY: dict[str, str] = { SI_UNIT_BY_QUANTITY: dict[str, str] = {
"acceleration": "m/s2",
"area": "m2",
"dimensionless": "", "dimensionless": "",
"density": "kg/m³", "density": "kg/m³",
"flow_coefficient": "kg/(s*Pa^0.5)", "flow_coefficient": "kg/(s*Pa^0.5)",
"force": "N",
"heat_transfer_coefficient": "W/(m2*K)",
"internal_energy": "J", "internal_energy": "J",
"length": "m", "length": "m",
"mass": "kg", "mass": "kg",
@@ -20,13 +29,51 @@ SI_UNIT_BY_QUANTITY: dict[str, str] = {
"specific_enthalpy": "J/kg", "specific_enthalpy": "J/kg",
"specific_internal_energy": "J/kg", "specific_internal_energy": "J/kg",
"temperature": "K", "temperature": "K",
"translational_damping": "N/(m/s)",
"translational_stiffness": "N/m",
"time": "s",
"velocity": "m/s",
"volume": "m3", "volume": "m3",
"volume_flow": "m3/s",
"windage": "N/(m/s)^2",
} }
@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 ParameterCondition:
"""Match when the controlling parameter equals any declared value."""
parameter: str
values: tuple[float, ...]
def as_interface_dict(self) -> dict[str, object]:
return {
"parameter": self.parameter,
"values": list(self.values),
}
@dataclass(frozen=True) @dataclass(frozen=True)
class ParameterDefinition: class ParameterDefinition:
"""User-configurable model input expressed in the backend SI contract.""" """User-configurable model input expressed in the backend SI contract.
Every ``visible_when`` condition must match for the catalog parameter to
be visible; each individual condition matches any one of its ``values``.
"""
name: str name: str
default: float default: float
@@ -36,6 +83,10 @@ class ParameterDefinition:
minimum: float | None = None minimum: float | None = None
maximum: float | None = None maximum: float | None = None
minimum_exclusive: bool = False minimum_exclusive: bool = False
editor: ParameterEditor | None = None
options: tuple[ParameterOption, ...] = ()
description: str = ""
visible_when: tuple[ParameterCondition, ...] = ()
def validation_message(self, value: float) -> str | None: def validation_message(self, value: float) -> str | None:
if not isfinite(value): if not isfinite(value):
@@ -47,6 +98,11 @@ class ParameterDefinition:
return f"must be at least {self.minimum:g}" return f"must be at least {self.minimum:g}"
if self.maximum is not None and value > self.maximum: if self.maximum is not None and value > self.maximum:
return f"must be at most {self.maximum:g}" 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 return None
def as_interface_dict(self, *, value: float | None = None) -> dict[str, object]: def as_interface_dict(self, *, value: float | None = None) -> dict[str, object]:
@@ -62,6 +118,18 @@ class ParameterDefinition:
payload["minimum"] = self.minimum payload["minimum"] = self.minimum
if self.maximum is not None: if self.maximum is not None:
payload["maximum"] = self.maximum 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 self.visible_when:
payload["visibleWhen"] = [
condition.as_interface_dict() for condition in self.visible_when
]
if value is not None: if value is not None:
payload["value"] = value payload["value"] = value
return payload return payload
+424
View File
@@ -0,0 +1,424 @@
from __future__ import annotations
from app.simulation.core.errors import RecoverableTrialStateError
from dataclasses import dataclass
from math import acos, cos, isfinite, log, pi, sqrt
from app.simulation.performance import profile_property
UNIVERSAL_GAS_CONSTANT = 8.31446261815324
# Simcenter Amesim 2404 ``sag_reinit_eos_`` keeps more digits than the
# commonly printed Peng-Robinson constants 0.45724 and 0.07780.
PENG_ROBINSON_A_COEFFICIENT = 0.457235583
PENG_ROBINSON_B_COEFFICIENT = 0.07779607
@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 (
PENG_ROBINSON_A_COEFFICIENT
* UNIVERSAL_GAS_CONSTANT
* UNIVERSAL_GAS_CONSTANT
* self.critical_temperature
* self.critical_temperature
/ self.critical_pressure
)
@property
def b_parameter(self) -> float:
return (
PENG_ROBINSON_B_COEFFICIENT
* 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 alpha_temperature_second_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 (
self.kappa
/ (2.0 * self.critical_temperature * self.critical_temperature)
* (
self.kappa / reduced_temperature
+ alpha_base / (reduced_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 attractive_parameter_temperature_second_derivative(
self,
temperature: float,
) -> float:
return self.a_parameter * self.alpha_temperature_second_derivative(temperature)
@profile_property(
"pressure_from_molar_volume",
layer="kernel",
minimum_mode="audit",
)
def pressure_from_molar_volume(self, temperature: float, molar_volume: float) -> float:
self._validate_temperature(temperature)
if molar_volume <= self.b_parameter:
raise RecoverableTrialStateError("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
@profile_property(
"pressure_from_density",
layer="kernel",
minimum_mode="audit",
)
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)
@profile_property(
"pressure_temperature_derivative_at_density",
layer="kernel",
minimum_mode="audit",
)
def pressure_temperature_derivative_at_density(
self,
temperature: float,
density: float,
) -> float:
self._validate_temperature(temperature)
if density <= 0.0:
raise ValueError("Density must be positive.")
molar_volume = self.molar_mass / density
if molar_volume <= self.b_parameter:
raise RecoverableTrialStateError(
"Molar volume must be larger than Peng-Robinson b parameter."
)
b = self.b_parameter
denominator = molar_volume * (molar_volume + b) + b * (molar_volume - b)
return (
UNIVERSAL_GAS_CONSTANT / (molar_volume - b)
- self.attractive_parameter_temperature_derivative(temperature) / denominator
)
@profile_property(
"pressure_density_derivative_at_temperature",
layer="kernel",
minimum_mode="audit",
)
def pressure_density_derivative_at_temperature(
self,
temperature: float,
density: float,
) -> float:
self._validate_temperature(temperature)
if density <= 0.0:
raise ValueError("Density must be positive.")
molar_volume = self.molar_mass / density
if molar_volume <= self.b_parameter:
raise RecoverableTrialStateError(
"Molar volume must be larger than Peng-Robinson b parameter."
)
b = self.b_parameter
denominator = molar_volume * (molar_volume + b) + b * (molar_volume - b)
pressure_molar_volume_derivative = (
-UNIVERSAL_GAS_CONSTANT * temperature / (molar_volume - b) ** 2
+ self.attractive_parameter(temperature)
* 2.0
* (molar_volume + b)
/ denominator**2
)
molar_volume_density_derivative = -self.molar_mass / (density * density)
return pressure_molar_volume_derivative * molar_volume_density_derivative
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
@profile_property(
"compressibility_roots",
layer="kernel",
minimum_mode="audit",
)
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
@profile_property(
"compressibility_factor",
layer="kernel",
minimum_mode="audit",
)
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}")
@profile_property(
"molar_volume",
layer="kernel",
minimum_mode="audit",
)
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
@profile_property("density", layer="kernel", minimum_mode="audit")
def density(
self,
pressure: float,
temperature: float,
phase: str = "vapor",
) -> float:
return self.molar_mass / self.molar_volume(pressure, temperature, phase=phase)
@profile_property(
"residual_specific_enthalpy",
layer="kernel",
minimum_mode="audit",
)
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
@profile_property(
"residual_specific_internal_energy_at_density",
layer="kernel",
minimum_mode="audit",
)
def residual_specific_internal_energy_at_density(
self,
temperature: float,
density: float,
) -> float:
"""Return Peng-Robinson internal-energy departure, J/kg."""
self._validate_temperature(temperature)
if density <= 0.0:
raise ValueError("Density must be positive.")
molar_volume = self.molar_mass / density
b = self.b_parameter
if molar_volume <= b:
raise RecoverableTrialStateError(
"Molar volume must be larger than Peng-Robinson b parameter."
)
attractive = self.attractive_parameter(temperature)
d_attractive_d_temperature = (
self.attractive_parameter_temperature_derivative(temperature)
)
log_argument = (
molar_volume + (1.0 + sqrt(2.0)) * b
) / (
molar_volume + (1.0 - sqrt(2.0)) * b
)
residual_molar_internal_energy = (
temperature * d_attractive_d_temperature - attractive
) * log(log_argument) / (2.0 * sqrt(2.0) * b)
return residual_molar_internal_energy / self.molar_mass
@profile_property(
"residual_isochoric_heat_capacity_at_density",
layer="kernel",
minimum_mode="audit",
)
def residual_isochoric_heat_capacity_at_density(
self,
temperature: float,
density: float,
) -> float:
"""Return the constant-volume heat-capacity departure, J/kg/K."""
self._validate_temperature(temperature)
if density <= 0.0:
raise ValueError("Density must be positive.")
molar_volume = self.molar_mass / density
b = self.b_parameter
if molar_volume <= b:
raise RecoverableTrialStateError(
"Molar volume must be larger than Peng-Robinson b parameter."
)
log_argument = (
molar_volume + (1.0 + sqrt(2.0)) * b
) / (
molar_volume + (1.0 - sqrt(2.0)) * b
)
residual_molar_cv = (
temperature
* self.attractive_parameter_temperature_second_derivative(temperature)
* log(log_argument)
/ (2.0 * sqrt(2.0) * b)
)
return residual_molar_cv / self.molar_mass
@staticmethod
def _validate_temperature(temperature: float) -> None:
if temperature <= 0.0:
raise RecoverableTrialStateError("Temperature must be positive.")
@classmethod
def _validate_pressure_temperature(cls, pressure: float, temperature: float) -> None:
if pressure <= 0.0:
raise RecoverableTrialStateError("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,
# Simcenter Amesim 2404 helium_eos.data.
acentric_factor=-0.382,
)
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)
+96
View File
@@ -87,6 +87,96 @@ class PortDefinition:
unit="J/kg", unit="J/kg",
order=30, order=30,
), ),
PortVariableDefinition(
"volume",
"signal",
"directed",
label="外部容积",
quantity="volume",
unit="m3",
result_visible=False,
order=40,
),
PortVariableDefinition(
"volume_flow",
"signal",
"directed",
label="外部容积变化率",
quantity="volume_flow",
unit="m3/s",
result_visible=False,
order=50,
),
),
)
@classmethod
def mechanical_translational(
cls,
name: str,
*,
nominal_role: Literal["inlet", "outlet", "bidirectional"] = "bidirectional",
) -> PortDefinition:
return cls(
name=name,
kind="physical",
domain="mechanical",
nominal_role=nominal_role,
positive_flow_direction="intoComponent",
variables=(
PortVariableDefinition(
"x",
"effort",
"equal",
label="位移",
quantity="length",
unit="m",
order=10,
),
PortVariableDefinition(
"v",
"effort",
"equal",
label="速度",
quantity="velocity",
unit="m/s",
order=20,
),
PortVariableDefinition(
"f",
"flow",
"sumToZero",
label="力",
quantity="force",
unit="N",
order=30,
),
),
)
@classmethod
def signal(
cls,
name: str,
*,
nominal_role: Literal["input", "output"],
domain: str = "signal",
) -> PortDefinition:
return cls(
name=name,
kind="signal",
domain=domain,
nominal_role=nominal_role,
variables=(
PortVariableDefinition(
"signal",
"signal",
"directed",
label="信号值",
quantity="dimensionless",
unit="",
order=10,
),
), ),
) )
@@ -108,6 +198,12 @@ class PortState:
p: float = 0.0 p: float = 0.0
m_flow: float = 0.0 m_flow: float = 0.0
h_outflow: float = 0.0 h_outflow: float = 0.0
volume: float = 0.0
volume_flow: float = 0.0
signal: float = 0.0
x: float = 0.0
v: float = 0.0
f: float = 0.0
definition: PortDefinition | None = field(default=None, repr=False, compare=False) definition: PortDefinition | None = field(default=None, repr=False, compare=False)
@classmethod @classmethod
@@ -0,0 +1,25 @@
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,
prepare_test_mql_run,
run_prepared_test_mql,
run_test_mql,
)
__all__ = [
"PreparedTestMqlRun",
"TestMqlRunConfig",
"TestMqlRunResult",
"TestMqlSimulationResult",
"TestMqlSystem",
"prepare_test_mql_run",
"run_prepared_test_mql",
"run_test_mql",
]
@@ -0,0 +1,77 @@
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
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 app.simulation.reporting.test_mql_output_schema import (
TestMqlOutputSchema,
build_test_mql_output_schema,
)
from app.simulation.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,
)
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,468 @@
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
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 app.simulation.reporting.test_mql_output_schema import (
TestMqlOutputSchema,
build_test_mql_output_schema,
)
from app.simulation.reporting.test_mql_output_validation import (
TestMqlValidatedOutput,
compare_validated_test_mql_output,
validate_test_mql_output,
)
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 app.simulation.examples.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
+151
View File
@@ -0,0 +1,151 @@
from __future__ import annotations
import ast
import operator
from dataclasses import dataclass
from math import isfinite
from typing import Any
from app.simulation.core.peng_robinson import HELIUM_PR, PengRobinsonFluid
from app.simulation.examples.test_mql.system 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)}")
@@ -0,0 +1,451 @@
from __future__ import annotations
import re
import tarfile
from dataclasses import dataclass
from pathlib import Path
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
@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"<LINE>.*?</LINE>", 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"<LINE>.*?</LINE>", 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"<LINE>.*?</LINE>", 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"<LINE>.*?</LINE>", 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}>.*?</{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"<IVAR>.*?</IVAR>", 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"<EVAR>.*?</EVAR>", 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}>(.*?)</{tag_name}>", block, flags=re.DOTALL)
return match.group(1).strip() if match is not None else None
+102
View File
@@ -0,0 +1,102 @@
from __future__ import annotations
import re
from collections import Counter
from dataclasses import dataclass
from app.simulation.reporting.amesim_results import AmesimResults
from app.simulation.reporting.test_mql_variables import (
TestMqlVariableCatalog,
build_test_mql_variable_catalog,
)
from app.simulation.examples.test_mql.system 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
@@ -0,0 +1,544 @@
from __future__ import annotations
from dataclasses import dataclass
from app.simulation.examples.test_mql.primitives.mechanical import (
AmesimElasticEndstop,
AmesimMassFrictionEndstops,
AmesimPistonGeometry,
circular_area,
mm_to_m,
)
from app.simulation.reporting.amesim_results import AmesimResults
from app.simulation.reporting.test_mql_variables import (
TestMqlVariableCatalog,
build_test_mql_variable_catalog,
)
from app.simulation.examples.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=int(component.parameter_value("useFriction")) == 2,
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)
+215
View File
@@ -0,0 +1,215 @@
from __future__ import annotations
from dataclasses import dataclass
from app.simulation.examples.test_mql.system 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"
}
@@ -0,0 +1,273 @@
from __future__ import annotations
from dataclasses import dataclass
from app.simulation.examples.test_mql.primitives.pneumatic import (
HELIUM_PNEUMATIC_GAS,
AmesimPneumaticGas,
AmesimPneumaticOrifice,
AmesimPneumaticVolume,
AmesimVariablePneumaticVolume,
)
from app.simulation.examples.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 app.simulation.examples.test_mql.system 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
@@ -0,0 +1,194 @@
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from app.simulation.examples.test_mql.primitives.pneumatic import (
HELIUM_PNEUMATIC_GAS,
AmesimPneumaticGas,
)
from app.simulation.examples.test_mql.primitives.pneumatic_lines import (
AmesimPnl0001Pipe,
AmesimPnl0002Pipe,
AmesimPnl0003Pipe,
AmesimPnl00rPipe,
)
from app.simulation.examples.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)
@@ -0,0 +1,128 @@
from __future__ import annotations
from app.simulation.examples.test_mql.closure import TestMqlPneumaticChamberSegmentSpec
from app.simulation.examples.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,
}
@@ -0,0 +1 @@
"""Calibrated component primitives used only by the ``test_mql`` example."""
@@ -0,0 +1,168 @@
from __future__ import annotations
from dataclasses import dataclass
from math import pi
MM_TO_M = 1.0e-3
M_TO_MM = 1.0e3
M3_TO_CM3 = 1.0e6
M3_PER_S_TO_L_PER_MIN = 60_000.0
def circular_area(diameter_m: float) -> float:
if diameter_m < 0.0:
raise ValueError("diameter_m must be non-negative.")
return pi * diameter_m * diameter_m / 4.0
def mm_to_m(value: float) -> float:
return value * MM_TO_M
def m_to_mm(value: float) -> float:
return value * M_TO_MM
@dataclass(frozen=True)
class AmesimPistonGeometry:
"""Geometry relations used by AMESim PNRP17 pneumatic piston variables."""
piston_diameter_m: float
rod_diameter_m: float = 0.0
zero_length_m: float = 0.0
@property
def piston_area_m2(self) -> float:
return circular_area(self.piston_diameter_m)
@property
def rod_area_m2(self) -> float:
return circular_area(self.rod_diameter_m)
@property
def annulus_area_m2(self) -> float:
return self.piston_area_m2 - self.rod_area_m2
def chamber_length_m(self, port4_displacement_m: float, port5_displacement_m: float) -> float:
return self.zero_length_m + port5_displacement_m - port4_displacement_m
def chamber_length_mm(self, port4_displacement_m: float, port5_displacement_m: float) -> float:
return m_to_mm(self.chamber_length_m(port4_displacement_m, port5_displacement_m))
@property
def chamber_area_m2(self) -> float:
return self.annulus_area_m2
def chamber_volume_m3(self, port4_displacement_m: float, port5_displacement_m: float) -> float:
return self.chamber_area_m2 * self.chamber_length_m(
port4_displacement_m,
port5_displacement_m,
)
def chamber_volume_cm3(self, port4_displacement_m: float, port5_displacement_m: float) -> float:
return self.chamber_volume_m3(port4_displacement_m, port5_displacement_m) * M3_TO_CM3
def chamber_volume_rate_m3_s(self, port4_velocity_m_s: float, port5_velocity_m_s: float) -> float:
return self.chamber_area_m2 * (port5_velocity_m_s - port4_velocity_m_s)
def chamber_volume_rate_l_min(self, port4_velocity_m_s: float, port5_velocity_m_s: float) -> float:
return self.chamber_volume_rate_m3_s(
port4_velocity_m_s,
port5_velocity_m_s,
) * M3_PER_S_TO_L_PER_MIN
@dataclass(frozen=True)
class AmesimElasticEndstop:
"""Contact force part of AMESim LSTP00A elastic endstop."""
contact_stiffness_n_per_m: float
contact_damping_n_per_m_per_s: float = 0.0
gap0_m: float = 0.0
def penetration_m_from_gap_mm(self, gap_mm: float) -> float:
return max(-(mm_to_m(gap_mm) - self.gap0_m), 0.0)
def static_contact_force(self, gap_mm: float) -> float:
return self.contact_stiffness_n_per_m * self.penetration_m_from_gap_mm(gap_mm)
def contact_force(self, gap_mm: float, penetration_velocity_m_s: float = 0.0) -> float:
if self.penetration_m_from_gap_mm(gap_mm) <= 0.0:
return 0.0
damping_force = self.contact_damping_n_per_m_per_s * penetration_velocity_m_s
return max(self.static_contact_force(gap_mm) + damping_force, 0.0)
@dataclass(frozen=True)
class AmesimMassFrictionEndstops:
"""Parameter and observable helpers for AMESim MECMAS21 translation masses."""
mass_kg: float
lower_limit_m: float
upper_limit_m: float
lower_stiffness_n_per_m: float
upper_stiffness_n_per_m: float
lower_damping_n_per_m_per_s: float = 0.0
upper_damping_n_per_m_per_s: float = 0.0
viscous_friction_n_per_m_per_s: float = 0.0
coulomb_friction_n: float = 0.0
stiction_force_n: float = 0.0
windage_n_per_m2_per_s2: float = 0.0
def lower_penetration_m(self, displacement_m: float) -> float:
return max(self.lower_limit_m - displacement_m, 0.0)
def upper_penetration_m(self, displacement_m: float) -> float:
return max(displacement_m - self.upper_limit_m, 0.0)
def lower_static_force_magnitude(self, displacement_m: float) -> float:
return self.lower_stiffness_n_per_m * self.lower_penetration_m(displacement_m)
def upper_static_force_magnitude(self, displacement_m: float) -> float:
return self.upper_stiffness_n_per_m * self.upper_penetration_m(displacement_m)
def viscous_friction_force(self, velocity_m_s: float) -> float:
return -self.viscous_friction_n_per_m_per_s * velocity_m_s
def windage_force(self, velocity_m_s: float) -> float:
return -self.windage_n_per_m2_per_s2 * velocity_m_s * abs(velocity_m_s)
def dry_friction_force(self, velocity_m_s: float) -> float:
if velocity_m_s > 0.0:
return -self.coulomb_friction_n
if velocity_m_s < 0.0:
return self.coulomb_friction_n
return 0.0
def limit_contact_force(self, displacement_m: float, velocity_m_s: float) -> float:
lower_force = self.lower_static_force_magnitude(displacement_m)
if lower_force > 0.0:
lower_force += max(-self.lower_damping_n_per_m_per_s * velocity_m_s, 0.0)
upper_force = self.upper_static_force_magnitude(displacement_m)
if upper_force > 0.0:
upper_force += max(self.upper_damping_n_per_m_per_s * velocity_m_s, 0.0)
return lower_force - upper_force
def derivatives(
self,
*,
velocity_m_s: float,
displacement_m: float,
port_1_force_n: float = 0.0,
port_2_force_n: float = 0.0,
external_force_n: float = 0.0,
) -> tuple[float, float]:
total_force = (
port_1_force_n
+ port_2_force_n
+ external_force_n
+ self.viscous_friction_force(velocity_m_s)
+ self.windage_force(velocity_m_s)
+ self.dry_friction_force(velocity_m_s)
+ self.limit_contact_force(displacement_m, velocity_m_s)
)
return total_force / self.mass_kg, velocity_m_s
@@ -0,0 +1,437 @@
from __future__ import annotations
from dataclasses import dataclass
from math import pi, sqrt
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)
class AmesimPneumaticGas:
"""Caloric constants plus Peng-Robinson EOS for AMESim pneumatic components."""
fluid: PengRobinsonFluid = HELIUM_PR
cp: float = 5193.0
cv: float = 3116.0
@property
def gamma(self) -> float:
return self.cp / self.cv
@property
def R_gas(self) -> float:
return self.fluid.specific_gas_constant
def density(self, pressure: float, temperature: float) -> float:
return self.fluid.density(pressure, temperature)
def pressure(self, density: float, temperature: float) -> float:
return self.fluid.pressure_from_density(temperature, density)
def specific_internal_energy(self, temperature: float) -> float:
return self.cv * temperature
def specific_enthalpy(self, temperature: float) -> float:
return self.cp * temperature
def specific_reference_enthalpy(
self,
temperature: float,
reference_temperature: float = 298.15,
) -> float:
return self.cp * (temperature - reference_temperature)
def reference_temperature_from_specific_enthalpy(
self,
specific_enthalpy: float,
reference_temperature: float = 298.15,
) -> float:
if self.cp <= 0.0:
raise ValueError("cp must be positive.")
return reference_temperature + specific_enthalpy / self.cp
def pressure_reference_enthalpy(
self,
pressure: float,
temperature: float,
reference_pressure: float = 101_300.0,
reference_temperature: float = 298.15,
) -> float:
return (
self.specific_reference_enthalpy(temperature, reference_temperature)
+ self.fluid.residual_specific_enthalpy(pressure, temperature)
- self.fluid.residual_specific_enthalpy(
reference_pressure,
reference_temperature,
)
)
def pressure_transport_enthalpy(
self,
pressure: float,
temperature: float,
reference_pressure: float = 101_300.0,
reference_temperature: float = 298.15,
) -> float:
"""Convert AMESim reference enthalpy to the absolute-energy state basis."""
return (
self.pressure_reference_enthalpy(
pressure,
temperature,
reference_pressure,
reference_temperature,
)
+ self.cp * reference_temperature
)
def temperature_from_internal_energy(self, specific_internal_energy: float) -> float:
if self.cv <= 0.0:
raise ValueError("cv must be positive.")
return specific_internal_energy / self.cv
HELIUM_PNEUMATIC_GAS = AmesimPneumaticGas()
def liters_to_m3(value: float) -> float:
return value * 1.0e-3
def m3_to_cm3(value: float) -> float:
return value * 1.0e6
def cm3_to_m3(value: float) -> float:
return value * 1.0e-6
def kg_to_g(value: float) -> float:
return value * 1.0e3
def mm2_to_m2(value: float) -> float:
return value * 1.0e-6
def diameter_mm_to_area_m2(diameter_mm: float) -> float:
diameter_m = diameter_mm * 1.0e-3
return pi * diameter_m * diameter_m / 4.0
class AmesimPneumaticVolume(DynamicComponent):
"""First-pass AMESim pneumatic control volume using helium PR pressure closure."""
def __init__(
self,
name: str,
volume: float,
gas: AmesimPneumaticGas = HELIUM_PNEUMATIC_GAS,
p0: float = 101_325.0,
T0: float = 293.15,
heat_transfer_coefficient: float = 0.0,
heat_transfer_area: float = 0.0,
external_temperature_k: float = 293.15,
) -> None:
if volume <= 0.0:
raise ValueError("volume must be positive.")
if heat_transfer_coefficient < 0.0:
raise ValueError("heat_transfer_coefficient must be non-negative.")
if heat_transfer_area < 0.0:
raise ValueError("heat_transfer_area must be non-negative.")
if external_temperature_k <= 0.0:
raise ValueError("external_temperature_k must be positive.")
super().__init__(name=name)
self.volume = volume
self.gas = gas
self.heat_transfer_coefficient = heat_transfer_coefficient
self.heat_transfer_area = heat_transfer_area
self.external_temperature = external_temperature_k
rho0 = gas.density(p0, T0)
m0 = rho0 * volume
U0 = m0 * gas.specific_internal_energy(T0)
self.state = VolumeState(m=m0, U=U0)
self.port_a = PortState()
self.port_b = PortState()
@classmethod
def from_liters(
cls,
name: str,
volume_liters: float,
gas: AmesimPneumaticGas = HELIUM_PNEUMATIC_GAS,
p0: float = 101_325.0,
T0: float = 293.15,
heat_transfer_coefficient: float = 0.0,
heat_transfer_area: float = 0.0,
external_temperature_k: float = 293.15,
) -> "AmesimPneumaticVolume":
return cls(
name=name,
volume=liters_to_m3(volume_liters),
gas=gas,
p0=p0,
T0=T0,
heat_transfer_coefficient=heat_transfer_coefficient,
heat_transfer_area=heat_transfer_area,
external_temperature_k=external_temperature_k,
)
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 volume_cm3(self) -> float:
return m3_to_cm3(self.volume)
def volume_rate_m3_s(self) -> float:
return 0.0
def thermal_energy_flow_w(self, temperature_k: float | None = None) -> float:
temperature = self.properties().T if temperature_k is None else temperature_k
return (
self.heat_transfer_coefficient
* self.heat_transfer_area
* (self.external_temperature - temperature)
)
def gas_mass_g(self) -> float:
return kg_to_g(self.state.m)
def pressure_gauge_pa(self, reference_pressure_pa: float = 101_300.0) -> float:
return self.properties().p - reference_pressure_pa
def properties(self) -> ThermodynamicProperties:
if self.state.m <= 0.0:
raise ValueError("volume mass must stay positive.")
T = self.gas.temperature_from_internal_energy(self.state.U / self.state.m)
rho = self.state.m / self.volume
p = self.gas.pressure(rho, T)
u = self.state.U / self.state.m
h = self.gas.specific_enthalpy(T)
self.port_a.p = p
self.port_a.h_outflow = h
self.port_b.p = p
self.port_b.h_outflow = h
return ThermodynamicProperties(p=p, T=T, rho=rho, u=u, h=h)
def derivatives(self, inlet_h: float, m_flow: float) -> VolumeState:
return VolumeState(
m=m_flow,
U=m_flow * inlet_h + self.thermal_energy_flow_w(),
)
def derivatives_from_two_connections(
self,
*,
port_a_m_flow: float,
connected_h_a: float,
port_b_m_flow: float,
connected_h_b: float,
internal_h: float,
volume_rate_m3_s: float | None = None,
) -> VolumeState:
properties = self.properties()
inlet_h_a = self.connection_inlet_enthalpy(
port_m_flow=port_a_m_flow,
connected_h=connected_h_a,
internal_h=internal_h,
)
inlet_h_b = self.connection_inlet_enthalpy(
port_m_flow=port_b_m_flow,
connected_h=connected_h_b,
internal_h=internal_h,
)
return VolumeState(
m=port_a_m_flow + port_b_m_flow,
U=(
port_a_m_flow * inlet_h_a
+ port_b_m_flow * inlet_h_b
+ self.thermal_energy_flow_w(properties.T)
- properties.p * (
self.volume_rate_m3_s()
if volume_rate_m3_s is None
else volume_rate_m3_s
)
),
)
class AmesimVariablePneumaticVolume(AmesimPneumaticVolume):
"""PNCH012-style volume with a dead volume plus an external moving volume."""
def __init__(
self,
name: str,
dead_volume: float,
gas: AmesimPneumaticGas = HELIUM_PNEUMATIC_GAS,
p0: float = 101_325.0,
T0: float = 293.15,
external_volume: float = 0.0,
heat_transfer_coefficient: float = 0.0,
heat_transfer_area: float = 0.0,
external_temperature_k: float = 293.15,
) -> None:
if dead_volume <= 0.0:
raise ValueError("dead_volume must be positive.")
if dead_volume + external_volume <= 0.0:
raise ValueError("total volume must be positive.")
self.dead_volume = dead_volume
self.external_volume = external_volume
self.external_volume_rate = 0.0
super().__init__(
name=name,
volume=dead_volume + external_volume,
gas=gas,
p0=p0,
T0=T0,
heat_transfer_coefficient=heat_transfer_coefficient,
heat_transfer_area=heat_transfer_area,
external_temperature_k=external_temperature_k,
)
@classmethod
def from_liters(
cls,
name: str,
dead_volume_liters: float,
gas: AmesimPneumaticGas = HELIUM_PNEUMATIC_GAS,
p0: float = 101_325.0,
T0: float = 293.15,
external_volume_liters: float = 0.0,
heat_transfer_coefficient: float = 0.0,
heat_transfer_area: float = 0.0,
external_temperature_k: float = 293.15,
) -> "AmesimVariablePneumaticVolume":
return cls(
name=name,
dead_volume=liters_to_m3(dead_volume_liters),
gas=gas,
p0=p0,
T0=T0,
external_volume=liters_to_m3(external_volume_liters),
heat_transfer_coefficient=heat_transfer_coefficient,
heat_transfer_area=heat_transfer_area,
external_temperature_k=external_temperature_k,
)
def volume_rate_m3_s(self) -> float:
return self.external_volume_rate
def set_external_volume_m3(
self,
external_volume: float,
external_volume_rate_m3_s: float = 0.0,
) -> None:
if self.dead_volume + external_volume <= 0.0:
raise ValueError("total volume must be positive.")
self.external_volume = external_volume
self.external_volume_rate = external_volume_rate_m3_s
self.volume = self.dead_volume + self.external_volume
class AmesimPneumaticOrifice(AlgebraicComponent):
"""First-pass PNOR001/PNVO001-style compressible helium orifice.
This is a calibrated placeholder boundary for the Python port. It preserves
AMESim-style area and coefficient inputs, but final parity must be checked
against AMESim CSV results before treating it as numerically equivalent.
"""
def __init__(
self,
name: str,
area: float,
flow_coefficient: float = 1.0,
gas: AmesimPneumaticGas = HELIUM_PNEUMATIC_GAS,
opening: float = 1.0,
) -> None:
if area < 0.0:
raise ValueError("area must be non-negative.")
if flow_coefficient < 0.0:
raise ValueError("flow_coefficient must be non-negative.")
super().__init__(name=name)
self.area = area
self.flow_coefficient = flow_coefficient
self.gas = gas
self.opening = opening
self.port_a = PortState()
self.port_b = PortState()
@classmethod
def from_mm2(
cls,
name: str,
area_mm2: float,
flow_coefficient: float = 1.0,
gas: AmesimPneumaticGas = HELIUM_PNEUMATIC_GAS,
opening: float = 1.0,
) -> "AmesimPneumaticOrifice":
return cls(
name=name,
area=mm2_to_m2(area_mm2),
flow_coefficient=flow_coefficient,
gas=gas,
opening=opening,
)
@property
def effective_area(self) -> float:
opening = min(max(self.opening, 0.0), 1.0)
return self.area * opening
def mass_flow(self, p_a: float, p_b: float, upstream_temperature: float) -> float:
if p_a == p_b or self.effective_area == 0.0 or self.flow_coefficient == 0.0:
return 0.0
if p_a > p_b:
return compressible_orifice_mass_flow(
upstream_pressure=p_a,
downstream_pressure=p_b,
upstream_temperature=upstream_temperature,
area=self.effective_area,
flow_coefficient=self.flow_coefficient,
gas=self.gas,
)
return -compressible_orifice_mass_flow(
upstream_pressure=p_b,
downstream_pressure=p_a,
upstream_temperature=upstream_temperature,
area=self.effective_area,
flow_coefficient=self.flow_coefficient,
gas=self.gas,
)
def compressible_orifice_mass_flow(
*,
upstream_pressure: float,
downstream_pressure: float,
upstream_temperature: float,
area: float,
flow_coefficient: float,
gas: AmesimPneumaticGas = HELIUM_PNEUMATIC_GAS,
) -> float:
if upstream_pressure <= 0.0 or downstream_pressure < 0.0:
raise ValueError("pressures must be non-negative and upstream pressure must be positive.")
if upstream_temperature <= 0.0:
raise ValueError("upstream_temperature must be positive.")
if area < 0.0 or flow_coefficient < 0.0:
raise ValueError("area and flow_coefficient must be non-negative.")
if downstream_pressure >= upstream_pressure or area == 0.0 or flow_coefficient == 0.0:
return 0.0
gamma = gas.gamma
pressure_ratio = max(downstream_pressure / upstream_pressure, 0.0)
critical_ratio = (2.0 / (gamma + 1.0)) ** (gamma / (gamma - 1.0))
coefficient = flow_coefficient * area * upstream_pressure / sqrt(gas.R_gas * upstream_temperature)
if pressure_ratio <= critical_ratio:
flow_function = sqrt(gamma) * (2.0 / (gamma + 1.0)) ** ((gamma + 1.0) / (2.0 * (gamma - 1.0)))
else:
term = pressure_ratio ** (2.0 / gamma) - pressure_ratio ** ((gamma + 1.0) / gamma)
flow_function = sqrt((2.0 * gamma / (gamma - 1.0)) * max(term, 0.0))
return coefficient * flow_function
@@ -0,0 +1,881 @@
from __future__ import annotations
from dataclasses import dataclass
from math import log10, pi, sqrt
from app.simulation.examples.test_mql.primitives.pneumatic import (
HELIUM_PNEUMATIC_GAS,
AmesimPneumaticGas,
compressible_orifice_mass_flow,
diameter_mm_to_area_m2,
)
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)
class AmesimPnl0001Diagnostics:
mass_flow_kg_s: float
reynolds_number: float
gas_velocity_m_s: float
friction_factor: float
pressure_drop_pa: float
class _DarcyPipeResistanceMixin:
diameter: float
length: float
relative_roughness: float
area: float
def _mass_flow_for_pressure_drop(
self,
pressure_drop_pa: float,
*,
density: float,
temperature: float,
) -> float:
if pressure_drop_pa <= 0.0:
return 0.0
upper = 1.0e-9
while self._darcy_pressure_drop(
upper,
density=density,
temperature=temperature,
) < pressure_drop_pa:
upper *= 10.0
if upper > 1.0e3:
raise ValueError("unable to bracket pneumatic pipe resistance flow")
lower = 0.0
for _ in range(48):
middle = 0.5 * (lower + upper)
if self._darcy_pressure_drop(
middle,
density=density,
temperature=temperature,
) < pressure_drop_pa:
lower = middle
else:
upper = middle
return 0.5 * (lower + upper)
def pn2pipefr_mass_flow(
self,
*,
port_1_pressure_pa: float,
port_1_temperature_k: float,
port_2_pressure_pa: float,
port_2_temperature_k: float,
length: float | None = None,
) -> float:
pressure_difference = port_1_pressure_pa - port_2_pressure_pa
if pressure_difference == 0.0:
return 0.0
upstream_pressure = max(port_1_pressure_pa, port_2_pressure_pa)
downstream_pressure = min(port_1_pressure_pa, port_2_pressure_pa)
upstream_temperature = (
port_1_temperature_k
if pressure_difference > 0.0
else port_2_temperature_k
)
resistance_length = self.length if length is None else length
if resistance_length <= 0.0:
raise ValueError("length must be positive")
def target_flow(mass_flow_kg_s: float) -> float:
reynolds = self._reynolds_number(mass_flow_kg_s, upstream_temperature)
friction_factor = self._friction_factor(reynolds)
flow_coefficient = sqrt(
self.diameter / (resistance_length * friction_factor)
)
return compressible_orifice_mass_flow(
upstream_pressure=upstream_pressure,
downstream_pressure=downstream_pressure,
upstream_temperature=upstream_temperature,
area=self.area,
flow_coefficient=flow_coefficient,
gas=self.gas,
)
flow_coefficient = sqrt(self.diameter / (resistance_length * 0.02))
magnitude = compressible_orifice_mass_flow(
upstream_pressure=upstream_pressure,
downstream_pressure=downstream_pressure,
upstream_temperature=upstream_temperature,
area=self.area,
flow_coefficient=flow_coefficient,
gas=self.gas,
)
for _ in range(12):
next_magnitude = target_flow(magnitude)
if abs(next_magnitude - magnitude) <= max(1.0e-12, abs(magnitude) * 1.0e-9):
magnitude = next_magnitude
break
magnitude = 0.5 * (magnitude + next_magnitude)
return magnitude if pressure_difference > 0.0 else -magnitude
def _darcy_pressure_drop(
self,
mass_flow_kg_s: float,
*,
density: float,
temperature: float,
) -> float:
if mass_flow_kg_s == 0.0:
return 0.0
reynolds = self._reynolds_number(mass_flow_kg_s, temperature)
friction_factor = self._friction_factor(reynolds)
velocity = mass_flow_kg_s / (density * self.area)
magnitude = (
friction_factor
* (self.length / self.diameter)
* density
* velocity
* velocity
/ 2.0
)
return magnitude if mass_flow_kg_s > 0.0 else -magnitude
def _reynolds_number(self, mass_flow_kg_s: float, temperature: float) -> float:
viscosity = helium_dynamic_viscosity(temperature)
return 4.0 * abs(mass_flow_kg_s) / (pi * self.diameter * viscosity)
def _friction_factor(self, reynolds_number: float) -> float:
if reynolds_number <= 0.0:
return 64_000_000.0
laminar = 64.0 / reynolds_number
if reynolds_number <= 2_300.0:
return laminar
turbulent = 1.0 / (
-1.8
* log10(
(self.relative_roughness / 3.7) ** 1.11
+ 6.9 / reynolds_number
)
) ** 2
if reynolds_number >= 4_000.0:
return turbulent
fraction = (reynolds_number - 2_300.0) / 1_700.0
return laminar + fraction * (turbulent - laminar)
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 simulation convention:
positive values enter the pipe storage.
AMESim's proprietary ``pn2pipefr`` utility is represented by an
optional calibrated linear conductance when a model-specific baseline
supports it; otherwise the component falls back to an auditable
Darcy-Weisbach law. Both paths preserve the real geometry, state count,
mass/energy balance, heat-transfer parameter, and observable diagnostics.
"""
def __init__(
self,
name: str,
*,
diameter_mm: float,
length_m: float,
relative_roughness: float,
polytropic_constant: float = 1.35,
heat_transfer_coefficient: float = 0.0,
external_temperature_k: float = 293.15,
calibrated_linear_conductance: float | None = None,
gas: AmesimPneumaticGas = HELIUM_PNEUMATIC_GAS,
p0: float = 101_325.0,
T0: float = 293.15,
) -> None:
if diameter_mm <= 0.0:
raise ValueError("diameter_mm must be positive")
if length_m <= 0.0:
raise ValueError("length_m must be positive")
if relative_roughness < 0.0:
raise ValueError("relative_roughness must be non-negative")
if polytropic_constant <= 0.0:
raise ValueError("polytropic_constant must be positive")
if heat_transfer_coefficient < 0.0:
raise ValueError("heat_transfer_coefficient must be non-negative")
if external_temperature_k <= 0.0:
raise ValueError("external_temperature_k must be positive")
if (
calibrated_linear_conductance is not None
and calibrated_linear_conductance <= 0.0
):
raise ValueError("calibrated_linear_conductance must be positive")
super().__init__(name=name)
self.diameter = diameter_mm * 1.0e-3
self.length = length_m
self.relative_roughness = relative_roughness
self.polytropic_constant = polytropic_constant
self.heat_transfer_coefficient = heat_transfer_coefficient
self.external_temperature = external_temperature_k
self.calibrated_linear_conductance = calibrated_linear_conductance
self.gas = gas
self.area = diameter_mm_to_area_m2(diameter_mm)
self.volume = self.area * self.length
self.heat_transfer_area = pi * self.diameter * self.length
rho0 = gas.density(p0, T0)
mass0 = rho0 * self.volume
self.state = VolumeState(
m=mass0,
U=mass0 * gas.specific_internal_energy(T0),
)
self.port_1 = PortState()
self.port_2 = 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:
if self.state.m <= 0.0:
raise ValueError("pipe mass must stay positive")
temperature = self.gas.temperature_from_internal_energy(
self.state.U / self.state.m
)
density = self.state.m / self.volume
pressure = self.gas.pressure(density, temperature)
properties = ThermodynamicProperties(
p=pressure,
T=temperature,
rho=density,
u=self.state.U / self.state.m,
h=self.gas.specific_enthalpy(temperature),
)
self.port_2.p = pressure
self.port_2.h_outflow = properties.h
return properties
def gas_mass_g(self) -> float:
return self.state.m * 1.0e3
def resistance_mass_flow(
self,
*,
port_1_pressure_pa: float,
port_1_temperature_k: float,
) -> float:
"""Return mass flow from port 1 into the port-2 storage in kg/s."""
if port_1_pressure_pa <= 0.0:
raise ValueError("port_1_pressure_pa must be positive")
if port_1_temperature_k <= 0.0:
raise ValueError("port_1_temperature_k must be positive")
internal = self.properties()
pressure_difference = port_1_pressure_pa - internal.p
if pressure_difference == 0.0:
return 0.0
if self.calibrated_linear_conductance is not None:
return (
self.calibrated_linear_conductance
* pressure_difference
/ sqrt(internal.T)
)
upstream_pressure = max(port_1_pressure_pa, internal.p)
upstream_temperature = (
port_1_temperature_k if pressure_difference > 0.0 else internal.T
)
density = self.gas.density(upstream_pressure, upstream_temperature)
magnitude = self._mass_flow_for_pressure_drop(
abs(pressure_difference),
density=density,
temperature=upstream_temperature,
)
return magnitude if pressure_difference > 0.0 else -magnitude
def diagnostics(
self,
*,
mass_flow_kg_s: float,
temperature_k: float | None = None,
) -> AmesimPnl0001Diagnostics:
properties = self.properties()
temperature = temperature_k or properties.T
reynolds = self._reynolds_number(mass_flow_kg_s, temperature)
friction_factor = self._friction_factor(reynolds)
velocity = mass_flow_kg_s / (properties.rho * self.area)
pressure_drop = self._darcy_pressure_drop(
mass_flow_kg_s,
density=properties.rho,
temperature=temperature,
)
return AmesimPnl0001Diagnostics(
mass_flow_kg_s=mass_flow_kg_s,
reynolds_number=reynolds,
gas_velocity_m_s=velocity,
friction_factor=friction_factor,
pressure_drop_pa=pressure_drop,
)
def darcy_pressure_drop_for_state(
self,
*,
mass_flow_kg_s: float,
pressure_pa: float,
temperature_k: float,
) -> float:
if pressure_pa <= 0.0:
raise ValueError("pressure_pa must be positive")
if temperature_k <= 0.0:
raise ValueError("temperature_k must be positive")
density = self.gas.density(pressure_pa, temperature_k)
return self._darcy_pressure_drop(
mass_flow_kg_s,
density=density,
temperature=temperature_k,
)
def derivatives_from_connections(
self,
*,
port_1_m_flow: float,
connected_h_1: float,
port_2_m_flow: float,
connected_h_2: float,
) -> VolumeState:
internal = self.properties()
# Default first-pass PNL0001 behavior uses the historical internal-energy
# approximation. AMESim-specific transport-enthalpy corrections are kept
# behind derivatives_from_transport_enthalpy_connections so they can be
# applied only where validated against baseline data.
inlet_u_1 = (
connected_h_1 / self.gas.gamma
if port_1_m_flow > 0.0
else internal.u
)
inlet_u_2 = (
connected_h_2 / self.gas.gamma
if port_2_m_flow > 0.0
else internal.u
)
heat_flow = (
self.heat_transfer_coefficient
* self.heat_transfer_area
* (self.external_temperature - internal.T)
)
return VolumeState(
m=port_1_m_flow + port_2_m_flow,
U=port_1_m_flow * inlet_u_1 + port_2_m_flow * inlet_u_2 + heat_flow,
)
def derivatives_from_transport_enthalpy_connections(
self,
*,
port_1_m_flow: float,
connected_h_1: float,
port_2_m_flow: float,
connected_h_2: float,
) -> VolumeState:
internal = self.properties()
inlet_h_1 = connected_h_1 if port_1_m_flow > 0.0 else internal.h
inlet_h_2 = connected_h_2 if port_2_m_flow > 0.0 else internal.h
heat_flow = (
self.heat_transfer_coefficient
* self.heat_transfer_area
* (self.external_temperature - internal.T)
)
return VolumeState(
m=port_1_m_flow + port_2_m_flow,
U=port_1_m_flow * inlet_h_1 + port_2_m_flow * inlet_h_2 + heat_flow,
)
class AmesimPnl0003Pipe(_DarcyPipeResistanceMixin, DynamicComponent):
"""First-pass AMESim ``PNL0003`` (C-R-C) pipe.
The two pipe-end compliances are represented as equal half-volume gas
stores connected by the same auditable Darcy resistance used for PNL0001.
Center flow is positive from port 1 storage to port 2 storage.
"""
state_size = 4
def __init__(
self,
name: str,
*,
diameter_mm: float,
length_m: float,
relative_roughness: float,
polytropic_constant: float = 1.35,
heat_transfer_coefficient: float = 0.0,
external_temperature_k: float = 293.15,
gas: AmesimPneumaticGas = HELIUM_PNEUMATIC_GAS,
p1_0: float = 101_325.0,
T1_0: float = 293.15,
p2_0: float = 101_325.0,
T2_0: float = 293.15,
) -> None:
if diameter_mm <= 0.0:
raise ValueError("diameter_mm must be positive")
if length_m <= 0.0:
raise ValueError("length_m must be positive")
if relative_roughness < 0.0:
raise ValueError("relative_roughness must be non-negative")
if polytropic_constant <= 0.0:
raise ValueError("polytropic_constant must be positive")
if heat_transfer_coefficient < 0.0:
raise ValueError("heat_transfer_coefficient must be non-negative")
if external_temperature_k <= 0.0:
raise ValueError("external_temperature_k must be positive")
super().__init__(name=name)
self.diameter = diameter_mm * 1.0e-3
self.length = length_m
self.relative_roughness = relative_roughness
self.polytropic_constant = polytropic_constant
self.heat_transfer_coefficient = heat_transfer_coefficient
self.external_temperature = external_temperature_k
self.gas = gas
self.area = diameter_mm_to_area_m2(diameter_mm)
self.volume = self.area * self.length
self.compliance_volume = self.volume / 2.0
self.heat_transfer_area = pi * self.diameter * self.length
self.state_1 = self._initial_state(p1_0, T1_0)
self.state_2 = self._initial_state(p2_0, T2_0)
self.port_1 = PortState()
self.port_2 = PortState()
def _initial_state(self, pressure: float, temperature: float) -> VolumeState:
rho = self.gas.density(pressure, temperature)
mass = rho * self.compliance_volume
return VolumeState(
m=mass,
U=mass * self.gas.specific_internal_energy(temperature),
)
def get_state_vector(self) -> list[float]:
return [*self.state_1.as_vector(), *self.state_2.as_vector()]
def set_state_vector(self, values: list[float]) -> None:
if len(values) != 4:
raise ValueError("PNL0003 state vector requires four values")
self.state_1 = VolumeState.from_vector(values[:2])
self.state_2 = VolumeState.from_vector(values[2:])
def properties_1(self) -> ThermodynamicProperties:
properties = self._properties(self.state_1)
self.port_1.p = properties.p
self.port_1.h_outflow = properties.h
return properties
def properties_2(self) -> ThermodynamicProperties:
properties = self._properties(self.state_2)
self.port_2.p = properties.p
self.port_2.h_outflow = properties.h
return properties
def _properties(self, state: VolumeState) -> ThermodynamicProperties:
if state.m <= 0.0:
raise ValueError("pipe mass must stay positive")
temperature = self.gas.temperature_from_internal_energy(state.U / state.m)
density = state.m / self.compliance_volume
pressure = self.gas.pressure(density, temperature)
return ThermodynamicProperties(
p=pressure,
T=temperature,
rho=density,
u=state.U / state.m,
h=self.gas.specific_enthalpy(temperature),
)
def gas_mass_g(self) -> float:
return (self.state_1.m + self.state_2.m) * 1.0e3
def resistance_mass_flow(self) -> float:
"""Return center mass flow from port 1 storage to port 2 storage."""
port_1 = self.properties_1()
port_2 = self.properties_2()
pressure_difference = port_1.p - port_2.p
if pressure_difference == 0.0:
return 0.0
upstream = port_1 if pressure_difference > 0.0 else port_2
magnitude = self._mass_flow_for_pressure_drop(
abs(pressure_difference),
density=upstream.rho,
temperature=upstream.T,
)
return magnitude if pressure_difference > 0.0 else -magnitude
def diagnostics(
self,
*,
mass_flow_kg_s: float,
temperature_k: float | None = None,
) -> AmesimPnl0001Diagnostics:
port_1 = self.properties_1()
port_2 = self.properties_2()
temperature = temperature_k or (port_1.T if mass_flow_kg_s >= 0.0 else port_2.T)
density = port_1.rho if mass_flow_kg_s >= 0.0 else port_2.rho
reynolds = self._reynolds_number(mass_flow_kg_s, temperature)
friction_factor = self._friction_factor(reynolds)
velocity = mass_flow_kg_s / (density * self.area)
pressure_drop = self._darcy_pressure_drop(
mass_flow_kg_s,
density=density,
temperature=temperature,
)
return AmesimPnl0001Diagnostics(
mass_flow_kg_s=mass_flow_kg_s,
reynolds_number=reynolds,
gas_velocity_m_s=velocity,
friction_factor=friction_factor,
pressure_drop_pa=pressure_drop,
)
def derivatives_from_connections(
self,
*,
port_1_m_flow: float,
connected_h_1: float,
port_2_m_flow: float,
connected_h_2: float,
) -> tuple[VolumeState, VolumeState]:
port_1 = self.properties_1()
port_2 = self.properties_2()
center_flow = self.resistance_mass_flow()
heat_flow_each = (
self.heat_transfer_coefficient
* self.heat_transfer_area
* (self.external_temperature - 0.5 * (port_1.T + port_2.T))
/ 2.0
)
port_1_external_h = self.connection_inlet_enthalpy(
port_m_flow=port_1_m_flow,
connected_h=connected_h_1,
internal_h=port_1.h,
)
port_2_external_h = self.connection_inlet_enthalpy(
port_m_flow=port_2_m_flow,
connected_h=connected_h_2,
internal_h=port_2.h,
)
port_1_center_h = self.connection_inlet_enthalpy(
port_m_flow=-center_flow,
connected_h=port_2.h,
internal_h=port_1.h,
)
port_2_center_h = self.connection_inlet_enthalpy(
port_m_flow=center_flow,
connected_h=port_1.h,
internal_h=port_2.h,
)
return (
VolumeState(
m=port_1_m_flow - center_flow,
U=(
port_1_m_flow * port_1_external_h
- center_flow * port_1_center_h
+ heat_flow_each
),
),
VolumeState(
m=port_2_m_flow + center_flow,
U=(
port_2_m_flow * port_2_external_h
+ center_flow * port_2_center_h
+ heat_flow_each
),
),
)
class AmesimPnl0002Pipe(_DarcyPipeResistanceMixin, DynamicComponent):
"""First-pass AMESim ``PNL0002`` (R-C-R) pipe.
The center compliance owns the gas state. Positive connection mass flows
enter that center storage from each external port.
"""
state_size = 2
def __init__(
self,
name: str,
*,
diameter_mm: float,
length_m: float,
relative_roughness: float,
polytropic_constant: float = 1.35,
heat_transfer_coefficient: float = 0.0,
external_temperature_k: float = 293.15,
gas: AmesimPneumaticGas = HELIUM_PNEUMATIC_GAS,
pctr_0: float = 101_325.0,
Tctr_0: float = 293.15,
) -> None:
if diameter_mm <= 0.0:
raise ValueError("diameter_mm must be positive")
if length_m <= 0.0:
raise ValueError("length_m must be positive")
if relative_roughness < 0.0:
raise ValueError("relative_roughness must be non-negative")
if polytropic_constant <= 0.0:
raise ValueError("polytropic_constant must be positive")
if heat_transfer_coefficient < 0.0:
raise ValueError("heat_transfer_coefficient must be non-negative")
if external_temperature_k <= 0.0:
raise ValueError("external_temperature_k must be positive")
super().__init__(name=name)
self.diameter = diameter_mm * 1.0e-3
self.length = length_m
self.relative_roughness = relative_roughness
self.polytropic_constant = polytropic_constant
self.heat_transfer_coefficient = heat_transfer_coefficient
self.external_temperature = external_temperature_k
self.gas = gas
self.area = diameter_mm_to_area_m2(diameter_mm)
self.volume = self.area * self.length
self.heat_transfer_area = pi * self.diameter * self.length
self._resistance_length = self.length / 2.0
rho0 = gas.density(pctr_0, Tctr_0)
mass0 = rho0 * self.volume
self.state = VolumeState(
m=mass0,
U=mass0 * gas.specific_internal_energy(Tctr_0),
)
self.port_1 = PortState()
self.port_2 = 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:
if self.state.m <= 0.0:
raise ValueError("pipe mass must stay positive")
temperature = self.gas.temperature_from_internal_energy(
self.state.U / self.state.m
)
density = self.state.m / self.volume
pressure = self.gas.pressure(density, temperature)
properties = ThermodynamicProperties(
p=pressure,
T=temperature,
rho=density,
u=self.state.U / self.state.m,
h=self.gas.specific_enthalpy(temperature),
)
self.port_1.p = pressure
self.port_1.h_outflow = properties.h
self.port_2.p = pressure
self.port_2.h_outflow = properties.h
return properties
def gas_mass_g(self) -> float:
return self.state.m * 1.0e3
def port_mass_flow(
self,
*,
port_pressure_pa: float,
port_temperature_k: float,
) -> float:
"""Return mass flow from an external port into the center storage."""
if port_pressure_pa <= 0.0:
raise ValueError("port_pressure_pa must be positive")
if port_temperature_k <= 0.0:
raise ValueError("port_temperature_k must be positive")
center = self.properties()
pressure_difference = port_pressure_pa - center.p
if pressure_difference == 0.0:
return 0.0
upstream_pressure = max(port_pressure_pa, center.p)
upstream_temperature = (
port_temperature_k if pressure_difference > 0.0 else center.T
)
density = self.gas.density(upstream_pressure, upstream_temperature)
magnitude = self._mass_flow_for_resistance_pressure_drop(
abs(pressure_difference),
density=density,
temperature=upstream_temperature,
)
return magnitude if pressure_difference > 0.0 else -magnitude
def _mass_flow_for_resistance_pressure_drop(
self,
pressure_drop_pa: float,
*,
density: float,
temperature: float,
) -> float:
original_length = self.length
self.length = self._resistance_length
try:
return self._mass_flow_for_pressure_drop(
pressure_drop_pa,
density=density,
temperature=temperature,
)
finally:
self.length = original_length
def diagnostics(
self,
*,
mass_flow_kg_s: float,
temperature_k: float | None = None,
) -> AmesimPnl0001Diagnostics:
properties = self.properties()
temperature = temperature_k or properties.T
reynolds = self._reynolds_number(mass_flow_kg_s, temperature)
friction_factor = self._friction_factor(reynolds)
velocity = mass_flow_kg_s / (properties.rho * self.area)
original_length = self.length
self.length = self._resistance_length
try:
pressure_drop = self._darcy_pressure_drop(
mass_flow_kg_s,
density=properties.rho,
temperature=temperature,
)
finally:
self.length = original_length
return AmesimPnl0001Diagnostics(
mass_flow_kg_s=mass_flow_kg_s,
reynolds_number=reynolds,
gas_velocity_m_s=velocity,
friction_factor=friction_factor,
pressure_drop_pa=pressure_drop,
)
def derivatives_from_connections(
self,
*,
port_1_m_flow: float,
connected_h_1: float,
port_2_m_flow: float,
connected_h_2: float,
) -> VolumeState:
center = self.properties()
inlet_h_1 = self.connection_inlet_enthalpy(
port_m_flow=port_1_m_flow,
connected_h=connected_h_1,
internal_h=center.h,
)
inlet_h_2 = self.connection_inlet_enthalpy(
port_m_flow=port_2_m_flow,
connected_h=connected_h_2,
internal_h=center.h,
)
heat_flow = (
self.heat_transfer_coefficient
* self.heat_transfer_area
* (self.external_temperature - center.T)
)
return VolumeState(
m=port_1_m_flow + port_2_m_flow,
U=port_1_m_flow * inlet_h_1 + port_2_m_flow * inlet_h_2 + heat_flow,
)
class AmesimPnl00rPipe(_DarcyPipeResistanceMixin, AlgebraicComponent):
"""First-pass AMESim ``PNL00R`` (R) pipe resistance."""
def __init__(
self,
name: str,
*,
diameter_mm: float,
length_m: float,
relative_roughness: float,
gas: AmesimPneumaticGas = HELIUM_PNEUMATIC_GAS,
) -> None:
if diameter_mm <= 0.0:
raise ValueError("diameter_mm must be positive")
if length_m <= 0.0:
raise ValueError("length_m must be positive")
if relative_roughness < 0.0:
raise ValueError("relative_roughness must be non-negative")
super().__init__(name=name)
self.diameter = diameter_mm * 1.0e-3
self.length = length_m
self.relative_roughness = relative_roughness
self.gas = gas
self.area = diameter_mm_to_area_m2(diameter_mm)
self.port_1 = PortState()
self.port_2 = PortState()
def mass_flow(
self,
*,
port_1_pressure_pa: float,
port_1_temperature_k: float,
port_2_pressure_pa: float,
port_2_temperature_k: float,
) -> float:
"""Return mass flow from port 1 to port 2 in kg/s."""
if port_1_pressure_pa <= 0.0 or port_2_pressure_pa <= 0.0:
raise ValueError("port pressures must be positive")
if port_1_temperature_k <= 0.0 or port_2_temperature_k <= 0.0:
raise ValueError("port temperatures must be positive")
pressure_difference = port_1_pressure_pa - port_2_pressure_pa
if pressure_difference == 0.0:
return 0.0
upstream_pressure = max(port_1_pressure_pa, port_2_pressure_pa)
upstream_temperature = (
port_1_temperature_k
if pressure_difference > 0.0
else port_2_temperature_k
)
density = self.gas.density(upstream_pressure, upstream_temperature)
magnitude = self._mass_flow_for_pressure_drop(
abs(pressure_difference),
density=density,
temperature=upstream_temperature,
)
return magnitude if pressure_difference > 0.0 else -magnitude
def diagnostics(
self,
*,
mass_flow_kg_s: float,
pressure_pa: float,
temperature_k: float,
) -> AmesimPnl0001Diagnostics:
density = self.gas.density(pressure_pa, temperature_k)
reynolds = self._reynolds_number(mass_flow_kg_s, temperature_k)
friction_factor = self._friction_factor(reynolds)
velocity = mass_flow_kg_s / (density * self.area)
pressure_drop = self._darcy_pressure_drop(
mass_flow_kg_s,
density=density,
temperature=temperature_k,
)
return AmesimPnl0001Diagnostics(
mass_flow_kg_s=mass_flow_kg_s,
reynolds_number=reynolds,
gas_velocity_m_s=velocity,
friction_factor=friction_factor,
pressure_drop_pa=pressure_drop,
)
def helium_dynamic_viscosity(temperature_k: float) -> float:
"""Sutherland approximation centered on the test_mql initial condition."""
if temperature_k <= 0.0:
raise ValueError("temperature_k must be positive")
reference_temperature = 293.15
reference_viscosity = 2.0e-5
sutherland_constant = 79.4
return (
reference_viscosity
* (temperature_k / reference_temperature) ** 1.5
* (reference_temperature + sutherland_constant)
/ (temperature_k + sutherland_constant)
)
+100
View File
@@ -0,0 +1,100 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import UTC, datetime
from pathlib import Path
from app.simulation.paths import PROJECT_ROOT, SIMULATION_RUNS_DIR
from app.simulation.examples.test_mql.system import (
TestMqlRunConfig,
TestMqlSimulationResult,
TestMqlSystem,
)
@dataclass(frozen=True)
class PreparedTestMqlRun:
run_config: TestMqlRunConfig
repo_root: Path
output_dir: Path
@dataclass(frozen=True)
class TestMqlRunResult:
run_config: TestMqlRunConfig
prepared_run: PreparedTestMqlRun
system: TestMqlSystem
result: TestMqlSimulationResult
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
def prepare_test_mql_run(
*,
run_config: TestMqlRunConfig | None = None,
output_dir: Path | None = None,
) -> PreparedTestMqlRun:
return PreparedTestMqlRun(
run_config=run_config or TestMqlRunConfig(),
repo_root=PROJECT_ROOT,
output_dir=output_dir or _default_run_output_dir(),
)
def run_prepared_test_mql(prepared_run: PreparedTestMqlRun) -> TestMqlRunResult:
system = TestMqlSystem()
result = system.simulate(prepared_run.run_config)
prepared_run.output_dir.mkdir(parents=True, exist_ok=True)
summary_path = prepared_run.output_dir / "test_mql_model_summary.txt"
summary_path.write_text(format_test_mql_summary(system), encoding="utf-8")
return TestMqlRunResult(
run_config=prepared_run.run_config,
prepared_run=prepared_run,
system=system,
result=result,
summary_path=summary_path,
)
def run_test_mql(
*,
run_config: TestMqlRunConfig | None = None,
output_dir: Path | None = None,
) -> TestMqlRunResult:
return run_prepared_test_mql(
prepare_test_mql_run(run_config=run_config, output_dir=output_dir)
)
def main() -> None:
run = run_test_mql()
print(format_test_mql_summary(run.system), end="")
print(f"Samples: {len(run.result.t)}")
print(f"Output directory: {run.prepared_run.output_dir}")
if __name__ == "__main__":
main()
@@ -0,0 +1,80 @@
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import UTC, datetime
from pathlib import Path
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: AMESIM_TEST_MQL_ARCHIVE_PATH
)
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:
timestamp = datetime.now(UTC).strftime("test_mql_baseline_%Y%m%d_%H%M%S_%f")
return SIMULATION_RUNS_DIR / 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()
File diff suppressed because it is too large. Load diff
@@ -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"]
@@ -0,0 +1,78 @@
from __future__ import annotations
from dataclasses import dataclass
from app.simulation.core.base import Component, DynamicComponent
@dataclass(frozen=True)
class Connection:
source_component: str
source_port: str
target_component: str
target_port: str
class SimulationNetwork:
"""Container for components, topology, and state-vector bookkeeping."""
def __init__(self, name: str) -> None:
self.name = name
self.components: dict[str, Component] = {}
self.connections: list[Connection] = []
def add_component(self, component: Component) -> None:
if component.name in self.components:
raise ValueError(f"Duplicate component name: {component.name}")
self.components[component.name] = component
def connect(
self,
source_component: str,
source_port: str,
target_component: str,
target_port: str,
) -> None:
self.connections.append(
Connection(
source_component=source_component,
source_port=source_port,
target_component=target_component,
target_port=target_port,
)
)
def dynamic_components(self) -> list[DynamicComponent]:
return [
component
for component in self.components.values()
if isinstance(component, DynamicComponent)
]
def initial_state_vector(self) -> list[float]:
values: list[float] = []
for component in self.dynamic_components():
values.extend(component.get_state_vector())
return values
def apply_state_vector(self, values: list[float]) -> None:
cursor = 0
for component in self.dynamic_components():
next_cursor = cursor + component.state_size
component.set_state_vector(values[cursor:next_cursor])
cursor = next_cursor
if cursor != len(values):
raise ValueError("State vector length does not match dynamic components.")
def summary(self) -> str:
lines = [f"Network: {self.name}", "Components:"]
for name, component in self.components.items():
lines.append(f" - {name}: {component.__class__.__name__}")
lines.append("Connections:")
for conn in self.connections:
lines.append(
f" - {conn.source_component}.{conn.source_port}"
f" -> {conn.target_component}.{conn.target_port}"
)
return "\n".join(lines)
File diff suppressed because it is too large. Load diff
Loaded 100 of 362 files, more files were not shown because too many files have changed in this diff. Show more