Skip to content

ENH: Implementing 3-dof-simulation #745

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
236 changes: 236 additions & 0 deletions docs/examples/3_DOF_TRIAL.ipynb

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions rocketpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
Parachute,
RailButtons,
Rocket,
BaseRocket,
PointMassRocket,
Tail,
TrapezoidalFins,
)
Expand Down
222 changes: 222 additions & 0 deletions rocketpy/motors/PointMassMotor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
from functools import cached_property
import numpy as np
import csv
import math
from typing import Callable # Import Callable from the typing module

from rocketpy.mathutils.function import Function, funcify_method
from .motor import Motor

class PointMassMotor(Motor):
"""Class representing a motor modeled as a point mass.

Inherits from the Motor class and simplifies the model to a thrust-producing
object without detailed structural components. The total mass of the motor
will vary with propellant consumption, similar to a standard motor. However,
its inertia components and the center of propellant mass are considered zero
and fixed at the motor's reference point, respectively.
"""

def __init__(
self,
thrust_source,
dry_mass: float,
propellant_initial_mass: float, # Now explicitly required
burn_time: float = None,
propellant_final_mass: float = None,
reshape_thrust_curve: bool = False,
interpolation_method: str = "linear",
):
"""Initialize the PointMassMotor class.

This motor simplifies the physical model by considering its mass to be
concentrated at a single point, effectively setting all its inertia
components to zero. Propellant mass variation and exhaust velocity
are still simulated and derived from the thrust and propellant consumption
characteristics, similar to the base Motor class.

Parameters
----------
thrust_source : int, float, callable, str, numpy.ndarray, Function
Thrust source. Can be a constant value (int, float), a callable
function of time, a path to a CSV file, a NumPy array, or a
RocketPy `Function` object.
dry_mass : float
Total dry mass of the motor in kg.
propellant_initial_mass : float
Initial mass of the propellant in kg. This is a required parameter
as the point mass motor will still simulate propellant consumption.
burn_time : float, optional
Total burn time of the motor in seconds. Required if `thrust_source`
is a constant value or a callable function, and `propellant_final_mass`
is not provided. If `thrust_source` is a CSV, array, or Function,
the burn time is derived from it.
propellant_final_mass : float, optional
Final mass of the propellant in kg. Required if `thrust_source`
is a callable function and `burn_time` is not provided. If not
provided, it is calculated by the base Motor class.
reshape_thrust_curve : bool, optional
Whether to reshape the thrust curve to start at t=0 and end at
burn_time. Defaults to False.
interpolation_method : str, optional
Interpolation method for the thrust curve, if applicable.
Defaults to 'linear'.

Raises
------
ValueError
If insufficient data is provided for mass flow rate calculation.
TypeError
If an invalid type is provided for `thrust_source`.
"""
# Input validation: Ensure critical parameters for mass flow are present
if isinstance(thrust_source, (int, float, Callable)):
if propellant_initial_mass is None:
raise ValueError(
"For constant or callable thrust, 'propellant_initial_mass' is required."
)
if burn_time is None and propellant_final_mass is None:
raise ValueError(
"For constant or callable thrust, either 'burn_time' or "
"'propellant_final_mass' must be provided."
)

elif isinstance(thrust_source, (Function, np.ndarray, str)):
if propellant_initial_mass is None:
raise ValueError(
"For thrust from a Function, NumPy array, or CSV, 'propellant_initial_mass' is required."
)
else:
raise TypeError(
"Invalid 'thrust_source' type. Must be int, float, callable, str, numpy.ndarray, or Function."
)

self.propellant_initial_mass = propellant_initial_mass
self.propellant_final_mass = propellant_final_mass

# Call the superclass constructor, passing center_of_dry_mass_position=0
super().__init__(
thrust_source=thrust_source,
dry_inertia=(0, 0, 0), # Inertia is zero for a point mass
nozzle_radius=0, # Nozzle radius is irrelevant for a point mass model
center_of_dry_mass_position=0, # Pass 0 directly to the superclass
dry_mass=dry_mass,
nozzle_position=0, # Nozzle is at the motor's origin
burn_time=burn_time,
reshape_thrust_curve=reshape_thrust_curve, # Pass the user-provided value
interpolation_method=interpolation_method,
coordinate_system_orientation="nozzle_to_combustion_chamber", # Standard orientation
)

# Removed the thrust method. It will now be inherited directly from the Motor base class,
# which already correctly handles the conversion of thrust_source to a Function
# and exposes it as a cached property.

# Removed the total_mass override. The base Motor class's total_mass property
# will now correctly calculate dry_mass + propellant_mass(t), which is the desired
# varying mass behavior for the point mass motor.

# Removed the center_of_dry_mass_position override. It is now passed directly
# to the super().__init__ as 0.

@cached_property
@funcify_method("Time (s)", "Propellant Mass (kg)")
def center_of_propellant_mass(self) -> Function:
"""Returns the position of the center of mass of the propellant.

For a point mass motor, the propellant's center of mass is considered
to be at the origin (0) of the motor's coordinate system.

Returns
-------
Function
A Function object representing the center of propellant mass (always 0).
"""
return 0

# Removed exhaust_velocity override. It will now be inherited from the Motor base class,
# which calculates it dynamically based on thrust and mass_flow_rate.

@cached_property
@funcify_method("Time (s)", "Inertia (kg·m²)")
def propellant_I_11(self) -> Function:
"""Returns the propellant moment of inertia around the x-axis.

For a point mass motor, this is always zero.

Returns
-------
Function
A Function object representing zero propellant inertia.
"""
return 0

@cached_property
@funcify_method("Time (s)", "Inertia (kg·m²)")
def propellant_I_12(self) -> Function:
"""Returns the propellant product of inertia I_xy.

For a point mass motor, this is always zero.

Returns
-------
Function
A Function object representing zero propellant inertia.
"""
return 0

@cached_property
@funcify_method("Time (s)", "Inertia (kg·m²)")
def propellant_I_13(self) -> Function:
"""Returns the propellant product of inertia I_xz.

For a point mass motor, this is always zero.

Returns
-------
Function
A Function object representing zero propellant inertia.
"""
return 0

@cached_property
@funcify_method("Time (s)", "Inertia (kg·m²)")
def propellant_I_22(self) -> Function:
"""Returns the propellant moment of inertia around the y-axis.

For a point mass motor, this is always zero.

Returns
-------
Function
A Function object representing zero propellant inertia.
"""
return 0

@cached_property
@funcify_method("Time (s)", "Inertia (kg·m²)")
def propellant_I_23(self) -> Function:
"""Returns the propellant product of inertia I_yz.

For a point mass motor, this is always zero.

Returns
-------
Function
A Function object representing zero propellant inertia.
"""
return 0

@cached_property
@funcify_method("Time (s)", "Inertia (kg·m²)")
def propellant_I_33(self) -> Function:
"""Returns the propellant moment of inertia around the z-axis.

For a point mass motor, this is always zero.

Returns
-------
Function
A Function object representing zero propellant inertia.
"""
return 0
2 changes: 2 additions & 0 deletions rocketpy/rocket/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@
from rocketpy.rocket.components import Components
from rocketpy.rocket.parachute import Parachute
from rocketpy.rocket.rocket import Rocket
from rocketpy.rocket.rocket import BaseRocket
from rocketpy.rocket.rocket import PointMassRocket
81 changes: 81 additions & 0 deletions rocketpy/rocket/rocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,88 @@
from rocketpy.rocket.parachute import Parachute
from rocketpy.tools import parallel_axis_theorem_from_com

class BaseRocket:
"""Base class for a rocket model with minimal attributes and methods."""
def __init__(self, mass):
self.mass = mass
self.motor = None
def add_motor(self, motor):
self.motor = motor
self.evaluate_total_mass()
def evaluate_total_mass(self):
if self.motor:
return self.mass + self.motor.total_mass
return self.mass


Comment on lines +27 to +40
Copy link
Member

Choose a reason for hiding this comment

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

This class seems useless right now. You can just remove it

class PointMassRocket(BaseRocket):
"""Rocket modeled as a point mass for 3-DOF simulations."""
Comment on lines +41 to +42
Copy link
Member

Choose a reason for hiding this comment

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

Maybe having this inherit from the Rocket class will make the implementation easier. Then all_info and plots and prints will already be defined


def __init__(self, mass, drag_coefficient=0.4, radius=0.05):
super().__init__(mass)

# Basic configuration
self.drag_coefficient = drag_coefficient
self.radius = radius # in meters
self.area = math.pi * self.radius**2

# Coordinate system configuration
self.coordinate_system_orientation = "tail_to_nose"
self.center_of_dry_mass_position = 0 # arbitrary for point mass
self.dry_mass = mass # dry_mass = structure + dry motor, here it's same as base mass
self.nozzle_position = 0 # or another reference point like -1 * length/2

# Components
self.parachutes = []
self._controllers = []
self.air_brakes = []
self.sensors = Components()
self.aerodynamic_surfaces = Components()
self.rail_buttons = Components()
self.surfaces_cp_to_cdm = {}

# Drag models
self.power_on_drag = Function(
drag_coefficient,
"Mach Number",
"Power-On Drag Coefficient",
interpolation="constant"
)
self.power_off_drag = Function(
drag_coefficient * 1.2,
"Mach Number",
"Power-Off Drag Coefficient",
interpolation="constant"
)
def add_parachute(
self, name, cd_s, trigger, sampling_rate=100, lag=0, noise=(0, 0, 0)
):
parachute = Parachute(name, cd_s, trigger, sampling_rate, lag, noise)
self.parachutes.append(parachute)
self.total_mass = None
self.evaluate_total_mass()
return self.parachutes[-1]

def evaluate_total_mass(self):
"""Returns Function of total mass (dry + motor)."""
if self.motor is None:
print("Please associate this rocket with a motor!")
return False

motor_mass_func = (
self.motor.total_mass if hasattr(self.motor.total_mass, "get_value_opt")
else Function(lambda t: self.motor.total_mass)
)
Comment on lines +95 to +98
Copy link
Collaborator

Choose a reason for hiding this comment

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

This check seems to be in place in order to convert to a Function the self.motor.total_mass if it is not one. However, the attribute from the motor class is annotated with funcify, so it will always be a Function.


self.total_mass = Function(
lambda t: self.mass + motor_mass_func(t),
inputs="Time (s)",
outputs="Total Mass (Rocket + Motor + Propellant) (kg)",
title="Total Mass (Rocket + Motor + Propellant)"
)
return self.total_mass


# pylint: disable=too-many-instance-attributes, too-many-public-methods, too-many-instance-attributes
class Rocket:
"""Keeps rocket information.
Expand Down
Loading
Loading