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
2 changes: 1 addition & 1 deletion rocketpy/plots/flight_plots.py
Original file line number Diff line number Diff line change
Expand Up @@ -694,7 +694,7 @@ def _rocket_axial_display_coordinate(self, value, display_length):
"""Map a rocket axial coordinate onto the centered display model."""
coordinates = [
float(position.z)
for _surface, position in self.flight.rocket.aerodynamic_surfaces
for _surface, position, _ref_factor in self.flight.rocket.aerodynamic_surfaces
]
coordinates.extend(
[
Expand Down
4 changes: 2 additions & 2 deletions rocketpy/plots/rocket_plots.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ def _draw_aerodynamic_surfaces(self, ax, vis_args, plane, surfaces):
# diameter changes. The final point of the last surface is the final
# point of the last tube

for surface, position in surfaces:
for surface, position, _ref_factor in surfaces:
if isinstance(surface, NoseCone):
self._draw_nose_cone(ax, surface, position.z, drawn_surfaces, vis_args)
elif isinstance(surface, Tail):
Expand Down Expand Up @@ -645,7 +645,7 @@ def _draw_nozzle_tube(self, last_radius, last_x, nozzle_position, ax, vis_args):
def _draw_rail_buttons(self, ax, vis_args):
"""Draws the rail buttons of the rocket."""
try:
buttons, pos = self.rocket.rail_buttons[0]
buttons, pos, _ref_factor = self.rocket.rail_buttons[0]
lower = pos.z
upper = lower + buttons.buttons_distance * self.rocket._csys
ax.scatter(
Expand Down
5 changes: 2 additions & 3 deletions rocketpy/prints/rocket_prints.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,19 +102,18 @@ def rocket_aerodynamics_quantities(self):
None
"""
print("\nAerodynamics Lift Coefficient Derivatives\n")
for surface, _ in self.rocket.aerodynamic_surfaces:
for surface, _position, ref_factor in self.rocket.aerodynamic_surfaces:
if isinstance(surface, GenericSurface):
continue
name = surface.name
# ref_factor corrects lift for different reference areas
ref_factor = (surface.rocket_radius / self.rocket.radius) ** 2
print(
f"{name} Lift Coefficient Derivative: "
f"{ref_factor * surface.clalpha(0):.3f}/rad"
)

print("\nCenter of Pressure\n")
for surface, position in self.rocket.aerodynamic_surfaces:
for surface, position, _ref_factor in self.rocket.aerodynamic_surfaces:
name = surface.name
cpz = surface.cp[2] # relative to the user defined coordinate system
print(
Expand Down
30 changes: 23 additions & 7 deletions rocketpy/rocket/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ class Components:
A list of named tuples representing all the components and their
positions relative to the rocket.
component_tuple : namedtuple
A named tuple representing a component and its position within the
rocket.
A named tuple representing a component, its position within the
rocket, and an optional reference-area correction factor.
"""

def __init__(self):
"""Initialize an empty components list instance."""
self.component_tuple = namedtuple("component_tuple", "component position")
self.component_tuple = namedtuple(
"component_tuple", "component position ref_factor", defaults=(1.0,)
)
self._components = []

# List of components and their positions to avoid extra for loops in
Expand All @@ -34,6 +36,7 @@ def __repr__(self):
components_str = "\n".join(
[
f"\tComponent: {str(c.component):80} Position: {c.position}"
f" Ref Factor: {c.ref_factor}"
for c in self._components
]
)
Expand All @@ -52,7 +55,7 @@ def __iter__(self):
"""Return an iterator over the list of components."""
return iter(self._components)

def add(self, component, position):
def add(self, component, position, ref_factor=1.0):
"""Add a component to the list of components.

Parameters
Expand All @@ -62,14 +65,19 @@ def add(self, component, position):
position : int, float
The position of the component relative to the rocket's
coordinate system origin.
ref_factor : int, float, optional
Reference-area correction factor associating the component to the
rocket reference area. Defaults to 1.0 when not applicable.

Returns
-------
None
"""
self.__component_list.append(component)
self.__position_list.append(position)
self._components.append(self.component_tuple(component, position))
self._components.append(
self.component_tuple(component, position, ref_factor)
)

def get_by_type(self, component_type):
"""Search the list of components and return a list with all the
Expand Down Expand Up @@ -207,7 +215,11 @@ def sort_by_position(self, reverse=False):
def to_dict(self, **kwargs): # pylint: disable=unused-argument
return {
"components": [
{"component": c.component, "position": c.position}
{
"component": c.component,
"position": c.position,
"ref_factor": c.ref_factor,
}
for c in self._components
]
}
Expand All @@ -216,5 +228,9 @@ def to_dict(self, **kwargs): # pylint: disable=unused-argument
def from_dict(cls, data):
components = cls()
for component in data["components"]:
components.add(component["component"], component["position"])
components.add(
component["component"],
component["position"],
ref_factor=component.get("ref_factor", 1.0),
)
return components
20 changes: 12 additions & 8 deletions rocketpy/rocket/rocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -660,11 +660,10 @@ def evaluate_center_of_pressure(self):

# Calculate total lift coefficient derivative and center of pressure
if len(self.aerodynamic_surfaces) > 0:
for aero_surface, position in self.aerodynamic_surfaces:
for aero_surface, position, ref_factor in self.aerodynamic_surfaces:
if isinstance(aero_surface, GenericSurface):
continue
# ref_factor corrects lift for different reference areas
ref_factor = (aero_surface.rocket_radius / self.radius) ** 2
self.total_lift_coeff_der += ref_factor * aero_surface.clalpha
self.cp_position += (
ref_factor
Expand All @@ -687,7 +686,7 @@ def evaluate_surfaces_cp_to_cdm(self):
Dictionary mapping the relative position of each aerodynamic
surface center of pressure to the rocket's center of mass.
"""
for surface, position in self.aerodynamic_surfaces:
for surface, position, _ref_factor in self.aerodynamic_surfaces:
self.__evaluate_single_surface_cp_to_cdm(surface, position)
return self.surfaces_cp_to_cdm

Expand Down Expand Up @@ -788,7 +787,7 @@ def warn_if_unstable(self):
"""
has_generic_surface = any(
isinstance(aero_surface, GenericSurface)
for aero_surface, _position in self.aerodynamic_surfaces
for aero_surface, _position, _ref_factor in self.aerodynamic_surfaces
)
if has_generic_surface:
return False
Expand Down Expand Up @@ -1163,7 +1162,12 @@ def __add_single_surface(self, surface, position):
self.rail_buttons = Components()
self.rail_buttons.add(surface, position)
else:
self.aerodynamic_surfaces.add(surface, position)
# ref_factor corrects lift for different reference areas
if getattr(surface, "rocket_radius", None) is not None:
ref_factor = (surface.rocket_radius / self.radius) ** 2
else:
ref_factor = 1.0
self.aerodynamic_surfaces.add(surface, position, ref_factor=ref_factor)
self.__evaluate_single_surface_cp_to_cdm(surface, position)

def add_surfaces(self, surfaces, positions):
Expand Down Expand Up @@ -2270,10 +2274,10 @@ def from_dict(cls, data):
position=data["motor_position"],
)

for surface, position in data["aerodynamic_surfaces"]:
for surface, position, _ref_factor in data["aerodynamic_surfaces"]:
rocket.add_surfaces(surfaces=surface, positions=position)

for button, position in data["rail_buttons"]:
for button, position, _ref_factor in data["rail_buttons"]:
rocket.set_rail_buttons(
upper_button_position=position[2] + button.buttons_distance,
lower_button_position=position[2],
Expand All @@ -2284,7 +2288,7 @@ def from_dict(cls, data):
for parachute in data["parachutes"]:
rocket.parachutes.append(parachute)

for sensor, position in data["sensors"]:
for sensor, position, _ref_factor in data["sensors"]:
rocket.add_sensor(sensor, position)

for air_brake in data["air_brakes"]:
Expand Down
10 changes: 5 additions & 5 deletions rocketpy/simulation/flight.py
Original file line number Diff line number Diff line change
Expand Up @@ -898,7 +898,7 @@ def __measure_sensors(self, component_sensors, u_dot, t=None, y_sol=None):
Parameters
----------
component_sensors : list
List of (sensor, position) tuples.
List of (sensor, position, ref_factor) component tuples.
u_dot : array_like
State derivative vector.
t : float, optional
Expand All @@ -911,7 +911,7 @@ def __measure_sensors(self, component_sensors, u_dot, t=None, y_sol=None):
if y_sol is None:
y_sol = self.y_sol

for sensor, position in component_sensors:
for sensor, position, _ref_factor in component_sensors:
relative_position = position - self.rocket._csys * Vector(
[0, 0, self.rocket.center_of_dry_mass_position]
)
Expand Down Expand Up @@ -2087,7 +2087,7 @@ def u_dot(self, t, u, post_processing=False): # pylint: disable=too-many-locals
# Calculate lift and moment for each component of the rocket
velocity_in_body_frame = Vector([vx_b, vy_b, vz_b])
w = Vector([omega1, omega2, omega3])
for aero_surface, _ in self.rocket.aerodynamic_surfaces:
for aero_surface, _, _ref_factor in self.rocket.aerodynamic_surfaces:
# Component cp relative to CDM in body frame
comp_cp = self.rocket.surfaces_cp_to_cdm[aero_surface]
# Component absolute velocity in body frame
Expand Down Expand Up @@ -2343,7 +2343,7 @@ def u_dot_generalized_3dof(self, t, u, post_processing=False):
# Velocity in body frame
vb_body = Kt @ v

for surface, _ in self.rocket.aerodynamic_surfaces:
for surface, _, _ref_factor in self.rocket.aerodynamic_surfaces:
cp = self.rocket.surfaces_cp_to_cdm[surface]
vb_component = vb_body + (w ^ cp)

Expand Down Expand Up @@ -2609,7 +2609,7 @@ def u_dot_generalized(self, t, u, post_processing=False): # pylint: disable=too
# Get rocket velocity in body frame
velocity_in_body_frame = Kt @ v
# Calculate lift and moment for each component of the rocket
for aero_surface, _ in self.rocket.aerodynamic_surfaces:
for aero_surface, _, _ref_factor in self.rocket.aerodynamic_surfaces:
# Component cp relative to CDM in body frame
comp_cp = self.rocket.surfaces_cp_to_cdm[aero_surface]
# Component absolute velocity in body frame
Expand Down
2 changes: 1 addition & 1 deletion rocketpy/stochastic/stochastic_rocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ def __reset_components(self, components, seed):
input component.
"""
new_components = Components()
for stochastic_obj, _ in components:
for stochastic_obj, _position, _ref_factor in components:
stochastic_obj_position_info = self.__components_map[stochastic_obj]
stochastic_obj._set_stochastic(seed)
new_components.add(
Expand Down
94 changes: 94 additions & 0 deletions tests/unit/rocket/test_components.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""Unit tests for rocketpy.rocket.components.Components."""

import pytest

from rocketpy.mathutils.vector_matrix import Vector
from rocketpy.rocket.components import Components


def test_components_add_default_ref_factor():
"""Components.add stores ref_factor=1.0 when omitted."""
components = Components()
component = object()
position = Vector([0, 0, 1.0])

components.add(component, position)

assert len(components) == 1
assert components[0].component is component
assert components[0].position == position
assert components[0].ref_factor == pytest.approx(1.0)


def test_components_add_custom_ref_factor():
"""Components.add stores an explicit ref_factor on the component tuple."""
components = Components()
component = object()
position = Vector([0, 0, -0.5])
ref_factor = 0.25

components.add(component, position, ref_factor=ref_factor)

stored = components[0]
assert stored.component is component
assert stored.position == position
assert stored.ref_factor == pytest.approx(ref_factor)


def test_components_to_dict_from_dict_preserves_ref_factor():
"""Serialization round-trip keeps ref_factor, defaulting when absent."""
components = Components()
component = {"name": "dummy"}
position = Vector([0, 0, 0.1])
components.add(component, position, ref_factor=4.0)

restored = Components.from_dict(components.to_dict())
assert restored[0].ref_factor == pytest.approx(4.0)

legacy = Components.from_dict(
{"components": [{"component": component, "position": position}]}
)
assert legacy[0].ref_factor == pytest.approx(1.0)


def test_rocket_add_surfaces_stores_computed_ref_factor(calisto):
"""Rocket aero-surface add path stores (surface.rocket_radius / rocket.radius)**2."""
from rocketpy import NoseCone

surface_radius = calisto.radius / 2
expected_ref_factor = (surface_radius / calisto.radius) ** 2
nose = NoseCone(
length=0.55829,
kind="vonkarman",
base_radius=surface_radius,
rocket_radius=surface_radius,
name="Half-radius nose",
)

calisto.add_surfaces(nose, 1.16)
stored = next(
entry for entry in calisto.aerodynamic_surfaces if entry.component is nose
)

assert stored.ref_factor == pytest.approx(expected_ref_factor)
assert stored.ref_factor == pytest.approx(0.25)


def test_rocket_add_surfaces_matching_radius_stores_unit_ref_factor(calisto):
"""Matching surface and rocket radii store ref_factor of 1.0."""
from rocketpy import NoseCone

nose = NoseCone(
length=0.55829,
kind="vonkarman",
base_radius=calisto.radius,
rocket_radius=calisto.radius,
name="Matching nose",
)

calisto.add_surfaces(nose, 1.16)
stored = next(
entry for entry in calisto.aerodynamic_surfaces if entry.component is nose
)

assert stored.ref_factor == pytest.approx(1.0)
4 changes: 3 additions & 1 deletion tests/unit/rocket/test_rocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -914,7 +914,9 @@ def test_add_trapezoidal_fins_two_fins_warns_but_succeeds(calisto):
fins = calisto.add_trapezoidal_fins(
2, span=0.1, root_chord=0.12, tip_chord=0.04, position=-1.0
)
assert fins in [surface for surface, _ in calisto.aerodynamic_surfaces]
assert fins in [
surface for surface, _position, _ref_factor in calisto.aerodynamic_surfaces
]


def test_unstable_rocket_warning_raised(calisto):
Expand Down