Skip to content

Commit 8fd3b16

Browse files
committed
ENH: apply review changes
1 parent fb236c6 commit 8fd3b16

24 files changed

Lines changed: 2649 additions & 969 deletions

rocketpy/plots/flight_plots.py

Lines changed: 131 additions & 85 deletions
Large diffs are not rendered by default.

rocketpy/plots/rocket_plots.py

Lines changed: 78 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
import matplotlib.pyplot as plt
44
import numpy as np
55

6+
from rocketpy.mathutils.function import Function
67
from rocketpy.mathutils.vector_matrix import Vector
78
from rocketpy.motors import EmptyMotor, HybridMotor, LiquidMotor, SolidMotor
89
from rocketpy.rocket.aero_surface import Fin, Fins, NoseCone, Tail
910
from rocketpy.rocket.aero_surface.generic_surface import GenericSurface
1011

11-
from .plot_helpers import show_or_save_plot
12+
from .plot_helpers import show_or_save_fig, show_or_save_plot
1213

1314

1415
class _RocketPlots:
@@ -55,9 +56,28 @@ def reduced_mass(self):
5556

5657
self.rocket.reduced_mass()
5758

59+
def _caliber_to_length_percent(self):
60+
"""Return the (forward, inverse) pair converting a margin in calibers to
61+
a percentage of the rocket's overall aerodynamic length.
62+
63+
A margin in calibers is ``distance / (2 * radius)``; the same distance as
64+
a fraction of the body length is ``distance / length``. The two therefore
65+
differ only by the constant factor ``2 * radius / length`` (times 100 for
66+
a percentage), so the length-percentage scale is a plain rescaling of the
67+
caliber scale and can be drawn as a secondary axis. See
68+
:attr:`rocketpy.Rocket.length`.
69+
"""
70+
factor = 2 * self.rocket.radius / self.rocket.length * 100
71+
return (lambda calibers: calibers * factor, lambda percent: percent / factor)
72+
5873
def static_margin(self, *, filename=None):
5974
"""Plots static margin of the rocket as a function of time.
6075
76+
A secondary y-axis on the right expresses the same margin as a
77+
percentage of the rocket's overall length (see
78+
:attr:`rocketpy.Rocket.length`), the convention often used in hobby
79+
rocketry, alongside the primary caliber (diameter) axis.
80+
6181
Parameters
6282
----------
6383
filename : str | None, optional
@@ -70,30 +90,59 @@ def static_margin(self, *, filename=None):
7090
-------
7191
None
7292
"""
93+
self._plot_static_margin(self.rocket.static_margin, "Static Margin", filename)
94+
95+
def _plot_static_margin(self, margin, title, filename):
96+
"""Draw a static-margin-vs-time line plot with a caliber primary y-axis
97+
and a length-percentage secondary y-axis."""
98+
time = np.linspace(0, self.rocket.motor.burn_out_time, 200)
99+
values = margin.get_value(time)
100+
101+
fig, ax = plt.subplots()
102+
ax.plot(time, values)
103+
ax.set_xlabel("Time (s)")
104+
ax.set_ylabel("Static Margin (calibers)")
105+
ax.set_title(title)
106+
ax.grid(True)
73107

74-
self.rocket.static_margin(filename=filename)
108+
secondary_axis = ax.secondary_yaxis(
109+
"right", functions=self._caliber_to_length_percent()
110+
)
111+
secondary_axis.set_ylabel("Static Margin (% of length)")
75112

76-
def stability_margin(self):
77-
"""Plots static margin of the rocket as a function of time.
113+
show_or_save_fig(fig, filename)
114+
115+
def stability_margin(self, *, filename=None):
116+
"""Plots the stability margin of the rocket as a function of Mach number
117+
and time, at zero angle of attack (the design surface).
118+
119+
Parameters
120+
----------
121+
filename : str | None, optional
122+
The path the plot should be saved to. By default None, in which case
123+
the plot will be shown instead of saved.
78124
79125
Returns
80126
-------
81127
None
82128
"""
83-
84-
self.rocket.stability_margin.plot_2d(
129+
self._design_stability_margin(self.rocket.stability_margin).plot_2d(
85130
lower=0,
86131
upper=[2, self.rocket.motor.burn_out_time], # Mach 2 and burnout
87132
samples=[20, 20],
88133
disp_type="surface",
89134
alpha=1,
135+
filename=filename,
90136
)
91137

92138
def static_margin_yaw(self, *, filename=None):
93139
"""Plots the yaw-plane static margin of the rocket as a function of
94140
time. Only meaningful for non-axisymmetric rockets; for an axisymmetric
95141
rocket it is identical to :meth:`static_margin`.
96142
143+
A secondary y-axis expresses the margin as a percentage of the rocket's
144+
overall length (see :attr:`rocketpy.Rocket.length`).
145+
97146
Parameters
98147
----------
99148
filename : str | None, optional
@@ -106,23 +155,42 @@ def static_margin_yaw(self, *, filename=None):
106155
-------
107156
None
108157
"""
109-
self.rocket.static_margin_yaw(filename=filename)
158+
self._plot_static_margin(
159+
self.rocket.static_margin_yaw, "Static Margin (Yaw Plane)", filename
160+
)
110161

111-
def stability_margin_yaw(self):
162+
def stability_margin_yaw(self, *, filename=None):
112163
"""Plots the yaw-plane stability margin of the rocket as a function of
113164
Mach number and time. Only meaningful for non-axisymmetric rockets; for
114165
an axisymmetric rocket it is identical to :meth:`stability_margin`.
115166
167+
Parameters
168+
----------
169+
filename : str | None, optional
170+
The path the plot should be saved to. By default None, in which case
171+
the plot will be shown instead of saved.
172+
116173
Returns
117174
-------
118175
None
119176
"""
120-
self.rocket.stability_margin_yaw.plot_2d(
177+
self._design_stability_margin(self.rocket.stability_margin_yaw).plot_2d(
121178
lower=0,
122179
upper=[2, self.rocket.motor.burn_out_time], # Mach 2 and burnout
123180
samples=[20, 20],
124181
disp_type="surface",
125182
alpha=1,
183+
filename=filename,
184+
)
185+
186+
@staticmethod
187+
def _design_stability_margin(margin):
188+
"""The zero-incidence (Mach, time) slice of an angle-of-attack-aware
189+
stability margin, as a 2-D Function for surface plotting."""
190+
return Function(
191+
lambda mach, time: margin.get_value_opt(0.0, mach, time),
192+
inputs=["Mach", "Time (s)"],
193+
outputs=margin.__outputs__[0],
126194
)
127195

128196
# pylint: disable=too-many-statements
@@ -179,51 +247,6 @@ def drag_curves(self, *, filename=None):
179247
plt.grid(True)
180248
show_or_save_plot(filename)
181249

182-
def aerodynamic_coefficients(self, *, filename=None):
183-
"""Plots the rocket's total aerodynamic coefficients -- normal force
184-
``C_N`` and pitch moment ``C_m`` (about the center of dry mass) -- versus
185-
angle of attack, for a range of Mach numbers. The drag coefficient versus
186-
Mach is shown by :meth:`drag_curves`.
187-
188-
Parameters
189-
----------
190-
filename : str | None, optional
191-
The path the plot should be saved to. By default None, in which case
192-
the plot will be shown instead of saved. Supported file endings are:
193-
eps, jpg, jpeg, pdf, pgf, png, ps, raw, rgba, svg, svgz, tif, tiff
194-
and webp (these are the formats supported by matplotlib).
195-
196-
Returns
197-
-------
198-
None
199-
"""
200-
alphas_deg = np.linspace(0, 15, 40)
201-
alphas_rad = np.radians(alphas_deg)
202-
203-
_, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 4.5))
204-
for mach in (0.1, 0.5, 0.8, 1.2, 2.0):
205-
coeffs = [
206-
self.rocket.aerodynamic_coefficients(a, 0.0, mach) for a in alphas_rad
207-
]
208-
ax1.plot(
209-
alphas_deg, [c["normal_force"] for c in coeffs], label=f"Mach {mach}"
210-
)
211-
ax2.plot(
212-
alphas_deg, [c["pitch_moment"] for c in coeffs], label=f"Mach {mach}"
213-
)
214-
215-
ax1.set_title("Normal Force Coefficient")
216-
ax1.set_xlabel("Angle of Attack (deg)")
217-
ax1.set_ylabel(r"$C_N$")
218-
ax1.legend(loc="best", shadow=True)
219-
ax1.grid(True)
220-
ax2.set_title("Pitch Moment Coefficient (about CDM)")
221-
ax2.set_xlabel("Angle of Attack (deg)")
222-
ax2.set_ylabel(r"$C_m$")
223-
ax2.grid(True)
224-
plt.tight_layout()
225-
show_or_save_plot(filename)
226-
227250
def thrust_to_weight(self):
228251
"""
229252
Plots the motor thrust force divided by rocket weight as a function of time.
@@ -746,67 +769,15 @@ def _draw_center_of_mass_and_pressure(self, ax, plane="xz"):
746769
"""Draws the center of mass and center of pressure of the rocket.
747770
748771
The red dot is the (linear) aerodynamic center, conventionally labeled
749-
the center of pressure. A translucent red band through it shows the
750-
range over which the *nonlinear* center of pressure travels as the
751-
incidence angle grows (angle of attack in the xz plane, sideslip in the
752-
yz plane).
772+
the center of pressure.
753773
"""
754774
# Draw center of mass and center of pressure
755775
cm = self.rocket.center_of_mass(0)
756776
ax.scatter(cm, 0, color="#1565c0", label="Center of Mass", s=10)
757777

758778
cp = self.rocket.aerodynamic_center(0)
759-
760-
# Center of pressure travel band: sweep the nonlinear center of
761-
# pressure over the relevant incidence angle and shade its min-max span.
762-
cp_min, cp_max = self._center_of_pressure_range(plane)
763-
if cp_max > cp_min:
764-
ax.plot(
765-
[cp_min, cp_max],
766-
[0, 0],
767-
color="red",
768-
alpha=0.3,
769-
linewidth=4,
770-
solid_capstyle="butt",
771-
zorder=9,
772-
label="Center of Pressure Range",
773-
)
774-
775779
ax.scatter(cp, 0, label="Center of Pressure", color="red", s=10, zorder=10)
776780

777-
def _center_of_pressure_range(self, plane, max_angle=np.deg2rad(15), samples=31):
778-
"""Min and max nonlinear center-of-pressure position over an incidence
779-
sweep.
780-
781-
Sweeps the angle of attack (xz plane) or sideslip (yz plane) from 0 to
782-
``max_angle`` and reconstructs the nonlinear center of pressure --
783-
``x_cdm + csys * d * Cm / CN`` -- from the rocket aerodynamic
784-
coefficients (:meth:`Rocket.aerodynamic_coefficients_full`), returning
785-
the extent of its travel. The center of pressure is singular at zero
786-
incidence (``CN -> 0``); those samples are skipped.
787-
"""
788-
rocket = self.rocket
789-
csys = rocket._csys
790-
diameter = 2 * rocket.radius
791-
cdm = rocket.center_of_dry_mass_position
792-
angles = np.linspace(0, max_angle, samples)
793-
positions = []
794-
for angle in angles:
795-
if plane == "yz":
796-
coeffs = rocket.aerodynamic_coefficients_full(0.0, angle, 0.0)
797-
force, moment = coeffs["cY"], coeffs["cn"]
798-
else:
799-
coeffs = rocket.aerodynamic_coefficients_full(angle, 0.0, 0.0)
800-
force, moment = coeffs["cN"], coeffs["cm"]
801-
if force == 0:
802-
continue
803-
position = cdm + csys * diameter * moment / force
804-
if np.isfinite(position):
805-
positions.append(position)
806-
if len(positions) == 0:
807-
return (0.0, 0.0)
808-
return (float(min(positions)), float(max(positions)))
809-
810781
def _draw_sensors(self, ax, sensors, plane):
811782
"""Draw the sensor as a small thick line at the position of the sensor,
812783
with a vector pointing in the direction normal of the sensor. Get the
@@ -888,7 +859,6 @@ def all(self):
888859
print("Drag Plots")
889860
print("-" * 20) # Separator for Drag Plots
890861
self.drag_curves()
891-
self.aerodynamic_coefficients()
892862

893863
# Stability Plots
894864
print("\nStability Plots")

0 commit comments

Comments
 (0)