规范仿真模型库并完善前端交互

归档仿真模型并补充组件目录、建模规范与校验。

完善控制台、默认节点、视图适配及前端自动化测试。
This commit is contained in:
ljz committed 2026-07-29 15:44:16 +08:00
1 parent f7f1078911
commit bbc88a6bbb
71 files changed
+4574 -806

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
"""Component implementations for the Python system model."""
+282
View File
@@ -0,0 +1,282 @@
# 元件建模规范与示例
规范的权威版本位于
[`docs/component-model-authoring-spec-v1.md`](../../../docs/component-model-authoring-spec-v1.md)。
本文档保留在组件目录中,作为离模型源码最近的完整示例;若两者不一致,应在同一次
修改中同步,不能让示例形成另一套规则。
本文档是 `app/simulation/components` 下新增元件的最小开发规范。当前
`experimental` 是用于验证规范的临时组件库;后续正式模型应建立独立组件库,
不要继续堆放在 `experimental` 中。
目标是让元件的端口、输入参数和可展示结果都由元件类显式声明,避免 XML
校验、求解器和前端分别维护同一份含义。
## 一、元件类必须声明的内容
每个对外注册的元件类至少需要声明以下六个类属性:
```python
MODEL_TYPE = "example_component"
MODEL_VERSION = "1.0.0"
PORTS = (...)
PARAMETERS = (...)
RESULT_VARIABLES = (...)
DISPLAY = ...
```
- `MODEL_TYPE`:稳定的模型类型标识,对应 System XML 中的 `Component/@type`。发布后不要随意改名。
- `MODEL_VERSION`:模型契约版本,采用 `主版本.次版本.修订版本`。
- `PORTS`:端口契约,包括端口名、物理域、变量和正流量方向。
- `PARAMETERS`:用户可配置的输入参数,包括默认值、物理量、SI 单位和取值范围。
- `RESULT_VARIABLES`:允许写入仿真结果并显示在结果页的组件级变量。端口结果由 `PORTS` 中的端口变量定义自动生成。
- `DISPLAY`:组件库名称、分类、图标、排序和端口画布位置,不参与物理求解。
元件构造函数还必须:
1. 调用 `super().__init__(name)`。
2. 使用 `set_parameter_values()` 保存规范化后的输入参数。
3. 使用 `register_declared_port()` 创建已声明端口。
4. 若声明了组件结果变量,实现 `component_result_values()` 并返回对应数值;标准热力学容腔可以直接继承 `ThermodynamicVolumeComponent` 的实现。
5. 实现统一的类方法 `create()`,接收规范化后的 SI 参数。
## 二、输入参数与结果变量
输入参数和仿真结果必须分开声明:
- 输入参数描述一次仿真开始前由用户配置的量,例如 `volume`、`p0`、`T0`。
- 结果变量描述随时间变化、允许绘图的量,例如 `p`、`T`、`m`、`m_flow`。
- 求解器缓存、中间残差和调试字段不得自动暴露为结果变量。
- 参数名和结果变量名使用稳定的英文机器标识;`label` 专门用于界面显示。
参数定义示例:
```python
ParameterDefinition(
name="volume",
label="容积",
quantity="volume",
unit="m3",
default=0.1,
minimum=0.0,
minimum_exclusive=True,
)
```
结果变量定义示例:
```python
ResultVariableDefinition(
name="p",
label="压力",
quantity="pressure",
unit="Pa",
category="thermodynamic",
order=30,
)
```
## 三、命名和单位约定
- 模型类型、参数、端口和变量名使用 `snake_case`,已有热力学惯例 `T`、`U` 可以保留。
- 输入参数保存和计算统一使用 SI 基准值;界面单位换算不能改变后端存储值。
- 无量纲参数的 `unit` 使用空字符串。
- `quantity` 表示稳定的物理量类型,例如 `pressure`、`temperature`、`mass_flow`,不能使用界面文案代替。
- 正质量流量统一定义为流入元件,即 `positiveFlowDirection="intoComponent"`。
- 端口变量 `p`、`m_flow`、`h_outflow` 的连接规则由 `PortDefinition.pneumatic()` 统一提供。
## 四、完整示例:单端口储气容腔
下面的示例展示一个可直接接入当前框架的动态元件。真实新增元件时应放入独立的 `.py` 文件,并补充对应测试。
```python
from __future__ import annotations
from collections.abc import Mapping
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,
THERMODYNAMIC_VOLUME_RESULT_VARIABLES,
)
from app.simulation.core.medium import IdealGasMedium, ThermodynamicProperties
from app.simulation.core.ports import PortDefinition
from app.simulation.core.state import VolumeState
class ExampleVolume(ThermodynamicVolumeComponent):
MODEL_TYPE = "example_volume"
MODEL_VERSION = "1.0.0"
PORTS = (
PortDefinition.pneumatic("port_a", nominal_role="bidirectional"),
)
PARAMETERS = (
ParameterDefinition(
name="volume",
label="容积",
quantity="volume",
unit="m3",
default=0.1,
minimum=0.0,
minimum_exclusive=True,
),
ParameterDefinition(
name="p0",
label="初始压力",
quantity="pressure",
unit="Pa",
default=100000.0,
minimum=0.0,
minimum_exclusive=True,
),
ParameterDefinition(
name="T0",
label="初始温度",
quantity="temperature",
unit="K",
default=300.0,
minimum=0.0,
minimum_exclusive=True,
),
)
RESULT_VARIABLES = THERMODYNAMIC_VOLUME_RESULT_VARIABLES
DISPLAY = ComponentDisplaySpec(
label="示例容腔",
library_id="experimental",
category_id="storage",
symbol="generic",
ports=(PortDisplaySpec("port_a", "left"),),
order=90,
)
def __init__(
self,
name: str,
medium: IdealGasMedium,
volume: float = 0.1,
p0: float = 100000.0,
T0: float = 300.0,
) -> None:
super().__init__(name)
self.set_parameter_values(
{"volume": volume, "p0": p0, "T0": T0}
)
self.medium = medium
self.V = volume
initial_mass = p0 * volume / (medium.R_gas * T0)
initial_energy = initial_mass * medium.specific_internal_energy(T0)
self.state = VolumeState(m=initial_mass, U=initial_energy)
self.port_a = self.register_declared_port("port_a")
@classmethod
def create(
cls,
*,
name: str,
medium: IdealGasMedium,
parameters: Mapping[str, float],
) -> ExampleVolume:
return cls(
name=name,
medium=medium,
volume=parameters["volume"],
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 refresh_thermodynamic_ports(self) -> ThermodynamicProperties:
properties = self.medium.properties_from_mU(
self.state.m, self.state.U, self.V
)
self.port_a.p = properties.p
self.port_a.h_outflow = properties.h
return properties
def state_derivative_from_ports(
self,
connected_h: Mapping[str, float],
) -> list[float]:
properties = self.refresh_thermodynamic_ports()
inlet_h = self.connection_inlet_enthalpy(
port_m_flow=self.port_a.m_flow,
connected_h=connected_h["port_a"],
internal_h=properties.h,
)
return [self.port_a.m_flow, self.port_a.m_flow * inlet_h]
def pressure_flow_equation_residuals(
self,
) -> tuple[EquationResidual, ...]:
pressure = self.medium.properties_from_mU(
self.state.m, self.state.U, self.V
).p
return (
EquationResidual(
id=f"{self.name}:port_a_pressure_state",
owner="component",
owner_id=self.name,
relation="state",
variables=(f"{self.name}.port_a.p", f"{self.name}.state"),
role="effort",
value=self.port_a.p - pressure,
),
)
```
模型文件不再直接修改全局注册表。完成模型类后,只把类路径加入所属库
`library.py` 的 `models` 清单:
```python
models=(
# ...已有模型
"app.simulation.components.experimental.storage.example_volume:ExampleVolume",
)
```
后端会受控导入清单中的类,校验版本、分类、端口、参数、单位、显示信息和默认实例,
再自动建立注册表。校验通过后,`GET /api/components/catalog` 会输出该元件,
前端刷新时即可加载。
当前 `experimental` 仅用于规范验证;正式模型应先建立新的库声明,再把
`library_id` 指向正式库。
完成仿真后,每个已声明结果都会得到一条结构化元数据。前端应按字段筛选,不能再拆解 `key` 猜测含义:
```json
{
"key": "example_volume_1.port_a.m_flow",
"componentId": "example_volume_1",
"componentType": "example_volume",
"scope": "port",
"portName": "port_a",
"name": "m_flow",
"label": "质量流量",
"quantity": "mass_flow",
"unit": "kg/s",
"category": "flow",
"order": 20
}
```
## 五、新增元件检查清单
1. `MODEL_TYPE` 是否唯一,并与 XML 的模型类型一致。
2. 所有构造参数是否在 `PARAMETERS` 中声明并保存。
3. 所有端口是否在 `PORTS` 中声明并通过 `register_declared_port()` 创建。
4. `RESULT_VARIABLES` 与 `component_result_values()` 的键是否完全一致。
5. 结果变量是否包含明确的 `quantity`、`label`、`unit` 和显示顺序。
6. 是否只暴露有工程意义的结果,而非内部计算变量。
7. `MODEL_VERSION` 和 `DISPLAY` 是否完整,显示端口是否与物理端口完全一致。
8. 是否实现统一的 `create()`,并能用默认参数创建模型。
9. 模型类路径是否只加入所属库的 `library.py` 清单。
10. 是否补充参数边界、端口契约、目录输出、结果元数据和最小仿真的自动测试。
组件库、分类和自动发现的完整规则参见
[`组件库分类、发现与读取规范 v1`](../../../docs/component-library-spec-v1.md)。
@@ -0,0 +1,12 @@
"""Temporary component library used to validate the model authoring contract."""
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
@@ -0,0 +1 @@
"""Flow-path and resistance components."""
@@ -0,0 +1,118 @@
from __future__ import annotations
from collections.abc import Mapping
from math import sqrt
from app.simulation.core.base import AlgebraicComponent
from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
from app.simulation.core.equations import EquationResidual
from app.simulation.core.metadata import ParameterDefinition
from app.simulation.core.medium import IdealGasMedium
from app.simulation.core.ports import PortDefinition
class Orifice(AlgebraicComponent):
"""Python port of ModelicaModels.Myorifice."""
MODEL_TYPE = "orifice"
MODEL_VERSION = "1.0.0"
PORTS = (
PortDefinition.pneumatic("port_a", nominal_role="inlet"),
PortDefinition.pneumatic("port_b", nominal_role="outlet"),
)
PARAMETERS = (
ParameterDefinition(
"K",
1e-5,
label="流量系数",
quantity="flow_coefficient",
unit="kg/(s*Pa^0.5)",
minimum=0.0,
),
ParameterDefinition(
"opening",
1.0,
label="开度",
minimum=0.0,
maximum=1.0,
),
)
RESULT_VARIABLES = ()
DISPLAY = ComponentDisplaySpec(
label="孔板/阀门",
library_id="experimental",
category_id="flow",
symbol="orifice",
ports=(
PortDisplaySpec("port_a", "left", order=10),
PortDisplaySpec("port_b", "right", order=20),
),
order=40,
)
def __init__(self, name: str, opening: float = 1.0, K: float = 1e-5) -> None:
super().__init__(name=name)
self.set_parameter_values({"K": K, "opening": opening})
self.opening = opening
self.K = K
self.port_a = self.register_declared_port("port_a")
self.port_b = self.register_declared_port("port_b")
@classmethod
def create(
cls,
*,
name: str,
medium: IdealGasMedium,
parameters: Mapping[str, float],
) -> Orifice:
return cls(
name=name,
opening=parameters["opening"],
K=parameters["K"],
)
@property
def K_eff(self) -> float:
return self.K * max(self.opening, 0.001)
def mass_flow(self, p_a: float, p_b: float) -> float:
dp = p_a - p_b
if dp == 0.0:
return 0.0
return self.K_eff * sqrt(abs(dp)) * (1.0 if dp > 0.0 else -1.0)
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
return (
EquationResidual(
id=f"{self.name}:mass_flow_balance",
owner="component",
owner_id=self.name,
relation="sumToZero",
variables=(
f"{self.name}.port_a.m_flow",
f"{self.name}.port_b.m_flow",
),
role="flow",
value=self.port_a.m_flow + self.port_b.m_flow,
),
EquationResidual(
id=f"{self.name}:pressure_flow_relation",
owner="component",
owner_id=self.name,
relation="constitutive",
variables=(
f"{self.name}.port_a.p",
f"{self.name}.port_b.p",
f"{self.name}.port_a.m_flow",
),
role="flow",
value=self.port_a.m_flow
- self.mass_flow(self.port_a.p, self.port_b.p),
),
)
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
self.port_a.h_outflow = connected_h["port_b"]
self.port_b.h_outflow = connected_h["port_a"]
@@ -0,0 +1,10 @@
"""Compatibility import for the TestModel-only dynamic pipe.
The public ``pipe`` catalog model is ``ResistivePipe``. New code should import
this legacy dynamic model from ``app.simulation.examples.testmodel.dynamic_pipe``.
"""
from app.simulation.examples.testmodel.dynamic_pipe import Pipe
__all__ = ("Pipe",)
@@ -0,0 +1,185 @@
from __future__ import annotations
from collections.abc import Mapping
from math import pi
from app.simulation.core.base import AlgebraicComponent
from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
from app.simulation.core.equations import EquationResidual
from app.simulation.core.metadata import ParameterDefinition
from app.simulation.core.medium import IdealGasMedium
from app.simulation.core.ports import PortDefinition
class ResistivePipe(AlgebraicComponent):
"""Quasi-steady Darcy resistance used by topology-driven simulation."""
MODEL_TYPE = "pipe"
MODEL_VERSION = "1.0.0"
PORTS = (
PortDefinition.pneumatic("port_a", nominal_role="inlet"),
PortDefinition.pneumatic("port_b", nominal_role="outlet"),
)
PARAMETERS = (
ParameterDefinition(
"length",
5.0,
label="长度",
quantity="length",
unit="m",
minimum=0.0,
minimum_exclusive=True,
),
ParameterDefinition(
"diameter",
0.02,
label="直径",
quantity="length",
unit="m",
minimum=0.0,
minimum_exclusive=True,
),
ParameterDefinition(
"lambda_darcy",
0.02,
label="摩阻系数",
minimum=0.0,
),
ParameterDefinition(
"p0",
1e5,
label="初始压力",
quantity="pressure",
unit="Pa",
minimum=0.0,
minimum_exclusive=True,
),
ParameterDefinition(
"T0",
300.0,
label="初始温度",
quantity="temperature",
unit="K",
minimum=0.0,
minimum_exclusive=True,
),
)
RESULT_VARIABLES = ()
DISPLAY = ComponentDisplaySpec(
label="管段",
library_id="experimental",
category_id="flow",
symbol="pipe",
ports=(
PortDisplaySpec("port_a", "left", order=10),
PortDisplaySpec("port_b", "right", order=20),
),
order=30,
)
def __init__(
self,
name: str,
medium: IdealGasMedium,
L: float = 5.0,
D: float = 0.02,
lambda_darcy: float = 0.02,
p0: float = 1e5,
T0: float = 300.0,
) -> None:
super().__init__(name=name)
self.set_parameter_values(
{
"length": L,
"diameter": D,
"lambda_darcy": lambda_darcy,
"p0": p0,
"T0": T0,
}
)
self.medium = medium
self.L = L
self.D = D
self.lambda_darcy = lambda_darcy
self.p0 = p0
self.T0 = T0
self.area = pi * D * D / 4.0
initial_h = medium.specific_enthalpy(T0)
self.port_a = self.register_declared_port("port_a")
self.port_a.p = p0
self.port_a.h_outflow = initial_h
self.port_b = self.register_declared_port("port_b")
self.port_b.p = p0
self.port_b.h_outflow = initial_h
@classmethod
def create(
cls,
*,
name: str,
medium: IdealGasMedium,
parameters: Mapping[str, float],
) -> ResistivePipe:
return cls(
name=name,
medium=medium,
L=parameters["length"],
D=parameters["diameter"],
lambda_darcy=parameters["lambda_darcy"],
p0=parameters["p0"],
T0=parameters["T0"],
)
def pressure_drop(self, m_flow_a: float, p_a: float, p_b: float) -> float:
average_pressure = max(0.5 * (p_a + p_b), 1.0)
density = max(self.medium.density(average_pressure, self.T0), 1e-12)
resistance = self.lambda_darcy * (self.L / self.D)
return (
resistance
* m_flow_a
* abs(m_flow_a)
/ (2.0 * density * self.area * self.area)
)
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
return (
EquationResidual(
id=f"{self.name}:mass_flow_balance",
owner="component",
owner_id=self.name,
relation="sumToZero",
variables=(
f"{self.name}.port_a.m_flow",
f"{self.name}.port_b.m_flow",
),
role="flow",
value=self.port_a.m_flow + self.port_b.m_flow,
),
EquationResidual(
id=f"{self.name}:darcy_pressure_loss",
owner="component",
owner_id=self.name,
relation="constitutive",
variables=(
f"{self.name}.port_a.p",
f"{self.name}.port_b.p",
f"{self.name}.port_a.m_flow",
),
role="effort",
value=(
self.port_a.p
- self.port_b.p
- self.pressure_drop(
self.port_a.m_flow,
self.port_a.p,
self.port_b.p,
)
),
),
)
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
self.port_a.h_outflow = connected_h["port_b"]
self.port_b.h_outflow = connected_h["port_a"]
@@ -0,0 +1 @@
"""Flow junction components."""
@@ -0,0 +1,266 @@
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 Tee(AlgebraicComponent):
"""Python port of ModelicaModels.Mytee."""
MODEL_TYPE = "tee"
MODEL_VERSION = "1.0.0"
PORTS = (
PortDefinition.pneumatic("port_in", nominal_role="bidirectional"),
PortDefinition.pneumatic("port_out1", nominal_role="bidirectional"),
PortDefinition.pneumatic("port_out2", nominal_role="bidirectional"),
)
PARAMETERS = ()
RESULT_VARIABLES = ()
DISPLAY = ComponentDisplaySpec(
label="三通",
library_id="experimental",
category_id="junctions",
symbol="tee",
ports=(
PortDisplaySpec("port_in", "left", order=10),
PortDisplaySpec("port_out1", "right", order=20),
PortDisplaySpec("port_out2", "right", order=30),
),
order=50,
)
def __init__(self, name: str) -> None:
super().__init__(name=name)
self.set_parameter_values({})
self.port_in = self.register_declared_port("port_in")
self.port_out1 = self.register_declared_port("port_out1")
self.port_out2 = self.register_declared_port("port_out2")
@classmethod
def create(
cls,
*,
name: str,
medium: IdealGasMedium,
parameters: Mapping[str, float],
) -> Tee:
return cls(name=name)
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
return (
EquationResidual(
id=f"{self.name}:common_pressure_out1",
owner="component",
owner_id=self.name,
relation="equal",
variables=(f"{self.name}.port_in.p", f"{self.name}.port_out1.p"),
role="effort",
value=self.port_in.p - self.port_out1.p,
),
EquationResidual(
id=f"{self.name}:common_pressure_out2",
owner="component",
owner_id=self.name,
relation="equal",
variables=(f"{self.name}.port_in.p", f"{self.name}.port_out2.p"),
role="effort",
value=self.port_in.p - self.port_out2.p,
),
EquationResidual(
id=f"{self.name}:mass_flow_balance",
owner="component",
owner_id=self.name,
relation="sumToZero",
variables=(
f"{self.name}.port_in.m_flow",
f"{self.name}.port_out1.m_flow",
f"{self.name}.port_out2.m_flow",
),
role="flow",
value=(
self.port_in.m_flow
+ self.port_out1.m_flow
+ self.port_out2.m_flow
),
),
)
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
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 * enthalpy for m_flow, enthalpy in incoming
) / total_flow
else:
values = list(connected_h.values())
mixed_h = sum(values) / len(values) if values else 0.0
for port in self.ports.values():
port.h_outflow = mixed_h
def mixed_inlet_enthalpy(
self,
branch1_m_flow: float,
branch1_h: float,
branch2_m_flow: float,
branch2_h: float,
fallback_h: float = 0.0,
) -> float:
positive_1 = max(branch1_m_flow, 0.0)
positive_2 = max(branch2_m_flow, 0.0)
total = positive_1 + positive_2
if total <= 1e-9:
return fallback_h
return (positive_1 * branch1_h + positive_2 * branch2_h) / total
def inlet_stream_enthalpy(
self,
branch1_m_flow: float,
branch1_h: float,
branch2_m_flow: float,
branch2_h: float,
fallback_h: float,
) -> float:
"""Approximate `inStream(port_in.h_outflow)` for the current tee topology."""
return self.mixed_inlet_enthalpy(
branch1_m_flow,
branch1_h,
branch2_m_flow,
branch2_h,
fallback_h=fallback_h,
)
def branch_actual_stream_enthalpy(
self,
branch_m_flow: float,
branch_h: float,
inlet_h: float,
) -> float:
"""Approximate `actualStream(branch.h_outflow)` for a tee branch port."""
return inlet_h if branch_m_flow > 0.0 else branch_h
@staticmethod
def _solve_linear_2x2(
a11: float,
a12: float,
a21: float,
a22: float,
b1: float,
b2: float,
) -> tuple[float, float] | None:
determinant = a11 * a22 - a12 * a21
if abs(determinant) <= 1e-12:
return None
x1 = (b1 * a22 - b2 * a12) / determinant
x2 = (a11 * b2 - a21 * b1) / determinant
return x1, x2
def solve_branch_outlet_flows_from_energy_balance(
self,
*,
ratio_branch1: float,
ratio_branch2: float,
inlet_h_branch1: float,
inlet_h_branch2: float,
branch1_h: float,
branch2_h: float,
inlet_h: float,
q_in_branch1: float,
q_in_branch2: float,
tolerance: float = 1e-12,
) -> tuple[float, float]:
"""Solve branch outlet flows for the current three-port downstream tee use-case."""
rhs_branch1 = q_in_branch1 * inlet_h_branch1
rhs_branch2 = q_in_branch2 * inlet_h_branch2
def solve_both_forward() -> tuple[float, float] | None:
return self._solve_linear_2x2(
(1.0 + ratio_branch1) * branch1_h,
ratio_branch1 * branch2_h,
ratio_branch2 * branch1_h,
(1.0 + ratio_branch2) * branch2_h,
rhs_branch1,
rhs_branch2,
)
def solve_one_reverse(
*,
branch1_reverse: bool,
) -> tuple[float, float] | None:
if branch1_reverse:
return self._solve_linear_2x2(
inlet_h * (1.0 + ratio_branch1),
ratio_branch1 * inlet_h,
ratio_branch2 * inlet_h,
branch2_h + ratio_branch2 * inlet_h,
rhs_branch1,
rhs_branch2,
)
return self._solve_linear_2x2(
branch1_h + ratio_branch1 * inlet_h,
ratio_branch1 * inlet_h,
ratio_branch2 * inlet_h,
inlet_h * (1.0 + ratio_branch2),
rhs_branch1,
rhs_branch2,
)
def solve_both_reverse() -> tuple[float, float] | None:
return self._solve_linear_2x2(
inlet_h * (1.0 + ratio_branch1),
ratio_branch1 * inlet_h,
ratio_branch2 * inlet_h,
inlet_h * (1.0 + ratio_branch2),
rhs_branch1,
rhs_branch2,
)
candidate_solvers = (
(
solve_both_forward,
lambda q1, q2: q1 >= -tolerance and q2 >= -tolerance,
),
(
lambda: solve_one_reverse(branch1_reverse=True),
lambda q1, q2: q1 < -tolerance and q2 >= -tolerance and q1 + q2 > tolerance,
),
(
lambda: solve_one_reverse(branch1_reverse=True),
lambda q1, q2: q1 < -tolerance and q2 >= -tolerance and q1 + q2 <= tolerance,
),
(
lambda: solve_one_reverse(branch1_reverse=False),
lambda q1, q2: q2 < -tolerance and q1 >= -tolerance and q1 + q2 > tolerance,
),
(
lambda: solve_one_reverse(branch1_reverse=False),
lambda q1, q2: q2 < -tolerance and q1 >= -tolerance and q1 + q2 <= tolerance,
),
(
solve_both_reverse,
lambda q1, q2: q1 < -tolerance and q2 < -tolerance,
),
)
for solver, predicate in candidate_solvers:
candidate = solver()
if candidate is None:
continue
q_out_branch1, q_out_branch2 = candidate
if predicate(q_out_branch1, q_out_branch2):
return q_out_branch1, q_out_branch2
return solve_both_forward() or (0.0, 0.0)
@@ -0,0 +1,28 @@
"""Manifest for the temporary library used to validate component authoring."""
from app.simulation.core.catalog import (
ComponentCategorySpec,
ComponentLibrarySpec,
)
LIBRARY = ComponentLibrarySpec(
id="experimental",
label="临时测试组件库",
version="0.1.0",
source_package="app.simulation.components.experimental",
temporary=True,
order=100,
categories=(
ComponentCategorySpec(id="storage", label="储能元件", order=10),
ComponentCategorySpec(id="flow", label="流动元件", order=20),
ComponentCategorySpec(id="junctions", label="连接元件", order=30),
),
models=(
"app.simulation.components.experimental.storage.cylinder:Cylinder",
"app.simulation.components.experimental.storage.tank:Tank",
"app.simulation.components.experimental.flow.resistive_pipe:ResistivePipe",
"app.simulation.components.experimental.flow.orifice:Orifice",
"app.simulation.components.experimental.junctions.tee:Tee",
),
)
@@ -0,0 +1 @@
"""Storage and thermodynamic volume components."""
@@ -0,0 +1,155 @@
from __future__ import annotations
from collections.abc import Mapping
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,
THERMODYNAMIC_VOLUME_RESULT_VARIABLES,
)
from app.simulation.core.medium import IdealGasMedium, ThermodynamicProperties
from app.simulation.core.ports import PortDefinition
from app.simulation.core.state import VolumeState
class Cylinder(ThermodynamicVolumeComponent):
"""Python port of ModelicaModels.Mycylinder."""
MODEL_TYPE = "cylinder"
MODEL_VERSION = "1.0.0"
PORTS = (PortDefinition.pneumatic("port_b", nominal_role="outlet"),)
PARAMETERS = (
ParameterDefinition(
"volume",
0.01,
label="容积",
quantity="volume",
unit="m3",
minimum=0.0,
minimum_exclusive=True,
),
ParameterDefinition(
"p0",
35e6,
label="初始压力",
quantity="pressure",
unit="Pa",
minimum=0.0,
minimum_exclusive=True,
),
ParameterDefinition(
"T0",
300.0,
label="初始温度",
quantity="temperature",
unit="K",
minimum=0.0,
minimum_exclusive=True,
),
)
RESULT_VARIABLES = THERMODYNAMIC_VOLUME_RESULT_VARIABLES
DISPLAY = ComponentDisplaySpec(
label="气瓶",
library_id="experimental",
category_id="storage",
symbol="cylinder",
ports=(PortDisplaySpec("port_b", "right"),),
order=10,
)
def __init__(
self,
name: str,
medium: IdealGasMedium,
V: float = 0.01,
p0: float = 35e6,
T0: float = 300.0,
) -> None:
super().__init__(name=name)
self.set_parameter_values({"volume": V, "p0": p0, "T0": T0})
self.medium = medium
self.V = V
m0 = p0 * V / (medium.R_gas * T0)
U0 = m0 * medium.specific_internal_energy(T0)
self.state = VolumeState(m=m0, U=U0)
self.port_b = self.register_declared_port("port_b")
@classmethod
def create(
cls,
*,
name: str,
medium: IdealGasMedium,
parameters: Mapping[str, float],
) -> Cylinder:
return cls(
name=name,
medium=medium,
V=parameters["volume"],
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.V)
self.port_b.p = props.p
self.port_b.h_outflow = props.h
return props
def refresh_thermodynamic_ports(self) -> ThermodynamicProperties:
return self.properties()
def state_derivative_from_ports(
self,
connected_h: Mapping[str, float],
) -> list[float]:
properties = self.properties()
derivative = self.derivatives_from_connection(
connected_h=connected_h["port_b"],
port_m_flow=self.port_b.m_flow,
internal_h=properties.h,
)
return derivative.as_vector()
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
pressure = self.medium.properties_from_mU(
self.state.m,
self.state.U,
self.V,
).p
return (
EquationResidual(
id=f"{self.name}:port_b_pressure_state",
owner="component",
owner_id=self.name,
relation="state",
variables=(f"{self.name}.port_b.p", f"{self.name}.state"),
role="effort",
value=self.port_b.p - pressure,
),
)
def derivatives_from_connection(
self,
*,
connected_h: float,
port_m_flow: float,
internal_h: float,
) -> VolumeState:
inlet_h = self.connection_inlet_enthalpy(
port_m_flow=port_m_flow,
connected_h=connected_h,
internal_h=internal_h,
)
return self.derivatives(inlet_h, port_m_flow)
def derivatives(self, inlet_h: float, m_flow: float) -> VolumeState:
return VolumeState(m=m_flow, U=m_flow * inlet_h)
@@ -0,0 +1,155 @@
from __future__ import annotations
from collections.abc import Mapping
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,
THERMODYNAMIC_VOLUME_RESULT_VARIABLES,
)
from app.simulation.core.medium import IdealGasMedium, ThermodynamicProperties
from app.simulation.core.ports import PortDefinition
from app.simulation.core.state import VolumeState
class Tank(ThermodynamicVolumeComponent):
"""Python port of ModelicaModels.Mytank."""
MODEL_TYPE = "tank"
MODEL_VERSION = "1.0.0"
PORTS = (PortDefinition.pneumatic("port_a", nominal_role="inlet"),)
PARAMETERS = (
ParameterDefinition(
"volume",
0.1,
label="容积",
quantity="volume",
unit="m3",
minimum=0.0,
minimum_exclusive=True,
),
ParameterDefinition(
"p0",
1e5,
label="初始压力",
quantity="pressure",
unit="Pa",
minimum=0.0,
minimum_exclusive=True,
),
ParameterDefinition(
"T0",
300.0,
label="初始温度",
quantity="temperature",
unit="K",
minimum=0.0,
minimum_exclusive=True,
),
)
RESULT_VARIABLES = THERMODYNAMIC_VOLUME_RESULT_VARIABLES
DISPLAY = ComponentDisplaySpec(
label="贮箱",
library_id="experimental",
category_id="storage",
symbol="tank",
ports=(PortDisplaySpec("port_a", "left"),),
order=20,
)
def __init__(
self,
name: str,
medium: IdealGasMedium,
V: float = 0.1,
p0: float = 1e5,
T0: float = 300.0,
) -> None:
super().__init__(name=name)
self.set_parameter_values({"volume": V, "p0": p0, "T0": T0})
self.medium = medium
self.V = V
m0 = p0 * V / (medium.R_gas * T0)
U0 = m0 * medium.specific_internal_energy(T0)
self.state = VolumeState(m=m0, U=U0)
self.port_a = self.register_declared_port("port_a")
@classmethod
def create(
cls,
*,
name: str,
medium: IdealGasMedium,
parameters: Mapping[str, float],
) -> Tank:
return cls(
name=name,
medium=medium,
V=parameters["volume"],
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.V)
self.port_a.p = props.p
self.port_a.h_outflow = props.h
return props
def refresh_thermodynamic_ports(self) -> ThermodynamicProperties:
return self.properties()
def state_derivative_from_ports(
self,
connected_h: Mapping[str, float],
) -> list[float]:
properties = self.properties()
derivative = self.derivatives_from_connection(
connected_h=connected_h["port_a"],
port_m_flow=self.port_a.m_flow,
internal_h=properties.h,
)
return derivative.as_vector()
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
pressure = self.medium.properties_from_mU(
self.state.m,
self.state.U,
self.V,
).p
return (
EquationResidual(
id=f"{self.name}:port_a_pressure_state",
owner="component",
owner_id=self.name,
relation="state",
variables=(f"{self.name}.port_a.p", f"{self.name}.state"),
role="effort",
value=self.port_a.p - pressure,
),
)
def derivatives_from_connection(
self,
*,
connected_h: float,
port_m_flow: float,
internal_h: float,
) -> VolumeState:
inlet_h = self.connection_inlet_enthalpy(
port_m_flow=port_m_flow,
connected_h=connected_h,
internal_h=internal_h,
)
return self.derivatives(inlet_h, port_m_flow)
def derivatives(self, inlet_h: float, m_flow: float) -> VolumeState:
return VolumeState(m=m_flow, U=m_flow * inlet_h)