封面为 AI 生成的工程概念插画。正文架构图依据源码手工组织、程序绘制,实机截图另行标注。
在流程图上放一台加热器,把入口和出口连起来,再指定出口温度,看上去只做了几次鼠标操作。但我自己写流程模拟软件以后,越来越在意按钮背后的几个问题:加热器拿到的入口状态从哪里来?它怎样知道该用哪套物性?出口状态算完以后,谁负责把结果送给下一个设备?
这些问题如果靠“大家都能访问一个全局对象”来解决,最初确实省事。等到同一类设备出现多个实例、流程开始反复重算,或者希望接入外部模型时,共享状态、生命周期和计算职责就会纠缠在一起。界面上只是多了一个图标,代码里却可能已经多了一条隐蔽依赖。
这篇文章就沿着一台加热器,把 RadishFlow 当前的职责分层、实际调用链和下一步的扩展边界拆开。实现状态以 2026 年 9 月 29 日核对的源码为准:现有计算主线是六类内建单元、简化物性和无回路的序贯模块求解;模型目录、通用插件发现、多策略装配属于设计方向。
一、先看全图:流程模拟软件里有哪些不同的工作
图 1|职责架构图。实线区域对应当前代码,虚线区域是规划。按责任组织层次,不表示所有依赖都已严格单向,也不是部署架构图。离线 HTML 中可点击放大。
我把这张图的中心理解为一条从“用户意图”到“计算结果”的链条。Studio 接收编辑操作;应用协调代码检查请求、装配本次运行的物性服务;流程层描述节点与物流;求解器安排执行;单元模型完成局部计算;结果再回到工作区。
这里有三条边界值得单独看。
第一条是流程定义与流程执行的边界。一张流程图要能保存设备、参数、连接和用户指定的物流状态。但保存了这些信息,并不代表已经有一个正在运行的求解器。rf-model 与 rf-flowsheet 承担模型表达和图结构相关工作;rf-solver 才负责把流程组织成一次计算。
第二条是全局调度与局部物理模型的边界。求解器需要知道先算谁、入口是否可用、出口如何传递;加热器需要知道给定入口和出口条件后,应返回怎样的物流状态。把这两种责任拆开,局部模型才不必自己遍历整张流程图。
第三条是计算内核与外部互操作的边界。Rust 内核不直接理解 COM 对象。CAPE-OPEN 对接中的 Windows 接口、对象激活与类型转换,应留在 .NET/COM 适配边界。这种划分要靠实际调用关系维护,不能只靠文件夹名称保证。
图里也保留了当前的不整齐:部分应用命令在 rf-ui 和 Studio 桥接代码中,并没有独立成熟的应用服务层;物性数据缓存仍触及存储 IO;CLI 仍带有桌面相关依赖;rf-canvas 还是占位。因此,这是一张可以拿源码逐项对照的现状图,同时也暴露出后续需要收敛的依赖。
二、物流不是屏幕上的一根线
画布上的物流连线主要表达连接关系。计算需要的却是一组带身份的状态数据。当前模型中有这样的结构,以下为源码节选,省略派生与序列化标记:
pub struct MaterialStreamState {
pub id: StreamId,
pub name: String,
pub temperature_k: f64,
pub pressure_pa: f64,
pub total_molar_flow_mol_s: f64,
pub overall_mole_fractions: Composition,
pub phases: Vec<PhaseState>,
pub bubble_dew_window: Option<BubbleDewWindow>,
}
这里的 StreamId 解决“这是哪股物流”;温度、压力、流量和组成解决“这股物流处于什么状态”;相信息和泡露点窗口承载局部计算补充的结果。端口连接负责把这些状态送到正确的设备入口。
我特别在意身份与数值的区别。用户把物流从 S-01 改名为“回收液”,不应该让连接关系失效。求解得到了新的温度,也不应该靠显示名称去猜它属于哪个对象。稳定 ID 让界面名称、拓扑关系与计算数据之间有一条明确的关联。
但是,结构里出现一个 f64,也不意味着问题已经解决。字段名可以表达约定单位,工程意义仍要由模型契约和校验保证:组成是否归一、总流量是否非负、压力是否处于有效域、相分率是否合理。把这些问题留给显示层补救,会让错误在计算链条中传播得很远。
当前 UnitNode 保存的则是另一类信息:节点 ID、名称、kind、端口和参数。它是工程中的设备对象;其 kind 用来选择内建实现,端口决定连接,参数表达用户规格。工程里保存一个 Heater 节点,与内存里创建一个可执行的 Heater 对象,是两个步骤。
三、单元怎样调用物性:把依赖摆在函数入口
当前单元的关键入口如下。这仍是源码节选,省略辅助校验方法,不是完整可编译文件。
#[derive(Clone, Copy, Default)]
pub struct UnitOperationServices<'a> {
pub thermo: Option<&'a dyn ThermoProvider>,
pub flash_solver: Option<&'a dyn TpFlashSolver>,
}
pub trait UnitOperation {
fn kind(&self) -> BuiltinUnitKind;
fn run(
&self,
services: &UnitOperationServices<'_>,
inputs: &UnitOperationInputs,
) -> RfResult<UnitOperationOutputs>;
}
这段接口提供了一个很具体的约束:单元拿到的是本次执行的输入和显式提供的服务。它不需要去 Studio 查当前选中的物性包,也不应该从某个隐含的全局变量里寻找另一股物流。
&dyn ThermoProvider 是对物性服务契约的引用,不是“给每台设备复制一份物性数据库”。同一次运行里,多个单元可以使用同一套服务;服务的存在期由装配方控制,Rust 的引用生命周期参与约束。Option 也不等于所有单元都能在没有物性的情况下运行:需要物性的具体实现仍会检查服务是否存在,并返回带上下文的错误。
以当前 Heater 为例,它从入口取得流量和组成,从出口模板取得指定温度与压力,再构造出口物流;对于适用状态,调用相应物性与闪蒸辅助逻辑,补充焓等状态信息。这里的“输出”是一组按端口归属的状态,交给上层继续处理。
这个接口的价值很朴素:可以单独给 Heater 提供一组输入和测试服务,检查它的局部行为;也可以在流程运行时传入正式服务。模型计算不需要连带启动画布。
但接口名称不能替模型背书。当前 Heater 主路径按指定出口温度与压力构造状态,并没有实现“给定热负荷,反求出口温度”的完整逆问题。当前常热容焓近似主要是显热口径,缺少完整潜热与参考态处理。Mixer 的温度混合和 Valve 的保温降压也有明确简化。工程师需要知道的,既包括函数怎样调用,也包括函数究竟在算什么。
四、沿着一次运行追到结果
图 2|从 Studio 到求解快照的当前同步调用链。solved_streams 是本次运行中的流股映射,不是一个已经实现的独立后台任务数据库。
应用桥接代码先校验运行请求,记录文档 revision,推进工作区运行状态,再装配物性服务。当前流程级 property_package_id 被交给 PropertyPackageProvider::load_system,得到 ThermoSystem,随后构造现有的物性提供者和 TP 闪蒸求解器。
进入 SequentialModularSolver 以后,工作可以用下面这段弱代码表达。它是根据实现压缩的执行逻辑,变量名作了简化:
order = topological_unit_order(flowsheet)
solved_streams = Map<StreamId, MaterialStreamState>()
steps = []
for node_id in order:
node = flowsheet.unit(node_id)
validate_node(node)
operation = instantiate_operation(node, flowsheet)
inputs = collect_inlets(node, solved_streams)
validate_inputs_and_parameters(operation, inputs)
outputs = operation.run(services, inputs)
publish_outlets_to_map(node, outputs, solved_streams)
steps.append(record_consumed_and_produced_states())
return SolverSnapshot(solved_streams, steps, diagnostics)
这段逻辑解释了两件容易被流程图隐藏的事。
一是上游出口必须先成为可用状态,下游才能读取它。拓扑排序负责找出满足依赖的顺序。当前求解器只接受无环结构,检测到循环会报错;它没有在这个 for 循环之外悄悄补上回流收敛器。
二是同一条“工程内的物流”可以在不同运行中对应不同的计算结果。solved_streams 保存这次计算已经产生的状态;步骤记录保存各单元消耗和产出的状态副本,最终一起组成结果快照。应用层收到成功结果后,再登记快照和日志。
这里的 revision 让结果可以带着其计算来源被记录下来,但不要据此推导出一个现成的后台任务系统。当前路径是同步调用,尚未实现独立冻结工程副本、异步运行,以及返回结果时按版本决定是否发布的完整协议。未来做后台求解时,这些才需要成为明确的状态机与提交规则。
例如,用户启动计算后又修改 Heater 的出口温度,旧计算返回的结果究竟应该展示在哪里?合理设计应允许它作为历史结果被保留,同时避免把它误标为新参数的当前解。这里需要的是“输入版本—运行—结果”的对应关系,单纯在线程池里运行同一个函数并不能自动解决。
图 3|2026 年 9 月 29 日实机取景:本机已有的 9 月 28 日构建,运行 Heater–Flash 示例后选中 Heater。截图未生成式修改。界面上的收敛状态说明本次软件路径完成,不证明简化物性达到了工业精度。
五、为什么“能读物性包”还不等于“有插件系统”
当前物性包提供者的契约很短:
pub trait PropertyPackageProvider {
fn list_manifests(&self) -> Vec<PropertyPackageManifest>;
fn load_system(&self, package_id: &str)
-> RfResult<ThermoSystem>;
}
它能列出描述信息,并根据 ID 装载热力学系统数据。但这个“包”首先是数据与配置意义上的包。当前桥接层在装载之后,仍然创建固定的 PlaceholderThermoProvider 和 PlaceholderTpFlashSolver 实现。
所以,新增一份组分数据,与新增一套可执行的状态方程算法,工作量和边界不同。前者主要改变数据;后者还要回答计算契约、有效域、闪蒸协作、错误语义和运行生命周期。把二者都叫“物性插件”,容易让读者误以为扫描一个目录就能切换任意热力学引擎。
内建单元也一样。当前工厂按 unit.kind 做固定分支,构造 Feed、Mixer、Heater、Cooler、Valve 或 FlashDrum。加入一种新设备,通常还需要补类型、端口规格、参数表达、工厂分支与行为校验,并不是把一个动态库放进文件夹就结束了。
这并不妨碍我们先把调用契约分清楚。相反,静态实现足够具体,可以帮助我们发现未来开放接口真正需要表达什么。例如,设备支持的是“局部输入到输出计算”,还是“导出残差方程”,或者还能提供导数?如果没有这层能力描述,界面上列出一个名字,并不足以说明求解器能否使用它。
六、模型定义、工程实例、运行实例,为什么要分开
图 4|上半部是当前固定工厂,下半部是模型生命周期的规划拆分。虚线框不是已经落地的接口。
假设同一个工程里有两台 Heater。它们共享同一类模型的端口约定和计算代码,但各自的名称、连接和出口温度不同。模型定义可以共享,工程实例必须独立。进一步说,同一台 Heater 参加两次求解,其临时缓存和外部服务句柄也不应因为属于同一工程对象而被无条件复用。
因此,规划中的三个对象分别回答三个问题:模型定义回答“这是什么模型、有哪些能力”;工程实例回答“用户在这张流程图里如何配置它”;运行实例回答“它在这一次求解中持有哪些临时状态与资源”。
这会直接影响保存格式。工程文件适合保存模型标识、版本要求、参数和连接;不适合把 COM 指针或某次 Newton 迭代的缓存当成可移植数据。重新打开工程时,需要据此恢复可运行对象,并校验模型是否仍然可用。
也会影响并发策略。若一个外部模型内部可变且不支持并发,就不能因为两个工程实例使用了同一模型定义,便让它们共享一个活动对象。这里需要显式的实例化和资源管理规则;当前代码中的服务引用方式并不自动构成第三方组件线程安全保证。
物性同样存在“共享参数与具体状态”的区别。IDAES 2.9.0 的物性架构把参数块与状态块分开,为多个状态对象引用公共参数提供了一个可参考的例子。这是架构上的参照,不意味着两个项目的模型表示或求解方式相同。
RadishFlow 当前主要按流程级物性包装配服务。若未来允许不同单元绑定不同物性模型,还必须定义跨边界的状态口径:组分顺序怎么映射、焓参考态是否一致、下游是否需要重新闪蒸。只给每个设备加一个下拉框,并不能保证跨包传递的数值具有一致意义。
七、CAPE-OPEN:组件与宿主,是两套责任
讨论开放性时,最容易混在一起的,是“让别人调用我的模型”和“让我调用别人的模型”。
当前已有的是受限的对外适配路径:外部流程模拟环境作为宿主,通过 .NET/COM 适配层调用 RadishFlow 暴露的能力。项目记录中有特定范围的 DWSIM、COFE 验证;这不能外推成任意组件、任意流程、任意版本都兼容。
要让 RadishFlow 自己成为加载外部组件的宿主,还要承担发现与激活、物料对象映射、端口连接、参数交互、错误翻译、对象释放等工作。尤其是跨语言边界,Rust 的局部借用关系不会替 COM 对象管理全部生命周期。
例如,内部物流用自己的 StreamId 和组分表示;外部组件可能要求按其接口提供组成数组和物性查询。适配层必须保证数组顺序、单位与相态含义一致。一次调用成功,只能说明调用发生了;这些含义一致,才接近我们想要的工程互操作。
CO-LaN 的 Unit Operation 文档 v6.25 与 Thermodynamics 1.1 规范分别描述了相关单元通信和物性接口职责。这里的 v6.25 是文档版本,不能当作一个最新软件版本;规范也不能替代具体宿主与组件组合的验收。
八、这张架构图,最后要落到可检查的契约上
如果下一步加入一台更复杂的设备,我希望能逐层回答:它把哪些信息存入工程?运行时需要什么物性能力?局部计算失败时怎样指出对象和条件?它能被哪种求解策略装配?换一个实例或重新运行时,哪些状态必须重建?
这也是我现在保留简化边界、继续整理接口的原因。界面、模型目录、物性和求解器都需要发展,但每增加一项能力,都应该能说清楚它改变了哪条契约,以及怎样验证这条契约。
当我们沿着 Heater 的一次执行,能从参数追到服务、从输入追到出口、从出口追到结果快照,架构图才开始成为有用的工程工具。接下来要走向回流与多策略求解,真正的问题也随之清楚了:局部模型的内部变量,是由它自己消掉,还是交给全局求解器一起求?另一篇文章会用一个可以手算和复算的回流例子,把这个区别展开。




