From a044a6d6704da9a3297234997e061b1401c0816a Mon Sep 17 00:00:00 2001 From: MateusStano Date: Fri, 23 Jun 2023 15:17:52 -0300 Subject: [PATCH] ENH: changes due to new RailButtons and Component classes --- rocketpy/Dispersion.py | 19 +- rocketpy/monte_carlo/mc_aero_surfaces.py | 7 +- rocketpy/monte_carlo/mc_rocket.py | 243 ++++++++++++----------- 3 files changed, 142 insertions(+), 127 deletions(-) diff --git a/rocketpy/Dispersion.py b/rocketpy/Dispersion.py index 20854a716..5ccfc5a14 100644 --- a/rocketpy/Dispersion.py +++ b/rocketpy/Dispersion.py @@ -349,6 +349,9 @@ def run_dispersion( # Checks export_list self.export_list = self.__check_export_list(export_list) + # Initializes inputs_dict in case of error in the first iteration + inputs_dict = {} + # Initialize counter and timer i = self.num_of_loaded_sims if append else 0 initial_wall_time = time() @@ -383,23 +386,26 @@ def run_dispersion( ] for item in d.items() ) + # TODO: I believe the positions are not being saved + # need to check if they are and fix if not if self.rocket.motors: - for motor in self.rocket.motors: + for motor in self.rocket.motors.get_components(): inputs_dict.update(motor.last_rnd_dict) if self.rocket.nosecones: - for nosecone in self.rocket.nosecones: + for nosecone in self.rocket.nosecones.get_components(): inputs_dict.update(nosecone.last_rnd_dict) if self.rocket.fins: - for fin in self.rocket.fins: + for fin in self.rocket.fins.get_components(): inputs_dict.update(fin.last_rnd_dict) if self.rocket.tails: - for tail in self.rocket.tails: + for tail in self.rocket.tails.get_components(): inputs_dict.update(tail.last_rnd_dict) if self.rocket.parachutes: for parachute in self.rocket.parachutes: inputs_dict.update(parachute.last_rnd_dict) - if self.rocket.rail_buttons: - inputs_dict.update(self.rocket.rail_buttons.last_rnd_dict) + if self.rocket.rail_buttons.get_components(): + for rail_buttons in self.rocket.rail_buttons.get_components(): + inputs_dict.update(rail_buttons.last_rnd_dict) # Export inputs and outputs to file self.__export_flight_data( setting=inputs_dict, @@ -410,6 +416,7 @@ def run_dispersion( except (TypeError, ValueError, KeyError, AttributeError) as error: print(f"Error on iteration {i}: {error}\n") error_file.write(f"{inputs_dict}\n") + raise error except KeyboardInterrupt: print("Keyboard Interrupt, file saved.") error_file.write(f"{inputs_dict}\n") diff --git a/rocketpy/monte_carlo/mc_aero_surfaces.py b/rocketpy/monte_carlo/mc_aero_surfaces.py index d4c38b3f6..1576d6d4a 100644 --- a/rocketpy/monte_carlo/mc_aero_surfaces.py +++ b/rocketpy/monte_carlo/mc_aero_surfaces.py @@ -189,9 +189,9 @@ class as a base class, see its documentation for more information. """ rail_buttons: RailButtons = Field(..., exclude=True) - upper_button_position: Any = 0 - lower_button_position: Any = 0 + buttons_distance: Any = 0 angular_position: Any = 0 + name: List[StrictStr] = [] def create_object(self): """Creates a RailButtons object from the randomly generated input arguments. @@ -207,8 +207,7 @@ def create_object(self): """ gen_dict = next(self.dict_generator()) obj = RailButtons( - upper_button_position=gen_dict["upper_button_position"], - lower_button_position=gen_dict["lower_button_position"], + buttons_distance=gen_dict["buttons_distance"], angular_position=gen_dict["angular_position"], ) return obj diff --git a/rocketpy/monte_carlo/mc_rocket.py b/rocketpy/monte_carlo/mc_rocket.py index b2845f1e1..738c0e9d7 100644 --- a/rocketpy/monte_carlo/mc_rocket.py +++ b/rocketpy/monte_carlo/mc_rocket.py @@ -5,10 +5,11 @@ from typing import Any, List, Union from pydantic import Field, FilePath, PrivateAttr +from rocketpy import Components from rocketpy.tools import get_distribution -from ..AeroSurface import EllipticalFins, NoseCone, Tail, TrapezoidalFins +from ..AeroSurface import EllipticalFins, NoseCone, RailButtons, Tail, TrapezoidalFins from ..Rocket import Rocket from .DispersionModel import DispersionModel from .mc_aero_surfaces import ( @@ -45,22 +46,22 @@ class McRocket(DispersionModel): powerOffDragFactor: Any = (1, 0) powerOnDragFactor: Any = (1, 0) # Private attributes for the add methods - _motors: list = PrivateAttr() - _nosecones: list = PrivateAttr() - _fins: list = PrivateAttr() - _tails: list = PrivateAttr() + _motors: Components = PrivateAttr() + _nosecones: Components = PrivateAttr() + _fins: Components = PrivateAttr() + _tails: Components = PrivateAttr() _parachutes: list = PrivateAttr() - _rail_buttons: McRailButtons = PrivateAttr() + _rail_buttons: Components = PrivateAttr() def __init__(self, **kwargs): """Initializes private attributes and calls DispersionModel __init__""" super().__init__(**kwargs) - self._motors = [] - self._nosecones = [] - self._fins = [] - self._tails = [] + self._motors = Components() + self._nosecones = Components() + self._fins = Components() + self._tails = Components() self._parachutes = [] - self._rail_buttons = None + self._rail_buttons = Components() # getters for attributes of the add methods @property @@ -87,9 +88,31 @@ def parachutes(self): def rail_buttons(self): return self._rail_buttons - def _validate_position(self, position, obj, attr_name): + def _validate_position(self, position): """Checks if 'position' argument was correctly inputted in the 'add' - methods. The logic is the same as in the set_attr root validator.""" + methods. Position can be a tuple or a list. If it is a tuple, it must + have length 2 or 3. If it has length 2, the first item is the nominal + value of the position, and the second item is the standard deviation. + If it has length 3, the first item is the nominal value, the second + item is the standard deviation, and the third item is a string with the + name of a numpy.random distribution function. If position is a list, + If a list is passed, the code will chose a random item of the list + in each simulation ran. + + Parameters + ---------- + position : tuple, list + Position inputted in the 'add' methods. + component_tuple :namedtuple + + + Returns + ------- + tuple + Tuple with the nominal value, standard deviation, and distribution + function. + + """ # checks if tuple if isinstance(position, tuple): # checks if tuple has acceptable length @@ -101,87 +124,49 @@ def _validate_position(self, position, obj, attr_name): assert isinstance( position[0], (int, float) ), f"\nposition: \n\tFirst item of tuple must be either an int or float" - # if len is two can either be (nom_val,std) or (std,'dist_func') + # if len is two can only be (nom_val,std) if len(position) == 2: - # checks if second value is either string or int/float - assert isinstance( - position[1], (int, float, str) - ), f"position: second item of tuple must be an int, float or string. If the first value refers to the nominal value of 'position', then the item's second value should be the desired standard deviation. If the first value is the standard deviation, then the item's second value should be a string containing a name of a numpy.random distribution function" - # if second item is not str, then (nom_val, std) - if not isinstance(position[1], str): - return (position[0], position[1], get_distribution("normal")) - # if second item is str, then (nom_val, std, str) - else: - # tries to get position from object - # this checks if the object has the position attribute - # meaning it was defined using a rocket add method - # if not, then position must be inputted in McRocket add methods - try: - nom_value = getattr(obj, attr_name) - except: - raise AttributeError( - "Attribute 'position' not found. Position should be passed in the 'position' argument of the 'add' method." - ) - return (nom_value, position[0], get_distribution(position[1])) + # checks if second value is int/float + assert isinstance(position[1], (int, float)), ( + f"\nposition:" + + " \n\tSecond item of tuple must be an int or float." + + " representing the desired standard deviation." + ) + return (position[0], position[1], get_distribution("normal")) # if len is three, then (nom_val, std, 'dist_func') if len(position) == 3: - assert isinstance( - position[1], (int, float) - ), f"position: second item of tuple must be either an int or float, representing the standard deviation to be used in the simulation" - assert isinstance( - position[2], str - ), f"position: third item of tuple must be a string containing the name of a valid numpy.random distribution function" + assert isinstance(position[1], (int, float)), ( + f"\nposition:" + + " \n\tSecond item of tuple must be either an int or float," + + " representing the standard deviation to be used in the" + + " simulation" + ) + assert isinstance(position[2], str), ( + f"\nposition:" + + " \n\tThird item of tuple must be a string containing" + + " the name of a valid numpy.random distribution function" + ) return (position[0], position[1], get_distribution(position[2])) elif isinstance(position, list): - # checks if input list is empty, meaning nothing was inputted - # and values should be gotten from class - if len(position) == 0: - # tries to get position from object - # this checks if the object has the position attribute - # meaning it was defined using a rocket add method - # if not, then position must be inputted in McRocket add methods - try: - nom_value = getattr(obj, attr_name) - except: - raise AttributeError( - "Attribute 'position' not found, it should be passed in the 'position' argument of the 'add' method." - ) - return [nom_value] - else: - # guarantee all values are valid (ints or floats) - assert all( - isinstance(item, (int, float)) for item in position - ), f"\nposition: \n\tItems in list must be either ints or floats" - # all good, sets inputs - return position - elif isinstance(position, (int, float)): - # not list or tuple, must be an int or float - # get attr and returns (nom_value, std) - - # tries to get position from object - # this checks if the object has the position attribute - # meaning it was defined using a rocket add method - # if not, then position must be inputted in McRocket add methods - try: - nom_value = getattr(obj, attr_name) - except: - raise AttributeError( - "Attribute 'position' not found. Position should be passed in the 'position' argument of the 'add' method." - ) - return (nom_value, position, get_distribution("normal")) + # guarantee all values are valid (ints or floats) + assert all( + isinstance(item, (int, float)) for item in position + ), f"\nposition: \n\tItems in list must be either ints or floats" + # all good, sets inputs + return position else: raise ValueError( f"The 'position' argument must be tuple, list, int or float" ) - def addMotor(self, motor, position=[]): + def addMotor(self, motor, position): """Adds a motor to the McRocket object. Parameters ---------- motor : McSolidMotor The motor to be added to the rocket. Must be a McSolidMotor type. - position : int, float, tuple, list, optional + position : int, float, tuple, list Position of the motor in relation to rocket's coordinate system. If float or int, refers to the standard deviation. In this case, the nominal value of that attribute will come from the motor object @@ -207,18 +192,17 @@ def addMotor(self, motor, position=[]): # checks if input is a McSolidMotor type if not isinstance(motor, McSolidMotor): raise TypeError("motor must be of McMotor type") - motor.position = self._validate_position(position, self.rocket, "motorPosition") - self.motors.append(motor) + self.motors.add(motor, self._validate_position(position)) return None - def addNose(self, nose, position=[]): + def addNose(self, nose, position): """Adds a nose cone to the McRocket object. Parameters ---------- - nose : McNoseCone + nose : McNoseCone #TODO add NoseCone type and include in the description The nose cone to be added to the rocket. Must be a McNoseCone type. - position : int, float, tuple, list, optional + position : int, float, tuple, list Position of the nose cone in relation to rocket's coordinate system. If float or int, refers to the standard deviation. In this case, the nominal value of that attribute will come from the nose cone object @@ -249,18 +233,17 @@ def addNose(self, nose, position=[]): if isinstance(nose, NoseCone): # create McNoseCone nose = McNoseCone(nosecone=nose) - nose.position = self._validate_position(position, nose.nosecone, "position") - self.nosecones.append(nose) + self.nosecones.add(nose, self._validate_position(position)) return None - def addTrapezoidalFins(self, fins, position=[]): + def addTrapezoidalFins(self, fins, position): """Adds a trapezoidal fin set to the McRocket object. Parameters ---------- fins : McTrapezoidalFins The trapezoidal fin set to be added to the rocket. Must be a McTrapezoidalFins type. - position : int, float, tuple, list, optional + position : int, float, tuple, list Position of the trapezoidal fin set in relation to rocket's coordinate system. If float or int, refers to the standard deviation. In this case, the nominal value of that attribute will come from the trapezoidal fin set object @@ -289,20 +272,17 @@ def addTrapezoidalFins(self, fins, position=[]): if isinstance(fins, TrapezoidalFins): # create McTrapezoidalFins fins = McTrapezoidalFins(trapezoidalFins=fins) - fins.position = self._validate_position( - position, fins.trapezoidalFins, "position" - ) - self.fins.append(fins) + self.fins.add(fins, self._validate_position(position)) return None - def addEllipticalFins(self, fins, position=[]): + def addEllipticalFins(self, fins, position): """Adds a elliptical fin set to the McRocket object. Parameters ---------- fins : McEllipticalFins The elliptical fin set to be added to the rocket. Must be a McEllipticalFins type. - position : int, float, tuple, list, optional + position : int, float, tuple, list Position of the elliptical fin set in relation to rocket's coordinate system. If float or int, refers to the standard deviation. In this case, the nominal value of that attribute will come from the elliptical fin set object @@ -331,20 +311,17 @@ def addEllipticalFins(self, fins, position=[]): if isinstance(fins, EllipticalFins): # create McEllipticalFins fins = McEllipticalFins(ellipticalFins=fins) - fins.position = self._validate_position( - position, fins.ellipticalFins, "position" - ) - self.fins.append(fins) + self.fins.add(fins, self._validate_position(position)) return None - def addTail(self, tail, position=[]): + def addTail(self, tail, position): """Adds a tail to the McRocket object. Parameters ---------- tail : McTail The tail to be added to the rocket. Must be a McTail type. - position : int, float, tuple, list, optional + position : int, float, tuple, list Position of the tail in relation to rocket's coordinate system. If float or int, refers to the standard deviation. In this case, the nominal value of that attribute will come from the tail object @@ -373,8 +350,7 @@ def addTail(self, tail, position=[]): if isinstance(tail, Tail): # create McTail tail = McTail(tail=tail) - tail.position = self._validate_position(position, tail.tail, "position") - self.tails.append(tail) + self.tails.add(tail, self._validate_position(position)) return None def addParachute(self, parachute): @@ -404,6 +380,7 @@ def addParachute(self, parachute): def setRailButtons( self, rail_buttons, + lower_button_position, ): """Set rail buttons to the McRocket object. @@ -412,6 +389,21 @@ def setRailButtons( rail_buttons : McRailButtons The rail buttons to be added to the rocket. This must be a McRailButtons type. + position : int, float, tuple, list + Position of the lower rail button (closest to the nozzle) + in relation to rocket's coordinate system. If float or int, + refers to the standard deviation. In this case, the nominal + value of that attribute will come from the tail object passed. + If the distribution function needs to be specified, then a + tuple with the standard deviation as the first item, and the string + containing the name a numpy.random distribution function can be + passed e.g. (std, "dist_function"). + If a tuple with a nominal value and a standard deviation is passed, + then it will take priority over the tail object attribute's value. + A third item can also be added to the tuple specifying the + distribution function e.g. (nom_value, std, "dist_function"). + If a list is passed, the code will chose a random item of the list + in each simulation of the dispersion. Returns ------- @@ -422,9 +414,14 @@ def setRailButtons( TypeError In case the input is not a McRailButtons type. """ - if not isinstance(rail_buttons, McRailButtons): + if not isinstance(rail_buttons, (McRailButtons, RailButtons)): raise TypeError("rail_buttons must be of McRailButtons type") - self.rail_buttons = rail_buttons + if isinstance(rail_buttons, RailButtons): + # create McRailButtons + rail_buttons = McRailButtons(rail_buttons=rail_buttons) + self.rail_buttons.add( + rail_buttons, self._validate_position(lower_button_position) + ) return None def create_object(self): @@ -457,23 +454,27 @@ def create_object(self): if self.motors: for motor in self.motors: - m = motor.create_object() - obj.addMotor(m, m.position) + m = motor.component.create_object() + position_rnd = motor.position[-1](*motor.position[:-1]) + obj.addMotor(m, position_rnd) if self.nosecones: for nosecone in self.nosecones: - nose = nosecone.create_object() - obj.addSurface(nose, nose.position) + n = nosecone.component.create_object() + position_rnd = nosecone.position[-1](*nosecone.position[:-1]) + obj.addSurfaces(n, position_rnd) if self.fins: for fin in self.fins: - f = fin.create_object() - obj.addSurface(f, f.position) + f = fin.component.create_object() + position_rnd = fin.position[-1](*fin.position[:-1]) + obj.addSurfaces(f, position_rnd) if self.tails: for tail in self.tails: - t = tail.create_object() - obj.addSurface(t, t.position) + t = tail.component.create_object() + position_rnd = tail.position[-1](*tail.position[:-1]) + obj.addSurfaces(t, position_rnd) if self.parachutes: for parachute in self.parachutes: @@ -487,11 +488,19 @@ def create_object(self): noise=p.noise, ) - if self.rail_buttons: - r = self.rail_buttons.create_object() - obj.setRailButtons( - position=(r.upper_button_position, r.lower_button_position), - angular_position=r.angular_position, - ) + if len(self.rail_buttons) != 0: + for rail_buttons in self.rail_buttons: + r = rail_buttons.component.create_object() + lower_button_position_rnd = rail_buttons.position[-1]( + *rail_buttons.position[:-1] + ) + upper_button_position_rnd = ( + r.buttons_distance + lower_button_position_rnd + ) + obj.setRailButtons( + upper_button_position=upper_button_position_rnd, + lower_button_position=lower_button_position_rnd, + angular_position=r.angular_position, + ) return obj