Skip to content

Conversation

@ayoubdsp
Copy link

@ayoubdsp ayoubdsp commented Nov 6, 2025

feat(core): Implement 6-DOF motor clustering and dynamic inertia

Pull request type

  • Code changes (bugfix, features)
  • Code maintenance (refactoring, formatting, tests)
  • ReadMe, Docs and GitHub updates
  • Other (please describe):

Checklist

  • Tests for the changes have been added (if needed)
  • Docs have been reviewed and added / updated
  • Lint (black rocketpy/ tests/) has passed locally
  • All tests (pytest tests -m slow --runslow) have passed locally
  • CHANGELOG.md has been updated (if relevant)

Current behavior

Currently, RocketPy only supports a single motor per Rocket object. The 6-DOF simulation assumes a purely axial thrust and cannot model multi-motor configurations (clusters) or propulsion-induced torques.

The inertia calculation logic across the various motor classes (SolidMotor, HybridMotor, LiquidMotor) is inconsistent. Crucially, the Parallel Axis Theorem (PAT) is not applied dynamically (i.e., as a function of time) for complex, time-varying mass distributions, leading to inaccuracies in 6-DOF simulations.

New behavior

This PR introduces a major feature: the ability to define, simulate, and visualize rockets with motor clusters in a full 6-DOF environment.

This was achieved through a deep refactor of the core motor, rocket, and simulation classes:

  1. New ClusterMotor.py Class:

    • Adds a ClusterMotor class that aggregates multiple Motor instances.
    • Manages the 3D position ([x, y, z]) and 3D orientation (Vector) of each sub-motor.
    • Calculates the aggregated 3D Center of Mass (CoM) and time-varying 3D Inertia Tensor for the entire cluster using the Parallel Axis Theorem.
    • Provides get_total_thrust_vector(t) and get_total_moment(t, ref_point) to feed the 6-DOF solver.
  2. Dynamic Inertia Tools (tools.py):

    • Adds a complete set of dynamic Parallel Axis Theorem (PAT) functions (e.g., parallel_axis_theorem_I11, _I12, etc.).
    • These functions are "dynamic" because they accept rocketpy.Function objects as inputs (for time-varying mass or distance vectors) and return a new Function that calculates the PAT at runtime.
  3. 6-DOF Solver Update (flight.py):

    • The u_dot_generalized method is now the standard 6-DOF solver.
    • It correctly ingests the total 3D thrust vector and 3D moment vector from the rocket.motor (whether it's a single motor or a ClusterMotor).
    • It solves the full rigid-body Euler's equations of motion to calculate angular acceleration, enabling the simulation of non-axial thrust and torques.
  4. Rocket Class Update (rocket.py):

    • Adds a new add_cluster_motor() method.
    • add_motor() is updated to detect and handle ClusterMotor objects.
    • All CoM attributes (e.g., center_of_dry_mass_position) are refactored to be 3D Vector objects instead of 1D scalars.
    • Inertia evaluation methods (evaluate_dry_inertias, evaluate_inertias) are updated to use the new dynamic PAT functions.
  5. Motor Subclass Refactor (motor.py, solid_motor.py, hybrid_motor.py, liquid_motor.py):

    • The base Motor class __init__ is refactored to use the new dynamic PAT tools, establishing a new "inertia contract".
    • Subclasses must now provide propellant inertia relative to their own propellant CoM (e.g., propellant_I_11_from_propellant_CM).
    • SolidMotor, HybridMotor, and LiquidMotor are all updated to comply with this new contract, ensuring consistent and correct inertia calculations.
    • Fixes .eng file parsing in motor.py and solid_motor.py (which use space delimiters and semicolon comments).
  6. Visualization (rocket_plots.py):

    • Rocket.draw() is updated to correctly visualize ClusterMotor configurations.
    • A new _generate_motor_patches helper function iterates through sub-motors and applies the correct 2D offset based on their 3D position, allowing them to be drawn side-by-side.

Breaking change

  • Yes

Impact:

  1. Center of Mass Attributes are now 3D Vectors: Attributes such as rocket.center_of_dry_mass_position, motor.center_of_mass, motor.center_of_propellant_mass, etc., are no longer float scalars representing the Z-axis position. They are now 3D Vector objects (or Functions that return Vectors).
  2. Internal Motor Inertia API: The internal "contract" for how Motor subclasses provide inertia has changed.

Migration Path:

  1. Any user code that accessed a CoM attribute and expected a float must be updated to access the .z component of the vector.
    • Before: z = my_rocket.center_of_dry_mass_position
    • After: z = my_rocket.center_of_dry_mass_position.z
  2. Any custom classes inheriting from Motor must be updated to provide propellant inertia relative to the propellant CoM (e.g., by setting self.propellant_I_11_from_propellant_CM).

Additional information

This feature was developed by IPSA SPACE SYSTEMS for the CONDOR project.

This class aggregates multiple motors' properties and provides a unified interface for rocket simulations.
Adds a new set of **dynamic** Parallel Axis Theorem (PAT) functions (`parallel_axis_theorem_I11`, `_I12`, etc.).
 These functions are "dynamic" because they can accept `rocketpy.Function` objects as inputs (for time-varying mass or distance vectors) and return a new `Function` that calculates the PAT at runtime. This is essential for correct time-varying inertia calculations.
Updates the 6-DOF solver (`u_dot_generalized`) to correctly
simulate cluster propulsion and non-axial thrust.

- The solver now calls `motor.get_total_thrust_vector(t)` and
  `motor.get_total_moment(t, rocket_cm)` to get the full
  3D forces and torques from the propulsion system.
- These vectors are used to solve the full rigid body equations
  of motion (Euler's equations) for angular acceleration.
- This enables correct 6-DOF simulation of motor clusters,
  vectored thrust, and thrust misalignments.
Refactors the `Rocket` class to integrate the new `ClusterMotor`
and handle 3D centers of mass and dynamic inertia.

- `add_motor()` is updated to detect `ClusterMotor` objects.
- `add_cluster_motor()` helper method added.
- `evaluate_center_of_mass` and `evaluate_center_of_dry_mass`
  now perform weighted 3D vector averaging for CoM.
- `evaluate_dry_inertias` and `evaluate_inertias` are updated
  to use the new dynamic `parallel_axis_theorem` functions.
- Internal attributes like `center_of_dry_mass_position` are
  now 3D `Vector`s instead of Z-axis scalars.
Refactors `HybridMotor.__init__` to comply with the new base
class inertia contract.

- The class now calculates the individual inertias of the liquid
  (tanks) and solid (grain) components relative to their own CoMs.
- It then uses the new dynamic PAT functions to aggregate these
  inertias relative to the *total propellant center of mass*.
- The result is stored in `self.propellant_I_xx_from_propellant_CM`
  for the `Motor` base class to consume.
Refactors `LiquidMotor` to comply with the new base class inertia
contract.

- The class now correctly calculates the aggregated inertia of all
  tanks relative to the total propellant center of mass (logic
  handled by base class and PAT tools).
- Corrects the `propellant_I_11`, `_I_22`, etc. methods to
  return the pre-calculated `propellant_I_xx_from_propellant_CM`
  attribute, preventing an incorrect double application of the
  Parallel Axis Theorem.
1.  **`.eng`/`.rse` File Pre-parsing:**
    * The logic to read `.eng` and `.rse` files (using `Motor.import_eng` and `Motor.import_rse`) has been moved from the base class into the `SolidMotor.__init__` method.
    * This ensures that the `thrust_source` data array is loaded and available *before* `super().__init__` is called.

2.  **Inertia Re-calculation:**
    * The original `SolidMotor` relied on the `motor.py` base class to handle all inertia calculations within `super().__init__`.
    * This was problematic because `evaluate_geometry()` (which defines the grain properties needed for inertia) was called *after* `super().__init__`.
    * This commit fixes this by adding a new block of code at the **end** of `SolidMotor.__init__` (after `evaluate_geometry()` has run).
    * This new block explicitly:
        1.  Assigns the now-valid propellant inertia methods (e.g., `self.propellant_I_11`) to the new "contract" attributes (e.g., `self.propellant_I_11_from_propellant_CM`).
        2.  Imports the dynamic `parallel_axis_theorem` tools.
        3.  **Re-calculates** the propellant inertia relative to the motor origin (using the PAT logic from `motor.py`).
        4.  **Re-calculates** the final total motor inertia (`self.I_11`, `self.I_33`, etc.) by summing the dry and (now correct) propellant inertias. This overwrites the incorrect values that were set during the initial `super().__init__` call.
1.  **Dynamic Inertia Calculation (Major Change):**
    * The `__init__` method now imports the new dynamic Parallel Axis Theorem (PAT) functions from `tools.py` (e.g., `parallel_axis_theorem_I11`, `_I12`, etc.).
    * A new "Inertia Contract" is established: `__init__` defines new abstract attributes (e.g., `self.propellant_I_11_from_propellant_CM = Function(0)`).
    * Subclasses (like `SolidMotor`, `HybridMotor`) are now *required* to provide their propellant inertia relative to their *own propellant CoM* by overriding these `_from_propellant_CM` attributes.
    * `__init__` (lines 280-333) immediately uses the dynamic PAT functions to calculate the propellant inertia tensor (e.g., `self.propellant_I_11`) relative to the **motor's origin**, based on these "contract" attributes.
    * `__init__` (lines 336-351) then calculates the **total motor inertia** (`self.I_11`, `self.I_33`, etc.) relative to the **motor's origin** by adding the (constant) dry inertia.

    *Note: This differs completely from the GitHub version, where inertia was calculated *later* inside the `@funcify_method` definitions (`I_11`, `I_33`, etc.) and relative to the *instantaneous center of mass*.*

2.  **`.eng` File Parsing Fix:**
    * The `__init__` method now includes logic (lines 235-240) to detect if `thrust_source` is a `.eng` file.
    * If true, it sets the `delimiter` to a space (" ") and `comments` to a semicolon (";"), correcting the parsing failure that occurs in the original `Function` constructor which defaults to commas.

3.  **`reference_pressure` Added:**
    * The `__init__` signature (line 229) now accepts `reference_pressure=None`.
    * This value is stored as `self.reference_pressure` (line 257) to be used in vacuum thrust calculations.

4.  **Modified Inertia Methods (`I_11`, `I_33`, etc.):**
    * The instance methods (e.g., `@funcify_method def I_11(self)`) starting at line 641 have been modified.
    * While the GitHub version used these methods to calculate inertia relative to the instantaneous CoM, this version's logic is updated, although it appears redundant given that the primary inertia calculation (relative to the origin) is now finalized in `__init__`.
1.  **Import `ClusterMotor`:**
    * The file now imports `ClusterMotor` (Line 5) to check the motor type.

2.  **Refactored `_draw_motor` Method (Line 234):**
    * This method is completely refactored. It now acts as a dispatcher,
      calling the new `_generate_motor_patches` helper function.
    * It checks `isinstance(self.rocket.motor, ClusterMotor)`.
    * If it's a cluster, it adds all patches generated by the helper.
    * If it's a simple motor, it uses the original logic (drawing the
      nozzle and chamber centered at y=0).
    * It also correctly calls `_draw_nozzle_tube` to connect the
      airframe to the start of the cluster or single motor.

3.  **New `_generate_motor_patches` Method (Line 259):**
    * This is an **entirely new** helper function.
    * It contains the core logic for drawing clusters.
    * It iterates through `cluster.motors` and `cluster.positions`.
    * For each sub-motor, it correctly calculates the 2D plot offset
      (using `sub_pos[0]` for the 'xz' plane or `sub_pos[1]` for 'yz')
      and the longitudinal position (`sub_pos[2]`).
    * It re-uses the plotting logic of the individual sub-motors
      (e.g., `_generate_combustion_chamber`, `_generate_grains`) but
      applies the correct `translate=(pos_z, offset)` transform.
    * This allows multiple motors to be drawn side-by-side.

4.  **Fix `_draw_center_of_mass_and_pressure` (Line 389):**
    * This method is updated to handle the new 3D `Vector` object
      returned by `self.rocket.center_of_mass(0)`.
    * It now accesses the Z-coordinate correctly using
      `cm_z = float(cm_vector.z.real)` instead of assuming the
      return value is a simple float.
@Gui-FernandesBR
Copy link
Member

This is impressive

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements support for motor clusters in RocketPy through a major refactoring of the parallel axis theorem (PAT) calculations and flight dynamics. The changes enable multi-motor configurations with proper inertia tensor calculations and thrust vectoring.

Key Changes:

  • Replaces scalar-based PAT with vector-based formulas supporting 3D inertia tensors (I11, I22, I33, I12, I13, I23)
  • Adds ClusterMotor class to manage multiple motors with position and orientation
  • Refactors u_dot_generalized method to support cluster thrust vectors and moments
  • Removes deprecated functions and FlightDataExporter methods, moving them back into Flight class

Reviewed Changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 29 comments.

Show a summary per file
File Description
rocketpy/tools.py Implements new vector-based parallel axis theorem functions; removes deprecated decorator and helper functions
rocketpy/simulation/flight.py Major refactoring of flight dynamics equations to support cluster motors; removes Coriolis forces; reverts FlightDataExporter methods; changes parachute physics
rocketpy/rocket/rocket.py Updates rocket initialization to support ClusterMotor; refactors center of mass and inertia calculations to use vectors
rocketpy/motors/motor.py Refactors Motor base class initialization order and adds vector-based PAT support
rocketpy/motors/solid_motor.py Adds support for .eng/.rse file imports; updates inertia calculations
rocketpy/motors/liquid_motor.py Updates to use new PAT functions with vector parameters
rocketpy/motors/hybrid_motor.py Major refactoring to support combined solid grain and liquid oxidizer inertias
rocketpy/motors/ClusterMotor New file implementing motor cluster functionality with aggregated properties
rocketpy/plots/rocket_plots.py Updates drawing methods to visualize motor clusters in 2D cross-sections

Comment on lines 1927 to 1928
ka = 1 # Added mass coefficient (depends on parachute's porosity)
R = 1.5 # Parachute radius
Copy link

Copilot AI Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded values for ka (added mass coefficient) and R (parachute radius) have been introduced. These should be retrieved from the parachute properties (self.parachute_added_mass_coefficient and self.parachute_radius) to maintain consistency with the original implementation and allow proper parachute configuration.

Suggested change
ka = 1 # Added mass coefficient (depends on parachute's porosity)
R = 1.5 # Parachute radius
ka = self.parachute_added_mass_coefficient # Added mass coefficient (depends on parachute's porosity)
R = self.parachute_radius # Parachute radius

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are these changes really necessary at this PR? Seems to be a problem with git here...

@Gui-FernandesBR
Copy link
Member

@ayoubdsp the idea looks great!!

I see a lot of breaking changes, and they make review much harder. But we will try our best.
I guess me and @MateusStano are the best people to review it.

Could you please follow the nest steps before our review?
1 - Ensure you have ran make format and make lint
2 - Address the comments made by github copilot in this PR. This will make our life much easier!

IMO the PR could be separated into 2-3 different PRs:

  • Rocket
  • Flight
  • Breaking changes (maybe combined with the previous)

However, as you've already created this one, we will work on top of it, trying to follow the same order as above while reviewing

Clear comments and cleaned code
Clean comments and cleared code
Correction of the comments and cleared the code
Clean comments and cleared code
Clean comments and clear code
Clean comments and cleared code
Clean comments and cleared code from copilot
Clean comments and cleared code with copilot review
Clean comments and clear code
Clean comments and clear code
Copy link
Member

@Gui-FernandesBR Gui-FernandesBR left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a few comments for now, and I will continue my review later

Addresses pull request feedback on the new ClusterMotor class.

The monolithic __init__ method was large and difficult to maintain. This commit refactors the constructor by breaking its logic into several smaller, private helper methods:

- `_validate_inputs`: Handles all input validation and normalization.
- `_initialize_basic_properties`: Calculates scalar values (mass, impulse, burn time).
- `_initialize_thrust_and_mass`: Sets up thrust, mass flow rate, and propellant mass Functions.
- `_initialize_center_of_mass`: Sets up CoM and CoPM Functions.
- `_initialize_inertia_properties`: Calculates dry inertia and sets up propellant inertia Functions.

This improves readability and separates concerns.

Additionally:
- Adds a NumPy-style docstring to the `__init__` method.
- Cleans up inline comments, retaining all essential docstrings.
@Gui-FernandesBR Gui-FernandesBR changed the base branch from master to develop November 10, 2025 18:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

2 participants