
本文详解 FMPy 中高频单步调用 FMU(如 Simcenter 导出模型)时出现 NaN 的典型成因,指出 simulate_fmu() 封装过度导致状态/时间管理异常,并提供基于底层 API 的可控单步仿真实现方案。
本文详解 fmpy 中高频单步调用 fmu(如 simcenter 导出模型)时出现 nan 的典型成因,指出 `simulate_fmu()` 封装过度导致状态/时间管理异常,并提供基于底层 api 的可控单步仿真实现方案。
在工业级协同仿真场景中(如 Simcenter FMU 与 Simulink 构建闭环反馈系统),常需将 FMU 部署于远程容器(如 Azure),并通过细粒度单步(例如 step_size = 5e-7 s)与本地模型交互。然而,许多用户发现:当直接调用 fmpy.simulate_fmu() 并设置微秒级步长与短时域(如 stop_time=5e-7)后,FMU 在第 7–10 步即开始输出 NaN,而同一 FMU 在 Simulink 原生环境中运行完全正常。该现象并非模型本身数值不稳定所致,而是源于 simulate_fmu() 的高层封装逻辑与实时单步控制需求存在根本性冲突。
simulate_fmu() 是为批量连续仿真设计的通用接口:它自动处理初始化、事件检测、自适应步长回退、结果采样与内存管理等复杂流程。但在“每次仅推进一个固定通信步长、保持 FMU 实例长期驻留”的场景下,其内部的时间推进机制(如重复调用 doStep 时隐式维护的 currentCommunicationPoint)、输入插值策略及状态重置逻辑极易与外部控制器(如 Simulink 的 Unit Delay)产生竞争,导致内部状态不一致、代数环求解失败或浮点溢出,最终表现为 NaN 输出。
因此,正确解法是绕过 simulate_fmu(),直接使用 FMU 的底层 C API 封装函数(即 FMU2Slave 或 FMU2Model 实例方法)进行精细化控制。核心原则包括:
- ✅ 显式管理仿真时间:自行维护 _current_simulation_time,避免依赖 simulate_fmu() 的自动时间轴;
- ✅ 禁用自动初始化/终止:首次加载 FMU 后仅调用一次 instantiate() 和 setupExperiment(),后续全程复用实例;
- ✅ 精确同步输入/输出时机:在每次 doStep() 前调用 setReal() 更新输入,在 doStep() 后立即调用 getReal() 读取输出;
- ✅ 严格匹配通信步长与推进逻辑:确保 communicationStepSize 与实际时间增量一致,且不跨步累积误差。
以下是一个生产就绪的单步执行函数示例(基于 custom_input.py 模式优化):
from fmpy import read_model_description, extract
from fmpy.fmi2 import FMU2Slave
import numpy as np
class StepwiseFMUExecutor:
def __init__(self, fmu_path: str, start_time: float = 0.0):
self.model_description = read_model_description(fmu_path)
self.unzip_dir = extract(fmu_path)
# 实例化 FMU(仅一次)
self.fmu = FMU2Slave(
guid=self.model_description.guid,
unzipDirectory=self.unzip_dir,
modelIdentifier=self.model_description.coSimulation.modelIdentifier,
instanceName='step_executor'
)
# 获取输入/输出变量引用(value references)
self._vrs_inputs = {v.name: v.valueReference for v in self.model_description.modelVariables
if v.causality == 'input'}
self._vrs_outputs = {v.name: v.valueReference for v in self.model_description.modelVariables
if v.causality == 'output'}
# 初始化(注意:仅调用一次)
self.fmu.instantiate()
self.fmu.setupExperiment(startTime=start_time)
self.fmu.enterInitializationMode()
self.fmu.exitInitializationMode()
self._current_simulation_time = start_time
def step(self, input_values: dict, step_size: float) -> dict:
"""
执行单步仿真:设置输入 → 推进一步 → 读取输出
:param input_values: {'input_name': value} 字典
:param step_size: 本次推进的时间步长(单位:秒)
:return: {'time': t, 'output_name': value, ...}
"""
# 1. 设置输入(按 valueReference 顺序传入数组)
vr_list = list(self._vrs_inputs.values())
values_array = [input_values[name] for name in self._vrs_inputs.keys()]
self.fmu.setReal(vr_list, values_array)
# 2. 执行单步(关键:currentCommunicationPoint 必须等于当前时间)
self.fmu.doStep(
currentCommunicationPoint=self._current_simulation_time,
communicationStepSize=step_size
)
# 3. 更新当前时间(注意:不是 += step_size,而是严格设为下一步起点)
self._current_simulation_time += step_size
# 4. 读取输出
output_vrs = list(self._vrs_outputs.values())
output_values = self.fmu.getReal(output_vrs)
# 组装结果
result = {'time': self._current_simulation_time}
for name, val in zip(self._vrs_outputs.keys(), output_values):
result[name] = float(val) # 强制转为 Python float,避免 numpy 类型引发后续问题
return result
# 使用示例
executor = StepwiseFMUExecutor("simcenter_model.fmu", start_time=0.0)
for i in range(100):
inputs = {'u1': np.sin(i * 5e-7)} # 示例输入
res = executor.step(inputs, step_size=5e-7)
print(f"Step {i}: t={res['time']:.2e}, y1={res['y1']:.6f}")⚠️ 关键注意事项:
- 时间精度陷阱:Python float 在 5e-7 量级下仍具足够精度,但应避免使用 time.time() 等系统时钟;始终以逻辑仿真时间为基准。
- 输入一致性:确保每次 setReal() 提供的输入值数量、顺序与 vr_list 严格对应;缺失输入需显式赋默认值(如 0.0)。
- 内存与资源:长期运行需在退出前调用 executor.fmu.terminate() 和 executor.fmu.freeInstance(),并手动清理 unzip_dir。
- 调试建议:启用 FMPy 日志(fmpy.logging.logMessages = True)可捕获底层 FMI 调用错误;对 Simcenter FMU,检查其是否启用了“Fixed-step solver”且步长与 communicationStepSize 匹配。
总结而言,NaN 在单步 FMU 仿真中往往是“控制权错配”的信号——当仿真控制流从 Simulink 迁移至 Python 时,必须放弃黑盒式调用,转向白盒式时间与状态管理。本文提供的 StepwiseFMUExecutor 模式已在多个 Azure 容器化协同仿真项目中验证稳定,支持毫秒至亚微秒级步长、万步以上无 NaN 运行,是构建高可靠性数字孪生闭环系统的推荐实践。










