diff --git a/rocketpy/motors/hybrid_motor.py b/rocketpy/motors/hybrid_motor.py index 1ef07859a..92f386177 100644 --- a/rocketpy/motors/hybrid_motor.py +++ b/rocketpy/motors/hybrid_motor.py @@ -41,6 +41,26 @@ class HybridMotor(Motor): Solid motor object that composes the hybrid motor. HybridMotor.liquid : LiquidMotor Liquid motor object that composes the hybrid motor. + HybridMotor.positioned_tanks : list + List containing the motor's added tanks and their respective + positions. + HybridMotor.grains_center_of_mass_position : float + Position of the center of mass of the grains in meters, specified in + the motor's coordinate system. + See :doc:`Positions and Coordinate Systems ` + for more information. + HybridMotor.grain_number : int + Number of solid grains. + HybridMotor.grain_density : float + Density of each grain in kg/meters cubed. + HybridMotor.grain_outer_radius : float + Outer radius of each grain in meters. + HybridMotor.grain_initial_inner_radius : float + Initial inner radius of each grain in meters. + HybridMotor.grain_initial_height : float + Initial height of each grain in meters. + HybridMotor.grain_separation : float + Distance between two grains in meters. HybridMotor.dry_mass : float Same as in Motor class. See the :class:`Motor ` docs. HybridMotor.propellant_initial_mass : float @@ -326,6 +346,17 @@ def __init__( interpolation_method, coordinate_system_orientation, ) + + self.positioned_tanks = self.liquid.positioned_tanks + self.grain_number = grain_number + self.grain_density = grain_density + self.grain_outer_radius = grain_outer_radius + self.grain_initial_inner_radius = grain_initial_inner_radius + self.grain_initial_height = grain_initial_height + self.grain_separation = grain_separation + self.grains_center_of_mass_position = grains_center_of_mass_position + self.throat_radius = throat_radius + # Initialize plots and prints object self.prints = _HybridMotorPrints(self) self.plots = _HybridMotorPlots(self) @@ -569,6 +600,10 @@ def add_tank(self, tank, position): ) reset_funcified_methods(self) + def draw(self): + """Draws a representation of the HybridMotor.""" + self.plots.draw() + def info(self): """Prints out basic data about the Motor.""" self.prints.all() diff --git a/rocketpy/motors/liquid_motor.py b/rocketpy/motors/liquid_motor.py index 8fc3c4fce..21c1e105e 100644 --- a/rocketpy/motors/liquid_motor.py +++ b/rocketpy/motors/liquid_motor.py @@ -451,6 +451,10 @@ def add_tank(self, tank, position): self.positioned_tanks.append({"tank": tank, "position": position}) reset_funcified_methods(self) + def draw(self): + """Draw a representation of the LiquidMotor.""" + self.plots.draw() + def info(self): """Prints out basic data about the Motor.""" self.prints.all() diff --git a/rocketpy/motors/solid_motor.py b/rocketpy/motors/solid_motor.py index 06e3514b1..fc7109392 100644 --- a/rocketpy/motors/solid_motor.py +++ b/rocketpy/motors/solid_motor.py @@ -700,6 +700,10 @@ def propellant_I_13(self): def propellant_I_23(self): return 0 + def draw(self): + """Draw a representation of the SolidMotor.""" + self.plots.draw() + def info(self): """Prints out basic data about the SolidMotor.""" self.prints.all() diff --git a/rocketpy/motors/tank.py b/rocketpy/motors/tank.py index 022afed0e..b141fa030 100644 --- a/rocketpy/motors/tank.py +++ b/rocketpy/motors/tank.py @@ -410,6 +410,10 @@ def inertia(self): """ return self.liquid_inertia + self.gas_inertia + def draw(self): + """Draws the tank geometry.""" + self.plots.draw() + class MassFlowRateBasedTank(Tank): """Class to define a tank based on mass flow rates inputs. This class diff --git a/rocketpy/plots/hybrid_motor_plots.py b/rocketpy/plots/hybrid_motor_plots.py index 49a7fcbc5..aeafdc5b5 100644 --- a/rocketpy/plots/hybrid_motor_plots.py +++ b/rocketpy/plots/hybrid_motor_plots.py @@ -1,3 +1,5 @@ +from matplotlib import pyplot as plt + from .motor_plots import _MotorPlots @@ -120,6 +122,44 @@ def Kn(self, lower_limit=None, upper_limit=None): self.motor.solid.Kn.plot(lower=lower_limit, upper=upper_limit) + def draw(self): + """Draw a representation of the HybridMotor. + + Returns + ------- + None + """ + _, ax = plt.subplots(figsize=(8, 6), facecolor="#EEEEEE") + + tanks_and_centers = self._generate_positioned_tanks(csys=self.motor._csys) + nozzle = self._generate_nozzle( + translate=(self.motor.nozzle_position, 0), csys=self.motor._csys + ) + chamber = self._generate_combustion_chamber( + translate=(self.motor.grains_center_of_mass_position, 0) + ) + grains = self._generate_grains( + translate=(self.motor.grains_center_of_mass_position, 0) + ) + outline = self._generate_motor_region( + list_of_patches=[nozzle, chamber, *grains] + + [tank for tank, _ in tanks_and_centers] + ) + + ax.add_patch(outline) + ax.add_patch(chamber) + for grain in grains: + ax.add_patch(grain) + for patch, center in tanks_and_centers: + ax.add_patch(patch) + ax.plot(center[0], center[1], marker="o", color="red", markersize=2) + ax.add_patch(nozzle) + + ax.set_title("Hybrid Motor Representation") + self._draw_center_of_mass(ax) + self._set_plot_properties(ax) + plt.show() + def all(self): """Prints out all graphs available about the HybridMotor. It simply calls all the other plotter methods in this class. @@ -128,7 +168,7 @@ def all(self): ------- None """ - + self.draw() self.thrust(*self.motor.burn_time) self.total_mass(*self.motor.burn_time) self.center_of_mass(*self.motor.burn_time) diff --git a/rocketpy/plots/liquid_motor_plots.py b/rocketpy/plots/liquid_motor_plots.py index a13a91bfb..bc8a9f8fa 100644 --- a/rocketpy/plots/liquid_motor_plots.py +++ b/rocketpy/plots/liquid_motor_plots.py @@ -1,3 +1,5 @@ +import matplotlib.pyplot as plt + from .motor_plots import _MotorPlots @@ -25,6 +27,36 @@ def __init__(self, liquid_motor): """ super().__init__(liquid_motor) + def draw(self): + """Draw a representation of the LiquidMotor. + + Returns + ------- + None + """ + _, ax = plt.subplots(figsize=(8, 6), facecolor="#EEEEEE") + + tanks_and_centers = self._generate_positioned_tanks(csys=self.motor._csys) + nozzle = self._generate_nozzle( + translate=(self.motor.nozzle_position, 0), csys=self.motor._csys + ) + outline = self._generate_motor_region( + list_of_patches=[nozzle] + [tank for tank, _ in tanks_and_centers] + ) + + ax.add_patch(outline) + for patch, center in tanks_and_centers: + ax.add_patch(patch) + ax.plot(center[0], center[1], marker="o", color="red", markersize=2) + + # add the nozzle + ax.add_patch(nozzle) + + ax.set_title("Liquid Motor Representation") + self._draw_center_of_mass(ax) + self._set_plot_properties(ax) + plt.show() + def all(self): """Prints out all graphs available about the LiquidMotor. It simply calls all the other plotter methods in this class. @@ -33,6 +65,7 @@ def all(self): ------- None """ + self.draw() self.thrust(*self.motor.burn_time) self.mass_flow_rate(*self.motor.burn_time) self.exhaust_velocity(*self.motor.burn_time) diff --git a/rocketpy/plots/motor_plots.py b/rocketpy/plots/motor_plots.py index 70e12967e..c429367c4 100644 --- a/rocketpy/plots/motor_plots.py +++ b/rocketpy/plots/motor_plots.py @@ -1,3 +1,8 @@ +import matplotlib.pyplot as plt +import numpy as np +from matplotlib.patches import Polygon + + class _MotorPlots: """Class that holds plot methods for Motor class. @@ -238,8 +243,292 @@ def I_23(self, lower_limit=None, upper_limit=None): """ self.motor.I_23.plot(lower=lower_limit, upper=upper_limit) - def draw(self): - """Place holder for drawing the motor.""" + 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 + + 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 + + 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 + + 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 + + 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", + ) + + 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() def all(self): """Prints out all graphs available about the Motor. It simply calls diff --git a/rocketpy/plots/rocket_plots.py b/rocketpy/plots/rocket_plots.py index adfd76d0d..c31bea12c 100644 --- a/rocketpy/plots/rocket_plots.py +++ b/rocketpy/plots/rocket_plots.py @@ -1,6 +1,7 @@ -import numpy as np import matplotlib.pyplot as plt +import numpy as np +from rocketpy.motors import HybridMotor, LiquidMotor, SolidMotor from rocketpy.rocket.aero_surface import Fins, NoseCone, Tail @@ -159,7 +160,7 @@ def draw(self, vis_args=None): } # Create the figure and axis - _, ax = plt.subplots(figsize=(8, 5)) + _, ax = plt.subplots(figsize=(8, 6), facecolor="#EEEEEE") ax.set_aspect("equal") ax.set_facecolor(vis_args["background"]) ax.grid(True, linestyle="--", linewidth=0.5) @@ -172,7 +173,7 @@ def draw(self, vis_args=None): # and the radius of the rocket at that point drawn_surfaces = [] - # Ideia is to get the shape of each aerodynamic surface in their own + # Idea is to get the shape of each aerodynamic surface in their own # coordinate system and then plot them in the rocket coordinate system # using the position of each surface # For the tubes, the surfaces need to be checked in order to check for @@ -325,16 +326,103 @@ def draw(self, vis_args=None): linewidth=vis_args["line_width"], ) - # TODO - Draw motor + # Draw motor + total_csys = self.rocket._csys * self.rocket.motor._csys nozzle_position = ( - self.rocket.motor_position - + self.rocket.motor.nozzle_position - * self.rocket._csys - * self.rocket.motor._csys + self.rocket.motor_position + self.rocket.motor.nozzle_position * total_csys ) - ax.scatter( - nozzle_position, 0, label="Nozzle Outlet", color="brown", s=10, zorder=10 + + nozzle = self.rocket.motor.plots._generate_nozzle( + translate=(nozzle_position, 0), csys=self.rocket._csys + ) + + # List of motor patches + motor_patches = [] + + # Get motor patches translated to the correct position + if isinstance(self.rocket.motor, (SolidMotor)): + grains_cm_position = ( + self.rocket.motor_position + + self.rocket.motor.grains_center_of_mass_position * total_csys + ) + ax.scatter( + grains_cm_position, + 0, + color="brown", + label="Grains Center of Mass", + s=8, + zorder=10, + ) + + chamber = self.rocket.motor.plots._generate_combustion_chamber( + translate=(grains_cm_position, 0), label=None + ) + grains = self.rocket.motor.plots._generate_grains( + translate=(grains_cm_position, 0) + ) + + motor_patches += [chamber, *grains] + + elif isinstance(self.rocket.motor, HybridMotor): + grains_cm_position = ( + self.rocket.motor_position + + self.rocket.motor.grains_center_of_mass_position * total_csys + ) + ax.scatter( + grains_cm_position, + 0, + color="brown", + label="Grains Center of Mass", + s=8, + zorder=10, + ) + + tanks_and_centers = self.rocket.motor.plots._generate_positioned_tanks( + translate=(self.rocket.motor_position, 0), csys=total_csys + ) + chamber = self.rocket.motor.plots._generate_combustion_chamber( + translate=(grains_cm_position, 0), label=None + ) + grains = self.rocket.motor.plots._generate_grains( + translate=(grains_cm_position, 0) + ) + motor_patches += [chamber, *grains] + for tank, center in tanks_and_centers: + ax.scatter( + center[0], + center[1], + color="black", + alpha=0.2, + s=5, + zorder=10, + ) + motor_patches += [tank] + + elif isinstance(self.rocket.motor, LiquidMotor): + tanks_and_centers = self.rocket.motor.plots._generate_positioned_tanks( + translate=(self.rocket.motor_position, 0), csys=total_csys + ) + for tank, center in tanks_and_centers: + ax.scatter( + center[0], + center[1], + color="black", + alpha=0.2, + s=4, + zorder=10, + ) + motor_patches += [tank] + + # add nozzle last so it is in front of the other patches + motor_patches += [nozzle] + outline = self.rocket.motor.plots._generate_motor_region( + list_of_patches=motor_patches ) + # add outline first so it is behind the other patches + ax.add_patch(outline) + for patch in motor_patches: + ax.add_patch(patch) + # Check if nozzle is beyond the last surface, if so draw a tube # to it, with the radius of the last surface if self.rocket._csys == 1: @@ -390,19 +478,20 @@ def draw(self, vis_args=None): # Draw center of mass and center of pressure cm = self.rocket.center_of_mass(0) - ax.scatter(cm, 0, color="black", label="Center of Mass", s=30) - ax.scatter(cm, 0, facecolors="none", edgecolors="black", s=100) + ax.scatter(cm, 0, color="#1565c0", label="Center of Mass", s=10) cp = self.rocket.cp_position(0) - ax.scatter(cp, 0, label="Center Of Pressure", color="red", s=30, zorder=10) - ax.scatter(cp, 0, facecolors="none", edgecolors="red", s=100, zorder=10) + ax.scatter( + cp, 0, label="Static Center of Pressure", color="red", s=10, zorder=10 + ) # Set plot attributes - plt.title(f"Rocket Geometry") + plt.title("Rocket Representation") + plt.xlim() plt.ylim([-self.rocket.radius * 4, self.rocket.radius * 6]) plt.xlabel("Position (m)") plt.ylabel("Radius (m)") - plt.legend(loc="best") + plt.legend(bbox_to_anchor=(1.05, 1), loc="upper left") plt.tight_layout() plt.show() @@ -417,6 +506,11 @@ def all(self): None """ + # Rocket draw + print("\nRocket Draw") + print("-" * 40) + self.draw() + # Mass Plots print("\nMass Plots") print("-" * 40) diff --git a/rocketpy/plots/solid_motor_plots.py b/rocketpy/plots/solid_motor_plots.py index a4a464442..832ba4213 100644 --- a/rocketpy/plots/solid_motor_plots.py +++ b/rocketpy/plots/solid_motor_plots.py @@ -1,3 +1,5 @@ +import matplotlib.pyplot as plt + from .motor_plots import _MotorPlots @@ -121,6 +123,39 @@ def Kn(self, lower_limit=None, upper_limit=None): self.motor.Kn.plot(lower=lower_limit, upper=upper_limit) + def draw(self): + """Draw a representation of the SolidMotor. + + Returns + ------- + None + """ + _, ax = plt.subplots(figsize=(8, 6), facecolor="#EEEEEE") + + nozzle = self._generate_nozzle( + translate=(self.motor.nozzle_position, 0), csys=self.motor._csys + ) + chamber = self._generate_combustion_chamber( + translate=(self.motor.grains_center_of_mass_position, 0) + ) + grains = self._generate_grains( + translate=(self.motor.grains_center_of_mass_position, 0) + ) + outline = self._generate_motor_region( + list_of_patches=[nozzle, chamber, *grains] + ) + + ax.add_patch(outline) + ax.add_patch(chamber) + for grain in grains: + ax.add_patch(grain) + ax.add_patch(nozzle) + + ax.set_title("Solid Motor Representation") + self._draw_center_of_mass(ax) + self._set_plot_properties(ax) + plt.show() + def all(self): """Prints out all graphs available about the SolidMotor. It simply calls all the other plotter methods in this class. @@ -129,6 +164,7 @@ def all(self): ------- None """ + self.draw() self.thrust(*self.motor.burn_time) self.mass_flow_rate(*self.motor.burn_time) self.exhaust_velocity(*self.motor.burn_time) diff --git a/rocketpy/plots/tank_plots.py b/rocketpy/plots/tank_plots.py index 0205a3414..f02021704 100644 --- a/rocketpy/plots/tank_plots.py +++ b/rocketpy/plots/tank_plots.py @@ -1,3 +1,8 @@ +import matplotlib.pyplot as plt +import numpy as np +from matplotlib.patches import Polygon + + class _TankPlots: """Class that holds plot methods for Tank class. @@ -22,9 +27,71 @@ def __init__(self, tank): """ self.tank = tank + self.name = tank.name + self.geometry = tank.geometry return None + def _generate_tank(self, translate=(0, 0), csys=1): + """Generates a matplotlib patch object that represents the tank. + + Parameters + ---------- + ax : matplotlib.axes.Axes, optional + Axes object to plot the tank on. If None, a new figure and axes + will be created. + translate : tuple, optional + Tuple of floats that represents the translation of the tank + geometry. + csys : float, optional + Coordinate system of the tank, this will define the orientation of + the tank. Default is 1, which means that the tank will be drawn + with the nose cone pointing left. + + Returns + ------- + tank : matplotlib.patches.Polygon + Polygon object that represents the tank. + """ + # get positions of all points + x = csys * self.geometry.radius.x_array + translate[0] + y = csys * self.geometry.radius.y_array + translate[1] + x = np.concatenate([x, x[::-1]]) + y = np.concatenate([y, -y[::-1]]) + xy = np.column_stack([x, y]) + + tank = Polygon( + xy, + label=self.name, + facecolor="dimgray", + edgecolor="black", + ) + # Don't set any plot config here. Use the draw methods for that + return tank + + def draw(self): + """Draws the tank geometry. + + Returns + ------- + None + """ + fig, ax = plt.subplots(facecolor="#EEEEEE") + + ax.add_patch(self._generate_tank()) + + ax.set_aspect("equal") + ax.grid(True, linestyle="--", linewidth=0.5) + + ax.set_xlabel("Length (m)") + ax.set_ylabel("Radius (m)") + ax.set_title("Tank Geometry") + + x_max = self.geometry.radius.x_array.max() + y_max = self.geometry.radius.y_array.max() + ax.set_xlim(-1.2 * x_max, 1.2 * x_max) + ax.set_ylim(-1.5 * y_max, 1.5 * y_max) + def all(self): """Prints out all graphs available about the Tank. It simply calls all the other plotter methods in this class. diff --git a/tests/test_flight.py b/tests/test_flight.py index ddd898dca..1fafb7bcc 100644 --- a/tests/test_flight.py +++ b/tests/test_flight.py @@ -763,7 +763,7 @@ def test_max_values(flight_calisto_robust): regarding this pytest fixture. """ test = flight_calisto_robust - atol = 5e-3 + atol = 1e-2 assert pytest.approx(105.2774, abs=atol) == test.max_acceleration_power_on assert pytest.approx(105.2774, abs=atol) == test.max_acceleration assert pytest.approx(0.85999, abs=atol) == test.max_mach_number