Co-simulation Examples
Co-simulation FMUs are self-contained simulation units that manage their own time stepping and internal solver. Unlike Model Exchange FMUs, Co-simulation FMUs encapsulate both the model equations and the numerical methods used to solve them. The master algorithm coordinates multiple FMUs by calling their do_step() method and exchanging input/output values.
Key Concepts
Co-simulation vs Model Exchange
- Co-simulation: FMU manages its own time stepping with
do_step()method - Model Exchange: External solver manages time stepping and calls derivative functions
FMU Lifecycle
The typical Co-simulation FMU lifecycle follows this sequence:
1. Instantiation: Create FMU instance with fmi3InstantiateCoSimulation()
2. Initialization: Set parameters and inputs with fmi3EnterInitializationMode()
3. Simulation: Repeatedly call fmi3DoStep() to advance time
4. Termination: Clean up with fmi3Terminate() and fmi3FreeInstance()
Required Methods
For Co-simulation FMUs, you must implement:
- __init__(): Initialize variables and register them
- do_step(): Advance simulation by one time step
- Optional: enter_initialization_mode(), exit_initialization_mode(), terminate()
Key Implementation Details
Variable Registration
Co-simulation FMUs support various variable types and causalities:
# Input variables (set by master algorithm)
self.register_variable(Float64("input_signal", causality=Fmi3Causality.input, start=0.0))
# Output variables (read by master algorithm)
self.register_variable(Float64("output_signal", causality=Fmi3Causality.output))
# Parameters (configurable, usually fixed during simulation)
self.register_variable(Float64("gain", causality=Fmi3Causality.parameter,
variability=Fmi3Variability.tunable, start=1.0))
PID Controller Example
from pythonfmu3 import Fmi3Causality, Fmi3Slave, Float64
class PIDController(Fmi3Slave):
"""PID Controller implementation"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.author = "PythonFMU3 Team"
self.description = "PID Controller"
# Inputs
self.setpoint = 0.0
self.measurement = 0.0
# Outputs
self.control_output = 0.0
# Parameters
self.kp = 1.0 # Proportional gain
self.ki = 0.1 # Integral gain
self.kd = 0.01 # Derivative gain
# Internal states
self.integral_error = 0.0
self.previous_error = 0.0
self.first_step = True
# Register variables
self.register_variable(Float64("setpoint", causality=Fmi3Causality.input, start=0.0))
self.register_variable(Float64("measurement", causality=Fmi3Causality.input, start=0.0))
self.register_variable(Float64("output", causality=Fmi3Causality.output))
self.register_variable(Float64("Kp", causality=Fmi3Causality.parameter, start=1.0))
self.register_variable(Float64("Ki", causality=Fmi3Causality.parameter, start=0.1))
self.register_variable(Float64("Kd", causality=Fmi3Causality.parameter, start=0.01))
def do_step(self, current_time, step_size):
# Calculate error
error = self.setpoint - self.measurement
# Proportional term
p_term = self.kp * error
# Integral term
self.integral_error += error * step_size
i_term = self.ki * self.integral_error
# Derivative term
if self.first_step:
d_term = 0.0
self.first_step = False
else:
d_term = self.kd * (error - self.previous_error) / step_size
# PID output
self.control_output = p_term + i_term + d_term
# Store error for next iteration
self.previous_error = error
return True
Units
Units can be added to this example through the Unit class along with register_units and the unit kwarg in register_variable
from pythonfmu3 import Unit
velocity_unit = Unit(name="m/s", m=1, s=-1)
which can now be utilized in the register variables call,
velocity_unit = Unit(name="m/s", m=1, s=-1)
self.register_units([velocity_unit])
self.register_variable(Float64("v", causality=Fmi3Causality.output, start=0, variability=Fmi3Variability.continuous, initial=Fmi3Initial.exact, unit=velocity_unit.name))
Arrays
If numpy is installed, arrays may be used with,
from pythonfmu3 import Fmi3Causality, Fmi3Variability, Fmi3Slave, Float64, UInt64, Fmi3Initial, Dimension
import numpy as np
class LinearTransform(Fmi3Slave):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.author = "..."
self.description = "LinearTransform"
self.time = 0.0
self.m = 2
self.n = 2
self.scalar = 2.00
self.u = np.array([1.0, 2.0]).reshape((self.m, 1))
self.offset = np.array([1.0, 2.0]).reshape((self.m, 1))
self.A = np.array([1.0, 1.0, 2.0, 1.0]).reshape((self.m, self.n))
self.y = np.array([0.0, 0.0]).reshape((self.m, 1))
self.register_variable(Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous))
self.register_variable(UInt64("m", causality=Fmi3Causality.structuralParameter, variability=Fmi3Variability.tunable, start=2))
self.register_variable(Float64("scalar", causality=Fmi3Causality.input, start=2.0))
self.register_variable(Float64("u", causality=Fmi3Causality.input, dimensions=[Dimension(start=f"{self.n}")]))
self.register_variable(Float64("offset", causality=Fmi3Causality.input, dimensions=[Dimension(start=f"{self.m}")]))
self.register_variable(Float64("A", causality=Fmi3Causality.parameter, variability=Fmi3Variability.tunable, dimensions=[Dimension(start=f"{self.m}"), Dimension(start=f"{self.n}")]))
self.register_variable(Float64("y", causality=Fmi3Causality.output, dimensions=[Dimension(valueReference="1")]))
def do_step(self, current_time, step_size):
self.y = self.scalar*self.A.dot(self.u) + self.offset
return True
The do_step function can also return a tuple of flags, corresponding to those expected by the FMI standard. This can be achieved through use of the FmiStepResult tuple,
from pythonfmu3 import Fmi3Status, Fmi3StepResult
def do_step(self, current_time, step_size):
self.y = 1 + 2 + 3 + 4 + ...
terminate = False
status = Fmi3Status.ok
return Fmi3StepResult(status=Fmi3Status.ok, terminateSimulation=terminate)
Create the FMU
pythonfmu3 build -f pythonslave.py myproject
In this example a python class named PythonSlave that extends Fmi3Slave is declared in a file named pythonslave.py,
where myproject is an optional folder containing additional project files required by the python script.
Project folders such as this will be recursively copied into the FMU. Multiple project files/folders may be added.
Integration and Testing
Using FMPy for Testing
# Example of co-simulation setup
import fmpy
from fmpy import simulate_fmu
import matplotlib.pyplot as plt
# Simulate the PID controller FMU
result = simulate_fmu(
filename='PIDController.fmu',
start_time=0.0,
stop_time=10.0,
step_size=0.01,
start_values={
'Kp': 2.0,
'Ki': 0.5,
'Kd': 0.1,
'setpoint': 1.0
},
input={
'measurement': [(0.0, 0.0), (1.0, 0.3), (5.0, 0.8), (10.0, 1.0)]
}
)
# Plot results
plt.figure(figsize=(12, 6))
plt.subplot(2, 1, 1)
plt.plot(result['time'], result['setpoint'], label='Setpoint')
plt.plot(result['time'], result['measurement'], label='Measurement')
plt.ylabel('Value')
plt.legend()
plt.grid(True)
plt.subplot(2, 1, 2)
plt.plot(result['time'], result['output'], label='Control Output')
plt.xlabel('Time [s]')
plt.ylabel('Control Signal')
plt.legend()
plt.grid(True)
plt.show()