# 元件建模规范与示例 规范的权威版本位于 [`docs/standard/component-model-authoring-spec-v1.md`](../../../docs/standard/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/standard/component-library-spec-v1.md)。