Skip to content

Commit 0765dcb

Browse files
ayoubdspGui-FernandesBR
authored andcommitted
Implement ClusterMotor class for motor aggregation
- 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. Update Rocket class for 3D inertia and ClusterMotor 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. Update HybridMotor to new dynamic inertia contract 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. Update LiquidMotor to new dynamic inertia contract 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. SolidMotor inertia logic for dynamic contract 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. Refactor base Motor class for dynamic 6-DOF inertia 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__`. Update Rocket.draw() to visualize ClusterMotor configurations 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. Update function.py Clear comments and cleaned code Updated ClusterMotor Clean comments and cleared code Hybrid Motor correction Correction of the comments and cleared the code Correction of LiquidMotor Clean comments and cleared code Correction of Motor Clean comments and clear code Correction of SolidMotor Clean comments and cleared code Correction of Rocketplots Clean comments and cleared code from copilot Correction of Rocket Clean comments and cleared code with copilot review Correction of Flight Clean comments and clear code Correction of tools Clean comments and clear code ClusterMotor initialization and add docstring 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. Refactor imports in solid_motor.py for clarity Simplify return statement for inertia tensor calculation Removing the # .real generated comment Removing the generated unuseful comment Implement deprecation warning decorator Added a deprecation decorator to warn users about obsolete functions and their alternatives. make format make lint partially fix tests few adjusts
1 parent 793e5f6 commit 0765dcb

File tree

13 files changed

+1903
-1246
lines changed

13 files changed

+1903
-1246
lines changed

rocketpy/_encoders.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import json
44
from datetime import datetime
55
from importlib import import_module
6+
from inspect import Parameter, signature
67

78
import numpy as np
89

@@ -71,11 +72,12 @@ def default(self, o):
7172
encoding["signature"] = get_class_signature(o)
7273
return encoding
7374
elif hasattr(o, "to_dict"):
74-
encoding = o.to_dict(
75-
include_outputs=self.include_outputs,
76-
discretize=self.discretize,
77-
allow_pickle=self.allow_pickle,
78-
)
75+
call_kwargs = {
76+
"include_outputs": self.include_outputs,
77+
"discretize": self.discretize,
78+
"allow_pickle": self.allow_pickle,
79+
}
80+
encoding = _call_to_dict_with_supported_kwargs(o, call_kwargs)
7981
encoding = remove_circular_references(encoding)
8082

8183
encoding["signature"] = get_class_signature(o)
@@ -195,6 +197,23 @@ def set_minimal_flight_attributes(flight, obj):
195197
flight.t_initial = flight.initial_solution[0]
196198

197199

200+
def _call_to_dict_with_supported_kwargs(obj, candidate_kwargs):
201+
"""Call obj.to_dict passing only supported keyword arguments."""
202+
method = obj.to_dict
203+
sig = signature(method)
204+
params = list(sig.parameters.values())
205+
206+
if any(param.kind == Parameter.VAR_KEYWORD for param in params):
207+
return method(**candidate_kwargs)
208+
209+
supported_kwargs = {
210+
name: candidate_kwargs[name]
211+
for name in sig.parameters
212+
if name != "self" and name in candidate_kwargs
213+
}
214+
return method(**supported_kwargs)
215+
216+
198217
def get_class_signature(obj):
199218
"""Returns the signature of a class so it can be identified on
200219
decoding. The signature is a dictionary with the module and

rocketpy/mathutils/inertia.py

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
"""Utilities related to inertia tensor transformations.
2+
3+
This module centralizes dynamic helpers for applying the parallel axis
4+
theorem (PAT). It lives inside ``rocketpy.mathutils`` so that functionality
5+
depending on :class:`rocketpy.mathutils.function.Function` does not leak into
6+
generic utility modules such as ``rocketpy.tools``.
7+
"""
8+
9+
from rocketpy.mathutils.function import Function
10+
from rocketpy.mathutils.vector_matrix import Vector
11+
12+
13+
def _pat_dynamic_helper(com_inertia_moment, mass, distance_vec_3d, axes_term_lambda):
14+
"""Apply the PAT to inertia moments, supporting static and dynamic inputs."""
15+
16+
is_dynamic = (
17+
isinstance(com_inertia_moment, Function)
18+
or isinstance(mass, Function)
19+
or isinstance(distance_vec_3d, Function)
20+
)
21+
22+
def get_val(arg, t):
23+
return arg(t) if isinstance(arg, Function) else arg
24+
25+
if not is_dynamic:
26+
d_vec = Vector(distance_vec_3d)
27+
mass_term = mass * axes_term_lambda(d_vec)
28+
return com_inertia_moment + mass_term
29+
30+
def new_source(t):
31+
d_vec_t = get_val(distance_vec_3d, t)
32+
mass_t = get_val(mass, t)
33+
inertia_t = get_val(com_inertia_moment, t)
34+
mass_term = mass_t * axes_term_lambda(d_vec_t)
35+
return inertia_t + mass_term
36+
37+
return Function(new_source, inputs="t", outputs="Inertia (kg*m^2)")
38+
39+
40+
def _pat_dynamic_product_helper(
41+
com_inertia_product, mass, distance_vec_3d, product_term_lambda
42+
):
43+
"""Apply the PAT to inertia products, supporting static and dynamic inputs."""
44+
45+
is_dynamic = (
46+
isinstance(com_inertia_product, Function)
47+
or isinstance(mass, Function)
48+
or isinstance(distance_vec_3d, Function)
49+
)
50+
51+
def get_val(arg, t):
52+
return arg(t) if isinstance(arg, Function) else arg
53+
54+
if not is_dynamic:
55+
d_vec = Vector(distance_vec_3d)
56+
mass_term = mass * product_term_lambda(d_vec)
57+
return com_inertia_product + mass_term
58+
59+
def new_source(t):
60+
d_vec_t = get_val(distance_vec_3d, t)
61+
mass_t = get_val(mass, t)
62+
inertia_t = get_val(com_inertia_product, t)
63+
mass_term = mass_t * product_term_lambda(d_vec_t)
64+
return inertia_t + mass_term
65+
66+
return Function(new_source, inputs="t", outputs="Inertia (kg*m^2)")
67+
68+
69+
# --- Public functions for the Parallel Axis Theorem ---
70+
71+
72+
def parallel_axis_theorem_I11(com_inertia_moment, mass, distance_vec_3d):
73+
"""Apply PAT to the I11 inertia term.
74+
75+
Parameters
76+
----------
77+
com_inertia_moment : float or Function
78+
Inertia moment relative to the component center of mass.
79+
mass : float or Function
80+
Mass of the component. If a Function, it must map time to mass.
81+
distance_vec_3d : array-like or Function
82+
Displacement vector from the component COM to the reference COM.
83+
84+
Returns
85+
-------
86+
float or Function
87+
Updated I11 value referenced to the new axis.
88+
"""
89+
90+
return _pat_dynamic_helper(
91+
com_inertia_moment, mass, distance_vec_3d, lambda d_vec: d_vec.y**2 + d_vec.z**2
92+
)
93+
94+
95+
def parallel_axis_theorem_I22(com_inertia_moment, mass, distance_vec_3d):
96+
"""Apply PAT to the I22 inertia term.
97+
98+
Parameters
99+
----------
100+
com_inertia_moment : float or Function
101+
Inertia moment relative to the component center of mass.
102+
mass : float or Function
103+
Mass of the component. If a Function, it must map time to mass.
104+
distance_vec_3d : array-like or Function
105+
Displacement vector from the component COM to the reference COM.
106+
107+
Returns
108+
-------
109+
float or Function
110+
Updated I22 value referenced to the new axis.
111+
"""
112+
113+
return _pat_dynamic_helper(
114+
com_inertia_moment, mass, distance_vec_3d, lambda d_vec: d_vec.x**2 + d_vec.z**2
115+
)
116+
117+
118+
def parallel_axis_theorem_I33(com_inertia_moment, mass, distance_vec_3d):
119+
"""Apply PAT to the I33 inertia term.
120+
121+
Parameters
122+
----------
123+
com_inertia_moment : float or Function
124+
Inertia moment relative to the component center of mass.
125+
mass : float or Function
126+
Mass of the component. If a Function, it must map time to mass.
127+
distance_vec_3d : array-like or Function
128+
Displacement vector from the component COM to the reference COM.
129+
130+
Returns
131+
-------
132+
float or Function
133+
Updated I33 value referenced to the new axis.
134+
"""
135+
136+
return _pat_dynamic_helper(
137+
com_inertia_moment, mass, distance_vec_3d, lambda d_vec: d_vec.x**2 + d_vec.y**2
138+
)
139+
140+
141+
def parallel_axis_theorem_I12(com_inertia_product, mass, distance_vec_3d):
142+
"""Apply PAT to the I12 inertia product.
143+
144+
Parameters
145+
----------
146+
com_inertia_product : float or Function
147+
Product of inertia relative to the component center of mass.
148+
mass : float or Function
149+
Mass of the component. If a Function, it must map time to mass.
150+
distance_vec_3d : array-like or Function
151+
Displacement vector from the component COM to the reference COM.
152+
153+
Returns
154+
-------
155+
float or Function
156+
Updated I12 value referenced to the new axis.
157+
"""
158+
159+
return _pat_dynamic_product_helper(
160+
com_inertia_product, mass, distance_vec_3d, lambda d_vec: d_vec.x * d_vec.y
161+
)
162+
163+
164+
def parallel_axis_theorem_I13(com_inertia_product, mass, distance_vec_3d):
165+
"""Apply PAT to the I13 inertia product.
166+
167+
Parameters
168+
----------
169+
com_inertia_product : float or Function
170+
Product of inertia relative to the component center of mass.
171+
mass : float or Function
172+
Mass of the component. If a Function, it must map time to mass.
173+
distance_vec_3d : array-like or Function
174+
Displacement vector from the component COM to the reference COM.
175+
176+
Returns
177+
-------
178+
float or Function
179+
Updated I13 value referenced to the new axis.
180+
"""
181+
182+
return _pat_dynamic_product_helper(
183+
com_inertia_product, mass, distance_vec_3d, lambda d_vec: d_vec.x * d_vec.z
184+
)
185+
186+
187+
def parallel_axis_theorem_I23(com_inertia_product, mass, distance_vec_3d):
188+
"""Apply PAT to the I23 inertia product.
189+
190+
Parameters
191+
----------
192+
com_inertia_product : float or Function
193+
Product of inertia relative to the component center of mass.
194+
mass : float or Function
195+
Mass of the component. If a Function, it must map time to mass.
196+
distance_vec_3d : array-like or Function
197+
Displacement vector from the component COM to the reference COM.
198+
199+
Returns
200+
-------
201+
float or Function
202+
Updated I23 value referenced to the new axis.
203+
"""
204+
205+
return _pat_dynamic_product_helper(
206+
com_inertia_product, mass, distance_vec_3d, lambda d_vec: d_vec.y * d_vec.z
207+
)
208+
209+
210+
__all__ = [
211+
"parallel_axis_theorem_I11",
212+
"parallel_axis_theorem_I22",
213+
"parallel_axis_theorem_I33",
214+
"parallel_axis_theorem_I12",
215+
"parallel_axis_theorem_I13",
216+
"parallel_axis_theorem_I23",
217+
]

rocketpy/motors/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from .cluster_motor import ClusterMotor
12
from .empty_motor import EmptyMotor
23
from .fluid import Fluid
34
from .hybrid_motor import HybridMotor

0 commit comments

Comments
 (0)