公开PNRP17并接通实时气动机械耦合

This commit is contained in:
huojiarong committed 2026-08-02 14:51:31 +00:00
1 parent 503394f3ed
commit 1f4027573f
18 files changed
+693 -29

No files matched your search

+2 -1
View File
@@ -9,7 +9,7 @@ from app.simulation.core.catalog import (
LIBRARY = ComponentLibrarySpec(
id="amesim",
label="AMESim 组件库",
version="0.2.0",
version="0.3.0",
source_package="app.simulation.components.amesim",
temporary=True,
order=200,
@@ -33,6 +33,7 @@ LIBRARY = ComponentLibrarySpec(
"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",
@@ -0,0 +1,228 @@
from __future__ import annotations
from collections.abc import Mapping
from math import 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
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"
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_4", "left", order=30),
PortDisplaySpec("port_2", "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_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 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,
}
@@ -249,10 +249,9 @@ class AmesimPnch012(ThermodynamicVolumeComponent):
"""AMESim PNCH012 variable-volume pneumatic chamber.
AMESim supplies four external volume and volume-rate inputs through the
chamber ports. The current public System XML contract has pneumatic ports
only, so this first public model exposes those external volume inputs as SI
parameters. This represents fixed or prescribed-volume PNCH012 cases and is
not yet the full mechanical-coupled submodel.
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"
@@ -433,14 +432,29 @@ class AmesimPnch012(ThermodynamicVolumeComponent):
) -> "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()), minimum_volume)
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())
return sum(self.external_volume_rates.values()) + self.connected_external_volume_rate()
def get_state_vector(self) -> list[float]:
return self.state.as_vector()
+10
View File
@@ -204,6 +204,16 @@ class Component(ABC):
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):
state_size = 2
+22
View File
@@ -87,6 +87,26 @@ class PortDefinition:
unit="J/kg",
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,
),
),
)
@@ -178,6 +198,8 @@ class PortState:
p: float = 0.0
m_flow: 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
@@ -0,0 +1,93 @@
from __future__ import annotations
from dataclasses import dataclass
from math import isfinite
from app.simulation.systems.network import Endpoint, SimulationNetwork
@dataclass(frozen=True)
class PneumaticVolumeDiagnostics:
propagated: int
output_ports: tuple[str, ...]
def as_dict(self) -> dict[str, object]:
return {
"propagated": self.propagated,
"outputPorts": list(self.output_ports),
}
class PneumaticVolumeResolver:
"""Propagate AMESim pneumatic external-volume connector variables."""
def __init__(self, network: SimulationNetwork) -> None:
self.network = network
self._connected_endpoint = self._build_connection_map()
self.last_diagnostics: PneumaticVolumeDiagnostics | None = None
def _build_connection_map(self) -> dict[Endpoint, Endpoint]:
result: dict[Endpoint, Endpoint] = {}
for connection in self.network.connections:
if connection.kind != "physical" or connection.domain != "pneumatic":
continue
first, second = connection.endpoints
result[first] = second
result[second] = first
return result
def solve(self) -> PneumaticVolumeDiagnostics:
for component in self.network.components.values():
for definition in component.port_definitions:
if definition.kind == "physical" and definition.domain == "pneumatic":
port = component.get_port(definition.name)
port.volume = 0.0
port.volume_flow = 0.0
outputs: dict[Endpoint, tuple[float, float]] = {}
for component in self.network.components.values():
for port_name, raw_values in component.pneumatic_volume_outputs().items():
port = component.get_port(port_name)
definition = port.definition
if (
definition is None
or definition.kind != "physical"
or definition.domain != "pneumatic"
):
raise ValueError(
f"Component {component.name} declares pneumatic volume output "
f"on non-pneumatic port {port_name}."
)
volume, volume_flow = (float(raw_values[0]), float(raw_values[1]))
if not isfinite(volume) or not isfinite(volume_flow):
raise ValueError(
f"Component {component.name}.{port_name} produced a non-finite "
"pneumatic volume value."
)
endpoint = Endpoint(component.name, port_name)
outputs[endpoint] = (volume, volume_flow)
port.volume = volume
port.volume_flow = volume_flow
propagated = 0
for endpoint, values in outputs.items():
connected = self._connected_endpoint.get(endpoint)
if connected is None:
continue
if connected in outputs:
raise ValueError(
"A pneumatic connection cannot contain two external-volume "
f"sources: {endpoint} and {connected}."
)
connected_port = self.network.components[connected.component].get_port(
connected.port
)
connected_port.volume, connected_port.volume_flow = values
propagated += 1
diagnostics = PneumaticVolumeDiagnostics(
propagated=propagated,
output_ports=tuple(sorted(str(endpoint) for endpoint in outputs)),
)
self.last_diagnostics = diagnostics
return diagnostics
+18 -1
View File
@@ -8,6 +8,7 @@ from typing import Literal
from app.simulation.core.base import DynamicComponent
from app.simulation.core.metadata import ResultVariableMetadata
from app.simulation.solvers.algebraic import PressureFlowSolver
from app.simulation.solvers.pneumatic_volume import PneumaticVolumeResolver
from app.simulation.solvers.solver import ODESolution, SolveIVPConfig, integrate_ode
from app.simulation.solvers.signal import SignalResolver
from app.simulation.solvers.stream import StreamResolver
@@ -233,6 +234,7 @@ class GenericFluidSystem:
self.network = network
self.dynamic_components = network.dynamic_components()
self.pressure_flow_solver = PressureFlowSolver(network)
self.pneumatic_volume_resolver = PneumaticVolumeResolver(network)
self.signal_resolver = SignalResolver(network)
self.stream_resolver = StreamResolver(network)
self.algebraic_solve_count = 0
@@ -240,6 +242,7 @@ class GenericFluidSystem:
self.max_algebraic_evaluations = 0
self.max_stream_iterations = 0
self.signal_propagation_count = 0
self.pneumatic_volume_propagation_count = 0
def initial_state_vector(self) -> list[float]:
return self.network.initial_state_vector()
@@ -253,8 +256,14 @@ class GenericFluidSystem:
for component in self.dynamic_components:
component.refresh_thermodynamic_ports()
algebraic = self.pressure_flow_solver.solve()
pneumatic_volume = self.pneumatic_volume_resolver.solve()
self.pneumatic_volume_propagation_count += pneumatic_volume.propagated
if pneumatic_volume.propagated:
for component in self.dynamic_components:
component.refresh_thermodynamic_ports()
algebraic = self.pressure_flow_solver.solve()
stream, connected_h = self.stream_resolver.solve()
self.algebraic_solve_count += 1
self.algebraic_solve_count += 1 + int(bool(pneumatic_volume.propagated))
self.max_algebraic_residual = max(
self.max_algebraic_residual,
algebraic.max_scaled_residual,
@@ -440,6 +449,14 @@ class GenericFluidSystem:
else None
),
},
"pneumaticVolume": {
"propagations": self.pneumatic_volume_propagation_count,
"last": (
self.pneumatic_volume_resolver.last_diagnostics.as_dict()
if self.pneumatic_volume_resolver.last_diagnostics is not None
else None
),
},
"stateCount": len(initial_state),
"sampleCount": len(series["time"]),
}
+11 -11
View File
@@ -1,6 +1,6 @@
# AMESim 子模型公开组件迁移矩阵
状态:21 个模型已在临时 AMESim 库完成第一版公开,持续校准中
状态:22 个模型已在临时 AMESim 库完成第一版公开,持续校准中
适用模型:`AmesimModels/test_mql.ame` / `app.simulation.examples.test_mql.system`
@@ -14,7 +14,7 @@
- 不把现有固定拓扑 `TestMqlSystem` 整体注册为公开拖拽组件。
- 第一版公开只表示模型契约、目录、XML 编译和最小仿真链路已接通,不表示 AMESim 精确语义已完成复刻。
- 气动、标量信号和一维机械模型按当前求解链路能力公开;事件与跨域耦合继续保留明确限制。
- 气动、标量信号和一维机械模型按当前求解链路能力公开;PNRP17 已打通首个实时气动—机械跨域闭合,事件和更一般跨域 DAE 仍保留明确限制。
- 固定算例专用的校准原语留在 `examples/test_mql/`,不与公共组件目录混为同一实现。
## 当前求解器能力边界
@@ -22,18 +22,18 @@
`app.simulation` 当前支持:
- 物理域:`pneumatic`、标量 `signal` 和一维 `mechanical`。
- 端口变量:气动 `p/m_flow/h_outflow`、信号 `signal`、机械 `x/v/f`。
- 端口变量:气动 `p/m_flow/h_outflow` 及定向 `volume/volume_flow`、信号 `signal`、机械 `x/v/f`。
- 流量符号:`m_flow > 0` 表示流入当前组件。
- 网络方程:气动连接压力相等、流量和为零、组件压力-流量残差与 stream 焓传播;标量信号传播;机械 `x/v` 等值和 `f` 平衡。
- 网络方程:气动连接压力相等、流量和为零、组件压力-流量残差、stream 焓传播和移动边界容积传播;标量信号传播;机械 `x/v` 等值和 `f` 平衡。
- 动态:半显式 ODE,动态组件提供质量和内能状态导数。
- 暂不支持:完整事件系统、一般高指数 DAE、气动-机械实时跨域组件,以及从 PNGD00 自动批量导入其他气体。公共编译链已经支持“介质 + 物性计算方法”定义元件:`gi=0` 使用内置空气理想气体,`gi=1..99` 引用画布中的显式介质物性实例;介质定义内部通过 `property_model` 下拉接口选择计算方法。氦气已提供 Peng-Robinson 首版。
- 暂不支持:完整事件系统、一般高指数 DAE、任意多域连接变量,以及从 PNGD00 自动批量导入其他气体。公共编译链已经支持“介质 + 物性计算方法”定义元件:`gi=0` 使用内置空气理想气体,`gi=1..99` 引用画布中的显式介质物性实例;介质定义内部通过 `property_model` 下拉接口选择计算方法。氦气已提供 Peng-Robinson 首版。
## 子模型族矩阵
| AMESim 子模型 | 数量 | AMESim 角色 | 当前公开状态 | 建议目标 | 先决条件 / 限制 |
| --- | ---: | --- | --- | --- | --- |
| `PNCH023` | 4 | 固定容积气室,带换热 | 第一版公开 | `amesim_pnch023`,`storage` | 已基于 `ThermodynamicVolumeComponent` 接入公开契约;仍需按 AMESim `cvol/extemp/kth/sth/gi` 复核质量、能量、换热方程。 |
| `PNCH012` | 8 | 变容气室,带换热 | 参数化第一版公开 | `amesim_pnch012`,`storage` | 已将 AMESim `vol1..4/dvol1..4` 外部体积输入映射为 SI 参数,支持固定/预设体积场景;实时机械耦合仍需跨域组件契约和闭合支持。 |
| `PNCH012` | 8 | 变容气室,带换热 | 实时变容第一版公开 | `amesim_pnch012`,`storage` | 保留 `vol1..4/dvol1..4` SI 参数,并可从 PNRP17 经气动端口接收实时外部容积与变化率;已完成代表性联合仿真,仍需完整 `test_mql` baseline 校准。 |
| `PNOR001` | 8 | 常系数气动孔口 | 第一版公开 | `amesim_pnor001`,`flow` | 已接入代数组件公开契约;仍需按 AMESim `cq/area/Cv/Kv/flowset/gi` 复核双向流、零压差正则化和单位换算。 |
| `PNVO001` | 8 | 信号调制气动孔口 | 第一版公开 | `amesim_pnvo001`,`flow` | 已接入名为 `res` 的标量信号输入端口,可由 `amesim_step0` 驱动开度;精确事件语义和 AMESim baseline 仍留后续修模。固定开度变体 `amesim_pnvo001_fixed` 继续保留。 |
| `PN3NODE2` | 8 | 三端气动节点,压力温度由 port 2 固定 | 第一版公开 | `amesim_pn3node2`,`junctions` | 已接入三端等压、流量守恒基础版;AMESim port 2 参考温压语义和 stream 混合仍需单独测试。 |
@@ -44,7 +44,7 @@
| `PNL0003` | 8 | C-R-C 动态管路 | 第一版公开 | `amesim_pnl0003`,`flow` | 已按公开契约接入两端四状态容积 + 中心摩擦流 + mode 2 换热项;大压差动态闭合和 AMESim baseline 误差仍留后续修模。 |
| `PNPL01` | 16 | 零气动流源 | 第一版公开 | `amesim_pnpl01`,`boundary` | 当前实现一端零流边界,只约束端口质量流量为 0;压力源/外部边界语义留后续扩展。 |
| `PNGD00` | 1 | 氦气气体定义 | 氦气 Peng-Robinson 首版公开 | `amesim_helium_medium`,`media` | 已映射源模型 `fluidType=12/eosType=6`;密度和压力使用 PR EOS,热量学暂用手册参考点的定比热闭合。完整压力相关残余焓、比热和真实气体临界流仍待状态相关物性接口。 |
| `PNRP17` | 8 | 气动活塞与移动体耦合 | 暂不公开 | 内部固定算例;后续跨域组件 | 需要跨域组件契约、实时气动-机械耦合,以及对应的事件、状态和闭合规则。 |
| `PNRP17` | 8 | 气动活塞与移动体耦合 | 第一版公开 | `amesim_pnrp17`,`mechanical` | 已接入 1 个气动端口和 4 个一维机械端口,按 `dp/dr/x0/gi` 计算环形有效面积、扫掠容积、容积变化率和表压作用力;已与 PNCH012、双质量块跑通 System XML 联合仿真,完整事件语义和 AMESim baseline 仍待校准。 |
| `MECMAS21` | 10 | 一维平动质量 | 第一版公开 | `amesim_mecmas21`,`mechanical` | 已接入一维机械端口 `x/v/f`、双端质量状态和基本摩擦/限位项,并跑通零力源与信号力源最小 System XML;完整 AMESim 接触/事件语义仍留后续对齐。 |
| `LMECHN1` | 2 | 动态线性机械节点 | 第一版公开 | `amesim_lmechn1`,`mechanical` | 已接入 9 端一维机械节点、端口位移/速度等值和节点力平衡,并跑通 `FORC -> LMECHN1 -> MECMAS21` 最小 System XML;端口朝向/符号细节留后续 AMESim baseline 对齐。 |
| `LSTP00A` | 8 | 弹性接触/端止动 | 第一版公开 | `amesim_lstp00a`,`mechanical` | 已接入两端一维机械端口、相对位移/速度接触力和最小 System XML 仿真;当前是连续罚函数基础版,完整 AMESim 事件/非光滑接触语义留后续 baseline 对齐。 |
@@ -82,13 +82,13 @@
- `PNVO001`:已第一版公开,保留固定开度变体。
- `UD00`:已第一版公开。
- `MECMAS21`、`F000`、`FORC`、`LSTP00A`、`LMECHN1`:已第一版公开。
- `PNRP17`
- `PNRP17`:已第一版公开,并完成 PNCH012 + 双质量块跨域闭合测试。
当前已具备一对一标量信号传播、一维机械端口基础闭合、弹性接触基础件和 9 端机械节点;剩余跨域组件仍需要继续设计气动-机械耦合、XML 协议、前端连线兼容和最小闭合系统测试。
当前已具备一对一标量信号传播、一维机械端口基础闭合、弹性接触基础件、9 端机械节点,以及针对移动边界的气动容积定向传播与二次代数闭合。下一阶段重点是完整 `test_mql` 拓扑和 AMESim baseline 校准。
## 下一步执行建议
1. 以 `app.simulation.components.amesim.library` 的 21 个模型为公开清单唯一来源。
1. 以 `app.simulation.components.amesim.library` 的 22 个模型为公开清单唯一来源。
2. 优先校准 `PNCH023 / PNOR001 / PN3NODE2 / P4NODE2 / PNL00R` 与动态管路的 AMESim baseline 误差。
3. 每次调整模型都同步补充目录校验、参数边界、System XML 编译和最小仿真测试。
4. 跨域能力下一步聚焦 `PNRP17` 或等价实时气动-机械耦合;在事件和闭合能力完成前保持暂不公开。
4. 用完整或代表性的 `test_mql` 画布校准 `PNRP17 + PNCH012` 的压力、力、位移和容积轨迹,并补齐事件边界语义。
+10
View File
@@ -3,6 +3,7 @@ import {
AmesimF000Symbol,
AmesimForcSymbol,
AmesimLmechn1Symbol,
AmesimPnrp17Symbol,
AmesimLstp00aSymbol,
AmesimMecmas21Symbol,
} from "./componentSymbols/mechanical";
@@ -194,6 +195,7 @@ const symbolRegistry = {
amesim_mecmas21: { presentation: "framed", render: AmesimMecmas21Symbol },
amesim_lstp00a: { presentation: "framed", render: AmesimLstp00aSymbol },
amesim_lmechn1: { presentation: "framed", render: AmesimLmechn1Symbol },
amesim_pnrp17: { presentation: "framed", render: AmesimPnrp17Symbol },
amesim_ideal_air_medium: {
presentation: "bare",
render: AmesimIdealAirMediumSymbol,
@@ -321,6 +323,14 @@ const symbolPortAnchors: Partial<
left: [7, 12, 17, 22, 27, 32, 37, 42].map((y) => ({ x: 5, y })),
right: [{ x: 59, y: 24 }],
},
amesim_pnrp17: {
left: [
{ x: 2, y: 24 },
{ x: 2, y: 10 },
{ x: 2, y: 38 },
],
right: [{ x: 62, y: 10 }, { x: 62, y: 38 }],
},
};
function isRegisteredComponentSymbol(symbol: string): symbol is RegisteredComponentSymbol {
@@ -215,3 +215,55 @@ export function AmesimLmechn1Symbol() {
</SymbolSvg>
);
}
export function AmesimPnrp17Symbol() {
return (
<SymbolSvg className="component-symbol-amesim-pnrp17" domain="mechanical">
<path
d="M2 24h17M2 10h11M2 38h11M51 10h11M51 38h11"
{...mechanicalStroke}
/>
<path
d="M7 6v8h6M7 34v8h6M57 6v8h-6M57 34v8h-6"
{...mechanicalStroke}
/>
<rect
fill={SYMBOL_FILL}
height="34"
rx="1"
stroke={SYMBOL_STROKE}
strokeWidth="2"
width="32"
x="18"
y="7"
/>
<path d="M38 8v32M38 24h12" {...mechanicalStroke} />
<path
d="M22 18h12M28 14l6 4-6 4"
fill="none"
stroke={SYMBOL_ACCENT}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.8"
/>
<path
d="M4 21v6M7 20v8M10 21v6"
fill="none"
stroke={SYMBOL_ACCENT}
strokeWidth="1.2"
/>
<text
dominantBaseline="central"
fill={SYMBOL_STROKE}
fontFamily="Arial, sans-serif"
fontSize="8"
fontWeight="600"
textAnchor="middle"
x="28"
y="32"
>
P
</text>
</SymbolSvg>
);
}
@@ -28,6 +28,7 @@ const symbolCases: SymbolCase[] = [
{ category: "mechanical", domain: "mechanical", modelType: "amesim_mecmas21" },
{ category: "mechanical", domain: "mechanical", modelType: "amesim_lstp00a" },
{ category: "mechanical", domain: "mechanical", modelType: "amesim_lmechn1" },
{ category: "mechanical", domain: "mechanical", modelType: "amesim_pnrp17" },
];
function physicalPort(
@@ -18,16 +18,15 @@ class AmesimComponentMigrationMatrixTests(unittest.TestCase):
self.assertEqual(missing, [])
def test_unsupported_domains_are_not_marked_as_public_candidates(self) -> None:
def test_pnrp17_is_marked_as_the_first_public_cross_domain_component(self) -> None:
text = MATRIX_PATH.read_text(encoding="utf-8")
unsupported = {
"PNRP17",
}
row = next(
line for line in text.splitlines() if "| `PNRP17` |" in line
)
for family in unsupported:
with self.subTest(family=family):
row = next(line for line in text.splitlines() if f"`{family}`" in line)
self.assertIn("暂不公开", row)
self.assertIn("第一版公开", row)
self.assertIn("`amesim_pnrp17`", row)
self.assertNotIn("暂不公开", row)
if __name__ == "__main__":
+1
View File
@@ -27,6 +27,7 @@ from tests.test_system_xml_protocol import physical_port
GAS_INDEXED_MODEL_TYPES = (
"amesim_pnch023",
"amesim_pnch012",
"amesim_pnrp17",
"amesim_pnor001",
"amesim_pnvo001_fixed",
"amesim_pnvo001",
+1 -1
View File
@@ -69,7 +69,7 @@ class AmesimHeliumPengRobinsonMediumTests(unittest.TestCase):
def test_definition_maps_local_selector_to_amesim_fluid_and_eos_codes(
self,
) -> None:
self.assertEqual(LIBRARY.version, "0.2.0")
self.assertEqual(LIBRARY.version, "0.3.0")
self.assertEqual(AMESIM_HELIUM_PENG_ROBINSON_PROPERTY_MODEL, 0)
self.assertEqual(AMESIM_HELIUM_PROPERTY_MODELS[0].value, 0)
self.assertEqual(AMESIM_HELIUM_PROPERTY_MODELS[0].eos_type, 6)
+205
View File
@@ -0,0 +1,205 @@
from __future__ import annotations
from math import pi
import unittest
from app.main import (
ReactFlowProjectPayload,
build_reactflow_system_xml,
compile_reactflow_network,
run_system_xml_simulation,
)
from app.simulation.components.amesim.mechanical.pistons import AmesimPnrp17
from app.simulation.core.medium import IdealGasMedium
from app.system_xml import validate_system_xml_document
from tests.test_amesim_mechanical_xml import MECMAS21_DEFAULTS, mechanical_port
from tests.test_generic_system_xml_simulation import component_node, physical_edge
from tests.test_system_xml_protocol import physical_port
def _pneumatic_port(name: str, side: str) -> dict[str, str]:
return physical_port(name, "bidirectional", side)
def _zero_force_node(name: str, side: str) -> dict[str, object]:
return component_node(
name,
"amesim_f000",
[mechanical_port("port_1", side)],
)
def _mass_node(name: str) -> dict[str, object]:
parameters = dict(MECMAS21_DEFAULTS)
parameters["mass"] = 2.0
return component_node(
name,
"amesim_mecmas21",
[
mechanical_port("port_1", "left"),
mechanical_port("port_2", "right"),
],
parameters,
)
def pnrp17_coupled_project() -> ReactFlowProjectPayload:
return ReactFlowProjectPayload(
name="amesim-pnrp17-coupled-smoke",
nodes=[
component_node(
"chamber_1",
"amesim_pnch012",
[
_pneumatic_port("port_1", "left"),
_pneumatic_port("port_2", "right"),
_pneumatic_port("port_3", "left"),
_pneumatic_port("port_4", "right"),
],
{
"gi": 0.0,
"cvol0": 0.01,
"kth": 0.0,
"sth": 0.1,
"extemp": 300.0,
"p0": 200000.0,
"T0": 300.0,
"vol1": 0.0,
"vol2": 0.0,
"vol3": 0.0,
"vol4": 0.0,
"dvol1": 0.0,
"dvol2": 0.0,
"dvol3": 0.0,
"dvol4": 0.0,
},
),
component_node(
"piston_1",
"amesim_pnrp17",
[
_pneumatic_port("port_1", "left"),
mechanical_port("port_2", "right"),
mechanical_port("port_3", "left"),
mechanical_port("port_4", "left"),
mechanical_port("port_5", "right"),
],
{"gi": 0.0, "dp": 0.1, "dr": 0.02, "x0": 0.0},
),
_mass_node("piston_mass"),
_mass_node("cylinder_mass"),
_zero_force_node("piston_free", "left"),
_zero_force_node("cylinder_free", "right"),
_zero_force_node("piston_mass_free", "left"),
_zero_force_node("cylinder_mass_free", "right"),
component_node(
"boundary_1",
"amesim_pnpl01",
[_pneumatic_port("port_1", "left")],
),
component_node(
"boundary_2",
"amesim_pnpl01",
[_pneumatic_port("port_1", "left")],
),
component_node(
"boundary_4",
"amesim_pnpl01",
[_pneumatic_port("port_1", "left")],
),
],
edges=[
physical_edge("edge-pneumatic", "chamber_1", "port_3", "piston_1", "port_1"),
physical_edge("edge-boundary-1", "chamber_1", "port_1", "boundary_1", "port_1"),
physical_edge("edge-boundary-2", "chamber_1", "port_2", "boundary_2", "port_1"),
physical_edge("edge-boundary-4", "chamber_1", "port_4", "boundary_4", "port_1"),
physical_edge("edge-piston-mass", "piston_1", "port_2", "piston_mass", "port_1"),
physical_edge("edge-piston-free", "piston_1", "port_5", "piston_free", "port_1"),
physical_edge("edge-piston-mass-free", "piston_mass", "port_2", "piston_mass_free", "port_1"),
physical_edge("edge-cylinder-mass", "piston_1", "port_3", "cylinder_mass", "port_1"),
physical_edge("edge-cylinder-free", "piston_1", "port_4", "cylinder_free", "port_1"),
physical_edge("edge-cylinder-mass-free", "cylinder_mass", "port_2", "cylinder_mass_free", "port_1"),
],
simulation={
"t_start": 0.0,
"t_stop": 0.005,
"step": 0.0025,
"max_step": 0.0005,
"method": "BDF",
},
)
class AmesimPnrp17Tests(unittest.TestCase):
def test_component_equations_match_pnrp17_geometry_and_signs(self) -> None:
medium = IdealGasMedium()
piston = AmesimPnrp17(
"piston_1",
medium,
gi=0.0,
dp=0.2,
dr=0.1,
x0=0.05,
)
piston.port_1.p = 300000.0
piston.port_2.x = piston.port_5.x = 0.3
piston.port_2.v = piston.port_5.v = 0.4
piston.port_3.x = piston.port_4.x = 0.1
piston.port_3.v = piston.port_4.v = -0.2
pressure_force = piston.pressure_force
piston.port_5.f = 10.0
piston.port_2.f = -pressure_force - piston.port_5.f
piston.port_4.f = 20.0
piston.port_3.f = pressure_force - piston.port_4.f
expected_area = pi * (0.2**2 - 0.1**2) / 4.0
self.assertAlmostEqual(piston.effective_area, expected_area)
self.assertAlmostEqual(piston.chamber_length, 0.25)
self.assertAlmostEqual(piston.chamber_volume, expected_area * 0.25)
self.assertAlmostEqual(piston.chamber_volume_flow, expected_area * 0.6)
for residual in piston.pressure_flow_equation_residuals():
self.assertAlmostEqual(residual.value, 0.0, msg=residual.id)
self.assertEqual(
piston.pneumatic_volume_outputs(),
{"port_1": (piston.chamber_volume, piston.chamber_volume_flow)},
)
def test_rod_diameter_cannot_exceed_piston_diameter(self) -> None:
with self.assertRaisesRegex(ValueError, "dr must not exceed"):
AmesimPnrp17(
"piston_1",
IdealGasMedium(),
dp=0.05,
dr=0.06,
)
def test_cross_domain_system_xml_compiles_and_simulates(self) -> None:
project = pnrp17_coupled_project()
xml = build_reactflow_system_xml(project)
report = validate_system_xml_document(xml)
self.assertTrue(report.valid, report.as_dict())
network = compile_reactflow_network(project)
self.assertTrue(network.pressure_flow_structure_dict()["isSquare"])
result = run_system_xml_simulation(xml)
self.assertTrue(result["success"], result["message"])
self.assertEqual(result["series"]["time"], [0.0, 0.0025, 0.005])
piston_volume = result["series"]["piston_1.volume"]
chamber_volume = result["series"]["chamber_1.vol"]
self.assertAlmostEqual(piston_volume[0], 0.0, places=12)
self.assertGreater(piston_volume[-1], piston_volume[0])
for chamber_value, piston_value in zip(chamber_volume, piston_volume):
self.assertAlmostEqual(chamber_value, 0.01 + piston_value, places=10)
self.assertGreater(result["series"]["piston_mass.x"][-1], 0.0)
self.assertLess(result["series"]["cylinder_mass.x"][-1], 0.0)
self.assertGreater(result["diagnostics"]["pneumaticVolume"]["propagations"], 0)
self.assertEqual(
result["diagnostics"]["pneumaticVolume"]["last"]["outputPorts"],
["piston_1.port_1"],
)
if __name__ == "__main__":
unittest.main()
+8 -1
View File
@@ -49,7 +49,7 @@ class ComponentCatalogTests(unittest.TestCase):
self.assertEqual(library["label"], "AMESim 组件库")
self.assertEqual([category["id"] for category in library["categories"]], ["media", "storage", "flow", "junctions", "boundary", "signals", "mechanical"])
self.assertEqual(set(components), {"amesim_ideal_air_medium", "amesim_helium_medium", "amesim_pnpl01", "amesim_step0", "amesim_ud00", "amesim_f000", "amesim_forc", "amesim_mecmas21", "amesim_lstp00a", "amesim_lmechn1", "amesim_pnch023", "amesim_pnch012", "amesim_pnor001", "amesim_pnvo001_fixed", "amesim_pnvo001", "amesim_pnl00r", "amesim_pnl0001", "amesim_pnl0002", "amesim_pnl0003", "amesim_pn3node2", "amesim_p4node2"})
self.assertEqual(set(components), {"amesim_ideal_air_medium", "amesim_helium_medium", "amesim_pnpl01", "amesim_step0", "amesim_ud00", "amesim_f000", "amesim_forc", "amesim_mecmas21", "amesim_lstp00a", "amesim_lmechn1", "amesim_pnrp17", "amesim_pnch023", "amesim_pnch012", "amesim_pnor001", "amesim_pnvo001_fixed", "amesim_pnvo001", "amesim_pnl00r", "amesim_pnl0001", "amesim_pnl0002", "amesim_pnl0003", "amesim_pn3node2", "amesim_p4node2"})
for model_type, component in components.items():
with self.subTest(model_type=model_type):
self.assertEqual(component["symbol"], model_type)
@@ -180,6 +180,13 @@ class ComponentCatalogTests(unittest.TestCase):
}
self.assertEqual(components["amesim_lmechn1"]["category"]["id"], "mechanical")
self.assertEqual([port["name"] for port in components["amesim_lmechn1"]["ports"]], [f"port_{index}" for index in range(1, 10)])
pnrp_parameters = {
parameter["name"]: parameter
for parameter in components["amesim_pnrp17"]["parameters"]
}
self.assertEqual(components["amesim_pnrp17"]["category"]["id"], "mechanical")
self.assertEqual([port["name"] for port in components["amesim_pnrp17"]["ports"]], ["port_1", "port_3", "port_4", "port_2", "port_5"])
self.assertEqual(pnrp_parameters["dp"]["unit"], "m")
self.assertEqual(lmechn_parameters["v1"]["maximum"], 8.0)
self.assertEqual(components["amesim_pnvo001"]["category"]["id"], "flow")
self.assertEqual([port["name"] for port in components["amesim_pnvo001"]["ports"]], ["res", "port_2", "port_3"])
+2
View File
@@ -28,6 +28,8 @@ class ComponentInterfaceTests(unittest.TestCase):
("p", "effort", "equal"),
("m_flow", "flow", "sumToZero"),
("h_outflow", "stream", "streamMix"),
("volume", "signal", "directed"),
("volume_flow", "signal", "directed"),
],
)
+2
View File
@@ -41,6 +41,7 @@ class ComponentRegistryTests(unittest.TestCase):
"amesim_mecmas21",
"amesim_lstp00a",
"amesim_lmechn1",
"amesim_pnrp17",
"amesim_pnch023",
"amesim_pnch012",
"amesim_pnor001",
@@ -78,6 +79,7 @@ class ComponentRegistryTests(unittest.TestCase):
self.assertEqual(models["amesim_mecmas21"].library.id, "amesim")
self.assertEqual(models["amesim_lstp00a"].library.id, "amesim")
self.assertEqual(models["amesim_lmechn1"].library.id, "amesim")
self.assertEqual(models["amesim_pnrp17"].library.id, "amesim")
self.assertEqual(models["amesim_pnch023"].library.id, "amesim")
self.assertEqual(models["amesim_pnch012"].library.id, "amesim")
self.assertEqual(models["amesim_pnor001"].library.id, "amesim")