Skip to main content

Integrating an FMU with Python

· 5 min read

Reviewed by Filip Åhfelt

If someone on your team already models a system in a simulation tool, you probably don't want to rewrite that model by hand as an ECU. With the fmu module in the RemotiveTopology Python framework you can load the exported FMU, connect it to the signals on your virtual buses, and run it as a behavioral model next to everything else in the topology.

TODO

Also needed in the docs: once this post is written, add (or link to) a matching how-to page under docs/remotive-topology.

What an FMU is and when you would use one​

A Functional Mock-up Unit is a packaged simulation model, following the FMI standard, that many modeling tools can export. It's a zip file with a model description (the inputs, outputs and default experiment settings) and the compiled model.

In RemotiveTopology, an FMU is one of the supported kinds of behavioral model, next to plain Python and Synopsys Silver. A behavioral model emulates the behavior of a real ECU, so an FMU is a good fit when the behavior you need already exists as a physics or control model: vehicle dynamics, a battery, a thermal system, or the classic bouncing ball. Instead of hard-coding values in a mock, the model reacts to what's on the bus and produces realistic outputs.

Prerequisites​

You need a working RemotiveTopology setup, as described in Install: Docker (or Podman), RemotiveCLI with an active subscription, and Python 3.10 or later for behavioral models.

The FMU support lives in the remotivelabs-topology package behind an extra, since it pulls in fmpy and numpy:

pip install remotivelabs-topology[fmu]

Then the FMU itself. The framework drives the model one step at a time, so it needs to be:

  • an FMI 2.0 or 3.0 Co-Simulation FMU,
  • configured with a fixed step size and no internal solver,
  • carrying a defaultExperiment in its model description (that's where the step size is read from).

If the FMU doesn't support co-simulation or lacks a default experiment, loading it fails with an error straight away, which is a nice early warning.

Loading the FMU from Python​

The entry point is FMUSimulation. Give it a path, and it reads the model description, instantiates the FMU and resets it to its initial state:

from pathlib import Path

from remotivelabs.topology.fmu import FMUSimulation

fmu = FMUSimulation(Path("speed_model.fmu"))
fmu.print() # dump model information and variables

print(fmu.step_size)
print(fmu.input_var_names)
print(fmu.output_var_names)

print() is the first thing I run on any new FMU: it shows the variable names you'll need in the next step. The step size comes from the default experiment, but you can override it with FMUSimulation(path, step_size=0.01).

Each call to await fmu.step() advances the simulation by one step, applies the current inputs and returns the outputs. You can also set an input directly with fmu.set_input_value("name", value); it sticks for all future steps, and an unknown name raises a ValueError.

Mapping FMU inputs and outputs to signals​

The FMUBehavioralModel class connects an FMUSimulation to the topology. Inputs are described with an InputMapping, a nested dictionary from namespace, to frame, to signal, to FMU input name. Namespaces are named <ECU>-<Channel>, as described in Instantiating a topology.

In this example (the ECU, frame and variable names are made up), the FMU gets its accelerator_pedal input from a signal on ChassisCan0:

from remotivelabs.topology.fmu import InputMapping

input_mapping: InputMapping = {
"SpeedModel-ChassisCan0": {
"PedalStatus": {
"AcceleratorPedalPosition": "accelerator_pedal",
},
},
}

Whenever a matching frame arrives, the model writes the signal value to the FMU input. Values outside the signal's min and max from the signal database are ignored, so a bad frame doesn't push the model into nonsense.

Outputs go the other way. FMUSimulation takes an on_output callback that runs after every step with a dictionary of output values. A simple way to put them on the bus is to let a restbus send the ECU's frames and update their signals from the callback:

from remotivelabs.topology.namespaces import filters
from remotivelabs.topology.namespaces.can import CanNamespace, RestbusConfig

chassis_can = CanNamespace(
"SpeedModel-ChassisCan0",
broker_client,
restbus_configs=[RestbusConfig([filters.SenderFilter(ecu_name="SpeedModel")])],
)

async def on_output(outputs):
await chassis_can.restbus.update_signals(("VehicleSpeed", outputs["vehicle_speed"]))

Running it as part of a topology​

Putting it together, the model script looks much like any other behavioral model. run_forever() starts a ticker at the FMU's step size, steps the simulation on every tick and handles inputs and control requests at the same time:

import asyncio
from pathlib import Path

from remotivelabs.broker import BrokerClient
from remotivelabs.topology.cli.behavioral_model import BehavioralModelArgs
from remotivelabs.topology.fmu import FMUBehavioralModel, FMUSimulation
from remotivelabs.topology.namespaces import filters
from remotivelabs.topology.namespaces.can import CanNamespace, RestbusConfig


async def main(args: BehavioralModelArgs):
async with BrokerClient(url=args.url, auth=args.auth) as broker_client:
chassis_can = CanNamespace(
"SpeedModel-ChassisCan0",
broker_client,
restbus_configs=[RestbusConfig([filters.SenderFilter(ecu_name="SpeedModel")])],
)

async def on_output(outputs):
await chassis_can.restbus.update_signals(("VehicleSpeed", outputs["vehicle_speed"]))

fmu = FMUSimulation(Path("speed_model.fmu"), on_output=on_output)

async with FMUBehavioralModel(
"SpeedModel",
fmu=fmu,
input_mapping={
"SpeedModel-ChassisCan0": {
"PedalStatus": {"AcceleratorPedalPosition": "accelerator_pedal"},
},
},
broker_client=broker_client,
namespaces=[chassis_can],
) as bm:
await bm.run_forever()


if __name__ == "__main__":
asyncio.run(main(BehavioralModelArgs.parse()))

A bonus you get for free: when the model receives a reboot request, it resets the FMU to its initial state along with the restbuses, so a test can start every case from a known state.

In the instance file, the model is added to its ECU like any Python behavioral model:

ecus:
SpeedModel:
models:
speed_model:
type: python
include: ../ecus
main: speed_model

The container that runs Python models needs the fmu extra installed, so add it to the image or Dockerfile you configure under settings.docker.python. Then generate and start the topology as usual:

remotive topology generate -f mytopology.instance.yaml build
docker compose -f build/mytopology/docker-compose.yml up

If the signals don't show up, check the namespace names first:

remotive broker signals namespaces --url http://localhost:50051

Where to go next​

About the author

Albin Theander

Works on RemotiveTopology and its Python framework, including FMU integration.

Reviewed by Filip Åhfelt