Skip to content
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

ENH: implement power series nose cones (#475) #603

Merged
merged 10 commits into from
Jun 25, 2024
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ You can install this version by running `pip install rocketpy==1.3.0`

- ENH: CP and Thrust Eccentricity Effects Generate Roll Moment [#617](https://github.com/RocketPy-Team/RocketPy/pull/617)
- ENH: Add Prandtl-Gauss transformation to NoseCone and Tail [#609](https://github.com/RocketPy-Team/RocketPy/pull/609)
- ENH: Implement power series nose cones[#603](https://github.com/RocketPy-Team/RocketPy/pull/603)
- DOC: Adds prometheus data, Spaceport America 2022 [#601](https://github.com/RocketPy-Team/RocketPy/pull/601)
- ENH: Pre-calculate attributes in Rocket class [#595](https://github.com/RocketPy-Team/RocketPy/pull/595)
- ENH: Complex step differentiation [#594](https://github.com/RocketPy-Team/RocketPy/pull/594)
Expand Down
80 changes: 75 additions & 5 deletions rocketpy/rocket/aero_surface.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,14 +132,15 @@ class NoseCone(AeroSurface):
Nose cone length. Has units of length and must be given in meters.
NoseCone.kind : string
Nose cone kind. Can be "conical", "ogive", "elliptical", "tangent",
"von karman", "parabolic" or "lvhaack".
"von karman", "parabolic", "powerseries" or "lvhaack".
NoseCone.bluffness : float
Ratio between the radius of the circle on the tip of the ogive and the
radius of the base of the ogive. Currently only used for the nose cone's
drawing. Must be between 0 and 1. Default is None, which means that the
nose cone will not have a sphere on the tip. If a value is given, the
nose cone's length will be slightly reduced because of the addition of
the sphere.
the sphere. Must be None or 0 if a "powerseries" nose cone kind is
specified.
NoseCone.rocket_radius : float
The reference rocket radius used for lift coefficient normalization,
in meters.
Expand All @@ -151,6 +152,10 @@ class NoseCone(AeroSurface):
rocket radius is assumed as 1, meaning that the nose cone has the same
radius as the rocket. If base radius is given, the ratio between base
radius and rocket radius is calculated and used for lift calculation.
NoseCone.power : float
Factor that controls the bluntness of the shape for a power series
nose cone. Must be between 0 and 1. It is ignored when other nose
cone types are used.
NoseCone.name : string
Nose cone name. Has no impact in simulation, as it is only used to
display data in a more organized matter.
Expand Down Expand Up @@ -187,6 +192,7 @@ def __init__(
base_radius=None,
bluffness=None,
rocket_radius=None,
power=None,
name="Nose Cone",
):
"""Initializes the nose cone. It is used to define the nose cone
Expand All @@ -198,7 +204,9 @@ def __init__(
Nose cone length. Has units of length and must be given in meters.
kind : string
Nose cone kind. Can be "conical", "ogive", "elliptical", "tangent",
"von karman", "parabolic" or "lvhaack".
"von karman", "parabolic", "powerseries" or "lvhaack". If
"powerseries" is used, the "power" argument must be assigned to a
value between 0 and 1.
base_radius : float, optional
Nose cone base radius. Has units of length and must be given in
meters. If not given, the ratio between ``base_radius`` and
Expand All @@ -209,11 +217,16 @@ def __init__(
nose cone's drawing. Must be between 0 and 1. Default is None, which
means that the nose cone will not have a sphere on the tip. If a
value is given, the nose cone's length will be reduced to account
for the addition of the sphere at the tip.
for the addition of the sphere at the tip. Must be None or 0 if a
"powerseries" nose cone kind is specified.
rocket_radius : int, float, optional
The reference rocket radius used for lift coefficient normalization.
If not given, the ratio between ``base_radius`` and
``rocket_radius`` will be assumed as 1.
power : float, optional
Factor that controls the bluntness of the shape for a power series
nose cone. Must be between 0 and 1. It is ignored when other nose
cone types are used.
name : str, optional
Nose cone name. Has no impact in simulation, as it is only used to
display data in a more organized matter.
Expand All @@ -233,6 +246,23 @@ def __init__(
f"Bluffness ratio of {bluffness} is out of range. It must be between 0 and 1."
)
self._bluffness = bluffness
if kind == "powerseries":
# Checks if bluffness is not being used
if (self.bluffness is not None) and (self.bluffness != 0):
raise ValueError(
"Parameter 'bluffness' must be None or 0 when using a nose cone kind 'powerseries'."
)

if power is None:
raise ValueError(
"Parameter 'power' cannot be None when using a nose cone kind 'powerseries'."
)

if power > 1 or power <= 0:
raise ValueError(
f"Power value of {power} is out of range. It must be between 0 and 1."
)
self._power = power
self.kind = kind

self.evaluate_lift_coefficient()
Expand Down Expand Up @@ -275,6 +305,22 @@ def length(self, value):
self.evaluate_center_of_pressure()
self.evaluate_nose_shape()

@property
def power(self):
return self._power

@power.setter
def power(self, value):
if value is not None:
if value > 1 or value <= 0:
raise ValueError(
f"Power value of {value} is out of range. It must be between 0 and 1."
)
self._power = value
self.evaluate_k()
self.evaluate_center_of_pressure()
self.evaluate_nose_shape()

@property
def kind(self):
return self._kind
Expand Down Expand Up @@ -336,7 +382,11 @@ def kind(self, value):
lambda x: self.base_radius
* ((2 * x / self.length - (x / self.length) ** 2) / (2 - 1))
)

elif value == "powerseries":
self.k = (2 * self.power) / ((2 * self.power) + 1)
self.y_nosecone = Function(
lambda x: self.base_radius * np.power(x / self.length, self.power)
)
else:
raise ValueError(
f"Nose Cone kind '{self.kind}' not found, "
Expand All @@ -347,6 +397,7 @@ def kind(self, value):
+ '\n\t"tangent"'
+ '\n\t"vonkarman"'
+ '\n\t"elliptical"'
+ '\n\t"powerseries"'
+ '\n\t"parabolic"\n'
)

Expand All @@ -360,6 +411,13 @@ def bluffness(self):

@bluffness.setter
def bluffness(self, value):
# prevents from setting bluffness on "powerseries" nose cones
if self.kind == "powerseries":
# Checks if bluffness is not being used
if (value is not None) and (value != 0):
raise ValueError(
"Parameter 'bluffness' must be None or 0 when using a nose cone kind 'powerseries'."
)
if value is not None:
if value > 1 or value < 0:
raise ValueError(
Expand Down Expand Up @@ -507,6 +565,18 @@ def evaluate_lift_coefficient(self):
)
return None

def evaluate_k(self):
"""Updates the self.k attribute used to compute the center of
pressure when using "powerseries" nose cones.

Returns
-------
None
"""
if self.kind == "powerseries":
self.k = (2 * self.power) / ((2 * self.power) + 1)
return None

def evaluate_center_of_pressure(self):
"""Calculates and returns the center of pressure of the nose cone in
local coordinates. The center of pressure position is saved and stored
Expand Down
17 changes: 14 additions & 3 deletions rocketpy/rocket/rocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -1024,7 +1024,14 @@ def add_tail(
return tail

def add_nose(
self, length, kind, position, bluffness=0, name="Nose Cone", base_radius=None
self,
length,
kind,
position,
bluffness=0,
power=None,
name="Nose Cone",
base_radius=None,
):
"""Creates a nose cone, storing its parameters as part of the
aerodynamic_surfaces list. Its parameters are the axial position
Expand All @@ -1038,14 +1045,17 @@ def add_nose(
Nose cone length or height in meters. Must be a positive
value.
kind : string
Nose cone type. Von Karman, conical, ogive, and lvhaack are
supported.
Nose cone type. Von Karman, conical, ogive, lvhaack and
powerseries are supported.
position : int, float
Nose cone tip coordinate relative to the rocket's coordinate system.
See `Rocket.coordinate_system_orientation` for more information.
bluffness : float, optional
Ratio between the radius of the circle on the tip of the ogive and
the radius of the base of the ogive.
power : float, optional
Factor that controls the bluntness of the nose cone shape when
using a 'powerseries' nose cone kind.
name : string
Nose cone name. Default is "Nose Cone".
base_radius : int, float, optional
Expand All @@ -1067,6 +1077,7 @@ def add_nose(
base_radius=base_radius or self.radius,
rocket_radius=base_radius or self.radius,
bluffness=bluffness,
power=power,
name=name,
)
self.add_surfaces(nose, position)
Expand Down
72 changes: 72 additions & 0 deletions tests/unit/test_aero_surfaces.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import pytest
from rocketpy import NoseCone

NOSECONE_LENGTH = 1
NOSECONE_BASE_RADIUS = 1
NOSECONE_ROCKET_RADIUS = 1
NOSECONE_KIND = "powerseries"


@pytest.mark.parametrize(
"power, bluffness",
[
(0, None),
(None, None),
(0, 0.5),
(None, 0.5),
(0.5, 0.1),
(-1, None),
(10, None),
],
)
def test_powerseries_nosecones_invalid_inputs(power, bluffness):
"""Checks if Exceptions are raised correctly when invalid inputs
are passed for the creation of power series nose cones
"""

# Tests for invalid power parameter
with pytest.raises(ValueError):
NoseCone(
length=NOSECONE_LENGTH,
base_radius=NOSECONE_BASE_RADIUS,
rocket_radius=NOSECONE_ROCKET_RADIUS,
kind=NOSECONE_KIND,
power=power,
bluffness=bluffness,
)


@pytest.mark.parametrize(
"power, invalid_power, new_power",
[
(0.1, -1, 0.05),
(0.5, 0, 0.7),
(0.9, 10, 1),
],
)
def test_powerseries_nosecones_setters(power, invalid_power, new_power):
"""Checks if Exceptions are raised correctly when the 'power' or
'bluffness' attributes are changed to invalid values. Also checks
that modifying the 'power' attribute also modifies the 'k' attribute.
"""
test_nosecone = NoseCone(
length=NOSECONE_LENGTH,
base_radius=NOSECONE_BASE_RADIUS,
rocket_radius=NOSECONE_ROCKET_RADIUS,
kind=NOSECONE_KIND,
power=power,
bluffness=None,
)
# Test invalid power value modification
with pytest.raises(ValueError):
test_nosecone.power = invalid_power

# Test invalid bluffness value modification
with pytest.raises(ValueError):
test_nosecone.bluffness = 0.5

# Checks if self.k was updated correctly
test_nosecone.power = new_power
expected_k = (2 * new_power) / ((2 * new_power) + 1)

assert pytest.approx(test_nosecone.k) == expected_k
Loading