Files
SystemSimulationApp/app/simulation/components/example.md
T

10 KiB

元件建模规范与示例

规范的权威版本位于 docs/standard/component-model-authoring-spec-v1.md。 本文档保留在组件目录中,作为离模型源码最近的完整示例;若两者不一致,应在同一次 修改中同步,不能让示例形成另一套规则。

本文档是 app/simulation/components 下新增元件的最小开发规范。当前 experimental 是用于验证规范的临时组件库;后续正式模型应建立独立组件库, 不要继续堆放在 experimental 中。

目标是让元件的端口、输入参数和可展示结果都由元件类显式声明,避免 XML 校验、求解器和前端分别维护同一份含义。

一、元件类必须声明的内容

每个对外注册的元件类至少需要声明以下六个类属性:

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 专门用于界面显示。

参数定义示例:

ParameterDefinition(
    name="volume",
    label="容积",
    quantity="volume",
    unit="m3",
    default=0.1,
    minimum=0.0,
    minimum_exclusive=True,
)

结果变量定义示例:

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 文件,并补充对应测试。

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 清单:

models=(
    # ...已有模型
    "app.simulation.components.experimental.storage.example_volume:ExampleVolume",
)

后端会受控导入清单中的类,校验版本、分类、端口、参数、单位、显示信息和默认实例, 再自动建立注册表。校验通过后,GET /api/components/catalog 会输出该元件, 前端刷新时即可加载。 当前 experimental 仅用于规范验证;正式模型应先建立新的库声明,再把 library_id 指向正式库。

完成仿真后,每个已声明结果都会得到一条结构化元数据。前端应按字段筛选,不能再拆解 key 猜测含义:

{
  "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。