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 13 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
121 changes: 121 additions & 0 deletions rocketpy/motors/PointMassMotor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
from functools import cached_property
import numpy as np
from ..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."""

def __init__(
self,
thrust_source,
dry_mass,
thrust_curve=None,
propellant_initial_mass=None,
propellant_final_mass=None,
burn_time=None,
center_of_dry_mass_position=0,
reshape_thrust_curve=False,
interpolation_method="linear",
coordinate_system_orientation="nozzle_to_combustion_chamber",
):
"""Initialize the PointMassMotor class.

Parameters
----------
thrust_source : int, float, callable, string, array, Function
Thrust source similar to the Motor class.
dry_mass : float
Total dry mass of the motor in kg.
thrust_curve : Function, np.array, or str (csv file), optional
Required if thrust_source is a csv file, Function, or np.array.
propellant_initial_mass : float, optional
Required if thrust_source is a csv file, Function, or np.array.
propellant_final_mass : float, optional
Required if thrust_source is callable.
burn_time : float or tuple of float, optional
Required if thrust_source is callable or if a thrust value is given.
center_of_dry_mass_position : float, optional
Initial position of the motor, default is 0.
interpolation_method : string, optional
Interpolation method for thrust curve, default is 'linear'.
"""
if isinstance(thrust_source, (Function, np.ndarray, str)):
if thrust_curve is None or propellant_initial_mass is None:
raise ValueError("thrust_curve and propellant_initial_mass are required for csv, Function, or np.array inputs.")
elif callable(thrust_source):
if any(param is None for param in [thrust_curve, propellant_initial_mass, burn_time, propellant_final_mass]):
raise ValueError("thrust_curve, propellant_initial_mass, burn_time, and propellant_final_mass are required for callable inputs.")
elif isinstance(thrust_source, (int, float)):
if any(param is None for param in [thrust_curve, propellant_initial_mass, burn_time]):
raise ValueError("thrust_curve, propellant_initial_mass, and burn_time are required when a thrust value is given.")

self._propellant_initial_mass = propellant_initial_mass
super().__init__(
thrust_source=thrust_source,
dry_inertia=(0, 0, 0),
nozzle_radius=0,
center_of_dry_mass_position=center_of_dry_mass_position,
dry_mass=dry_mass,
nozzle_position=0,
burn_time=burn_time,
reshape_thrust_curve=reshape_thrust_curve,
interpolation_method=interpolation_method,
coordinate_system_orientation=coordinate_system_orientation,
)
@funcify_method("Time (s)", "Thrust (N)")
Comment on lines +55 to +68
Copy link
Member

Choose a reason for hiding this comment

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

You are not using the propellant_final_mass at all. Unless you think this is valuable to simulate when considering motors that end burning with propellant still in them (I guess some hybrids and liquids could have that) then I think you can delete it.

Currently the propellant_mass function in being calculated with the total_mass_flow_rate property of the Motor Class. @phmbressan can you confirm that this makes sense for this case?

def thrust(self):
"""Returns the thrust of the motor as a function of time."""
return self.thrust_source

@funcify_method("Time (s)", "Acceleration (m/s^2)")
def total_mass(self):
"""Returns the constant total mass of the point mass motor."""
return self.dry_mass
Copy link
Collaborator

Choose a reason for hiding this comment

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

To me it makes sense to evaluate the mass_flow_rate of a point mass motor if the user inputs a thrust curve. The schema would be quite similar to what is already done in the Motor parent class (e.g. Motor.total_mass_flow_rate). Let me know what you think about it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes I was confused about what to implement for mass flow rate. Like different types of motors have different ways to calculate this. When it comes to point mass motor the problem I faced was that the input can be any kind of data irrespective of propellant nature. Thus could not understand which equations to implement for mass flow rate.


@funcify_method("Time (s)", "Acceleration (m/s^2)")
def acceleration(self):
"""Computes the acceleration of the motor as thrust divided by mass."""
return self.thrust() / self.total_mass

@funcify_method("Time (s)", "Propellant Mass (kg)")
def center_of_propellant_mass(self):
return 0

@funcify_method("Time (s)", "Exhaust Velocity (m/s)")
def exhaust_velocity(self):
return 2000 # m/s, estimated value
Comment on lines +87 to +89
Copy link
Member

Choose a reason for hiding this comment

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

I believe this should be calculate in the same way that SolidMotor does. @phmbressan can you confirm

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Similar confusion as mass flow rate here.


@funcify_method("Time (s)", "Propellant Mass (kg)")
def propellant_initial_mass(self):
return self._propellant_initial_mass

@property
def is_point_mass(self):
return True

@funcify_method("Time (s)", "Inertia (kg·m²)")
def propellant_I_11(self):
return 0

@funcify_method("Time (s)", "Inertia (kg·m²)")
def propellant_I_12(self):
return 0

@funcify_method("Time (s)", "Inertia (kg·m²)")
def propellant_I_13(self):
return 0

@funcify_method("Time (s)", "Inertia (kg·m²)")
def propellant_I_22(self):
return 0

@funcify_method("Time (s)", "Inertia (kg·m²)")
def propellant_I_23(self):
return 0

@funcify_method("Time (s)", "Inertia (kg·m²)")
def propellant_I_33(self):
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