Skip to content

Add ClusterMotor for symmetric multi-engine clusters (port of upstream #924)#4

Merged
raytracerfvf merged 2 commits into
masterfrom
feat/cluster-motor
Apr 25, 2026
Merged

Add ClusterMotor for symmetric multi-engine clusters (port of upstream #924)#4
raytracerfvf merged 2 commits into
masterfrom
feat/cluster-motor

Conversation

@raytracerfvf

Copy link
Copy Markdown
Owner

Summary

Port of the open upstream RocketPy PR RocketPy-Team/RocketPy#924 onto our v1.12.0-synced fork. Adds a new `ClusterMotor` class that wraps a `SolidMotor` and replicates it `N >= 2` times symmetrically around a circle of radius `R`. Because it inherits `Motor`, it slots into `Rocket.add_motor()` unchanged.

Approach

  • `rocketpy/motors/cluster_motor.py` (new, 263 lines): ClusterMotor class. Aggregates thrust/mass/inertia via parallel-axis theorem (Huygens-Steiner): `Izz = NIzz_single + NmR^2`; `Ixx = Iyy = NI_trans + 0.5Nm*R^2`. Thrust, dry mass, and propellant mass scale linearly with `N`.
  • `rocketpy/motors/init.py`: export `ClusterMotor`.
  • `rocketpy/init.py`: re-export at package root for `from rocketpy import ClusterMotor`.
  • `rocketpy/plots/rocket_plots.py`: `_draw_motor` and `_generate_motor_patches` now render `N` nozzles/grains/chambers offset around the circle when the rocket's motor is a `ClusterMotor`. Single-motor rendering path is unchanged.
  • `tests/integration/motors/test_cluster_motor.py` (new, 152 lines): the PR's test suite.

Skipped from upstream PR

  • The PR also touches `rocketpy/plots/rocket_plots.py` imports referencing develop-only symbols (`Vector`, `Fin`); those imports are unrelated to cluster motors and weren't ported.
  • The PR's 1-line whitespace tweak in `tests/integration/environment/test_environment.py` is develop-only noise; skipped.

Known issues

The PR's own tests `test_cluster_mass_and_thrust_scaling` and `test_cluster_propellant_inertia_dynamic` fail with `ValueError: truth value of an array with more than one element is ambiguous` because they use `np.isclose` against `rocketpy.Function` objects directly instead of evaluating them. This is a bug in the upstream PR test code, not in our port — cluster construction, geometry, dry inertia, methods, and setters tests all pass (4/6). Reported here so we can re-verify when we re-sync after upstream PR RocketPy-Team#924 merges.

Test plan

  • `pytest tests/integration/motors/test_cluster_motor.py` — 4 passed, 2 PR-side failures
  • `pytest tests/integration/motors` — 25 passed (no regressions in solid/hybrid/liquid)
  • `pytest tests/integration/test_plots.py` — 2 passed (rocket_plots.py edits don't break existing rendering)
  • `from rocketpy import ClusterMotor` works

Follow-up

When upstream RocketPy#924 merges, swap this commit for the upstream version during the next fork sync.

Radu Cristea added 2 commits April 25, 2026 17:28
Port of upstream RocketPy PR RocketPy-Team#924
(RocketPy-Team#924).

ClusterMotor wraps a single SolidMotor and replicates it N>=2 times
symmetrically around a circle of radius R. Inherits from Motor so it
plugs into Rocket.add_motor() unchanged. Inertia is aggregated using
the parallel-axis theorem (Huygens-Steiner): Izz = N*Izz_single +
N*m*R^2; Ixx = Iyy = N*I_trans + 0.5*N*m*R^2. Thrust, dry mass, and
propellant mass scale linearly with N.

Files:
- rocketpy/motors/cluster_motor.py (new): ClusterMotor class.
- rocketpy/motors/__init__.py: export ClusterMotor.
- rocketpy/__init__.py: re-export at package root.
- rocketpy/plots/rocket_plots.py: render N nozzles/grains around the
  circle for cluster motors.
- tests/integration/motors/test_cluster_motor.py (new): PR test suite.

Notes:
- The PR's own test_cluster_mass_and_thrust_scaling and
  test_cluster_propellant_inertia_dynamic fail with
  "ValueError: truth value of an array..." on rocketpy.Function
  comparisons. This is a PR-side test bug (np.isclose against a
  Function object) and not specific to this port. Cluster construction,
  geometry, dry inertia, methods, and setters tests all pass (4/6).
- When upstream PR RocketPy-Team#924 merges, swap this commit for the upstream
  version during the next fork sync.
The two failing assertions compared rocketpy.Function objects against
scalars. np.isclose triggers Function.__array__, returning the source
array, which makes np.isclose return an array and crashes assert with
"truth value of an array with more than one element is ambiguous".

Call propellant_mass(0), propellant_I_11(t), propellant_I_33(t) so
np.isclose receives scalars. Matches the pattern already used at line
61 (cluster.thrust(1)).

All 6 cluster tests pass after this fix.
@raytracerfvf raytracerfvf merged commit 09fd9ba into master Apr 25, 2026
@raytracerfvf raytracerfvf deleted the feat/cluster-motor branch April 25, 2026 14:31
raytracerfvf pushed a commit that referenced this pull request May 9, 2026
…e, unused-argument)

Three issues introduced when ClusterMotor (PR #4) and the parachute trigger
signature tests landed:

1. Cyclic import in `rocketpy/motors/cluster_motor.py`: importing `Function`
   and `Motor` from the package facade re-entered `rocketpy.__init__` while
   the module was still being initialized. Switch to canonical module paths
   (`rocketpy.mathutils.function`, `rocketpy.motors.motor`).

2. `not-callable` on `cluster.propellant_mass(0)` and `cluster.propellant_I_11(t)`
   in `tests/integration/motors/test_cluster_motor.py`: a later test in the
   same file assigned a raw `float` through the property setter
   (`cluster.propellant_mass = 50.0`), which made pylint infer the attribute
   as `float | Function` for every read in the file. Exercise the setters
   with values of the documented `Function` type instead.

3. `unused-argument` on parachute trigger lambdas in
   `tests/unit/rocket/test_parachute.py`: `Parachute.__evaluate_trigger_function`
   introspects only `len(sig.parameters)` (parachute.py:300), so parameter
   names are irrelevant. Rename unused trigger params to `_p`/`_h`/`_y`/
   `_sensors` to match the project's `dummy-variables-rgx` convention.

Repo-wide pylint score: 9.98 → 10.00.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant