Source code for rocketpy.rocket.aero_surface.nose_cone

import warnings

import numpy as np
from scipy.optimize import fsolve

from rocketpy.mathutils.function import Function
from rocketpy.plots.aero_surface_plots import _NoseConePlots
from rocketpy.prints.aero_surface_prints import _NoseConePrints

from .aero_surface import AeroSurface


[docs] class NoseCone(AeroSurface): """Keeps nose cone information. Note ---- Local coordinate system: - the origin at the tip of the nose cone and - the Z axis along the longitudinal axis of symmetry, positive downwards (top -> bottom). Attributes ---------- NoseCone.length : float Nose cone length. Has units of length and must be given in meters. NoseCone.kind : string Nose cone kind. Can be "conical", "ogive", "elliptical", "tangent", "von karman", "parabolic", "powerseries" or "lvhaack". NoseCone.bluffness : float Ratio between the radius of the circle on the tip of the ogive and the radius of the base of the ogive. Currently only used for the nose cone's drawing. Must be between 0 and 1. Default is None, which means that the nose cone will not have a sphere on the tip. If a value is given, the nose cone's length will be slightly reduced because of the addition of the sphere. Must be None or 0 if a "powerseries" nose cone kind is specified. NoseCone.rocket_radius : float The reference rocket radius used for lift coefficient normalization, in meters. NoseCone.base_radius : float Nose cone base radius. Has units of length and must be given in meters. NoseCone.radius_ratio : float Ratio between the nose cone base radius and the rocket radius. Has no units. If base radius is not given, the ratio between base radius and rocket radius is assumed as 1, meaning that the nose cone has the same radius as the rocket. If base radius is given, the ratio between base radius and rocket radius is calculated and used for lift calculation. NoseCone.power : float Factor that controls the bluntness of the shape for a power series nose cone. Must be between 0 and 1. It is ignored when other nose cone types are used. NoseCone.name : string Nose cone name. Has no impact in simulation, as it is only used to display data in a more organized matter. NoseCone.cp : tuple Tuple with the x, y and z local coordinates of the nose cone center of pressure. Has units of length and is given in meters. NoseCone.cpx : float Nose cone local center of pressure x coordinate. Has units of length and is given in meters. NoseCone.cpy : float Nose cone local center of pressure y coordinate. Has units of length and is given in meters. NoseCone.cpz : float Nose cone local center of pressure z coordinate. Has units of length and is given in meters. NoseCone.cl : Function Function which defines the lift coefficient as a function of the angle of attack and the Mach number. Takes as input the angle of attack in radians and the Mach number. Returns the lift coefficient. NoseCone.clalpha : float Lift coefficient slope. Has units of 1/rad. NoseCone.plots : plots.aero_surface_plots._NoseConePlots This contains all the plots methods. Use help(NoseCone.plots) to know more about it. NoseCone.prints : prints.aero_surface_prints._NoseConePrints This contains all the prints methods. Use help(NoseCone.prints) to know more about it. """
[docs] def __init__( # pylint: disable=too-many-statements self, length, kind, base_radius=None, bluffness=None, rocket_radius=None, power=None, name="Nose Cone", ): """Initializes the nose cone. It is used to define the nose cone length, kind, center of pressure and lift coefficient curve. Parameters ---------- length : float Nose cone length. Has units of length and must be given in meters. kind : string Nose cone kind. Can be "conical", "ogive", "elliptical", "tangent", "von karman", "parabolic", "powerseries" or "lvhaack". If "powerseries" is used, the "power" argument must be assigned to a value between 0 and 1. base_radius : float, optional Nose cone base radius. Has units of length and must be given in meters. If not given, the ratio between ``base_radius`` and ``rocket_radius`` will be assumed as 1. bluffness : float, optional Ratio between the radius of the circle on the tip of the ogive and the radius of the base of the ogive. Currently only used for the nose cone's drawing. Must be between 0 and 1. Default is None, which means that the nose cone will not have a sphere on the tip. If a value is given, the nose cone's length will be reduced to account for the addition of the sphere at the tip. Must be None or 0 if a "powerseries" nose cone kind is specified. rocket_radius : int, float, optional The reference rocket radius used for lift coefficient normalization. If not given, the ratio between ``base_radius`` and ``rocket_radius`` will be assumed as 1. power : float, optional Factor that controls the bluntness of the shape for a power series nose cone. Must be between 0 and 1. It is ignored when other nose cone types are used. name : str, optional Nose cone name. Has no impact in simulation, as it is only used to display data in a more organized matter. Returns ------- None """ rocket_radius = rocket_radius or base_radius super().__init__(name, np.pi * rocket_radius**2, 2 * rocket_radius) self._rocket_radius = rocket_radius self._base_radius = base_radius self._length = length if bluffness is not None: if bluffness > 1 or bluffness < 0: # pragma: no cover raise ValueError( f"Bluffness ratio of {bluffness} is out of range. " "It must be between 0 and 1." ) self._bluffness = bluffness if kind == "powerseries": # Checks if bluffness is not being used if (self.bluffness is not None) and (self.bluffness != 0): raise ValueError( "Parameter 'bluffness' must be None or 0 when using a nose cone kind 'powerseries'." ) if power is None: raise ValueError( "Parameter 'power' cannot be None when using a nose cone kind 'powerseries'." ) if power > 1 or power <= 0: raise ValueError( f"Power value of {power} is out of range. It must be between 0 and 1." ) self._power = power self.kind = kind self.evaluate_lift_coefficient() self.evaluate_center_of_pressure() self.plots = _NoseConePlots(self) self.prints = _NoseConePrints(self)
@property def rocket_radius(self): return self._rocket_radius @rocket_radius.setter def rocket_radius(self, value): self._rocket_radius = value self.evaluate_geometrical_parameters() self.evaluate_lift_coefficient() self.evaluate_nose_shape() @property def base_radius(self): return self._base_radius @base_radius.setter def base_radius(self, value): self._base_radius = value self.evaluate_geometrical_parameters() self.evaluate_lift_coefficient() self.evaluate_nose_shape() @property def length(self): return self._length @length.setter def length(self, value): self._length = value self.evaluate_center_of_pressure() self.evaluate_nose_shape() @property def power(self): return self._power @power.setter def power(self, value): if value is not None: if value > 1 or value <= 0: raise ValueError( f"Power value of {value} is out of range. It must be between 0 and 1." ) self._power = value self.evaluate_k() self.evaluate_center_of_pressure() self.evaluate_nose_shape() @property def kind(self): return self._kind @kind.setter def kind(self, value): # pylint: disable=too-many-statements # Analyzes nosecone type # Sets the k for Cp calculation # Sets the function which creates the respective curve self._kind = value value = (value.replace(" ", "")).lower() if value == "conical": self.k = 2 / 3 self.y_nosecone = Function(lambda x: x * self.base_radius / self.length) elif value == "lvhaack": self.k = 0.563 def theta(x): return np.arccos(1 - 2 * max(min(x / self.length, 1), 0)) self.y_nosecone = Function( lambda x: self.base_radius * (theta(x) - np.sin(2 * theta(x)) / 2 + (np.sin(theta(x)) ** 3) / 3) ** (0.5) / (np.pi**0.5) ) elif value in ["tangent", "tangentogive", "ogive"]: rho = (self.base_radius**2 + self.length**2) / (2 * self.base_radius) volume = np.pi * ( self.length * rho**2 - (self.length**3) / 3 - (rho - self.base_radius) * rho**2 * np.arcsin(self.length / rho) ) area = np.pi * self.base_radius**2 self.k = 1 - volume / (area * self.length) self.y_nosecone = Function( lambda x: np.sqrt(rho**2 - (min(x - self.length, 0)) ** 2) + (self.base_radius - rho) ) elif value == "elliptical": self.k = 1 / 3 self.y_nosecone = Function( lambda x: self.base_radius * np.sqrt(1 - ((x - self.length) / self.length) ** 2) ) elif value == "vonkarman": self.k = 0.5 def theta(x): return np.arccos(1 - 2 * max(min(x / self.length, 1), 0)) self.y_nosecone = Function( lambda x: self.base_radius * (theta(x) - np.sin(2 * theta(x)) / 2) ** (0.5) / (np.pi**0.5) ) elif value == "parabolic": self.k = 0.5 self.y_nosecone = Function( lambda x: self.base_radius * ((2 * x / self.length - (x / self.length) ** 2) / (2 - 1)) ) elif value == "powerseries": self.k = (2 * self.power) / ((2 * self.power) + 1) self.y_nosecone = Function( lambda x: self.base_radius * np.power(x / self.length, self.power) ) else: # pragma: no cover raise ValueError( f"Nose Cone kind '{self.kind}' not found, " + "please use one of the following Nose Cone kinds:" + '\n\t"conical"' + '\n\t"ogive"' + '\n\t"lvhaack"' + '\n\t"tangent"' + '\n\t"vonkarman"' + '\n\t"elliptical"' + '\n\t"powerseries"' + '\n\t"parabolic"\n' ) self.evaluate_center_of_pressure() self.evaluate_geometrical_parameters() self.evaluate_nose_shape() @property def bluffness(self): return self._bluffness @bluffness.setter def bluffness(self, value): # prevents from setting bluffness on "powerseries" nose cones if self.kind == "powerseries": # Checks if bluffness is not being used if (value is not None) and (value != 0): raise ValueError( "Parameter 'bluffness' must be None or 0 when using a nose cone kind 'powerseries'." ) if value is not None and not 0 <= value <= 1: # pragma: no cover raise ValueError( f"Bluffness ratio of {value} is out of range. " "It must be between 0 and 1." ) self._bluffness = value self.evaluate_nose_shape()
[docs] def evaluate_geometrical_parameters(self): """Calculates and saves nose cone's radius ratio. Returns ------- None """ # If base radius is not given, the ratio between base radius and # rocket radius is assumed as 1, meaning that the nose cone has the # same radius as the rocket if self.base_radius is None and self.rocket_radius is not None: self.radius_ratio = 1 self.base_radius = self.rocket_radius elif self.base_radius is not None and self.rocket_radius is None: self.radius_ratio = 1 self.rocket_radius = self.base_radius # If base radius is given, the ratio between base radius and rocket # radius is calculated elif self.base_radius is not None and self.rocket_radius is not None: self.radius_ratio = self.base_radius / self.rocket_radius else: raise ValueError( "Either base radius or rocket radius must be given to " "calculate the nose cone radius ratio." ) self.fineness_ratio = self.length / (2 * self.base_radius)
[docs] def evaluate_nose_shape(self): # pylint: disable=too-many-statements """Calculates and saves nose cone's shape as lists and re-evaluates the nose cone's length for a given bluffness ratio. The shape is saved as two vectors, one for the x coordinates and one for the y coordinates. Returns ------- None """ number_of_points = 127 density_modifier = 3 # increase density of points to improve accuracy def find_x_intercept(x): # find the tangential intersection point between the circle and nosec curve return x + self.y_nosecone(x) * self.y_nosecone.differentiate_complex_step( x ) # Calculate a function to find the radius of the nosecone curve def find_radius(x): return (self.y_nosecone(x) ** 2 + (x - find_x_intercept(x)) ** 2) ** 0.5 # Check bluffness circle and choose whether to use it or not if self.bluffness is None or self.bluffness == 0: # Set up parameters to continue without bluffness r_circle, circle_center, x_init = 0, 0, 0 else: # Calculate circle radius r_circle = self.bluffness * self.base_radius if self.kind == "elliptical": def test_circle(x): # set up a circle at the starting position to test bluffness return np.sqrt(r_circle**2 - (x - r_circle) ** 2) # Check if bluffness circle is too small if test_circle(1e-03) <= self.y_nosecone(1e-03): # Raise a warning warnings.warn( "WARNING: The chosen bluffness ratio is too small for " "the selected nose cone category, thereby the effective " "bluffness will be 0." ) # Set up parameters to continue without bluffness r_circle, circle_center, x_init = 0, 0, 0 else: # Find the intersection point between circle and nosecone curve x_init = fsolve(lambda x: find_radius(x[0]) - r_circle, r_circle)[0] circle_center = find_x_intercept(x_init) else: # Find the intersection point between circle and nosecone curve x_init = fsolve(lambda x: find_radius(x[0]) - r_circle, r_circle)[0] circle_center = find_x_intercept(x_init) # Calculate a function to create the circle at the correct position def create_circle(x): return abs(r_circle**2 - (x - circle_center) ** 2) ** 0.5 # Define a function for the final shape of the curve with a circle at the tip def final_shape(x): return self.y_nosecone(x) if x >= x_init else create_circle(x) # Vectorize the final_shape function final_shape_vec = np.vectorize(final_shape) # Create the vectors X and Y with the points of the curve nosecone_x = (self.length - (circle_center - r_circle)) * ( np.linspace(0, 1, number_of_points) ** density_modifier ) nosecone_y = final_shape_vec(nosecone_x + (circle_center - r_circle)) # Evaluate final geometry parameters self.shape_vec = [nosecone_x, nosecone_y] if abs(nosecone_x[-1] - self.length) >= 0.001: # 1 millimeter self._length = nosecone_x[-1] print( "Due to the chosen bluffness ratio, the nose " f"cone length was reduced to {self.length} m." ) self.fineness_ratio = self.length / (2 * self.base_radius)
[docs] def evaluate_lift_coefficient(self): """Calculates and returns nose cone's lift coefficient. The lift coefficient is saved and returned. This function also calculates and saves its lift coefficient derivative. Returns ------- None """ # Calculate clalpha self.clalpha = Function( lambda mach: 2 * self.radius_ratio**2, "Mach", f"Lift coefficient derivative for {self.name}", ) self.cl = Function( lambda alpha, mach: self.clalpha(mach) * alpha, ["Alpha (rad)", "Mach"], "Cl", )
[docs] def evaluate_k(self): """Updates the self.k attribute used to compute the center of pressure when using "powerseries" nose cones. Returns ------- None """ if self.kind == "powerseries": self.k = (2 * self.power) / ((2 * self.power) + 1)
[docs] def evaluate_center_of_pressure(self): """Calculates and returns the center of pressure of the nose cone in local coordinates. The center of pressure position is saved and stored as a tuple. Local coordinate origin is found at the tip of the nose cone. Returns ------- self.cp : tuple Tuple containing cpx, cpy, cpz. """ self.cpz = self.k * self.length self.cpy = 0 self.cpx = 0 self.cp = (self.cpx, self.cpy, self.cpz) return self.cp
[docs] def draw(self, *, filename=None): """Draw the nosecone shape along with some important information, including the center of pressure position. Parameters ---------- 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.plots.draw(filename=filename)
[docs] def info(self): """Prints and plots summarized information of the nose cone. Return ------ None """ self.prints.geometry() self.prints.lift()
[docs] def all_info(self): """Prints and plots all the available information of the nose cone. Returns ------- None """ self.prints.all() self.plots.all()
def to_dict(self, include_outputs=False): data = { "_length": self._length, "_kind": self._kind, "_base_radius": self._base_radius, "_bluffness": self._bluffness, "_rocket_radius": self._rocket_radius, "_power": self._power, "name": self.name, } if include_outputs: data["cp"] = self.cp data["clalpha"] = self.clalpha data["cl"] = self.cl return data @classmethod def from_dict(cls, data): return cls( length=data["_length"], kind=data["_kind"], base_radius=data["_base_radius"], bluffness=data["_bluffness"], rocket_radius=data["_rocket_radius"], power=data["_power"], name=data["name"], )