Skip to content
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
1 change: 1 addition & 0 deletions rocketpy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from .control import _Controller
from .environment import Environment, EnvironmentAnalysis
from .exceptions import InvalidInertiaError, InvalidParameterError, UnstableRocketWarning
from .mathutils import (
Function,
PiecewiseFunction,
Expand Down
19 changes: 19 additions & 0 deletions rocketpy/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""Custom exceptions and warnings for RocketPy."""


class RocketPyError(Exception):
"""Base class for all RocketPy exceptions."""


class InvalidParameterError(RocketPyError, ValueError):
"""Raised when a constructor parameter has an invalid value (e.g. negative
radius or mass)."""


class InvalidInertiaError(RocketPyError, ValueError):
"""Raised when the inertia tuple/list does not have the expected length."""


class UnstableRocketWarning(UserWarning):
"""Issued when the rocket's static margin is negative at motor ignition,
indicating an aerodynamically unstable configuration."""
28 changes: 28 additions & 0 deletions rocketpy/rocket/rocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
)
from rocketpy.rocket.aero_surface.fins.free_form_fins import FreeFormFins
from rocketpy.rocket.aero_surface.generic_surface import GenericSurface
from rocketpy.exceptions import InvalidInertiaError, InvalidParameterError, UnstableRocketWarning
from rocketpy.rocket.components import Components
from rocketpy.rocket.parachute import Parachute
from rocketpy.tools import (
Expand Down Expand Up @@ -308,6 +309,22 @@ def __init__( # pylint: disable=too-many-statements
+ '"tail_to_nose" and "nose_to_tail".'
)

# Validate inputs
if not isinstance(radius, (int, float)) or radius <= 0:
raise InvalidParameterError(
f"Rocket radius must be a positive number, got {radius!r}."
)
if not isinstance(mass, (int, float)) or mass <= 0:
raise InvalidParameterError(
f"Rocket mass must be a positive number, got {mass!r}."
)
if not isinstance(inertia, (tuple, list)) or len(inertia) not in (3, 6):
raise InvalidInertiaError(
"Inertia must be a tuple or list with 3 components (I_11, I_22, I_33) "
"or 6 components (I_11, I_22, I_33, I_12, I_13, I_23), "
f"got length {len(inertia) if isinstance(inertia, (tuple, list)) else 'N/A'}."
)

# Define rocket inertia attributes in SI units
self.mass = mass
inertia = (*inertia, 0, 0, 0) if len(inertia) == 3 else inertia
Expand Down Expand Up @@ -725,6 +742,17 @@ def evaluate_static_margin(self):
self.static_margin.set_discrete(
lower=0, upper=self.motor.burn_out_time, samples=200
)
# Warn the user if the rocket is aerodynamically unstable at ignition
initial_static_margin = self.static_margin.get_value_opt(0)
if initial_static_margin < 0:
warnings.warn(
f"The rocket has a negative static margin ({initial_static_margin:.2f} cal) "
"at motor ignition (t=0), indicating an aerodynamically unstable "
"configuration. Check the placement of fins and nose cone relative "
"to the center of mass.",
UnstableRocketWarning,
stacklevel=2,
)
return self.static_margin

def evaluate_dry_inertias(self):
Expand Down
43 changes: 43 additions & 0 deletions tests/unit/rocket/test_rocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import pytest

from rocketpy import Function, NoseCone, Rocket, SolidMotor
from rocketpy.exceptions import InvalidInertiaError, InvalidParameterError
from rocketpy.mathutils.vector_matrix import Vector
from rocketpy.motors.empty_motor import EmptyMotor
from rocketpy.motors.motor import Motor
Expand Down Expand Up @@ -835,3 +836,45 @@ def test_drag_input_types_supported_for_power_on_and_power_off(tmp_path):

assert rocket.power_off_drag_7d(*query_point) == pytest.approx(expected)
assert rocket.power_on_drag_7d(*query_point) == pytest.approx(expected)


@pytest.mark.parametrize("radius", [-1, 0, -0.001])
def test_rocket_invalid_radius_raises(radius):
"""InvalidParameterError must be raised for non-positive radius values."""
with pytest.raises(InvalidParameterError, match="radius"):
Rocket(
radius=radius,
mass=10,
inertia=(0.1, 0.1, 0.01),
power_off_drag=0.3,
power_on_drag=0.3,
center_of_mass_without_motor=0,
)


@pytest.mark.parametrize("mass", [-1, 0, -0.001])
def test_rocket_invalid_mass_raises(mass):
"""InvalidParameterError must be raised for non-positive mass values."""
with pytest.raises(InvalidParameterError, match="mass"):
Rocket(
radius=0.05,
mass=mass,
inertia=(0.1, 0.1, 0.01),
power_off_drag=0.3,
power_on_drag=0.3,
center_of_mass_without_motor=0,
)


@pytest.mark.parametrize("inertia", [(0.1,), (0.1, 0.1), (0.1, 0.1, 0.01, 0.0, 0.0)])
def test_rocket_invalid_inertia_length_raises(inertia):
"""InvalidInertiaError must be raised when inertia tuple has wrong length."""
with pytest.raises(InvalidInertiaError):
Rocket(
radius=0.05,
mass=10,
inertia=inertia,
power_off_drag=0.3,
power_on_drag=0.3,
center_of_mass_without_motor=0,
)
Loading