Creating Functional Mock-up Unit (FMU) models using PythonFMU and component-model
While the Functional Mock-up Interface (FMI) standard has revolutionized model exchange and co-simulation across tools, creating FMUs traditionally required specialized environments or complex C++ implementations. Python's dominance in machine learning (ML) and scientific computing makes it an ideal choice for FMU development - allowing modelers to leverage its vast libraries, rapid prototyping capabilities, and gentle learning curve. By using Python to create FMUs, developers can quickly transform their Python-based algorithms or ML models into standardized components that integrate seamlessly with any FMI-compliant simulation tool. As we will demonstrate through a practical example, this approach opens new possibilities for combining Python's accessibility with FMU's interoperability benefits.
Table of Contents
- Background
- Developing a Python simulation model
- Installing the PythonFMU package
- Developing the Python FMU
- Building the model
- Enhancing trust in simulation models
- Developing the component-model FMU
- Installing the component-model package
- Developing the component-model FMU
- Build component-model FMU
- Concluding remarks
1. Background
Using low-level programming languages for creating simulation models may result in long development times and shipping code that is also difficult to validate and verify and thus prone to mistakes. Using a tool like Python is a more intuitive solution and enables more developers to easily create simulation models that follow the Functional Mock-up Interface (FMI) standard. The FMI standard is an open specification defining containers and interfaces for dynamic simulation models. It is supported and maintained by the Modelica Association Project and ensures that simulation models each packaged as a Functional Mockup Unit (FMU) along with digital twin equipment, can be seamlessly interfaced and reused across various organizations. We see that using a language such as Python will make the process of creating complex FMUs quicker and more efficient. Only if performance challenges arise, may it be necessary to translate such a prototype model into a more performant compiled code.
To create models using Python, modelers can use the PythonFMU package, publicly available from PyPi. This package acts as a wrapper tool that enables developers to write their simulation model's logic as Python code and package it as a co-simulation FMU. This is achieved by offering a high-level interface that implements the required FMI functions for co-simulation. Similarly, PythonFMU also offers a Command Line Interface (CLI) to build the FMU files. Note that the FMU does not include the Python environment or the dependency code. This means that in order to run an FMU, you must have Python, and the required dependencies already installed locally. However, the tool does allow you to bundle a requirements.txt file with the FMU. This file specifies the names and versions of the dependencies, and it can be used to install them via the CLI, making the FMU more portable.
One of the main advantages of using Python for writing models is that it empowers developers to leverage a wide range of libraries available in the Python ecosystem. This includes scientific libraries such as NumPy and SciPy. Also, AI-related libraries like TensorFlow or PyTorch could be used for running inference in a simulation step, but it is recommended to use MLFMU in such cases. The MLFMU package is discussed in a separate blog post.
2. Developing a Python simulation model
In this post, we explore the process of building Python FMUs by implementing a simple example: a 3D bouncing ball. By focusing on a basic model, we can better understand how simulation models that follow the FMI standard work, laying the groundwork for more complex models.
-
Installing the PythonFMU package
Let's start by installing the PythonFMU dependency with PyPi:
-
Developing the Python FMU
You can find the source code for this section in the here.
Now, we can start writing the skeleton for our simulation model. The first thing we need to do is implement a class that inherits from the Fmi2Slave class provided by PythonFMU and add some variable ports to it. These variable ports refer to the inputs, outputs, and parameters of the co-simulation model, which can then be connected to other models.
# BouncingBall3D.py
from math import sqrt
from pythonfmu import Fmi2Slave, Fmi2Causality, Real
class BouncingBall3D(Fmi2Slave):
"""A Python-based BouncingBall model, using PythonFMU.
Features:
- The ball has a 3-D vector as position and speed
- As output variable the model estimates the next bouncing point
- As parameters, the restitution coefficient `e`, the gravitational acceleration `g` and the initial speed can be changed.
- Internal units are assumed as SI (m,s,rad)
"""
def __init__(self, **kwargs):
super().__init__(
name = "BouncingBall3D",
description="A Python-based BouncingBall model, using Model and Variable to construct an FMU",
author="DNV, SEACo project",
**kwargs
)
# Register variable ports for the model
self.posX = 0.0
self.register_variable(Real("posX", causality=Fmi2Causality.output))
self.posY = 0.0
self.register_variable(Real("posY", causality=Fmi2Causality.output))
self.posZ = 10.0
self.register_variable(Real("posZ", causality=Fmi2Causality.output))
self.speedX = 0.0
self.register_variable(Real("speedX", causality=Fmi2Causality.output))
self.speedY = 0.0
self.register_variable(Real("speedY", causality=Fmi2Causality.output))
self.speedZ = 0.0
self.register_variable(Real("speedZ", causality=Fmi2Causality.output))
self.p_bounceX = -1.0
self.register_variable(Real("p_bounceX", causality=Fmi2Causality.output))
self.p_bounceY = -1.0
self.register_variable(Real("p_bounceY", causality=Fmi2Causality.output))
self.g = 9.81 # Gravitational acceleration
self.register_variable(Real("g", causality=Fmi2Causality.parameter))
self.e = 0.0 # Coefficient of restitution
self.register_variable(Real("e", causality=Fmi2Causality.parameter))
# Internal states
self.stopped = False
self.min_speed_z = 1e-6
self.accelerationX = 0.0
self.accelerationY = 0.0
self.accelerationZ = -self.g
def do_step(self, _, dt):
...
For the bouncing ball model, we registered variables to output the position of the ball in XYZ coordinates as well as its speed in every direction. Similarly, we add the gravitational acceleration as a parameter so that we have the possibility to test how the ball behaves on a different planet 🪐. We also add a coefficient of restitution, which varies across different balls, as a measure of how much kinetic energy remains after the ball bounces compared to before it hits the ground. This can be mathematically formulated as:
With \(0<e<1\)
(\(e=0\) is fully inelastic and \(e=1\) is fully elastic)
Note that each FMU variable that we register maps to an instance attribute with the same name. In the background, PythonFMU updates these instance variables after every simulation step, assigning the new values coming into the port, i.e. if it's connected to a different model's outputs or if introducing a failure manually . Finally, we also added some internal states to store the acceleration of the bouncing ball in every direction, a minimal speed in the Z axis and a flag to know when the ball has stopped bouncing.
Next, a do_step function is introduced to the class; this is where we get to define how our model advances in time, from one time step to the next. So, let's get to it:
# BouncingBall3D.py
class BouncingBall3D(Fmi2Slave):
...
def do_step(self, time, dt):
"""Perform a simulation step from `self.time` to `self.time + dt`.
With respect to bouncing (self.t_bounce should be initialized to a negative value)
- t_bounce <= time: update t_bounce
- time < t_bounce <= time+dt: bouncing happens within time step
- t_bounce > time+dt: no bouncing. Just advance pos and speed
"""
if self.t_bounce < self.time: # calculate first bounce
self.t_bounce, self.p_bounce = self.next_bounce()
while self.t_bounce <= self.time + step_size: # bounce happens within step or at border
dt1 = self.t_bounce - self.time
self.posX = self.p_bounceX
self.posY = self.p_bounceY
self.speedZ += self.accelerationZ * dt1 # speed before bouncing
# speed reduction due to coefficient of restitution
self.speedX *= self.e
self.speedY *= self.e
self.speedZ *= -self.e # change also direction
if self.speedZ < self.min_speed_z:
self.stopped = True
self.accelerationZ = 0.0
self.speedZ = 0.0
self.posZ = 0.0
self.time += dt1 # jump to the exact bounce time
step_size -= dt1
self.t_bounce, self.p_bounce = self.next_bounce() # update to the next bounce
self.p_bounceX, self.p_bounceY = self.p_bounce[0:2]
if step_size > 0:
self.posX += self.speedX * step_size
self.posY += self.speedY * step_size
self.posZ += self.speedZ * step_size + 0.5 * self.accelerationZ * step_size**2
self.speedZ += self.accelerationZ * step_size
self.time += step_size
if self.posZ < 0: # avoid numeric issues
self.posZ = 0
return True
def next_bounce(self) -> tuple[float, np.ndarray]:
"""Calculate time of next bounce and position where the ground will be hit,
based on .time, .pos and .speed.
"""
if self.stopped: # stopped bouncing
return (1e300, np.array((1e300, 1e300, 0), float))
else:
dt_bounce = (self.speedZ + sqrt(self.speedZ**2 + 2 * self.g * self.posZ)) / self.g
p_bounceX = self.posX + self.speedX * dt_bounce # linear in x and y!
p_bounceY = self.posY + self.speedY * dt_bounce
return (self.time + dt_bounce, np.array((p_bounceX, p_bounceY, 0), float))
def setup_experiment(self, start_time: float):
"""Set initial (non-interface) variables."""
super().setup_experiment(start_time)
self.stopped = False
self.time = start_time
The do_step() function is responsible for updating the ball's position and speed over time. This function is called by the co-simulation algorithm at each timestep. As the ball moves, its position in the X, Y, and Z axes is updated according to the current speed and acceleration. When the ball hits the ground (Z-axis), it bounces, and its speed is reduced based on the coefficient of restitution. If the ball's speed becomes too small, it stops bouncing. It is necessary to use an explicit speed limit (min_speed_z) to avoid the numerical algorithm becoming unstable.
The next_bounce() function calculates when and where the ball will hit the ground next, based on its current speed and position. In this way, a simulation step can be split into before and after bounce, rendering the results as realistic as possible. As the ball bounces, its speed is updated to reflect both the gravitational pull and the energy loss during the bounce. The model continues to update the ball's position and speed until the ball comes to rest. This simple model helps demonstrate the basic physics of bouncing and energy loss in a controlled environment.
The setup_experiment() function is available within PythonFMU for setting the internal state variables and performing initialization. For example, the do_step() function expects the t_bounce and p_bounce values to be updated when entering the function, which can be ensured by including next_bounce() here.
-
Building the model
To export the code as an FMU file, we can simply run the following command:
3. Enhancing trust in simulation models
We note that there are a few array-like updates in our implementation of the model, for example, when we update the positions or speeds in all axes. Nowadays, Python offers functionality to run array operations using a simpler syntax that requires less code. Also, we haven't touched upon the units for the input and output variables. How could we know if the models from other providers are compatible with the units we used in our model? And how can we produce models that are more robust and trustworthy such that we can bring that trust up in the value chain?
We see a rising demand for tools that streamline the creation of trustworthy models. This need stems from the industry's tendency to use models for critical decisions, such that well-founded trust in simulation models becomes essential. This is especially relevant for increasingly complex systems that have become ubiquitous in the digital age. To help with this, DNV has developed the Recommended Practice RP-0513 Assurance of Simulation models, which aims to provide a framework for assuring simulation models throughout their lifecycle. In addition, DNV has developed the Python package component-model, which is built on top of PythonFMU and offers various features that ease the creation of component-model FMUs that follow DNV's RP. In the current version, the package includes:
- Support for vector ports, i.e. non-scalar variables.
- Unit definitions, i.e. transformation of variables during input and output.
- Range definitions and validations.
- A simple possibility to create the FMU directly from a script.
- The option to allow model class parameters to be passed on to the FMU. In this way, settings, such as ranges, can be changed by re-running the build() function, without having to change the model class.
4. Developing a component-model FMU
-
Installing the component-model package
Like PythonFMU, we can install the component-model package using PyPi:
-
Developing the component-model FMU
You can find the source code for this section in the here.
Component-model's features allow us to create a more robust implementation of the bouncing ball model. So, let's rewrite the interface specification of the model using things like variable ports, valid ranges and units:
# BouncingBall3D.py
from math import sqrt
import numpy as np
from component_model.model import Model
from component_model.variable import Variable
class BouncingBall3D(Model):
def __init__(
self,
name: str = "BouncingBall3D",
description="Another Python-based BouncingBall model, using Model and Variable to construct an FMU",
pos: tuple = ("0 m", "0 m", "10 inch"),
speed: tuple = ("1 m/s", "0 m/s", "0 m/s"),
g: float = "9.81 m/s^2",
e: float = 0.9,
min_speed_z: float = 1e-6,
**kwargs,
):
super().__init__(name, description, author="DNV, SEACo project", **kwargs)
self._pos = self._interface("pos", pos)
self._speed = self._interface("speed", speed)
self._g = self._interface("g", g)
self.a = np.array((0, 0, -self.g), float)
self._e = self._interface("e", e)
self.min_speed_z = min_speed_z
self.stopped = False
self.time = 0.0
self._p_bounce = self._interface("p_bounce", ("0m", "0m", "0m")) # Note: 3D, but z always 0
self.t_bounce, self.p_bounce = self.next_bounce()
def do_step(self, _, dt):
...
def _interface(self, name: str, start: str | float | tuple) -> Variable:
"""Define a FMU2 interface variable, using the variable interface.
Args:
name (str): base name of the variable
start (str|float|tuple): start value of the variable (optionally with units)
Returns
-------
the variable object. As a side effect the variable value is made available as self.<name>
"""
if name == "pos":
return Variable(
self,
name="pos",
description="The 3D position of the ball [m] (height in inch as displayUnit example.",
causality="output",
variability="continuous",
initial="exact",
start=start,
rng=((0, "100 m"), None, (0, "10 m")),
)
elif name == "speed":
return Variable(
self,
name="speed",
description="The 3D speed of the ball, i.e. d pos / step_size [m/s]",
causality="output",
variability="continuous",
initial="exact",
start=start,
rng=((0, "1 m/s"), None, ("-100 m/s", "100 m/s")),
)
elif name == "g":
return Variable(
self,
name="g",
description="The gravitational acceleration (absolute value).",
causality="parameter",
variability="fixed",
start=start,
rng=(),
)
elif name == "e":
return Variable(
self,
name="e",
description="The coefficient of restitution, i.e. |speed after| / |speed before| bounce.",
causality="parameter",
variability="fixed",
start=start,
rng=(),
)
elif name == "p_bounce":
return Variable(
self,
name="p_bounce",
description="The expected position of the next bounce as 3D vector",
causality="output",
variability="continuous",
start=start,
rng=(),
)
else:
raise KeyError(f"Interface variable {name} not included") from None
With component-model, the structure of the model looks the same as with PythonFMU, but instead of using the PythonFMU classes, we use Model and Variable from the component-model package. Behind the scenes, the Model class will ultimately inherit from the Fmi2Slave class from PythonFMU, though it adds the support for NumPy variables as ports, range and unit validation for model variables, and more. This significantly improves the readability of the model. Instead of computing states for variables separately, we can combine related variables such as position and velocity in all axes as arrays. Using component-model also makes it easier to separate the FMU interface definition from the model's inner workings (see the method _interface(), above). Similarly, we have explicitly declared the units for our ports. The internal unit system is defined according to the SI system (which is the default within component-model), but as shown for the z-position of the ball, variables can be defined in other units (e.g. inch) and will then be converted during input and output.
In the new implementation we are importing NumPy. As mentioned earlier in this post, PythonFMU does not bundle in the dependencies. To be able to port the used dependencies to a different environment, a requirements.txt file should be included in the FMU. Listing the dependencies of the model, these are stored inside the FMU file, so that it is possible to install the dependencies in a different environment before simulating. For the bouncing ball model, we can write the requirements as:
Finally, the do_step() function remains unchanged with only minimal updates on how we use the position and speed information as they're now vector variables. You can find all these changes in the GitHub link shared above.
-
Build component-model FMU
Finally, to build the new FMU using component model, we only need to write a short script, where we specify the path to our model file and the dependencies file.
# build.py
from component_model.model import Model
Model.build("../component_model/example_models/bouncing_ball.py", ["requirements.txt"])
Running this Python script will generate the FMU file, which will be placed in our current working directory.
When porting the FMU to a different environment, the following PythonFMU command can be run to read the dependencies from the requirements file stored in the FMU and install them in the active Python environment:
Note: We recommend running this command within a dedicated Python environment (e.g., a virtual environment) to avoid potential conflicts with other system packages.
5. Concluding remarks
In conclusion, the use of Python for developing Functional Mock-up Units (FMUs) offers a significant advantage in terms of accessibility and practicality. By leveraging Python's extensive libraries and its developer-friendly nature, the threshold to get started developing simulation models is lowered, empowering developers to iterate faster and more efficiently. This approach not only simplifies the development process but also ensures that the models can be seamlessly integrated with other FMI-compliant tools.
As it was demonstrated with the bouncing ball model, PythonFMU and component-model provide a powerful interface for creating complex models. Similarly, component-model also provides features like units, ranges and support for vectors, enabling developers to create more robust and trustworthy models. Python's simplicity and readability coupled with these powerful frameworks opens new possibilities for innovations and collaboration in the field of simulation. By embracing these tools and opportunities, developers can harness the full potential of Python to create high-quality FMUs that meet industry standards and drive advancements in simulation technology.
More blog posts on Simulations and FMU models
- The history of system simulations
- Creating FMU models using C++
- Creating FMU models from machine learning models
Points of Contact
Full Stack Developer, DNV
Senior Principal Specialist, DNV
Senior Researcher, DNV
Principal Specialist, DNV
Group Leader and Senior Researcher, DNV