Replace Python numerical kernels with native C execution
This commit is contained in:
1 parent
48da6be21c
commit
3b38f73fe0
227 files changed
+16801
-75499
No files matched your search
@@ -1,282 +1,14 @@
|
||||
# 元件建模规范与示例
|
||||
# 元件开发示例
|
||||
|
||||
规范的权威版本位于
|
||||
[`docs/standard/component-model-authoring-spec-v1.md`](../../../docs/standard/component-model-authoring-spec-v1.md)。
|
||||
本文档保留在组件目录中,作为离模型源码最近的完整示例;若两者不一致,应在同一次
|
||||
修改中同步,不能让示例形成另一套规则。
|
||||
权威规则见 [组件模型建模规范](../../../docs/standard/component-model-authoring-spec-v1.md)。当前模型采用 Python 声明、C 数值实现。
|
||||
|
||||
本文档是 `app/simulation/components` 下新增元件的最小开发规范。当前
|
||||
`experimental` 是用于验证规范的临时组件库;后续正式模型应建立独立组件库,
|
||||
不要继续堆放在 `experimental` 中。
|
||||
以气瓶为例:
|
||||
|
||||
目标是让元件的端口、输入参数和可展示结果都由元件类显式声明,避免 XML
|
||||
校验、求解器和前端分别维护同一份含义。
|
||||
1. 在 [cylinder.py](experimental/storage/cylinder.py) 声明 `MODEL_TYPE`、`MODEL_VERSION`、`PORTS`、`PARAMETERS`、`RESULT_VARIABLES`、`DISPLAY` 和 `create()`。
|
||||
2. 构造函数调用 `set_parameter_values()`、`register_declared_port()`,保存介质选择和容积。不要在 Python 中计算密度、内能或状态导数。
|
||||
3. 通过 `EQUATIONS` 声明端口压力与气瓶状态之间的约束;只保存变量名和关系。
|
||||
4. 在 [extended.py](../native_codegen/extended.py) 分配状态及输出位置,生成 `native_medium_init()` 初始化调用和气瓶质量/能量导数计算。
|
||||
5. 公共物性和数值公式由 [kernels.c](../../../native/components/kernels.c) 实现,积分和事件由 `native/runtime/` 处理。
|
||||
6. 加入组件库 `library.py` 及 C 版本白名单,验证目录/XML 合同、边界输入、逆流、守恒、RK45/BDF 和输出键。
|
||||
|
||||
## 一、元件类必须声明的内容
|
||||
|
||||
每个对外注册的元件类至少需要声明以下六个类属性:
|
||||
|
||||
```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/standard/component-library-spec-v1.md)。
|
||||
新增模型的参考值应来自独立解析结果、外部可信结果或已有冻结基准;不恢复第二套 Python 数值实现。
|
||||
Reference in new issue
Block a user