From 033e8d33b3313b05ad51b7c18fbfbfd5178b7e14 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Wed, 11 Oct 2023 01:06:02 -0300 Subject: [PATCH 01/17] ENH: _generate_nozzle draw method --- rocketpy/plots/__init__.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/rocketpy/plots/__init__.py b/rocketpy/plots/__init__.py index e69de29bb..50e1bec5c 100644 --- a/rocketpy/plots/__init__.py +++ b/rocketpy/plots/__init__.py @@ -0,0 +1,38 @@ +import numpy as np +from matplotlib.patches import Polygon + + +def _generate_nozzle(motor, translate=(0, 0), csys=1): + nozzle_radius = motor.nozzle_radius + nozzle_position = motor.nozzle_position + try: + throat_radius = motor.throat_radius + except AttributeError: + # Liquid motors don't have throat radius, let's estimate it + throat_radius = 0.01 + + # calculate length between throat and nozzle outlet using 15º angle + major_axis = (nozzle_radius - throat_radius) / np.tan(np.deg2rad(15)) + # calculate minor axis considering 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 + nozzle_position + translate[0] + y = y + translate[1] + + patch = Polygon( + np.column_stack([x, y]), + label="Nozzle", + facecolor="black", + edgecolor="black", + ) + motor.nozzle_length = major_axis + minor_axis + return patch From e514b8fdac7372a1a3d43155b839174b2b6cfb5f Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Wed, 11 Oct 2023 01:06:51 -0300 Subject: [PATCH 02/17] ENH: draw the solid motor --- rocketpy/motors/solid_motor.py | 4 + rocketpy/plots/solid_motor_plots.py | 145 ++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+) diff --git a/rocketpy/motors/solid_motor.py b/rocketpy/motors/solid_motor.py index 96f8a8931..c2bbcda6a 100644 --- a/rocketpy/motors/solid_motor.py +++ b/rocketpy/motors/solid_motor.py @@ -701,6 +701,10 @@ def propellant_I_13(self): def propellant_I_23(self): return 0 + def draw(self): + self.plots.draw() + return None + def info(self): """Prints out basic data about the SolidMotor.""" self.prints.all() diff --git a/rocketpy/plots/solid_motor_plots.py b/rocketpy/plots/solid_motor_plots.py index d8d80f3ce..3580add20 100644 --- a/rocketpy/plots/solid_motor_plots.py +++ b/rocketpy/plots/solid_motor_plots.py @@ -1,3 +1,10 @@ +import matplotlib.pyplot as plt +import numpy as np +from matplotlib.patches import Polygon + +from rocketpy.plots import _generate_nozzle + + class _SolidMotorPlots: """Class that holds plot methods for SolidMotor class. @@ -361,6 +368,144 @@ def I_23(self, lower_limit=None, upper_limit=None): return None + def draw(self): + """Draws a simple 2D representation of the SolidMotor.""" + fig, ax = plt.subplots(figsize=(10, 6)) + ax.set_aspect("equal") + + _csys = self.solid_motor._csys + + chamber = self._generate_combustion_chamber(_csys=_csys) + nozzle = _generate_nozzle(self.solid_motor) + grains = self._generate_grains(_csys=_csys, translate=(0, 0)) + + ax.add_patch(chamber) + for grain in grains: + ax.add_patch(grain) + ax.add_patch(nozzle) + + # self._draw_nozzle(ax, nozzle_height, csys) + # self._draw_combustion_chamber(ax, csys) + # self._draw_grains(ax, csys) + self._draw_center_of_mass(ax) + + self._set_plot_properties(ax) + plt.show() + return None + + def _generate_combustion_chamber(self, translate=(0, 0), csys=1): + # csys = self.solid_motor.csys + chamber_length = ( + abs( + self.solid_motor.center_of_dry_mass_position + - self.solid_motor.nozzle_position + ) + * 2 + ) * csys + x = np.array( + [ + self.solid_motor.nozzle_position, + self.solid_motor.nozzle_position, + self.solid_motor.nozzle_position + chamber_length, + self.solid_motor.nozzle_position + chamber_length, + ] + ) + y = np.array( + [ + self.solid_motor.nozzle_radius, + self.solid_motor.grain_outer_radius * 1.4, + self.solid_motor.grain_outer_radius * 1.4, + 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 the translate + x = x + translate[0] + y = y + translate[1] + + patch = Polygon( + np.column_stack([x, y]), + label="Combustion Chamber", + facecolor="lightslategray", + edgecolor="black", + ) + return patch + + def _generate_grains(self, translate=(0, 0), csys=1): + patches = [] + n_total = self.solid_motor.grain_number + separation = self.solid_motor.grain_separation + height = self.solid_motor.grain_initial_height + outer_radius = self.solid_motor.grain_outer_radius + inner_radius = self.solid_motor.grain_initial_inner_radius + + cm_teo = ( + csys * ((n_total / 2) * (height + separation)) + + self.solid_motor.nozzle_position + ) + cm_real = self.solid_motor.center_of_propellant_mass(0) + + init = abs(cm_teo - cm_real) * csys + + 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] + for n in range(n_total): + grain_start = init + csys * (separation / 2 + n * (height + separation)) + grain_end = grain_start + height * csys + 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] + 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 _draw_center_of_mass(self, ax): + ax.axhline(0, color="k", linestyle="--", alpha=0.5) # symmetry line + ax.plot( + [self.solid_motor.grains_center_of_mass_position], + [0], + "ro", + label="Grains Center of Mass", + ) + ax.plot( + [self.solid_motor.center_of_dry_mass_position], + [0], + "bo", + label="Center of Dry Mass", + ) + + def _set_plot_properties(self, ax): + ax.set_title("Solid Motor Representation") + ax.set_ylabel("Radius (m)") + ax.set_xlabel("Position (m)") + # ax.grid(True) + plt.ylim( + -self.solid_motor.grain_outer_radius * 1.2 * 1.7, + self.solid_motor.grain_outer_radius * 1.2 * 1.7, + ) + plt.legend(loc="upper left", bbox_to_anchor=(1, 1)) + def all(self): """Prints out all graphs available about the SolidMotor. It simply calls all the other plotter methods in this class. From 93b85d2f47fc37f0ad82be7bc6e2b38d036866b8 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Wed, 11 Oct 2023 01:08:17 -0300 Subject: [PATCH 03/17] ENH: add solid motor to rocket draw --- rocketpy/plots/rocket_plots.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/rocketpy/plots/rocket_plots.py b/rocketpy/plots/rocket_plots.py index e487918a9..eb2134ba1 100644 --- a/rocketpy/plots/rocket_plots.py +++ b/rocketpy/plots/rocket_plots.py @@ -1,6 +1,8 @@ -import numpy as np import matplotlib.pyplot as plt +import numpy as np +from rocketpy.motors import HybridMotor, LiquidMotor, SolidMotor +from rocketpy.plots import _generate_nozzle from rocketpy.rocket.aero_surface import Fins, NoseCone, Tail @@ -335,6 +337,18 @@ def draw(self, vis_args=None): ax.scatter( nozzle_position, 0, label="Nozzle Outlet", color="brown", s=10, zorder=10 ) + + if isinstance(self.rocket.motor, (SolidMotor, HybridMotor)): + chamber = self.rocket.motor.plots._generate_combustion_chamber( + translate=(nozzle_position, 0), csys=self.rocket._csys + ) + ax.add_patch(chamber) + grains = self.rocket.motor.plots._generate_grains( + translate=(nozzle_position, 0), csys=self.rocket._csys + ) + for grain in grains: + ax.add_patch(grain) + # 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: From 33c3116aea8a0370d8ac1e2716eb996407121a1d Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Wed, 11 Oct 2023 01:11:40 -0300 Subject: [PATCH 04/17] ENH: liquid motors draw --- rocketpy/motors/liquid_motor.py | 3 ++ rocketpy/motors/tank.py | 3 ++ rocketpy/plots/liquid_motor_plots.py | 76 ++++++++++++++++++++++++++++ rocketpy/plots/tank_plots.py | 58 +++++++++++++++++++++ 4 files changed, 140 insertions(+) diff --git a/rocketpy/motors/liquid_motor.py b/rocketpy/motors/liquid_motor.py index d89157e72..064267c11 100644 --- a/rocketpy/motors/liquid_motor.py +++ b/rocketpy/motors/liquid_motor.py @@ -444,6 +444,9 @@ def add_tank(self, tank, position): self.positioned_tanks.append({"tank": tank, "position": position}) reset_funcified_methods(self) + def draw(self): + return self.plots.draw() + def info(self): """Prints out basic data about the Motor.""" self.prints.all() diff --git a/rocketpy/motors/tank.py b/rocketpy/motors/tank.py index 0b7069141..ff575afb4 100644 --- a/rocketpy/motors/tank.py +++ b/rocketpy/motors/tank.py @@ -410,6 +410,9 @@ def inertia(self): """ return self.liquid_inertia + self.gas_inertia + def draw(self): + """Draws the tank geometry.""" + return self.plots.draw() class MassFlowRateBasedTank(Tank): """Class to define a tank based on mass flow rates inputs. This class diff --git a/rocketpy/plots/liquid_motor_plots.py b/rocketpy/plots/liquid_motor_plots.py index 0d53792ea..0f228074e 100644 --- a/rocketpy/plots/liquid_motor_plots.py +++ b/rocketpy/plots/liquid_motor_plots.py @@ -1,3 +1,12 @@ +import copy + +import matplotlib.pyplot as plt +import numpy as np +from matplotlib.patches import Polygon + +from rocketpy.plots import _generate_nozzle + + class _LiquidMotorPlots: """Class that holds plot methods for LiquidMotor class. @@ -256,6 +265,73 @@ def I_23(self, lower_limit=None, upper_limit=None): return None + def _generate_positioned_tanks(self, translate=(0, 0), csys=1): + """Generates a list of patches that represent the tanks of the + liquid_motor. + + Parameters + ---------- + None + + Returns + ------- + patches : list + List of patches that represent the tanks of the liquid_motor. + """ + colors = { + 0: ("black", "dimgray"), + 1: ("darkblue", "cornflowerblue"), + 2: ("darkgreen", "limegreen"), + 3: ("darkorange", "gold"), + 4: ("darkred", "tomato"), + 5: ("darkviolet", "violet"), + } + patches = [] + for idx, pos_tank in enumerate(self.liquid_motor.positioned_tanks): + tank = pos_tank["tank"] + position = pos_tank["position"] + trans = (position + translate[0], translate[1]) + patch = tank.plots._generate_tank(trans, csys) + patch.set_facecolor(colors[idx][1]) + patch.set_edgecolor(colors[idx][0]) + patches.append(patch) + return patches + + def _draw_center_of_interests(self, ax, translate=(0, 0)): + # center of dry mass position + # center of wet mass time = 0 + # center of wet mass time = end + return None + + def draw(self): + fig, ax = plt.subplots(facecolor="#EEEEEE") + + patches = self._generate_positioned_tanks() + for patch in patches: + ax.add_patch(patch) + + # add the nozzle + ax.add_patch(_generate_nozzle(self.liquid_motor, translate=(0, 0))) + + # find the maximum and minimum x and y values of the tanks + x_min = y_min = np.inf + x_max = y_max = -np.inf + for patch in patches: + x_min = min(x_min, patch.xy[:, 0].min()) + x_max = max(x_max, patch.xy[:, 0].max()) + y_min = min(y_min, patch.xy[:, 1].min()) + y_max = max(y_max, patch.xy[:, 1].max()) + + ax.set_aspect("equal") + ax.legend(bbox_to_anchor=(1.05, 1), loc="upper left") + ax.grid(True, linestyle="--", linewidth=0.5) + ax.set_ylim(y_min - 0.25, y_max + 0.25) + ax.set_xlim(x_min - 0.10, x_max + 0.10) + ax.set_xlabel("Position (m)") + ax.set_ylabel("Radius (m)") + ax.set_title("Liquid Motor Geometry") + plt.show() + def all(self): """Prints out all graphs available about the LiquidMotor. It simply calls all the other plotter methods in this class. diff --git a/rocketpy/plots/tank_plots.py b/rocketpy/plots/tank_plots.py index 0205a3414..b9239defb 100644 --- a/rocketpy/plots/tank_plots.py +++ b/rocketpy/plots/tank_plots.py @@ -1,3 +1,9 @@ +import matplotlib.pyplot as plt +import numpy as np +from matplotlib.animation import FuncAnimation +from matplotlib.patches import Polygon, Rectangle + + class _TankPlots: """Class that holds plot methods for Tank class. @@ -22,9 +28,61 @@ 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. + + 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): + 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. From ec37e7615d7ac83b739ebb7f496e96756493d96e Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Wed, 11 Oct 2023 01:19:37 -0300 Subject: [PATCH 05/17] MNT: fix some plot settings --- rocketpy/plots/rocket_plots.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/rocketpy/plots/rocket_plots.py b/rocketpy/plots/rocket_plots.py index eb2134ba1..302bba1db 100644 --- a/rocketpy/plots/rocket_plots.py +++ b/rocketpy/plots/rocket_plots.py @@ -161,7 +161,7 @@ def draw(self, vis_args=None): } # Create the figure and axis - _, ax = plt.subplots(figsize=(8, 5)) + _, ax = plt.subplots(figsize=(8, 6)) ax.set_aspect("equal") ax.set_facecolor(vis_args["background"]) ax.grid(True, linestyle="--", linewidth=0.5) @@ -349,6 +349,18 @@ def draw(self, vis_args=None): for grain in grains: ax.add_patch(grain) + elif isinstance(self.rocket.motor, LiquidMotor): + patches = self.rocket.motor.plots._generate_positioned_tanks( + translate=(self.rocket.motor_position, 0), csys=self.rocket._csys + ) + for patch in patches: + ax.add_patch(patch) + + nozzle = _generate_nozzle( + self.rocket.motor, translate=(nozzle_position, 0), csys=self.rocket._csys + ) + ax.add_patch(nozzle) + # 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: @@ -407,8 +419,8 @@ def draw(self, vis_args=None): ax.scatter(cm, 0, color="black", label="Center of Mass", s=30) ax.scatter(cm, 0, facecolors="none", edgecolors="black", s=100) - cp = self.rocket.cp_position - ax.scatter(cp, 0, label="Center Of Pressure", color="red", s=30, zorder=10) + cp = self.rocket.cp_position(0) + ax.scatter(cp, 0, label="CP (M=0)", color="red", s=30, zorder=10) ax.scatter(cp, 0, facecolors="none", edgecolors="red", s=100, zorder=10) # Set plot attributes @@ -416,7 +428,7 @@ def draw(self, vis_args=None): 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() From d1e4a371996537f9cc4552bca6b690260963a14b Mon Sep 17 00:00:00 2001 From: Lint Action Date: Wed, 11 Oct 2023 04:21:48 +0000 Subject: [PATCH 06/17] Fix code style issues with Black --- rocketpy/motors/tank.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rocketpy/motors/tank.py b/rocketpy/motors/tank.py index ff575afb4..6645e237b 100644 --- a/rocketpy/motors/tank.py +++ b/rocketpy/motors/tank.py @@ -414,6 +414,7 @@ def draw(self): """Draws the tank geometry.""" return self.plots.draw() + class MassFlowRateBasedTank(Tank): """Class to define a tank based on mass flow rates inputs. This class inherits from the Tank class. See the Tank class for more information From eb6a95d487d3c2677fbdbf6da85469e49464ab9d Mon Sep 17 00:00:00 2001 From: MateusStano Date: Thu, 16 Nov 2023 21:42:48 +0100 Subject: [PATCH 07/17] ENH: add basic grain and tanks atributes to hybrid --- rocketpy/motors/hybrid_motor.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/rocketpy/motors/hybrid_motor.py b/rocketpy/motors/hybrid_motor.py index a4468bc26..fb28a40f1 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 The total mass of the motor structure, including chambers and tanks, when it is empty and does not contain any propellant. @@ -325,6 +345,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) From 3e61371fdb5e1ddc2f7341cab89b9b2f1f6cc714 Mon Sep 17 00:00:00 2001 From: MateusStano Date: Thu, 16 Nov 2023 21:53:53 +0100 Subject: [PATCH 08/17] ENH: add motor draw related methods to MotorPlots --- rocketpy/plots/motor_plots.py | 274 +++++++++++++++++++++++++++++++++- 1 file changed, 272 insertions(+), 2 deletions(-) diff --git a/rocketpy/plots/motor_plots.py b/rocketpy/plots/motor_plots.py index 70e12967e..5330feec0 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,273 @@ 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. + + 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. + + 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 nozzle + 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 + ---------- + None + + 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): + 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 From 501c5816fdae6b236ecfae443b2f5d9c03a46018 Mon Sep 17 00:00:00 2001 From: MateusStano Date: Thu, 16 Nov 2023 21:57:41 +0100 Subject: [PATCH 09/17] ENH: motor drawings --- rocketpy/plots/hybrid_motor_plots.py | 43 ++++++++ rocketpy/plots/liquid_motor_plots.py | 85 ++++----------- rocketpy/plots/solid_motor_plots.py | 148 ++++----------------------- rocketpy/plots/tank_plots.py | 9 +- 4 files changed, 91 insertions(+), 194 deletions(-) diff --git a/rocketpy/plots/hybrid_motor_plots.py b/rocketpy/plots/hybrid_motor_plots.py index 49a7fcbc5..b3d3a0c92 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,47 @@ 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 + ) # Adjust markersize for better visibility + ax.add_patch(nozzle) + + ax.set_title("Hybrid Motor Representation") + self._draw_center_of_mass(ax) + self._set_plot_properties(ax) + plt.show() + return None + def all(self): """Prints out all graphs available about the HybridMotor. It simply calls all the other plotter methods in this class. diff --git a/rocketpy/plots/liquid_motor_plots.py b/rocketpy/plots/liquid_motor_plots.py index a5a89f818..ce2eb82c9 100644 --- a/rocketpy/plots/liquid_motor_plots.py +++ b/rocketpy/plots/liquid_motor_plots.py @@ -1,10 +1,4 @@ -import copy - import matplotlib.pyplot as plt -import numpy as np -from matplotlib.patches import Polygon - -from rocketpy.plots import _generate_nozzle from .motor_plots import _MotorPlots @@ -33,71 +27,34 @@ def __init__(self, liquid_motor): """ super().__init__(liquid_motor) - def _generate_positioned_tanks(self, translate=(0, 0), csys=1): - """Generates a list of patches that represent the tanks of the - liquid_motor. - - Parameters - ---------- - None + def draw(self): + """Draw a representation of the LiquidMotor. Returns ------- - patches : list - List of patches that represent the tanks of the liquid_motor. + None """ - colors = { - 0: ("black", "dimgray"), - 1: ("darkblue", "cornflowerblue"), - 2: ("darkgreen", "limegreen"), - 3: ("darkorange", "gold"), - 4: ("darkred", "tomato"), - 5: ("darkviolet", "violet"), - } - patches = [] - for idx, pos_tank in enumerate(self.liquid_motor.positioned_tanks): - tank = pos_tank["tank"] - position = pos_tank["position"] - trans = (position + translate[0], translate[1]) - patch = tank.plots._generate_tank(trans, csys) - patch.set_facecolor(colors[idx][1]) - patch.set_edgecolor(colors[idx][0]) - patches.append(patch) - return patches - - def _draw_center_of_interests(self, ax, translate=(0, 0)): - # center of dry mass position - # center of wet mass time = 0 - # center of wet mass time = end - return None - - def draw(self): - fig, ax = plt.subplots(facecolor="#EEEEEE") - - patches = self._generate_positioned_tanks() - for patch in patches: + _, 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(_generate_nozzle(self.liquid_motor, translate=(0, 0))) - - # find the maximum and minimum x and y values of the tanks - x_min = y_min = np.inf - x_max = y_max = -np.inf - for patch in patches: - x_min = min(x_min, patch.xy[:, 0].min()) - x_max = max(x_max, patch.xy[:, 0].max()) - y_min = min(y_min, patch.xy[:, 1].min()) - y_max = max(y_max, patch.xy[:, 1].max()) - - ax.set_aspect("equal") - ax.legend(bbox_to_anchor=(1.05, 1), loc="upper left") - ax.grid(True, linestyle="--", linewidth=0.5) - ax.set_ylim(y_min - 0.25, y_max + 0.25) - ax.set_xlim(x_min - 0.10, x_max + 0.10) - ax.set_xlabel("Position (m)") - ax.set_ylabel("Radius (m)") - ax.set_title("Liquid Motor Geometry") + 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): diff --git a/rocketpy/plots/solid_motor_plots.py b/rocketpy/plots/solid_motor_plots.py index 9d69c862f..4ceccdc33 100644 --- a/rocketpy/plots/solid_motor_plots.py +++ b/rocketpy/plots/solid_motor_plots.py @@ -1,8 +1,4 @@ import matplotlib.pyplot as plt -import numpy as np -from matplotlib.patches import Polygon - -from rocketpy.plots import _generate_nozzle from .motor_plots import _MotorPlots @@ -128,143 +124,39 @@ def Kn(self, lower_limit=None, upper_limit=None): self.motor.Kn.plot(lower=lower_limit, upper=upper_limit) def draw(self): - """Draws a simple 2D representation of the SolidMotor.""" - fig, ax = plt.subplots(figsize=(10, 6)) - ax.set_aspect("equal") + """Draw a representation of the SolidMotor. - _csys = self.solid_motor._csys + Returns + ------- + None + """ + _, ax = plt.subplots(figsize=(8, 6), facecolor="#EEEEEE") - chamber = self._generate_combustion_chamber(_csys=_csys) - nozzle = _generate_nozzle(self.solid_motor) - grains = self._generate_grains(_csys=_csys, translate=(0, 0)) + 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) - # self._draw_nozzle(ax, nozzle_height, csys) - # self._draw_combustion_chamber(ax, csys) - # self._draw_grains(ax, csys) + ax.set_title("Solid Motor Representation") self._draw_center_of_mass(ax) - self._set_plot_properties(ax) plt.show() return None - def _generate_combustion_chamber(self, translate=(0, 0), csys=1): - # csys = self.solid_motor.csys - chamber_length = ( - abs( - self.solid_motor.center_of_dry_mass_position - - self.solid_motor.nozzle_position - ) - * 2 - ) * csys - x = np.array( - [ - self.solid_motor.nozzle_position, - self.solid_motor.nozzle_position, - self.solid_motor.nozzle_position + chamber_length, - self.solid_motor.nozzle_position + chamber_length, - ] - ) - y = np.array( - [ - self.solid_motor.nozzle_radius, - self.solid_motor.grain_outer_radius * 1.4, - self.solid_motor.grain_outer_radius * 1.4, - 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 the translate - x = x + translate[0] - y = y + translate[1] - - patch = Polygon( - np.column_stack([x, y]), - label="Combustion Chamber", - facecolor="lightslategray", - edgecolor="black", - ) - return patch - - def _generate_grains(self, translate=(0, 0), csys=1): - patches = [] - n_total = self.solid_motor.grain_number - separation = self.solid_motor.grain_separation - height = self.solid_motor.grain_initial_height - outer_radius = self.solid_motor.grain_outer_radius - inner_radius = self.solid_motor.grain_initial_inner_radius - - cm_teo = ( - csys * ((n_total / 2) * (height + separation)) - + self.solid_motor.nozzle_position - ) - cm_real = self.solid_motor.center_of_propellant_mass(0) - - init = abs(cm_teo - cm_real) * csys - - 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] - for n in range(n_total): - grain_start = init + csys * (separation / 2 + n * (height + separation)) - grain_end = grain_start + height * csys - 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] - 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 _draw_center_of_mass(self, ax): - ax.axhline(0, color="k", linestyle="--", alpha=0.5) # symmetry line - ax.plot( - [self.solid_motor.grains_center_of_mass_position], - [0], - "ro", - label="Grains Center of Mass", - ) - ax.plot( - [self.solid_motor.center_of_dry_mass_position], - [0], - "bo", - label="Center of Dry Mass", - ) - - def _set_plot_properties(self, ax): - ax.set_title("Solid Motor Representation") - ax.set_ylabel("Radius (m)") - ax.set_xlabel("Position (m)") - # ax.grid(True) - plt.ylim( - -self.solid_motor.grain_outer_radius * 1.2 * 1.7, - self.solid_motor.grain_outer_radius * 1.2 * 1.7, - ) - plt.legend(loc="upper left", bbox_to_anchor=(1, 1)) - def all(self): """Prints out all graphs available about the SolidMotor. It simply calls all the other plotter methods in this class. diff --git a/rocketpy/plots/tank_plots.py b/rocketpy/plots/tank_plots.py index b9239defb..6e26ecf7c 100644 --- a/rocketpy/plots/tank_plots.py +++ b/rocketpy/plots/tank_plots.py @@ -1,7 +1,6 @@ import matplotlib.pyplot as plt import numpy as np -from matplotlib.animation import FuncAnimation -from matplotlib.patches import Polygon, Rectangle +from matplotlib.patches import Polygon class _TankPlots: @@ -67,6 +66,12 @@ def _generate_tank(self, translate=(0, 0), csys=1): return tank def draw(self): + """Draws the tank geometry. + + Returns + ------- + None + """ fig, ax = plt.subplots(facecolor="#EEEEEE") ax.add_patch(self._generate_tank()) From 1155b31ba1978a2d3f7b83ee01373a50c4e8e88d Mon Sep 17 00:00:00 2001 From: MateusStano Date: Thu, 16 Nov 2023 21:57:54 +0100 Subject: [PATCH 10/17] ENH: clean plots __init__ --- rocketpy/plots/__init__.py | 38 -------------------------------------- 1 file changed, 38 deletions(-) diff --git a/rocketpy/plots/__init__.py b/rocketpy/plots/__init__.py index 50e1bec5c..e69de29bb 100644 --- a/rocketpy/plots/__init__.py +++ b/rocketpy/plots/__init__.py @@ -1,38 +0,0 @@ -import numpy as np -from matplotlib.patches import Polygon - - -def _generate_nozzle(motor, translate=(0, 0), csys=1): - nozzle_radius = motor.nozzle_radius - nozzle_position = motor.nozzle_position - try: - throat_radius = motor.throat_radius - except AttributeError: - # Liquid motors don't have throat radius, let's estimate it - throat_radius = 0.01 - - # calculate length between throat and nozzle outlet using 15º angle - major_axis = (nozzle_radius - throat_radius) / np.tan(np.deg2rad(15)) - # calculate minor axis considering 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 + nozzle_position + translate[0] - y = y + translate[1] - - patch = Polygon( - np.column_stack([x, y]), - label="Nozzle", - facecolor="black", - edgecolor="black", - ) - motor.nozzle_length = major_axis + minor_axis - return patch From deafe5b49b72be2668f7cd77d53491d2f2fd4a97 Mon Sep 17 00:00:00 2001 From: MateusStano Date: Thu, 16 Nov 2023 21:58:23 +0100 Subject: [PATCH 11/17] DOC: draw function docs --- rocketpy/motors/hybrid_motor.py | 5 +++++ rocketpy/motors/liquid_motor.py | 1 + rocketpy/motors/solid_motor.py | 4 +++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/rocketpy/motors/hybrid_motor.py b/rocketpy/motors/hybrid_motor.py index fb28a40f1..092fa79a3 100644 --- a/rocketpy/motors/hybrid_motor.py +++ b/rocketpy/motors/hybrid_motor.py @@ -553,6 +553,11 @@ def add_tank(self, tank, position): ) reset_funcified_methods(self) + def draw(self): + """Draws a representation of the HybridMotor.""" + self.plots.draw() + return None + 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 064267c11..1019324bc 100644 --- a/rocketpy/motors/liquid_motor.py +++ b/rocketpy/motors/liquid_motor.py @@ -445,6 +445,7 @@ def add_tank(self, tank, position): reset_funcified_methods(self) def draw(self): + """Draw a representation of the LiquidMotor.""" return self.plots.draw() def info(self): diff --git a/rocketpy/motors/solid_motor.py b/rocketpy/motors/solid_motor.py index c2bbcda6a..cdc0ece6d 100644 --- a/rocketpy/motors/solid_motor.py +++ b/rocketpy/motors/solid_motor.py @@ -1,7 +1,8 @@ import numpy as np from scipy import integrate -from ..mathutils.function import Function, funcify_method, reset_funcified_methods +from ..mathutils.function import (Function, funcify_method, + reset_funcified_methods) from ..plots.solid_motor_plots import _SolidMotorPlots from ..prints.solid_motor_prints import _SolidMotorPrints from .motor import Motor @@ -702,6 +703,7 @@ def propellant_I_23(self): return 0 def draw(self): + """Draw a representation of the SolidMotor.""" self.plots.draw() return None From 16436a3ae8549471fd0df435b1ba720427127b96 Mon Sep 17 00:00:00 2001 From: MateusStano Date: Thu, 16 Nov 2023 22:02:38 +0100 Subject: [PATCH 12/17] ENH: add motors to rocket drawing --- rocketpy/plots/rocket_plots.py | 119 +++++++++++++++++++++++++-------- 1 file changed, 91 insertions(+), 28 deletions(-) diff --git a/rocketpy/plots/rocket_plots.py b/rocketpy/plots/rocket_plots.py index 2b8aab39b..0a9372c66 100644 --- a/rocketpy/plots/rocket_plots.py +++ b/rocketpy/plots/rocket_plots.py @@ -2,7 +2,6 @@ import numpy as np from rocketpy.motors import HybridMotor, LiquidMotor, SolidMotor -from rocketpy.plots import _generate_nozzle from rocketpy.rocket.aero_surface import Fins, NoseCone, Tail @@ -161,7 +160,7 @@ def draw(self, vis_args=None): } # Create the figure and axis - _, ax = plt.subplots(figsize=(8, 6)) + _, 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) @@ -174,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 @@ -327,39 +326,102 @@ 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 ) - if isinstance(self.rocket.motor, (SolidMotor, HybridMotor)): + # 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=(nozzle_position, 0), csys=self.rocket._csys + translate=(grains_cm_position, 0), label=None ) - ax.add_patch(chamber) grains = self.rocket.motor.plots._generate_grains( - translate=(nozzle_position, 0), csys=self.rocket._csys + translate=(grains_cm_position, 0) ) - for grain in grains: - ax.add_patch(grain) + 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): - patches = self.rocket.motor.plots._generate_positioned_tanks( - translate=(self.rocket.motor_position, 0), csys=self.rocket._csys + tanks_and_centers = self.rocket.motor.plots._generate_positioned_tanks( + translate=(self.rocket.motor_position, 0), csys=total_csys ) - for patch in patches: - ax.add_patch(patch) + 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] - nozzle = _generate_nozzle( - self.rocket.motor, translate=(nozzle_position, 0), csys=self.rocket._csys + motor_patches += [ + nozzle + ] # add nozzle last so it is in front of the other patches + outline = self.rocket.motor.plots._generate_motor_region( + list_of_patches=motor_patches ) - ax.add_patch(nozzle) + ax.add_patch(outline) # add outline first so it is behind the other patches + 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 @@ -416,15 +478,16 @@ 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)") From 6c40f3260bd5a1bbff6b96c09fa7a6905e226363 Mon Sep 17 00:00:00 2001 From: Lint Action Date: Thu, 16 Nov 2023 21:02:50 +0000 Subject: [PATCH 13/17] Fix code style issues with Black --- rocketpy/motors/solid_motor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rocketpy/motors/solid_motor.py b/rocketpy/motors/solid_motor.py index cdc0ece6d..dc2c36168 100644 --- a/rocketpy/motors/solid_motor.py +++ b/rocketpy/motors/solid_motor.py @@ -1,8 +1,7 @@ import numpy as np from scipy import integrate -from ..mathutils.function import (Function, funcify_method, - reset_funcified_methods) +from ..mathutils.function import Function, funcify_method, reset_funcified_methods from ..plots.solid_motor_plots import _SolidMotorPlots from ..prints.solid_motor_prints import _SolidMotorPrints from .motor import Motor From fe529d7f47a75b8c54eed52003a549bf9b48ec7f Mon Sep 17 00:00:00 2001 From: MateusStano <69485049+MateusStano@users.noreply.github.com> Date: Fri, 17 Nov 2023 21:27:28 -0300 Subject: [PATCH 14/17] Update rocketpy/plots/motor_plots.py Co-authored-by: Pedro Henrique Marinho Bressan <87212571+phmbressan@users.noreply.github.com> --- rocketpy/plots/motor_plots.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocketpy/plots/motor_plots.py b/rocketpy/plots/motor_plots.py index 5330feec0..d6dfa3a1f 100644 --- a/rocketpy/plots/motor_plots.py +++ b/rocketpy/plots/motor_plots.py @@ -328,7 +328,7 @@ def _generate_combustion_chamber( 0, ] ) - # we need to draw the other half of the nozzle + # 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 From dc943d027ae3dcd240979eb72068eb9c9aaf8228 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Sat, 18 Nov 2023 15:06:01 -0300 Subject: [PATCH 15/17] MNT: small adjustments before merging... - Remove useless return None - Remove inline comments - Add a few missing docstrings --- rocketpy/motors/hybrid_motor.py | 1 - rocketpy/motors/liquid_motor.py | 2 +- rocketpy/motors/solid_motor.py | 1 - rocketpy/motors/tank.py | 2 +- rocketpy/plots/hybrid_motor_plots.py | 5 +---- rocketpy/plots/motor_plots.py | 23 +++++++++++++++++++++-- rocketpy/plots/rocket_plots.py | 8 ++++---- rocketpy/plots/solid_motor_plots.py | 1 - rocketpy/plots/tank_plots.py | 4 ++++ 9 files changed, 32 insertions(+), 15 deletions(-) diff --git a/rocketpy/motors/hybrid_motor.py b/rocketpy/motors/hybrid_motor.py index 1c8569181..92f386177 100644 --- a/rocketpy/motors/hybrid_motor.py +++ b/rocketpy/motors/hybrid_motor.py @@ -603,7 +603,6 @@ def add_tank(self, tank, position): def draw(self): """Draws a representation of the HybridMotor.""" self.plots.draw() - return None def info(self): """Prints out basic data about the Motor.""" diff --git a/rocketpy/motors/liquid_motor.py b/rocketpy/motors/liquid_motor.py index 1a328256a..21c1e105e 100644 --- a/rocketpy/motors/liquid_motor.py +++ b/rocketpy/motors/liquid_motor.py @@ -453,7 +453,7 @@ def add_tank(self, tank, position): def draw(self): """Draw a representation of the LiquidMotor.""" - return self.plots.draw() + self.plots.draw() def info(self): """Prints out basic data about the Motor.""" diff --git a/rocketpy/motors/solid_motor.py b/rocketpy/motors/solid_motor.py index 3ce236d3a..fc7109392 100644 --- a/rocketpy/motors/solid_motor.py +++ b/rocketpy/motors/solid_motor.py @@ -703,7 +703,6 @@ def propellant_I_23(self): def draw(self): """Draw a representation of the SolidMotor.""" self.plots.draw() - return None def info(self): """Prints out basic data about the SolidMotor.""" diff --git a/rocketpy/motors/tank.py b/rocketpy/motors/tank.py index d9c3b09a5..b141fa030 100644 --- a/rocketpy/motors/tank.py +++ b/rocketpy/motors/tank.py @@ -412,7 +412,7 @@ def inertia(self): def draw(self): """Draws the tank geometry.""" - return self.plots.draw() + self.plots.draw() class MassFlowRateBasedTank(Tank): diff --git a/rocketpy/plots/hybrid_motor_plots.py b/rocketpy/plots/hybrid_motor_plots.py index b3d3a0c92..5c5537a7f 100644 --- a/rocketpy/plots/hybrid_motor_plots.py +++ b/rocketpy/plots/hybrid_motor_plots.py @@ -152,16 +152,13 @@ def draw(self): 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 - ) # Adjust markersize for better visibility + 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() - return None def all(self): """Prints out all graphs available about the HybridMotor. It simply calls diff --git a/rocketpy/plots/motor_plots.py b/rocketpy/plots/motor_plots.py index d6dfa3a1f..c429367c4 100644 --- a/rocketpy/plots/motor_plots.py +++ b/rocketpy/plots/motor_plots.py @@ -255,7 +255,9 @@ def _generate_nozzle(self, translate=(0, 0), csys=1): 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. + 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 ------- @@ -305,6 +307,9 @@ def _generate_combustion_chamber( 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 ------- @@ -414,7 +419,13 @@ def _generate_positioned_tanks(self, translate=(0, 0), csys=1): Parameters ---------- - None + 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 ------- @@ -444,6 +455,14 @@ def _generate_positioned_tanks(self, translate=(0, 0), csys=1): 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( diff --git a/rocketpy/plots/rocket_plots.py b/rocketpy/plots/rocket_plots.py index 0a9372c66..4025562ce 100644 --- a/rocketpy/plots/rocket_plots.py +++ b/rocketpy/plots/rocket_plots.py @@ -413,13 +413,13 @@ def draw(self, vis_args=None): ) motor_patches += [tank] - motor_patches += [ - nozzle - ] # add nozzle last so it is in front of the other patches + # 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 ) - ax.add_patch(outline) # add outline first so it is behind the other patches + # add outline first so it is behind the other patches + ax.add_patch(outline) for patch in motor_patches: ax.add_patch(patch) diff --git a/rocketpy/plots/solid_motor_plots.py b/rocketpy/plots/solid_motor_plots.py index 4ceccdc33..7a10bab76 100644 --- a/rocketpy/plots/solid_motor_plots.py +++ b/rocketpy/plots/solid_motor_plots.py @@ -155,7 +155,6 @@ def draw(self): self._draw_center_of_mass(ax) self._set_plot_properties(ax) plt.show() - return None def all(self): """Prints out all graphs available about the SolidMotor. It simply calls diff --git a/rocketpy/plots/tank_plots.py b/rocketpy/plots/tank_plots.py index 6e26ecf7c..f02021704 100644 --- a/rocketpy/plots/tank_plots.py +++ b/rocketpy/plots/tank_plots.py @@ -43,6 +43,10 @@ def _generate_tank(self, translate=(0, 0), csys=1): 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 ------- From 08542008344fddcf80673c3df795034e7f86e02f Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Sat, 18 Nov 2023 15:23:38 -0300 Subject: [PATCH 16/17] TST: Update atol value in test_max_values --- tests/test_flight.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From a64c0efb11ebfd2b5fb353d4e7f4f6f257c249c7 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Sat, 18 Nov 2023 15:29:55 -0300 Subject: [PATCH 17/17] ENH: Add draw methods to all_info() --- rocketpy/plots/hybrid_motor_plots.py | 2 +- rocketpy/plots/liquid_motor_plots.py | 1 + rocketpy/plots/rocket_plots.py | 5 +++++ rocketpy/plots/solid_motor_plots.py | 1 + 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/rocketpy/plots/hybrid_motor_plots.py b/rocketpy/plots/hybrid_motor_plots.py index 5c5537a7f..aeafdc5b5 100644 --- a/rocketpy/plots/hybrid_motor_plots.py +++ b/rocketpy/plots/hybrid_motor_plots.py @@ -168,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 ce2eb82c9..bc8a9f8fa 100644 --- a/rocketpy/plots/liquid_motor_plots.py +++ b/rocketpy/plots/liquid_motor_plots.py @@ -65,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/rocket_plots.py b/rocketpy/plots/rocket_plots.py index 4025562ce..c31bea12c 100644 --- a/rocketpy/plots/rocket_plots.py +++ b/rocketpy/plots/rocket_plots.py @@ -506,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 7a10bab76..832ba4213 100644 --- a/rocketpy/plots/solid_motor_plots.py +++ b/rocketpy/plots/solid_motor_plots.py @@ -164,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)