Skip to content

Commit 7943f37

Browse files
Merge develop into master to resolve issue 37
2 parents ab8acbe + e028d00 commit 7943f37

15 files changed

Lines changed: 678 additions & 23 deletions

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
---
2+
name: add-component-extractor
3+
description: Add or modify a rocket component extractor in the serialization pipeline (motor, fins, nose cone, parachute, transition, environment, flight, stored results, etc.). Use when a parameter is missing from parameters.json, when supporting a new OpenRocket feature, or when editing anything under rocketserializer/components/ or ork_extractor.py.
4+
---
5+
6+
# Adding a component extractor
7+
8+
The serializer builds `parameters.json` in `ork_extractor.ork_extractor()` by calling one
9+
`search_*` function per rocket concern from `rocketserializer/components/`. To add or extend
10+
a component, follow the existing pattern exactly.
11+
12+
## Anatomy of a component module
13+
Each `components/<thing>.py`:
14+
- Starts with `import logging` and `logger = logging.getLogger(__name__)`.
15+
- Exposes `search_<thing>(bs, ...)` returning a **dict or list of dicts** (never prints;
16+
logs instead).
17+
- Uses NumPy-style docstrings (Parameters / Returns).
18+
- Reads scalars from the BeautifulSoup tree `bs`; reads geometry/positions from the
19+
`elements` map produced by `open_rocket_wrangler.process_elements_position` (which needs
20+
the Java `ork` document). See `nose_cone.py` and `fins.py` for the `elements` pattern.
21+
22+
## Wiring it into the pipeline
23+
In `ork_extractor.py`:
24+
1. Import your function at the top with the other `from .components.<x> import ...` lines.
25+
2. Call it through the local `_safe_search(func, default_ret, *args)` wrapper so a failure
26+
logs and returns the default instead of aborting the whole extraction. Choose a sensible
27+
default (`{}`, `[]`, or a minimal dict) matching the shape callers expect.
28+
3. Assign the result into `settings["<key>"]`. The top-level keys are a **contract** with
29+
`nb_builder.py` and the acceptance golden files — don't rename existing keys casually.
30+
31+
Current keys: `id`, `environment`, `rocket`, `nosecones`, `trapezoidal_fins`,
32+
`elliptical_fins`, `tails`, `parachutes`, `rail_buttons`, `motors`, `flight`,
33+
`stored_results` (plus `rocket.drag_curve` and `motors.thrust_source` which point to CSVs
34+
written into the output folder).
35+
36+
## If the notebook must use the new value
37+
Add/adjust the matching `build_*` method in `nb_builder.NotebookBuilder` so the value flows
38+
into the generated RocketPy notebook. The builder reads only from `parameters.json`.
39+
40+
## Validate the change
41+
1. `make format && make lint`
42+
2. Run the pipeline on an example that exercises the component:
43+
`ork2json --filepath "examples/EPFL--BellaLui--2020/rocket.ork" --verbose`
44+
and inspect the resulting `parameters.json`.
45+
3. `pytest tests/acceptance/test_ork_extractor.py -v`. Acceptance tests compare against
46+
committed `examples/<name>/parameters.json`. If your change **intentionally** alters
47+
output, regenerate those golden files and review the diff carefully before committing.
48+
4. Add unit coverage under `tests/unit/` when practical.
49+
50+
## Remember the input limitations
51+
The pipeline assumes a single stage, single motor, and single nose cone, and English-only
52+
`.ork` files with at least one run simulation. Don't silently break these assumptions.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
name: openrocket-jpype-interop
3+
description: Work with the OpenRocket Java library through JPype — start/stop the JVM, load .ork documents, traverse the rocket component tree, and read data from both the XML (BeautifulSoup) and the live Java object. Use when touching openrocket_runtime.py, open_rocket_wrangler.py, ork_extractor.py, the JVM/Java setup, or anything that calls into net.sf.openrocket / info.openrocket.
4+
---
5+
6+
# OpenRocket ↔ JPype interop
7+
8+
## The two-source model
9+
`ork_extractor(bs, filepath, output_folder, ork)` reads from **two sources
10+
simultaneously** — always know which one a given value comes from:
11+
12+
- `bs` — a `BeautifulSoup` tree of the `.ork` XML (`_helpers.parse_ork_file`). Source of
13+
the simulation **datapoints** and most numeric parameters. Access via `bs.find(...)`,
14+
`bs.find_all("datapoint")`, and `bs.find("databranch").attrs["types"]` (the comma-joined
15+
column labels).
16+
- `ork` — the **live Java OpenRocket document** (`OpenRocketSession.load_doc`). Source of
17+
**geometry and component positions** that the XML doesn't expose cleanly. Reached via
18+
`ork.getRocket()` and traversed in `components/open_rocket_wrangler.py`.
19+
20+
When adding an extractor, prefer `bs` for scalar/sim data and only reach into `ork` when the
21+
value requires walking the component hierarchy.
22+
23+
## The JVM lifecycle (read before changing session code)
24+
JPype is pinned **`jpype1<1.5`**, which **cannot restart a JVM in one process**. Consequences:
25+
26+
- `OpenRocketSession.__exit__` must **not** call `jpype.shutdownJVM()`. It only disposes AWT
27+
windows. The JVM is cleaned up by JPype's atexit hook.
28+
- Never open a second `OpenRocketSession` in a test. `tests/conftest.py` opens **one**
29+
session at import time and caches every example's settings in `_cached_settings`.
30+
- To use the session: `with OpenRocketSession(jar_path, log_level="OFF") as s: doc = s.load_doc(path)`.
31+
32+
## Package namespaces
33+
OpenRocket moved from `net.sf.openrocket` (legacy, ≤ ~22) to `info.openrocket` (modern).
34+
`OpenRocketSession._resolve_packages()` tries legacy first, then modern, returning
35+
`(core, swing)`. If you support a new jar and a symbol is missing, extend that method rather
36+
than hard-coding a namespace anywhere else.
37+
38+
## Java version compatibility
39+
`ensure_java_compatibility(jar_path)` picks the minimum Java: OpenRocket 23+ → Java 17,
40+
else Java 8. On Windows it auto-discovers a JDK under `C:/Program Files/Java` (and Adoptium
41+
/ AdoptOpenJDK) and sets `JAVA_HOME`/`PATH`. On other OSes it only warns — the user must
42+
provide a compatible JVM. Jar version is parsed from the filename by `_jar_version_tuple`.
43+
44+
## Traversing the component tree
45+
`open_rocket_wrangler.py` helpers: `is_sub_component(ork)` (depth > 1 from root),
46+
`parent_is_a_stage(ork)` (parent class is `Stage`/`AxialStage`), and
47+
`process_elements_position(...)` which builds the position map used by fins, nose cone,
48+
transitions, and rail buttons. Component Java classes are identified by
49+
`ork.getClass().getSimpleName()`.
50+
51+
## Gotchas
52+
- Java calls raise `jpype.JException` (plus `AttributeError`/`TypeError`/`RuntimeError` when
53+
internals change between versions). Catch these narrowly — see `_block_loader`.
54+
- The `.ork` file is a **zip** containing `rocket.ork` (XML). `extract_ork_from_zip` handles
55+
both the zipped and already-extracted cases (it catches `BadZipFile`).
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
name: python-conventions
3+
description: Python style, tooling, and testing conventions for this repo — ruff/black formatting, pylint config, NumPy docstrings, logging, and the pytest layout (unit/integration/acceptance) with the single-JVM constraint. Use before writing or reviewing any Python change here so it passes CI on the first try.
4+
---
5+
6+
# Python conventions
7+
8+
## Formatting & linting (must pass CI)
9+
- **ruff** is the formatter and primary linter. Line length **88**, black-compatible.
10+
- Format: `make format` = `ruff check --select I --fix` (import sort) + `ruff format .`
11+
- Lint: `make ruff-lint` = `ruff check .`
12+
- **pylint** also runs: `make pylint` = `pylint examples/ rocketserializer/ tests/`. The
13+
disabled checks are in `pyproject.toml` `[tool.pylint]` (e.g. missing docstrings,
14+
too-many-locals/arguments, `raise-missing-from`, `fixme`). Don't re-enable them ad hoc.
15+
- CI (`.github/workflows/linter.yml`) runs `ruff format --check`, `ruff check`, and pylint
16+
on Python 3.9. Run `make lint` locally first.
17+
- Supported Python: 3.8–3.12 (`requires-python = ">= 3.8"`). Don't use syntax newer than 3.8.
18+
19+
## Docstrings & logging
20+
- **NumPy-style docstrings** with `Parameters` / `Returns` / `Raises` sections — match the
21+
existing modules (`_helpers.py`, `ork_extractor.py`).
22+
- Every module defines `logger = logging.getLogger(__name__)`. Log instead of `print`.
23+
User-facing CLI messages are prefixed with the command name, e.g. `[ork2json] ...`.
24+
- Public functions raise precise exceptions with actionable messages (`FileNotFoundError`,
25+
`ValueError`); the CLI surfaces them.
26+
27+
## Testing
28+
- Layout: `tests/unit/`, `tests/integration/`, `tests/acceptance/`.
29+
- Run: `make tests` (`pytest`); CI: `pytest tests/ -v --timeout=120` (the `--timeout` guards
30+
against a hung JVM). One case: `pytest tests/acceptance/test_ork_extractor.py -k valetudo -v`.
31+
- **Single-JVM rule:** JPype (`<1.5`) can't restart a JVM in-process, so `tests/conftest.py`
32+
opens exactly one `OpenRocketSession` and caches every example's settings as fixtures.
33+
Never open another session in a test.
34+
- **Acceptance tests are golden-file comparisons** against `examples/<name>/parameters.json`.
35+
Intentional output changes require regenerating and reviewing those files.
36+
37+
## Dependencies
38+
Runtime deps are in `requirements.in` (consumed by `pyproject.toml` dynamic dependencies);
39+
dev tools in `requirements-dev.txt`. Add runtime deps to `requirements.in`, not directly to
40+
`pyproject.toml`.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
name: rocketpy-simulation
3+
description: Understand how generated parameters.json maps onto the RocketPy API (Environment, SolidMotor/Motor, Rocket, Flight) and how nb_builder.py assembles a runnable RocketPy notebook. Use when editing nb_builder.py, changing what a component contributes to the notebook, debugging a generated simulation, or aligning output with a RocketPy version.
4+
---
5+
6+
# RocketPy simulation mapping
7+
8+
`rocketserializer` produces inputs for **RocketPy** (`rocketpy>=1.1.0`). The generated
9+
notebook (`nb_builder.NotebookBuilder`) reads `parameters.json` and constructs a standard
10+
RocketPy simulation. Keep the mapping below in mind when changing extraction or the builder.
11+
12+
## The RocketPy objects the notebook builds
13+
The `build_*` methods in `NotebookBuilder` map settings sections onto RocketPy classes:
14+
15+
- `build_environment``Environment` (latitude, longitude, elevation, date, atmospheric
16+
model). Fed by `settings["environment"]` + `settings["flight"]`.
17+
- `build_motor` → a motor object (e.g. `SolidMotor`) using `settings["motors"]`, with
18+
`thrust_source` pointing at the generated `thrust_source.csv`.
19+
- `build_rocket``Rocket` with mass/inertia/radius from `settings["rocket"]`, the
20+
`power_off`/`power_on` drag from the generated `drag_curve.csv`, then aerodynamic surfaces
21+
added via `add_nose`, `add_trapezoidal_fins` / `add_elliptical_fins`, `add_tail`, and
22+
parachutes via `add_parachute`, plus rail buttons.
23+
- `build_flight``Flight` (rocket + environment + rail length + inclination + heading).
24+
- `build_compare_results` → compares RocketPy output against `settings["stored_results"]`
25+
(the values OpenRocket itself computed), which is how a user sanity-checks the conversion.
26+
27+
## Practical guidance
28+
- The builder is a **codegen** step: each method appends notebook cells (`nbf.v4`), so you
29+
are writing Python *source strings* that must run under the pinned `rocketpy` version.
30+
When you reference a RocketPy method/argument, verify it exists in that version's API
31+
rather than assuming.
32+
- Coordinate systems and positions matter: RocketPy places surfaces relative to a reference
33+
(nose tip / center of dry mass). The positions come from `open_rocket_wrangler` — if a
34+
surface lands in the wrong place in the simulation, suspect the position map, not RocketPy.
35+
- `stored_results` are the ground truth from OpenRocket (apogee, max velocity, etc.). Use
36+
them to validate: a good conversion makes the RocketPy `Flight` results land close to the
37+
stored ones.
38+
39+
## When aligning with RocketPy
40+
If a generated notebook fails to run, first reproduce with an example
41+
(`ork2notebook --filepath "examples/NDRT--Rocket--2020/rocket.ork"`) and open the notebook.
42+
Fix the offending `build_*` method's emitted code, not the extraction, unless the underlying
43+
parameter is genuinely wrong in `parameters.json`.

.claude/settings.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(.venv/Scripts/python.exe -c \"import rocketserializer; print\\(''Import OK''\\)\")",
5+
"Bash(java -version)"
6+
]
7+
}
8+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
---
2+
name: add-component-extractor
3+
description: Add or modify a rocket component extractor in the serialization pipeline (motor, fins, nose cone, parachute, transition, environment, flight, stored results, etc.). Use when a parameter is missing from parameters.json, when supporting a new OpenRocket feature, or when editing anything under rocketserializer/components/ or ork_extractor.py.
4+
---
5+
6+
# Adding a component extractor
7+
8+
The serializer builds `parameters.json` in `ork_extractor.ork_extractor()` by calling one
9+
`search_*` function per rocket concern from `rocketserializer/components/`. To add or extend
10+
a component, follow the existing pattern exactly.
11+
12+
## Anatomy of a component module
13+
Each `components/<thing>.py`:
14+
- Starts with `import logging` and `logger = logging.getLogger(__name__)`.
15+
- Exposes `search_<thing>(bs, ...)` returning a **dict or list of dicts** (never prints;
16+
logs instead).
17+
- Uses NumPy-style docstrings (Parameters / Returns).
18+
- Reads scalars from the BeautifulSoup tree `bs`; reads geometry/positions from the
19+
`elements` map produced by `open_rocket_wrangler.process_elements_position` (which needs
20+
the Java `ork` document). See `nose_cone.py` and `fins.py` for the `elements` pattern.
21+
22+
## Wiring it into the pipeline
23+
In `ork_extractor.py`:
24+
1. Import your function at the top with the other `from .components.<x> import ...` lines.
25+
2. Call it through the local `_safe_search(func, default_ret, *args)` wrapper so a failure
26+
logs and returns the default instead of aborting the whole extraction. Choose a sensible
27+
default (`{}`, `[]`, or a minimal dict) matching the shape callers expect.
28+
3. Assign the result into `settings["<key>"]`. The top-level keys are a **contract** with
29+
`nb_builder.py` and the acceptance golden files — don't rename existing keys casually.
30+
31+
Current keys: `id`, `environment`, `rocket`, `nosecones`, `trapezoidal_fins`,
32+
`elliptical_fins`, `tails`, `parachutes`, `rail_buttons`, `motors`, `flight`,
33+
`stored_results` (plus `rocket.drag_curve` and `motors.thrust_source` which point to CSVs
34+
written into the output folder).
35+
36+
## If the notebook must use the new value
37+
Add/adjust the matching `build_*` method in `nb_builder.NotebookBuilder` so the value flows
38+
into the generated RocketPy notebook. The builder reads only from `parameters.json`.
39+
40+
## Validate the change
41+
1. `make format && make lint`
42+
2. Run the pipeline on an example that exercises the component:
43+
`ork2json --filepath "examples/EPFL--BellaLui--2020/rocket.ork" --verbose`
44+
and inspect the resulting `parameters.json`.
45+
3. `pytest tests/acceptance/test_ork_extractor.py -v`. Acceptance tests compare against
46+
committed `examples/<name>/parameters.json`. If your change **intentionally** alters
47+
output, regenerate those golden files and review the diff carefully before committing.
48+
4. Add unit coverage under `tests/unit/` when practical.
49+
50+
## Remember the input limitations
51+
The pipeline assumes a single stage, single motor, and single nose cone, and English-only
52+
`.ork` files with at least one run simulation. Don't silently break these assumptions.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
name: openrocket-jpype-interop
3+
description: Work with the OpenRocket Java library through JPype — start/stop the JVM, load .ork documents, traverse the rocket component tree, and read data from both the XML (BeautifulSoup) and the live Java object. Use when touching openrocket_runtime.py, open_rocket_wrangler.py, ork_extractor.py, the JVM/Java setup, or anything that calls into net.sf.openrocket / info.openrocket.
4+
---
5+
6+
# OpenRocket ↔ JPype interop
7+
8+
## The two-source model
9+
`ork_extractor(bs, filepath, output_folder, ork)` reads from **two sources
10+
simultaneously** — always know which one a given value comes from:
11+
12+
- `bs` — a `BeautifulSoup` tree of the `.ork` XML (`_helpers.parse_ork_file`). Source of
13+
the simulation **datapoints** and most numeric parameters. Access via `bs.find(...)`,
14+
`bs.find_all("datapoint")`, and `bs.find("databranch").attrs["types"]` (the comma-joined
15+
column labels).
16+
- `ork` — the **live Java OpenRocket document** (`OpenRocketSession.load_doc`). Source of
17+
**geometry and component positions** that the XML doesn't expose cleanly. Reached via
18+
`ork.getRocket()` and traversed in `components/open_rocket_wrangler.py`.
19+
20+
When adding an extractor, prefer `bs` for scalar/sim data and only reach into `ork` when the
21+
value requires walking the component hierarchy.
22+
23+
## The JVM lifecycle (read before changing session code)
24+
JPype is pinned **`jpype1<1.5`**, which **cannot restart a JVM in one process**. Consequences:
25+
26+
- `OpenRocketSession.__exit__` must **not** call `jpype.shutdownJVM()`. It only disposes AWT
27+
windows. The JVM is cleaned up by JPype's atexit hook.
28+
- Never open a second `OpenRocketSession` in a test. `tests/conftest.py` opens **one**
29+
session at import time and caches every example's settings in `_cached_settings`.
30+
- To use the session: `with OpenRocketSession(jar_path, log_level="OFF") as s: doc = s.load_doc(path)`.
31+
32+
## Package namespaces
33+
OpenRocket moved from `net.sf.openrocket` (legacy, ≤ ~22) to `info.openrocket` (modern).
34+
`OpenRocketSession._resolve_packages()` tries legacy first, then modern, returning
35+
`(core, swing)`. If you support a new jar and a symbol is missing, extend that method rather
36+
than hard-coding a namespace anywhere else.
37+
38+
## Java version compatibility
39+
`ensure_java_compatibility(jar_path)` picks the minimum Java: OpenRocket 23+ → Java 17,
40+
else Java 8. On Windows it auto-discovers a JDK under `C:/Program Files/Java` (and Adoptium
41+
/ AdoptOpenJDK) and sets `JAVA_HOME`/`PATH`. On other OSes it only warns — the user must
42+
provide a compatible JVM. Jar version is parsed from the filename by `_jar_version_tuple`.
43+
44+
## Traversing the component tree
45+
`open_rocket_wrangler.py` helpers: `is_sub_component(ork)` (depth > 1 from root),
46+
`parent_is_a_stage(ork)` (parent class is `Stage`/`AxialStage`), and
47+
`process_elements_position(...)` which builds the position map used by fins, nose cone,
48+
transitions, and rail buttons. Component Java classes are identified by
49+
`ork.getClass().getSimpleName()`.
50+
51+
## Gotchas
52+
- Java calls raise `jpype.JException` (plus `AttributeError`/`TypeError`/`RuntimeError` when
53+
internals change between versions). Catch these narrowly — see `_block_loader`.
54+
- The `.ork` file is a **zip** containing `rocket.ork` (XML). `extract_ork_from_zip` handles
55+
both the zipped and already-extracted cases (it catches `BadZipFile`).

0 commit comments

Comments
 (0)