Switched Bouncing Ball

This example demonstrates advanced event handling with multiple simultaneous events, event switching, and conditional event logic. The bouncing ball bounces on a table first, and then drops to the floor and continues to bounce there.

You can also find this example as a single file in the GitHub repository.

This example showcases:

  • Multiple ZeroCrossing events tracking different conditions

  • Condition events for conditional logic (count-based switching)

  • Dynamic event activation/deactivation using on()/off() methods

  • Nonlinear friction forces modeled with the Function block

First let’s import the Simulation and Connection classes along with the required blocks and event managers:

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

# Apply PathSim docs matplotlib style
plt.style.use('../pathsim_docs.mplstyle')

from pathsim import Simulation, Connection
from pathsim.blocks import Integrator, Constant, Function, Adder, Scope
from pathsim.solvers import RKBS32
from pathsim.events import ZeroCrossing, Condition

System Dynamics

The ball experiences:

  1. Gravitational acceleration downward

  2. Quadratic air resistance (Newton’s drag law)

  3. Elastic bounces at \(x = 0\) (floor) and \(x = -5\) (ceiling)

The equation of motion is:

\[\ddot{x} = -g - k \cdot \text{sign}(v) \cdot v^2\]

where \(k\) is the mass-normalized friction coefficient.

[2]:
# Simulation timestep
dt = 0.01

# Gravitational acceleration
g = 9.81

# Elasticity of bounce
b = 0.95

# Mass normalized friction coefficient
k = 0.2

# Initial values
x0, v0 = 1, 10

Block Diagram Construction

[3]:
# Newton friction (quadratic drag)
def fric(v):
    return -k * np.sign(v) * v**2

# Blocks that define the system
Ix = Integrator(x0)     # v -> x
Iv = Integrator(v0)     # a -> v
Fr = Function(fric)     # newton friction
Ad = Adder()
Cn = Constant(-g)       # gravitational acceleration
Sc = Scope(labels=["x", "v"])

blocks = [Ix, Iv, Fr, Ad, Cn, Sc]

# The connections between the blocks
connections = [
    Connection(Cn, Ad[0]),
    Connection(Fr, Ad[1]),
    Connection(Ad, Iv),
    Connection(Iv, Ix, Fr),
    Connection(Ix, Sc[0])
]

Event Managers

We define three events:

  1. E1: Table bounce at $x = 0$ (ZeroCrossing)

  2. E2: Floor bounce at $x = -5$ (ZeroCrossing)

  3. E3: Conditional switch after 10 floor bounces (Condition)

[4]:
# Event 1: Table bounce (x = 0)
def func_evt_1(t):
    *_, x = Ix()
    return x

def func_act_1(t):
    *_, x = Ix()
    *_, v = Iv()
    Ix.engine.set(abs(x))
    Iv.engine.set(-b*v)

E1 = ZeroCrossing(
    func_evt=func_evt_1,
    func_act=func_act_1,
    tolerance=1e-4
)
[5]:
# Event 2: Floor bounce (x = -5)
def func_evt_2(t):
    *_, x = Ix()
    return x + 5

def func_act_2(t):
    *_, x = Ix()
    *_, v = Iv()
    Ix.engine.set(abs(x + 5) - 5)
    Iv.engine.set(-b*v)

E2 = ZeroCrossing(
    func_evt=func_evt_2,
    func_act=func_act_2,
    tolerance=1e-4
)

The third event uses the Condition event type, which triggers based on a boolean condition rather than a zero crossing. Here it counts table bounces and disables detection after 10 occurrences:

[6]:
# Event 3: Conditional switch after 10 table bounces
E3 = Condition(
    func_evt=lambda *_: len(E1) >= 10,        # number of events 'E1' (bounces)
    func_act=lambda *_: [E1.off(), E3.off()]  # callback switches event tracking
)

The condition event checks if the table bounce event E1 has occurred 10 times. When this happens, it:

  1. Disables table bounce tracking with E1.off()

  2. Disables itself with E3.off()

After this point, only the flfoor bounce E2 remains active.

We initialize the simulation with the adaptive timestep RKBS32 solver (Runge-Kutta-Bogacki-Shampine 3(2) method) to enable backtracking for the event system to resolve event locations in time:

[7]:
# Initialize simulation
Sim = Simulation(
    blocks,
    connections,
    events=[E1, E2, E3],
    dt=dt,
    log=True,
    Solver=RKBS32,
    tolerance_lte_abs=1e-6,
    tolerance_lte_rel=1e-4
)
14:49:01 - INFO - LOGGING (log: True)
14:49:01 - INFO - BLOCKS (total: 6, dynamic: 2, static: 4, eventful: 0)
14:49:01 - INFO - GRAPH (nodes: 6, edges: 6, alg. depth: 3, loop depth: 0, runtime: 0.045ms)

Now let’s run the simulation:

[8]:
# Run the simulation
Sim.run(15)
14:49:01 - INFO - STARTING -> TRANSIENT (Duration: 15.00s)
14:49:01 - INFO - --------------------   1% | 0.0s<0.1s | 3892.4 it/s
14:49:01 - INFO - ####----------------  20% | 0.0s<0.1s | 5466.6 it/s
14:49:01 - INFO - ########------------  40% | 0.1s<0.1s | 5423.6 it/s
14:49:01 - INFO - ############--------  60% | 0.1s<0.0s | 5609.9 it/s
14:49:01 - INFO - ################----  80% | 0.1s<0.0s | 5588.7 it/s
14:49:01 - INFO - #################### 100% | 0.2s<--:-- | 5636.0 it/s
14:49:01 - INFO - FINISHED -> TRANSIENT (total steps: 862, successful: 595, runtime: 165.81 ms)
[8]:
{'total_steps': 862, 'successful_steps': 595, 'runtime_ms': 165.8144489992992}

Results

Let’s plot the results with all three event types marked differently:

[9]:
# Plot the recordings from the scope
time, [data_x] = Sc.read()
fig, ax = plt.subplots(figsize=(9, 4), dpi=130)
for t in E1:
    ax.axvline(t, ls="--", lw=1, c="gray", label="Table Bounce" if t == list(E1)[0] else None)
for t in E2:
    ax.axvline(t, ls="-.", lw=1, c="gray", label="Floor Bounce" if t == list(E2)[0] else None)
for t in E3:
    ax.axvline(t, ls="-", lw=1, c="gray", label="Switch")
ax.plot(time, data_x, label="x")
ax.set_xlabel("time [s]")
ax.legend()
plt.show()
ax.legend()
plt.show()
../_images/examples_switched_bouncing_ball_19_0.svg

Analysis

The plot reveals several interesting behaviors:

  1. Initial phase: The ball bounces on the table floor (\(x = 0\), dashed lines)

  2. Switch event: After 10 table bounces, the conditional event triggers (solid vertical line)

  3. Post-switch phase: Table bounces are no longer detected, so the ball passes through the table and falls to the floor where it continues to bounce

  4. Energy dissipation: Due to air resistance, the amplitude decreases over time

This demonstrates how event logic can dynamically change system behavior during simulation, useful for:

  • Mode switching in control systems

  • Modeling wear or failure after a certain number of cycles

  • Implementing complex state machines

Event Counts

We can also check how many times each event occurred:

[10]:
print(f"Table bounces (E1): {len(E1)}")
print(f"Floor bounces (E2): {len(E2)}")
print(f"Switch events (E3): {len(E3)}")
Table bounces (E1): 10
Floor bounces (E2): 15
Switch events (E3): 1

As expected, E1 should have exactly 10 events (the trigger condition), E3 should have 1 event (when it disabled itself), and E2 will vary depending on the system dynamics.