33import matplotlib .pyplot as plt
44import numpy as np
55
6+ from rocketpy .mathutils .function import Function
67from rocketpy .mathutils .vector_matrix import Vector
78from rocketpy .motors import EmptyMotor , HybridMotor , LiquidMotor , SolidMotor
89from rocketpy .rocket .aero_surface import Fin , Fins , NoseCone , Tail
910from 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
1415class _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 ("\n Stability Plots" )
0 commit comments