All notable changes to orlab are documented here. Follows Keep a Changelog and Semantic Versioning. History before 0.3.0 was reconstructed from the git log.
-
Dispersion listeners (
orlab.listeners):WindProfile— deterministic altitude-dependent wind on every supported version via the wind-model override hook (replaces built-in wind including turbulence; meteorological direction convention matchingsetWindDirection, verified by identical drift; component-wise vector interpolation, so 359°→1° blends through north) — andThrustFactor, the motor batch-variation knob. Both hold plain-Python state only: they pickle intoSimulationPoolworkers and survive OpenRocket's listener cloning. -
Helper.get_components_of_type(root, "BodyTube"): typed component lookup by class name (resolved against the loaded version), superclass, or interface —"MotorMount"finds every mount. -
The simulation-setup guide covers layered wind, the native 24.12 multi-level model (version-gated, stochastic) vs
WindProfile(cross-version, deterministic), and the listener-cloning caveats; the manifest checks the wind-override surface and the canary runs the wind case. -
Motor selection and swapping:
Helper.set_motor(sim, motor)changes the motor a simulation actually flies — by database designation (find_motor, which requiresmanufacturer=for the common hobby designations that exist from several makers), from a.eng/.rse/.zipthrust-curve file (load_motor, no database involved), or as a motor object — always keyed on the simulation's own flight configuration and read back afterwards. Raw assignment to the rocket's selected configuration succeeds and silently does nothing to the simulation (the ecosystem's classic dispersion bug — now pinned by an integration test on every version);get_motor(sim)reports the flying designation.delay=sets the ejection delay in seconds. Works on all four supported versions including 24.12 headless (the motor database loads fully there; only component presets are affected by the upstream gap). -
Docs: "Simulation setup" guide page (motor swapping, the verified one-liner setters, SI units).
-
The profile contract manifest checks the motor surface per version — including the MotorConfiguration package move after 15.03 and the two mount-accessor eras — and the monthly canary runs the motor case.
-
Parallel dispersion runner:
orlab.SimulationPool(ork_file, jar_path)runs studies across spawn-based worker processes — one JVM and one loaded document per worker. Tasks are plain mappings of the declarative keys (validated up front, including the rod-direction/into-wind interaction) plus an optional pinnedseed, or an importableworker_fn(helper, sim, task)for anything beyond the whitelist. Results are plain-PythonSimResult/StudyResultrecords (to_records()feedspandas.DataFramedirectly); per-task failures areSimErrordata carrying the Python traceback and the Java stack; interrupts, worker crashes, and worker-boot failures raiseorlab.errors.StudyAbortedwith the partial results preserved. Every task gets a unique pool-assigned 31-bit seed (Java's own randomization collides at 10k-run scale), read back after the run so any result can be replayed. Progress is a(done, total)callback, tqdm-compatible without a tqdm dependency. Workers restore the document's option baseline between tasks, ignore SIGINT (the parent owns ^C — verified live against running JVM workers), and discard stdout on POSIX by default (worker_stdout="inherit"to keep it; best-effort on Windows). -
orlab.parallel.DECLARATIVE_KEYS: the curated set ofSimulationOptionsknobs verified to apply-and-read-back identically on every supported OpenRocket version — launch rod length/angle/direction, launch-into-wind, launch altitude/latitude/longitude, wind speed average and direction — each mapped to its setter/getter with value type and unit. Notable verified interaction: OpenRocket launches into the wind by default, andlaunch_rod_directiononly takes effect together withlaunch_into_wind: False; the mapping's declaration order encodes that. The profile contract manifest checks every pair (plussetRandomSeed/getRandomSeed) on regeneration, an integration case round-trips them all on every matrix version, and the monthly canary runs the same case against the newest upstream release.
- Flight summaries:
Helper.get_summary(sim, branch_number=0)returns aFlightSummary— apogee, max velocity/acceleration/Mach, rail-exit and deployment velocities, stability windowed from rod departure to apogee (OpenRocket 24.12 computes stability through post-apogee tumble, where unwindowed minima are meaningless), descent rate, flat-earth landing position/distance/compass bearing, flight time, optimum delay (23.09+), and warnings. All values are builtin Python types (never Java/numpy), so summaries pickle cleanly into JVM-less processes; missing values are NaN, with one warning per process when the cause is the loaded OpenRocket version rather than the flight.print(summary)renders a sectioned report;to_dict()is a dispersion-table row. Helper.get_events(sim, branch_number=0): events per stage branch (default unchanged).- Docs: "Flight summaries" guide page;
monte_carlo.pyexample reads landing points throughget_summaryinstead of a hand-rolled listener and flat-earth math. - The profile contract manifest now checks the FlightData summary getters and branch accessors on every profile regeneration, and the monthly canary also runs the summary case against the newest upstream release.
- Tabular export:
Helper.export_csv(sim, path)writes the branch's full timeseries as UTF-8 CSV (stdlib-only; NaN as empty cells, lossless throughpandas.read_csv), andHelper.get_dataframe(sim)returns the same columns as a pandas DataFrame. Column labels carry the jar's own SI units (TYPE_ACCELERATION_TOTAL (m/s²)); dimensionless types get no suffix. pandas ships as orlab's first optional extra —pip install orlab[pandas](ororlab[all]) — and is lazy-imported with an error naming the extra; nothing else in orlab needs it.
- Jar management:
orlab.fetch_jar(version)downloads an OpenRocket release jar into a local cache (ORLAB_JAR_CACHE, else$XDG_CACHE_HOME/orlab-jars, else~/.cache/orlab-jars) and verifies its sha256 against pins shipped with orlab. Unpinned versions require an explicitsha256=; there is no way to skip verification (orlab.errors.JarVerificationError). Apython -m orlabCLI wraps it:fetch [version] [--sha256 HEX](stable, scriptable stdout: the jar path) andwhich(shows the jar the default resolution would use, and why). - Default jar resolution now ends at the fetch cache:
ORLAB_JAR→CLASSPATH→ newest supportedOpenRocket-*.jarin the current directory → newest verified jar in the cache.OpenRocketInstance()works with zero configuration after onepython -m orlab fetch.OpenRocketInstanceitself never downloads anything. orlab.jars.find_installed(): locates a desktop OpenRocket installation (jar, version, and — when the install bundles a Java 17+ runtime — a JVM to run it with), so a machine with the app needs no separate JDK or jar. Deliberately opt-in: the default jar resolution never selects a desktop install (a self-updating app could silently switch scripts to an unverified version); use the explicitOpenRocketInstance(str(inst.jar), jvm_path=inst.jvm)two-liner.ORLAB_OR_INSTALL_DIRoverrides the search; empty string disables it.- Docs: "Getting an OpenRocket jar" guide page.
- Guides on the docs site: simulation listeners (hooks, the clone/shared-state
contract), monte-carlo studies (the one-instance-many-sims pattern, seeds),
and working across OpenRocket versions (profiles, fallback, the
TYPE_PROPELLANT_MASS→TYPE_MOTOR_MASSrename, string escape hatch). Every code block executed against a real jar. - Documentation site at https://cameronbrooks11.github.io/orlab/
(mkdocs-material + mkdocstrings API reference, deployed from main by CI);
just docsbuilds it locally. The maintainer release notes moved there fromdocs/pypi_usage.md.
- The current-directory jar fallback picks the newest jar with an exact
version profile instead of the hardcoded
./OpenRocket-23.09.jar, and skips unprofiled or unparseable jar names (a26.xx-SNAPSHOTbuild in the cwd no longer shadows — nor is shadowed by — a supported release; pass such jars explicitly). With severalOpenRocket-*.jarfiles in the cwd, the newest supported one now wins. - The integration suite downloads its matrix jars through
orlab.fetch_jaritself instead of a private copy of the download/verify logic. - CI runs the unit suite on Windows (one
windows-latestcell, Python 3.14, jar-free) — the first automated check of orlab's path, cache, and file handling on a platform the README documents. - README rewritten: version-support matrix (all four OpenRocket versions CI-tested), working quickstart, lifecycle/seed/JVM-options notes, trimmed JDK setup, uv/just development workflow.
- Examples resolve
simple.orkrelative to the script (they previously only worked from the repository root), and their third-party dependencies are declared as theexamplesdependency group.
examples/simple_ork/lazy.pyworks with modern numpy (fminpasses a 1-element array thatmath.radiansno longer coerces).examples/simple_ork/monte_carlo.py: landing bearing usesatan2(theatanform was wrong for westward drift and divided by zero for pure north-south), and the reported mean bearing is a circular mean.
OpenRocketInstance(jvm_path=..., jvm_args=(...)): choose the JVM library and pass launch arguments (e.g.("-Xmx4g",)for large monte-carlo runs). TheMANUAL_JVM_PATHclass attribute is deprecated and honored with a warning for one release.Helper.run_simulation(..., randomize_seed=False)respects a seed already set on the simulation options; the default still randomizes before every run (previously unconditional and undocumented). On 24.12 a fixed seed reproduces results within one process, not across processes with wind enabled.py.typedmarker: consumer type checkers now see orlab's annotations.- The release workflow refuses to publish if checks or tests fail.
-
The JVM now outlives the
with OpenRocketInstance(...)block (JPype cannot restart a JVM, so shutting it down made any second instance in the same process fail with a confusing JPype error). Sequential blocks and notebook re-runs on the same jar reuse the running OpenRocket; a different jar path raisesOrlabErrorexplaining the one-jar-per-process constraint, and a startup failure after JVM launch raisesOrlabErrorinstead of poisoning retries silently. Helpers and listeners stay usable after the block; the JVM ends with the interpreter. -
import orlabno longer configures the root logger; consumers that relied on orlab's implicitlogging.basicConfigmust configure logging themselves (orlab's own INFO messages are otherwise hidden by Python's defaults). -
The jar path defaults to
$ORLAB_JAR, then the first existing jar on the legacy$CLASSPATH(list-aware), then./OpenRocket-23.09.jar— resolved when an instance is created, not at import. -
Bad jars raise
orlab.errors.NotAnOpenRocketJar(previouslyBadZipFile,KeyErrororValueErrordepending on how the jar was bad), and using aHelperon an unstarted instance raisesOrlabError(previously bareException).
- Version profiles: per-OpenRocket-version facts (package roots, startup path,
FlightDataType/FlightEventconstants) generated from the jars bytools/generate_profile.pyand checked in for 15.03, 22.02, 23.09 and 24.12. Unknown newer versions fall back to the nearest older profile with a warning, and a startup drift alarm warns when the loaded jar's constants differ from its profile. - OpenRocket 24.12+ starts fully headless via the official
OpenRocketCore.initialize()bootstrap — no window disposal, no display needed, and no private-field reflection on that path. orlab.errorswithOrlabError,UnsupportedFlightDataTypeandUnsupportedOpenRocketVersion.- Per-version integration harness (
just test-integration): subprocess-per-version cases against sha256-pinned OpenRocket jars (downloaded on demand) covering flight sanity, the enum surface against the profile, warning/abort events, listener exception propagation, and cross-version apogee agreement. CI runs the full matrix — all four OpenRocket versions on JDK 17 and 21, no display server — and a monthly canary runs the newest upstream release against the newest profile, opening an issue on failure.
FlightDataType/FlightEventare now generated as the union of constants across all profiled versions (four 24.12-only data types added); requesting a constant the loaded OpenRocket version does not expose raisesUnsupportedFlightDataTypenaming the versions that have it (previously a raw JPypeAttributeError). Enum members are now sorted by name — their numeric.values shifted and remain non-stable identifiers; use names.- Snapshot version strings (
26.xx-SNAPSHOT) parse instead of erroring.
FlightEvent.SIM_WARNandFlightEvent.SIM_ABORT(emitted by OpenRocket 24.12+) andFlightDataType.TYPE_MOTOR_MASS(the 22.02+ name for the propellant-mass series;TYPE_PROPELLANT_MASSremains for 15.03).
- Packaging migrated to
pyproject.toml(hatchling) with asrc/layout and uv-managed environments;setup.pyremoved. Public import surface unchanged. requires-pythonraised to>=3.10; license metadata now correctly declares GPL-2.0-only (matching the LICENSE file).- Quality gate:
justfile(fmt / lint / typecheck / check / test through uv), ruff + mypy clean, pre-commit with gitleaks,AGENTS.md, and a jar-free unit-test suite (version detection, package-root selection, iterator bridging, API-surface freeze). - CI: checks and a Python 3.10/3.14 unit-test matrix on every push and PR;
releases publish to PyPI via Trusted Publishing on the
pypienvironment.
Helper.get_events()no longer crashes on flight event types missing from the Python enum — on 24.12, simulation warnings are recorded asSIM_WARNevents and previously raised a bareKeyError. Unknown event types from future OpenRocket versions are now skipped with a logged warning, andtranslate_flight_eventraises a clearValueErrorinstead.Helper.get_events()return annotation matches its actual behavior (Dict[FlightEvent, List[float]]— a list of times per event).
- Dead
orlab._orhelpershim module andscripts/debug_jvm.py.
- OpenRocket 24.12 support (#2): the jar's version is read from its
build.propertiesbefore the JVM starts and the Java package roots (net.sf.openrocketvsinfo.openrocket.core/info.openrocket.swing) are selected automatically.instance.or_versionandinstance.openrocket_swingexposed; public API otherwise unchanged. - OpenRocket 15.03 works on modern JVMs (
--add-opensat JVM start). Verified by real simulations against 15.03, 22.02, 23.09, and 24.12.
Initial orlab releases (0.2.3–0.2.7): evolution of
orhelper — driver reorganization into
core//utils/, PyPI publication, docs and examples for OpenRocket 23.09.