FMU ME: Bouncing Ball

This example demonstrates Model Exchange FMU integration with PathSim. Unlike co-simulation FMUs, Model Exchange FMUs provide only the differential equations. PathSim’s solvers perform the numerical integration and event detection.

You can also find the FMU integration tests in the GitHub repository.

The bouncing ball combines continuous dynamics with discrete events:

\[\frac{dh}{dt} = v\]
\[\frac{dv}{dt} = g\]

where \(h\) is height, \(v\) is velocity, and \(g = -9.81\,\text{m/s}^2\). At impact (\(h = 0\)), velocity reverses with energy loss: \(v^+ = -e \cdot v^-\) where \(e \in [0,1]\) is the restitution coefficient.

This example demonstrates the ModelExchangeFMU block, which provides PathSim with continuous states and derivatives from an FMU. PathSim’s solvers handle integration, event detection, and error control.

Import and Setup

Note that FMPy must be installed to use FMU blocks:

pip install fmpy
[1]:
import numpy as np
import matplotlib.pyplot as plt

# Apply PathSim docs matplotlib style for consistent, theme-friendly figures
plt.style.use('../pathsim_docs.mplstyle')

from pathlib import Path
from pathsim import Simulation, Connection
from pathsim.blocks import ModelExchangeFMU, Scope
from pathsim.solvers import RKBS32

FMU Path

The FMU contains binaries for multiple platforms (Windows, Linux, macOS):

[2]:
notebook_dir = Path().resolve()
fmu_path = notebook_dir / "data" / "BouncingBall_ME.fmu"

# Verify FMU exists
if not fmu_path.exists():
    raise FileNotFoundError(f"FMU file not found at {fmu_path}")

System Definition

The ModelExchangeFMU block exposes continuous states $(h, v)$ and provides derivatives $(dot{h}, dot{v})$ to PathSim’s solvers. Event indicators signal zero-crossings for accurate bounce detection.

[3]:
# Create the Model Exchange FMU block
fmu = ModelExchangeFMU(
    fmu_path=str(fmu_path),
    instance_name="bouncing_ball",
    start_values={"e": 0.7},  # coefficient of restitution
    tolerance=1e-10,
    verbose=False
)

# Scope to record height and velocity
sco = Scope(labels=["h [m]", "v [m/s]"])

blocks = [fmu, sco]

# Connect FMU outputs to scope
connections = [
    Connection(fmu[0], sco[0]),  # height
    Connection(fmu[1], sco[1]),  # velocity
]

Display FMU metadata:

[4]:
# Display FMU metadata (accessed via fmu_wrapper)
md = fmu.fmu_wrapper.model_description
print(f"Model Name: {md.modelName}")
print(f"FMI Version: {fmu.fmu_wrapper.fmi_version}")
print(f"Description: {md.description}")
print(f"Generation Tool: {md.generationTool}")
print(f"\nContinuous states: {fmu.fmu_wrapper.n_states}")
print(f"Event indicators: {fmu.fmu_wrapper.n_event_indicators}")
print(f"Outputs: {len(fmu.fmu_wrapper.output_refs)}")
Model Name: BouncingBall
FMI Version: 2.0
Description: This model calculates the trajectory, over time, of a ball dropped from a height of 1 m
Generation Tool: Reference FMUs (v0.0.39)

Continuous states: 2
Event indicators: 1
Outputs: 2

Simulation Setup

PathSim integrates the FMU using an adaptive Runge-Kutta solver (RKBS32) with error control. The solver automatically adjusts timesteps for accurate event detection.

[5]:
# Initialize simulation
sim = Simulation(
    blocks,
    connections,
    dt=0.01,
    dt_max=0.01,
    Solver=RKBS32,
    tolerance_lte_rel=1e-6,
    tolerance_lte_abs=1e-9,
    log=True
)

# Run simulation
sim.run(4.0)
10:34:29 - INFO - LOGGING (log: True)
10:34:29 - INFO - BLOCKS (total: 2, dynamic: 1, static: 1, eventful: 1)
10:34:29 - INFO - GRAPH (nodes: 2, edges: 2, alg. depth: 1, loop depth: 0, runtime: 0.178ms)
10:34:29 - INFO - STARTING -> TRANSIENT (Duration: 4.00s)
10:34:29 - INFO - --------------------   1% | 0.0s<0.2s | 2041.4 it/s
10:34:29 - INFO - ####----------------  20% | 0.0s<0.1s | 3324.8 it/s
10:34:29 - INFO - ########------------  40% | 0.1s<0.1s | 3185.2 it/s
10:34:29 - INFO - ############--------  60% | 0.1s<0.1s | 3156.6 it/s
10:34:29 - INFO - ################----  80% | 0.2s<0.0s | 3351.6 it/s
10:34:30 - INFO - #################### 100% | 0.2s<--:-- | 3265.3 it/s
10:34:30 - INFO - FINISHED -> TRANSIENT (total steps: 639, successful: 578, runtime: 208.58 ms)
[5]:
{'total_steps': 639, 'successful_steps': 578, 'runtime_ms': 208.58417599993118}

Results

Plot the trajectory:

[6]:
sco.plot()
plt.show()
../_images/examples_fmu_model_exchange_bouncing_ball_16_0.svg

Event Visualization

Mark the detected bounce events:

[7]:
time, (h, v) = sco.read()

fig, axes = plt.subplots(2, 1, figsize=(9, 6), sharex=True, dpi=130)

# Mark bounce events
for ax in axes:
    for t in fmu.events[0]:
        ax.axvline(t, ls="--", lw=1, c="gray",
                   label="Bounce Event" if t == list(fmu.events[0])[0] else None)

axes[0].plot(time, h, lw=2)
axes[0].set_ylabel("Height [m]")
axes[0].legend()
axes[0].grid(True, alpha=0.3)

axes[1].plot(time, v, lw=2, color='orange')
axes[1].set_xlabel("Time [s]")
axes[1].set_ylabel("Velocity [m/s]")
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()
../_images/examples_fmu_model_exchange_bouncing_ball_18_0.svg