Listeners¶
A listener is Python code that OpenRocket calls while it simulates. With a listener you can record what happens at every step, change the starting conditions, or override a model such as the wind or the thrust. This is how the Monte Carlo example drops a rocket from a random altitude.
The basics¶
Subclass AbstractSimulationListener, override the hooks you need,
and pass instances to run_simulation:
"""Run your own Python code during a simulation with a simulation listener.
Subclass ``orhelper.AbstractSimulationListener``, override only the hooks you need and
pass instances to ``run_simulation``. The hooks are called from OpenRocket with Java
objects: here ``status`` is a Java ``SimulationStatus``.
OpenRocket may *clone* a listener (once per branch or run). The clone is a shallow copy,
so lists and dicts you create in ``__init__`` are shared with the original: read your
results from the instance you passed in, as done below.
"""
import numpy as np
import orhelper
from orhelper import FlightDataType
class AltitudeTracker(orhelper.AbstractSimulationListener):
"""Record the time and altitude after every simulation step."""
def __init__(self):
self.times = []
self.altitudes = []
def postStep(self, status):
self.times.append(status.getSimulationTime())
self.altitudes.append(status.getRocketPosition().z) # z is up
def endSimulation(self, status, simulation_exception):
print(f"Simulation ended after {status.getSimulationTime():.1f} s")
with orhelper.OpenRocketInstance(log_level="ERROR") as instance:
orh = orhelper.Helper(instance)
doc = orh.load_doc(orhelper.sample_ork_path())
sim = doc.getSimulation(0)
tracker = AltitudeTracker()
orh.run_simulation(sim, listeners=[tracker])
data = orh.get_timeseries(sim, [FlightDataType.TYPE_ALTITUDE])
print(f"The listener saw {len(tracker.times)} simulation steps")
print(f"Apogee seen by the listener: {max(tracker.altitudes):.1f} m")
print(f"Apogee in the stored data: {np.max(data[FlightDataType.TYPE_ALTITUDE]):.1f} m")
Every hook does nothing unless you override it. Their names are the Java ones (camelCase), and the
arguments are Java objects: status, for example, is OpenRocket's SimulationStatus, with methods such
as getSimulationTime() and getRocketPosition().
The hooks¶
| Group | Hooks | What you return |
|---|---|---|
| Lifecycle | startSimulation, endSimulation, postStep, and startSimulationBranch, endSimulationBranch |
nothing |
| Lifecycle | preStep |
True to take the step (the default), False to skip it |
| Events | addFlightEvent, handleFlightEvent, motorIgnition, recoveryDeviceDeployment |
True to let it happen (the default), False to prevent it |
| Computation | preWindModel, preGravityModel, preSimpleThrustCalculation, preAtmosphericModel, preFlightConditions, preAerodynamicCalculation, preMassCalculation, preAccelerationCalculation |
a replacement value, or None (nan for numbers) to leave it alone |
| Computation | the matching post... hooks |
a replacement for the computed value, or None (nan for numbers) |
The API reference documents each hook. Two things are worth knowing:
- The branch hooks (
startSimulationBranchandendSimulationBranch) exist only in OpenRocket 26.xx. In 22.02, 23.09 and 24.12, they are never called. preStepreturningFalseskips that simulation step, which does not advance the simulation, so don't returnFalseon every call.
Overriding a model¶
A computation hook that returns a value replaces what OpenRocket would use. For example, this listener blows a constant 10 m/s wind along the x axis, whatever the simulation's settings say:
Coordinate = instance.openrocket_core.util.Coordinate
class Gust(orhelper.AbstractSimulationListener):
def preWindModel(self, status):
return Coordinate(10.0, 0.0, 0.0)
helper.run_simulation(simulation, listeners=[Gust()])
With the wind set to 0 and the turbulence off, the sample rocket lands about 90 to 100 metres from the
pad with this listener, and on the pad without it. A hook that returns a number works the same way: a
preGravityModel that returns 3.0 lifts the sample rocket's apogee from about 51 m to about 94 m.
Return nan, not 0, to leave a number alone
The numeric hooks (gravity and thrust) use float("nan") for "no change". Returning 0.0 would set the
value to zero.
Keep your results in a list or dict¶
Numbers you assign in a hook are lost
OpenRocket doesn't run the listener object you pass in: it clones it, with a shallow copy. Lists
and dicts you create in __init__ are shared with the copy, so you can read them after the run. A
number or string that a hook assigns is changed on the copy only.
class Counter(orhelper.AbstractSimulationListener):
def __init__(self):
self.count = 0 # an int
self.calls = [] # a list
def preStep(self, status):
self.count += 1 # changes the copy only
self.calls.append(1) # the list is shared
return True
counter = Counter()
helper.run_simulation(simulation, listeners=[counter])
print(counter.count) # 0 (the copy was changed, not this object)
print(len(counter.calls)) # about 200 (the number of steps)
If you want a counter, keep it in a list or a dict (self.stats = {"steps": 0}), or a NumPy array.
Starting the rocket somewhere else¶
startSimulation can change the state of the simulation. This listener starts the rocket 1000 m above the
pad, like a drop test from a balloon: