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.
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
defaultExperimentin 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
- Instantiating a topology: behavioral models, mocks and Docker settings.
- Running a topology: generating, interactive mode and running tests.
- API and client library: the full Python API reference, including the
fmumodule. - Examples: the examples repository, a good place to start from a working setup.
