Notebooks and parallel runs¶
Both come from the same limit: a JVM can be started only once per Python process. See How orhelper works.
Jupyter notebooks¶
A notebook cell is not a good place for a with block: when you run the cell again, the JVM can't be
started again and you get JVMAlreadyStartedError. Instead, start
OpenRocket once and keep it for the whole session. The JVM stops when the kernel does.
import orhelper
if "instance" not in globals(): # makes this cell safe to run twice
instance = orhelper.OpenRocketInstance(log_level="ERROR")
instance.__enter__()
helper = orhelper.Helper(instance)
From then on, use helper in any cell. If you do get JVMAlreadyStartedError, restart the kernel.
The guided notebook in the repository walks through the whole workflow this way, with plots.
Parallel runs¶
Threads don't help, because there is a single JVM. Use processes, and let each one start its own JVM,
run a batch of simulations, and return plain Python results. A worker process can't be reused for a
second OpenRocketInstance, so give every worker one task: multiprocessing.Pool(maxtasksperchild=1)
does that. Start the workers with the spawn method so each gets a clean interpreter:
"""Run simulations in parallel: one OpenRocket JVM per worker process.
JPype can start the JVM only once per Python process, so threads can't help and a
worker process can't be reused for a second ``OpenRocketInstance``. Instead, each
worker is a fresh process (``maxtasksperchild=1``) that starts its own JVM, runs a
whole batch of simulations and returns plain Python results.
Pass a number to choose the number of workers: python parallel_runs.py 4
"""
import math
import multiprocessing
import sys
import numpy as np
import orhelper
from orhelper import FlightDataType
WIND_SPEEDS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] # m/s
def run_batch(wind_speeds):
"""Runs in a worker process: start a JVM, simulate every wind speed, return the results."""
with orhelper.OpenRocketInstance(log_level="ERROR") as instance:
orh = orhelper.Helper(instance)
doc = orh.load_doc(orhelper.sample_ork_path())
sim = doc.getSimulation(0)
results = []
for wind in wind_speeds:
sim.getOptions().setWindSpeedAverage(wind)
orh.run_simulation(sim)
data = orh.get_timeseries(sim, [FlightDataType.TYPE_ALTITUDE, FlightDataType.TYPE_POSITION_X,
FlightDataType.TYPE_POSITION_Y])
drift = math.hypot(data[FlightDataType.TYPE_POSITION_X][-1], data[FlightDataType.TYPE_POSITION_Y][-1])
results.append((wind, float(np.max(data[FlightDataType.TYPE_ALTITUDE])), drift))
return results
if __name__ == "__main__": # required: the worker processes import this file
workers = int(sys.argv[1]) if len(sys.argv) > 1 else 3
batches = [WIND_SPEEDS[i::workers] for i in range(workers)]
# "spawn" gives every worker a clean interpreter, which is what a JVM needs.
with multiprocessing.get_context("spawn").Pool(processes=workers, maxtasksperchild=1) as pool:
batch_results = pool.map(run_batch, batches, chunksize=1)
print(f"{'wind (m/s)':>11}{'apogee (m)':>12}{'drift (m)':>11}")
for wind, apogee, drift in sorted(row for batch in batch_results for row in batch):
print(f"{wind:>11}{apogee:>12.1f}{drift:>11.1f}")
Notes:
- Return numbers, strings and NumPy arrays from the workers. Java objects can't be sent between processes.
- Each process loads OpenRocket and its motor database, which takes a second or two, so give each worker a batch of simulations, not just one.
- The
if __name__ == "__main__":guard is required: the spawned workers import your script. - The same approach lets you test several OpenRocket versions: give each process a different
jar=argument.