Skip to content

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

  1. Background
  2. Developing a Python simulation model
  3. Installing the PythonFMU package
  4. Developing the Python FMU
  5. Building the model
  6. Enhancing trust in simulation models
  7. Developing the component-model FMU
  8. Installing the component-model package
  9. Developing the component-model FMU
  10. Build component-model FMU
  11. 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:

pip install pythonfmu
  • 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:

\[e= \frac{|v_{after}|}{|v_{before}|}\]

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:

pythonfmu build -f BouncingBall3D.py

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:

pip install component-model
  • 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:

# requirements.txt

numpy==2.1.2

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:

pythonfmu deploy -f BouncingBall3D.fmu

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


Points of Contact

Jorge Luis Mendez Jorge Luis Mendez
Full Stack Developer, DNV
Siegfried Eisinger Siegfried Eisinger
Senior Principal Specialist, DNV
Melih Akdağ Melih Akdağ
Senior Researcher, DNV
Claas Rostock Claas Rostock
Principal Specialist, DNV
Cesar Augusto Ramos de Carvalho Cesar Augusto Ramos de Carvalho
Group Leader and Senior Researcher, DNV