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:
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)
09:50:54 - INFO - LOGGING (log: True)
09:50:54 - INFO - BLOCKS (total: 2, dynamic: 1, static: 1, eventful: 1)
09:50:54 - INFO - GRAPH (nodes: 2, edges: 2, alg. depth: 1, loop depth: 0, runtime: 0.125ms)
09:50:54 - INFO - STARTING -> TRANSIENT (Duration: 4.00s)
09:50:54 - INFO - -------------------- 1% | 0.0s<0.1s | 3483.9 it/s
09:50:54 - INFO - ####---------------- 20% | 0.0s<0.1s | 5735.0 it/s
09:50:54 - INFO - ########------------ 40% | 0.0s<0.0s | 5781.7 it/s
09:50:54 - INFO - ############-------- 60% | 0.1s<0.1s | 5570.2 it/s
09:50:54 - INFO - ################---- 80% | 0.1s<0.0s | 5800.2 it/s
09:50:54 - INFO - #################### 100% | 0.1s<--:-- | 5573.9 it/s
09:50:54 - INFO - FINISHED -> TRANSIENT (total steps: 639, successful: 578, runtime: 122.70 ms)
[5]:
{'total_steps': 639, 'successful_steps': 578, 'runtime_ms': 122.69550699966203}
Results¶
Plot the trajectory:
[6]:
sco.plot()
plt.show()
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()