Source code for rocketpy.plots.motor_plots

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.animation import FuncAnimation
from matplotlib.patches import Polygon

from ..plots.plot_helpers import show_or_save_animation, show_or_save_plot


[docs] class _MotorPlots: """Class that holds plot methods for Motor class. Attributes ---------- _MotorPlots.motor : Motor Motor object that will be used for the plots. """
[docs] def __init__(self, motor): """Initializes _MotorClass class. Parameters ---------- motor : Motor Instance of the Motor class Returns ------- None """ self.motor = motor
[docs] def thrust(self, lower_limit=None, upper_limit=None, *, filename=None): """Plots thrust of the motor as a function of time. Parameters ---------- lower_limit : float Lower limit of the plot. Default is none, which means that the plot limits will be automatically calculated. upper_limit : float Upper limit of the plot. Default is none, which means that the plot limits will be automatically calculated. filename : str | None, optional The path the plot should be saved to. By default None, in which case the plot will be shown instead of saved. Supported file endings are: eps, jpg, jpeg, pdf, pgf, png, ps, raw, rgba, svg, svgz, tif, tiff and webp (these are the formats supported by matplotlib). Returns ------- None """ self.motor.thrust.plot(lower=lower_limit, upper=upper_limit, filename=filename)
[docs] def total_mass(self, lower_limit=None, upper_limit=None, *, filename=None): """Plots total_mass of the motor as a function of time. Parameters ---------- lower_limit : float Lower limit of the plot. Default is none, which means that the plot limits will be automatically calculated. upper_limit : float Upper limit of the plot. Default is none, which means that the plot limits will be automatically calculated. filename : str | None, optional The path the plot should be saved to. By default None, in which case the plot will be shown instead of saved. Supported file endings are: eps, jpg, jpeg, pdf, pgf, png, ps, raw, rgba, svg, svgz, tif, tiff and webp (these are the formats supported by matplotlib). Returns ------- None """ self.motor.total_mass.plot( lower=lower_limit, upper=upper_limit, filename=filename )
[docs] def propellant_mass(self, lower_limit=None, upper_limit=None, *, filename=None): """Plots propellant_mass of the motor as a function of time. Parameters ---------- lower_limit : float Lower limit of the plot. Default is None, which means that the plot limits will be automatically calculated. upper_limit : float Upper limit of the plot. Default is None, which means that the plot limits will be automatically calculated. filename : str | None, optional The path the plot should be saved to. By default None, in which case the plot will be shown instead of saved. Supported file endings are: eps, jpg, jpeg, pdf, pgf, png, ps, raw, rgba, svg, svgz, tif, tiff and webp (these are the formats supported by matplotlib). Returns ------- None """ self.motor.propellant_mass.plot( lower=lower_limit, upper=upper_limit, filename=filename )
[docs] def center_of_mass(self, lower_limit=None, upper_limit=None, *, filename=None): """Plots center_of_mass of the motor as a function of time. Parameters ---------- lower_limit : float Lower limit of the plot. Default is none, which means that the plot limits will be automatically calculated. upper_limit : float Upper limit of the plot. Default is none, which means that the plot limits will be automatically calculated. filename : str | None, optional The path the plot should be saved to. By default None, in which case the plot will be shown instead of saved. Supported file endings are: eps, jpg, jpeg, pdf, pgf, png, ps, raw, rgba, svg, svgz, tif, tiff and webp (these are the formats supported by matplotlib). Returns ------- None """ self.motor.center_of_mass.plot( lower=lower_limit, upper=upper_limit, filename=filename )
[docs] def mass_flow_rate(self, lower_limit=None, upper_limit=None, *, filename=None): """Plots mass_flow_rate of the motor as a function of time. Parameters ---------- lower_limit : float Lower limit of the plot. Default is none, which means that the plot limits will be automatically calculated. upper_limit : float Upper limit of the plot. Default is none, which means that the plot limits will be automatically calculated. filename : str | None, optional The path the plot should be saved to. By default None, in which case the plot will be shown instead of saved. Supported file endings are: eps, jpg, jpeg, pdf, pgf, png, ps, raw, rgba, svg, svgz, tif, tiff and webp (these are the formats supported by matplotlib). Returns ------- None """ self.motor.mass_flow_rate.plot( lower=lower_limit, upper=upper_limit, filename=filename )
[docs] def exhaust_velocity(self, lower_limit=None, upper_limit=None, *, filename=None): """Plots exhaust_velocity of the motor as a function of time. Parameters ---------- lower_limit : float Lower limit of the plot. Default is none, which means that the plot limits will be automatically calculated. upper_limit : float Upper limit of the plot. Default is none, which means that the plot limits will be automatically calculated. filename : str | None, optional The path the plot should be saved to. By default None, in which case the plot will be shown instead of saved. Supported file endings are: eps, jpg, jpeg, pdf, pgf, png, ps, raw, rgba, svg, svgz, tif, tiff and webp (these are the formats supported by matplotlib). Returns ------- None """ self.motor.exhaust_velocity.plot( lower=lower_limit, upper=upper_limit, filename=filename )
[docs] def inertia_tensor( self, lower_limit=None, upper_limit=None, show_products=False, *, filename=None ): """Plots all inertia tensors (I_11, I_22, I_33, I_12, I_13, I_23) of the motor as a function of time in a single chart. Parameters ---------- lower_limit : float Lower limit of the plot. Default is None, which means that the plot limits will be automatically calculated. upper_limit : float Upper limit of the plot. Default is None, which means that the plot limits will be automatically calculated. show_products : bool If True, the products of inertia (I_12, I_13, I_23) will be shown in the plot. Default is False. These are kept as hidden by default because they are usually very small compared to the main inertia components. filename : str | None, optional The path the plot should be saved to. By default None, in which case the plot will be shown instead of saved. Supported file endings are: eps, jpg, jpeg, pdf, pgf, png, ps, raw, rgba, svg, svgz, tif, tiff and webp (these are the formats supported by matplotlib). Returns ------- None """ lower = lower_limit or self.motor.burn_start_time upper = upper_limit or self.motor.burn_out_time time = np.linspace(lower, upper, 100) _, ax = plt.subplots() ax.plot(time, self.motor.I_11(time), label="I_11", linestyle="-") ax.plot(time, self.motor.I_22(time), label="I_22", linestyle="--", linewidth=3) ax.plot(time, self.motor.I_33(time), label="I_33", linestyle="-.") if show_products: ax.plot(time, self.motor.I_12(time), label="I_12", linestyle=":") ax.plot( time, self.motor.I_13(time), label="I_13", linestyle="-.", linewidth=2 ) ax.plot( time, self.motor.I_23(time), label="I_23", linestyle="--", linewidth=3 ) ax.set_title("Inertia tensor over time") ax.set_xlabel("Time (s)") ax.set_ylabel("Inertia (kg*m^2)") ax.legend() ax.grid(True) show_or_save_plot(filename)
[docs] def _generate_nozzle(self, translate=(0, 0), csys=1): """Generates a patch that represents the nozzle of the motor. It is simply a polygon with 5 vertices mirrored in the x axis. The nozzle is drawn in the origin and then translated and rotated to the correct position. Parameters ---------- translate : tuple Tuple with the x and y coordinates of the translation that will be applied to the nozzle. csys : float Coordinate system of the motor or rocket. This will define the orientation of the nozzle draw. Default is 1, which means that the nozzle will be drawn with its outlet pointing to the right. Returns ------- patch : matplotlib.patches.Polygon Patch that represents the nozzle of the liquid_motor. """ nozzle_radius = self.motor.nozzle_radius try: throat_radius = self.motor.throat_radius except AttributeError: # Liquid motors don't have throat radius, set a default value throat_radius = nozzle_radius / 3 # calculate length between throat and nozzle outlet assuming 15º angle major_axis = (nozzle_radius - throat_radius) / np.tan(np.deg2rad(15)) # calculate minor axis assuming a 45º angle minor_axis = (nozzle_radius - throat_radius) / np.tan(np.deg2rad(45)) # calculate x and y coordinates of the nozzle x = csys * np.array( [0, 0, major_axis, major_axis + minor_axis, major_axis + minor_axis] ) y = csys * np.array([0, nozzle_radius, throat_radius, nozzle_radius, 0]) # we need to draw the other half of the nozzle x = np.concatenate([x, x[::-1]]) y = np.concatenate([y, -y[::-1]]) # now we need to sum the position and the translate x = x + translate[0] y = y + translate[1] patch = Polygon( np.column_stack([x, y]), label="Nozzle", facecolor="black", edgecolor="black", ) return patch
[docs] def _generate_combustion_chamber( self, translate=(0, 0), label="Combustion Chamber" ): """Generates a patch that represents the combustion chamber of the motor. It is simply a polygon with 4 vertices mirrored in the x axis. The combustion chamber is drawn in the origin and must be translated. Parameters ---------- translate : tuple Tuple with the x and y coordinates of the translation that will be applied to the combustion chamber. label : str Label that will be used in the legend of the plot. Default is "Combustion Chamber". Returns ------- patch : matplotlib.patches.Polygon Patch that represents the combustion chamber of the motor. """ chamber_length = ( self.motor.grain_initial_height + self.motor.grain_separation ) * self.motor.grain_number x = np.array( [ 0, chamber_length, chamber_length, ] ) y = np.array( [ self.motor.grain_outer_radius * 1.3, self.motor.grain_outer_radius * 1.3, 0, ] ) # we need to draw the other half of the chamber x = np.concatenate([x, x[::-1]]) y = np.concatenate([y, -y[::-1]]) # the point of reference for the chamber is its center # so we need to subtract half of its length and add the translation x = x + translate[0] - chamber_length / 2 y = y + translate[1] patch = Polygon( np.column_stack([x, y]), label=label, facecolor="lightslategray", edgecolor="black", ) return patch
# pylint: disable=too-many-statements
[docs] def _generate_grains(self, translate=(0, 0)): """Generates a list of patches that represent the grains of the motor. Each grain is a polygon with 4 vertices mirrored in the x axis. The top and bottom vertices are the same for all grains, but the left and right vertices are different for each grain. The grains are drawn in the origin and must be translated. Parameters ---------- translate : tuple Tuple with the x and y coordinates of the translation that will be applied to the grains. Returns ------- patches : list List of patches that represent the grains of the motor. """ patches = [] numgrains = self.motor.grain_number separation = self.motor.grain_separation height = self.motor.grain_initial_height outer_radius = self.motor.grain_outer_radius inner_radius = self.motor.grain_initial_inner_radius total_length = ( self.motor.grain_number * (self.motor.grain_initial_height + (self.motor.grain_separation)) - self.motor.grain_separation ) inner_y = np.array([0, inner_radius, inner_radius, 0]) outer_y = np.array([inner_radius, outer_radius, outer_radius, inner_radius]) inner_y = np.concatenate([inner_y, -inner_y[::-1]]) outer_y = np.concatenate([outer_y, -outer_y[::-1]]) inner_y = inner_y + translate[1] outer_y = outer_y + translate[1] initial_grain_position = 0 for n in range(numgrains): grain_start = initial_grain_position grain_end = grain_start + height initial_grain_position = grain_end + separation x = np.array([grain_start, grain_start, grain_end, grain_end]) # draw the other half of the nozzle x = np.concatenate([x, x[::-1]]) # sum the translate x = x + translate[0] - total_length / 2 patch = Polygon( np.column_stack([x, outer_y]), facecolor="olive", edgecolor="khaki", ) patches.append(patch) patch = Polygon( np.column_stack([x, inner_y]), facecolor="khaki", edgecolor="olive", ) if n == 0: patch.set_label("Grains") patches.append(patch) return patches
[docs] def _generate_positioned_tanks(self, translate=(0, 0), csys=1): """Generates a list of patches that represent the tanks of the liquid_motor. Parameters ---------- translate : tuple Tuple with the x and y coordinates of the translation that will be applied to the tanks. csys : float Coordinate system of the motor or rocket. This will define the orientation of the tanks draw. Default is 1, which means that the tanks will be drawn with the nose cone pointing left. Returns ------- patches_and_centers : list List of tuples where the first item is the patch of the tank, and the second item is the geometrical center. """ colors = { 0: ("black", "dimgray"), 1: ("darkblue", "cornflowerblue"), 2: ("darkgreen", "limegreen"), 3: ("darkorange", "gold"), 4: ("darkred", "tomato"), 5: ("darkviolet", "violet"), } patches_and_centers = [] for idx, pos_tank in enumerate(self.motor.positioned_tanks): tank = pos_tank["tank"] position = pos_tank["position"] * csys geometrical_center = (position + translate[0], translate[1]) color_idx = idx % len(colors) # Use modulo operator to loop through colors patch = tank.plots._generate_tank(geometrical_center, csys) patch.set_facecolor(colors[color_idx][1]) patch.set_edgecolor(colors[color_idx][0]) patch.set_alpha(0.8) patches_and_centers.append((patch, geometrical_center)) return patches_and_centers
[docs] def _draw_center_of_mass(self, ax): """Draws a red circle in the center of mass of the motor. This can be used for grains center of mass and the center of dry mass. Parameters ---------- ax : matplotlib.axes.Axes Axes object to plot the center of mass on. """ ax.axhline(0, color="k", linestyle="--", alpha=0.5) # symmetry line try: ax.plot( [self.motor.grains_center_of_mass_position], [0], "ro", label="Grains Center of Mass", ) except AttributeError: pass ax.plot( [self.motor.center_of_dry_mass_position], [0], "bo", label="Center of Dry Mass", )
[docs] def _generate_motor_region(self, list_of_patches): """Generates a patch that represents the motor outline. It is simply a polygon with 4 vertices mirrored in the x axis. The outline is drawn considering all the patches that represent the motor. Parameters ---------- list_of_patches : list List of patches that represent the motor outline. Returns ------- patch : matplotlib.patches.Polygon Patch that represents the motor outline. """ # get max and min x and y values from all motor patches x_min = min(patch.xy[:, 0].min() for patch in list_of_patches) x_max = max(patch.xy[:, 0].max() for patch in list_of_patches) y_min = min(patch.xy[:, 1].min() for patch in list_of_patches) y_max = max(patch.xy[:, 1].max() for patch in list_of_patches) # calculate x and y coordinates of the motor outline x = np.array([x_min, x_max, x_max, x_min]) y = np.array([y_min, y_min, y_max, y_max]) # draw the other half of the outline x = np.concatenate([x, x[::-1]]) y = np.concatenate([y, -y[::-1]]) # create the patch polygon with no fill but outlined with dashed line patch = Polygon( np.column_stack([x, y]), facecolor="#bdbdbd", edgecolor="#bdbdbd", linestyle="--", linewidth=0.5, alpha=0.5, ) return patch
def _set_plot_properties(self, ax): ax.set_aspect("equal") ax.set_ymargin(0.8) plt.grid(True, linestyle="--", linewidth=0.2) plt.xlabel("Position (m)") plt.ylabel("Radius (m)") plt.legend(bbox_to_anchor=(1.05, 1), loc="upper left") plt.tight_layout()
[docs] def animate_propellant_mass(self, filename=None, fps=30): """Animates the propellant mass of the motor as a function of time. Parameters ---------- filename : str | None, optional The path the animation should be saved to. By default None, in which case the animation will be shown instead of saved.Supported file ending is: .gif fps : int, optional Frames per second for the animation. Default is 30. Returns ------- matplotlib.animation.FuncAnimation The created animation object. """ # Extract time and mass data times = self.motor.propellant_mass.x_array values = self.motor.propellant_mass.y_array # Create figure and axis fig, ax = plt.subplots() # Configure axis ax.set_xlim(times[0], times[-1]) ax.set_ylim(min(values), max(values)) ax.set_xlabel("Time (s)") ax.set_ylabel("Propellant Mass (kg)") ax.set_title("Propellant Mass Evolution") # Create line and current point marker (line,) = ax.plot([], [], lw=2, color="blue", label="Propellant Mass") (point,) = ax.plot([], [], "ko") ax.legend() # Initialization def init(): line.set_data([], []) point.set_data([], []) return line, point # Update per frame def update(frame_index): line.set_data(times[: frame_index + 1], values[: frame_index + 1]) point.set_data([times[frame_index]], [values[frame_index]]) return line, point # Build animation animation = FuncAnimation( fig, update, frames=len(times), init_func=init, interval=1000 / fps, blit=True, ) # Show or save animation show_or_save_animation(animation, filename, fps=fps) return animation
[docs] def all(self): """Prints out all graphs available about the Motor. It simply calls all the other plotter methods in this class. Returns ------- None """ self.thrust(*self.motor.burn_time) # self.mass_flow_rate(*self.motor.burn_time) self.exhaust_velocity(*self.motor.burn_time) self.total_mass(*self.motor.burn_time) self.propellant_mass(*self.motor.burn_time) self.center_of_mass(*self.motor.burn_time) self.inertia_tensor(*self.motor.burn_time)