From b99b030dc2d50dcf90603adc9f2ee492a4a7dbab Mon Sep 17 00:00:00 2001 From: Julio Machado Date: Tue, 6 Aug 2024 09:25:00 -0300 Subject: [PATCH 1/5] ENH: Implementing treatment for dry_mass parameter (get value from eng file by default if eng file exists) --- rocketpy/motors/hybrid_motor.py | 20 +++++------ rocketpy/motors/liquid_motor.py | 20 +++++------ rocketpy/motors/motor.py | 61 ++++++++++++++++++++++++++------- rocketpy/motors/solid_motor.py | 20 +++++------ 4 files changed, 78 insertions(+), 43 deletions(-) diff --git a/rocketpy/motors/hybrid_motor.py b/rocketpy/motors/hybrid_motor.py index 37fa1b12c..1a1bbb3db 100644 --- a/rocketpy/motors/hybrid_motor.py +++ b/rocketpy/motors/hybrid_motor.py @@ -302,16 +302,16 @@ class Function. Thrust units are Newtons. None """ super().__init__( - thrust_source, - dry_mass, - dry_inertia, - nozzle_radius, - center_of_dry_mass_position, - nozzle_position, - burn_time, - reshape_thrust_curve, - interpolation_method, - coordinate_system_orientation, + thrust_source=thrust_source, + dry_inertia=dry_inertia, + nozzle_radius=nozzle_radius, + center_of_dry_mass_position=center_of_dry_mass_position, + dry_mass=dry_mass, + nozzle_position=nozzle_position, + burn_time=burn_time, + reshape_thrust_curve=reshape_thrust_curve, + interpolation_method=interpolation_method, + coordinate_system_orientation=coordinate_system_orientation, ) self.liquid = LiquidMotor( thrust_source, diff --git a/rocketpy/motors/liquid_motor.py b/rocketpy/motors/liquid_motor.py index e519346c3..09e3aadd5 100644 --- a/rocketpy/motors/liquid_motor.py +++ b/rocketpy/motors/liquid_motor.py @@ -230,16 +230,16 @@ class Function. Thrust units are Newtons. "nozzle_to_combustion_chamber". """ super().__init__( - thrust_source, - dry_mass, - dry_inertia, - nozzle_radius, - center_of_dry_mass_position, - nozzle_position, - burn_time, - reshape_thrust_curve, - interpolation_method, - coordinate_system_orientation, + thrust_source=thrust_source, + dry_inertia=dry_inertia, + nozzle_radius=nozzle_radius, + center_of_dry_mass_position=center_of_dry_mass_position, + dry_mass=dry_mass, + nozzle_position=nozzle_position, + burn_time=burn_time, + reshape_thrust_curve=reshape_thrust_curve, + interpolation_method=interpolation_method, + coordinate_system_orientation=coordinate_system_orientation, ) self.positioned_tanks = [] diff --git a/rocketpy/motors/motor.py b/rocketpy/motors/motor.py index b37172348..b302594f2 100644 --- a/rocketpy/motors/motor.py +++ b/rocketpy/motors/motor.py @@ -149,10 +149,10 @@ class Motor(ABC): def __init__( self, thrust_source, - dry_mass, dry_inertia, nozzle_radius, center_of_dry_mass_position, + dry_mass=None, nozzle_position=0, burn_time=None, reshape_thrust_curve=False, @@ -251,7 +251,6 @@ class Function. Thrust units are Newtons. ) # Motor parameters - self.dry_mass = dry_mass self.interpolate = interpolation_method self.nozzle_position = nozzle_position self.nozzle_radius = nozzle_radius @@ -267,9 +266,10 @@ class Function. Thrust units are Newtons. self.dry_I_23 = inertia[5] # Handle .eng file inputs + self.description_eng_file = None if isinstance(thrust_source, str): if thrust_source[-3:] == "eng": - _, _, points = Motor.import_eng(thrust_source) + _, self.description_eng_file, points = Motor.import_eng(thrust_source) thrust_source = points # Evaluate raw thrust source @@ -278,6 +278,9 @@ class Function. Thrust units are Newtons. thrust_source, "Time (s)", "Thrust (N)", self.interpolate, "zero" ) + # Handle dry_mass input + self.dry_mass = dry_mass + # Handle burn_time input self.burn_time = burn_time @@ -343,6 +346,38 @@ def burn_time(self, burn_time): " argument must be specified." ) + @property + def dry_mass(self): + """Dry mass of the motor in kg. + + Returns + ------- + self.dry_mass : float + Motor dry mass in kg. + """ + return self._dry_mass + + @dry_mass.setter + def dry_mass(self, dry_mass): + """Sets dry mass of the motor in kg. + + Parameters + ---------- + dry_mass : float + Motor dry mass in kg. + """ + if dry_mass: + if isinstance(dry_mass, (int, float)): + self._dry_mass = dry_mass + else: + raise ValueError("Dry mass must be a number.") + elif self.description_eng_file: + self._dry_mass = float(self.description_eng_file[-2]) - float( + self.description_eng_file[-3] + ) + else: + raise ValueError("Dry mass must be specified.") + @cached_property def total_impulse(self): """Calculates and returns total impulse by numerical integration @@ -1152,16 +1187,16 @@ def __init__( "nozzle_to_combustion_chamber". """ super().__init__( - thrust_source, - dry_mass, - dry_inertia, - nozzle_radius, - center_of_dry_mass_position, - nozzle_position, - burn_time, - reshape_thrust_curve, - interpolation_method, - coordinate_system_orientation, + thrust_source=thrust_source, + dry_inertia=dry_inertia, + nozzle_radius=nozzle_radius, + center_of_dry_mass_position=center_of_dry_mass_position, + dry_mass=dry_mass, + nozzle_position=nozzle_position, + burn_time=burn_time, + reshape_thrust_curve=reshape_thrust_curve, + interpolation_method=interpolation_method, + coordinate_system_orientation=coordinate_system_orientation, ) self.chamber_radius = chamber_radius diff --git a/rocketpy/motors/solid_motor.py b/rocketpy/motors/solid_motor.py index fa56865f5..81faf453f 100644 --- a/rocketpy/motors/solid_motor.py +++ b/rocketpy/motors/solid_motor.py @@ -303,16 +303,16 @@ class Function. Thrust units are Newtons. None """ super().__init__( - thrust_source, - dry_mass, - dry_inertia, - nozzle_radius, - center_of_dry_mass_position, - nozzle_position, - burn_time, - reshape_thrust_curve, - interpolation_method, - coordinate_system_orientation, + thrust_source=thrust_source, + dry_inertia=dry_inertia, + nozzle_radius=nozzle_radius, + center_of_dry_mass_position=center_of_dry_mass_position, + dry_mass=dry_mass, + nozzle_position=nozzle_position, + burn_time=burn_time, + reshape_thrust_curve=reshape_thrust_curve, + interpolation_method=interpolation_method, + coordinate_system_orientation=coordinate_system_orientation, ) # Nozzle parameters self.throat_radius = throat_radius From 0aef5a17e0300e5d7bca7bd098cbce2c0d50a520 Mon Sep 17 00:00:00 2001 From: Julio Machado Date: Sat, 24 Aug 2024 02:58:08 -0300 Subject: [PATCH 2/5] ENH: creation of method to build a generic motor from a eng file --- rocketpy/motors/motor.py | 139 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/rocketpy/motors/motor.py b/rocketpy/motors/motor.py index b302594f2..8ed9e40e4 100644 --- a/rocketpy/motors/motor.py +++ b/rocketpy/motors/motor.py @@ -1214,6 +1214,145 @@ def __init__( self.prints = _MotorPrints(self) self.plots = _MotorPlots(self) + @staticmethod + def load_from_eng_file( + file_name, + nozzle_radius, + chamber_radius=None, + chamber_height=None, + chamber_position=0, + propellant_initial_mass=None, + dry_mass=None, + burn_time=None, + center_of_dry_mass_position=None, + dry_inertia=(0, 0, 0), + nozzle_position=0, + reshape_thrust_curve=False, + interpolation_method="linear", + coordinate_system_orientation="nozzle_to_combustion_chamber", + ): + """Loads motor data from a .eng file and processes it. + + Parameters + ---------- + file_name : string + Name of the .eng file. E.g. 'test.eng'. + nozzle_radius : int, float + Motor's nozzle outlet radius in meters. + chamber_radius : int, float, optional + The radius of a overall cylindrical chamber of propellant in meters. + chamber_height : int, float, optional + The height of a overall cylindrical chamber of propellant in meters. + chamber_position : int, float, optional + The position, in meters, of the centroid (half height) of the motor's + overall cylindrical chamber of propellant with respect to the motor's + coordinate system. + propellant_initial_mass : int, float, optional + The initial mass of the propellant in the motor. + dry_mass : int, float, optional + Same as in Motor class. See the :class:`Motor ` docs + burn_time: float, tuple of float, optional + Motor's burn time. + If a float is given, the burn time is assumed to be between 0 and + the given float, in seconds. + If a tuple of float is given, the burn time is assumed to be between + the first and second elements of the tuple, in seconds. + If not specified, automatically sourced as the range between the + first and last-time step of the motor's thrust curve. This can only + be used if the motor's thrust is defined by a list of points, such + as a .csv file, a .eng file or a Function instance whose source is a + list. + center_of_dry_mass_position : int, float, optional + The position, in meters, of the motor's center of mass with respect + to the motor's coordinate system when it is devoid of propellant. + If not specified, automatically sourced as the chamber position. + dry_inertia : tuple, list + Tuple or list containing the motor's dry mass inertia tensor + nozzle_position : int, float, optional + Motor's nozzle outlet position in meters, in the motor's coordinate + system. Default is 0, in which case the origin of the + coordinate system is placed at the motor's nozzle outlet. + reshape_thrust_curve : boolean, tuple, optional + If False, the original thrust curve supplied is not altered. If a + tuple is given, whose first parameter is a new burn out time and + whose second parameter is a new total impulse in Ns, the thrust + curve is reshaped to match the new specifications. May be useful + for motors whose thrust curve shape is expected to remain similar + in case the impulse and burn time varies slightly. Default is + False. Note that the Motor burn_time parameter must include the new + reshaped burn time. + interpolation_method : string, optional + Method of interpolation to be used in case thrust curve is given + coordinate_system_orientation : string, optional + Orientation of the motor's coordinate system. The coordinate system + is defined by the motor's axis of symmetry. The origin of the + coordinate system may be placed anywhere along such axis, such as + at the nozzle area, and must be kept the same for all other + positions specified. Options are "nozzle_to_combustion_chamber" and + "combustion_chamber_to_nozzle". Default is + "nozzle_to_combustion_chamber". + Returns + ------- + Generic Motor object + """ + if isinstance(file_name, str): + if file_name[-3:] == "eng": + comments, description, thrust_source = Motor.import_eng(file_name) + else: + raise ValueError("File must be a .eng file.") + else: + raise ValueError("File name must be a string.") + + thrust = Function(thrust_source, "Time (s)", "Thrust (N)", "linear", "zero") + + # handle eng parameters + + if chamber_radius: + if not isinstance(chamber_radius, (int, float)): + raise ValueError("Chamber radius must be a number.") + else: + chamber_radius = float(description[1]) # get motor diameter + + if chamber_height: + if not isinstance(chamber_height, (int, float)): + raise ValueError("Chamber height must be a number.") + else: + chamber_height = float(description[2]) # get motor length + + if propellant_initial_mass: + if not isinstance(propellant_initial_mass, (int, float)): + raise ValueError("Propellant initial mass must be a number.") + else: + propellant_initial_mass = float(description[-3]) + + if dry_mass: + if not isinstance(dry_mass, (int, float)): + raise ValueError("Dry mass must be a number.") + else: + total_mass = float(description[-2]) + dry_mass = total_mass - propellant_initial_mass + + e_vel = thrust.integral(*(0.0, 4.897)) / propellant_initial_mass + print(f"exhaust velocity: {e_vel:.2f} m/s ") + + gen_motor = GenericMotor( + thrust_source=thrust, + burn_time=burn_time, + chamber_radius=chamber_radius, + chamber_height=chamber_height, + chamber_position=chamber_position, + propellant_initial_mass=propellant_initial_mass, + nozzle_radius=nozzle_radius, + dry_mass=dry_mass, + center_of_dry_mass_position=center_of_dry_mass_position, + dry_inertia=dry_inertia, + nozzle_position=nozzle_position, + reshape_thrust_curve=reshape_thrust_curve, + interpolation_method=interpolation_method, + coordinate_system_orientation=coordinate_system_orientation, + ) + return gen_motor + @cached_property def propellant_initial_mass(self): """Calculates the initial mass of the propellant. From 93d05e24c05e60d9312ef3cf7b0471b0eca1b1c5 Mon Sep 17 00:00:00 2001 From: Julio Machado Date: Mon, 26 Aug 2024 21:56:07 -0300 Subject: [PATCH 3/5] ENH: Trying to pass in pylint. Missing to resolve: too-many-statements / R0915, row 1218. --- rocketpy/motors/motor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocketpy/motors/motor.py b/rocketpy/motors/motor.py index 8ed9e40e4..6aed1a916 100644 --- a/rocketpy/motors/motor.py +++ b/rocketpy/motors/motor.py @@ -1297,7 +1297,7 @@ def load_from_eng_file( """ if isinstance(file_name, str): if file_name[-3:] == "eng": - comments, description, thrust_source = Motor.import_eng(file_name) + _, description, thrust_source = Motor.import_eng(file_name) else: raise ValueError("File must be a .eng file.") else: From 71204ece3081c7b58b4b26fb2b6ef73f62941b0b Mon Sep 17 00:00:00 2001 From: Julio Machado Date: Sun, 8 Sep 2024 18:25:43 -0300 Subject: [PATCH 4/5] ENH: Improvements and adjustments to the load_from_eng_file method, creation of tests for the method, creation of documentation as well --- docs/user/motors/genericmotor.rst | 55 ++++++ docs/user/motors/motors.rst | 6 + rocketpy/motors/motor.py | 273 +++++++++++++++--------------- tests/unit/test_genericmotor.py | 48 ++++++ 4 files changed, 242 insertions(+), 140 deletions(-) create mode 100644 docs/user/motors/genericmotor.rst diff --git a/docs/user/motors/genericmotor.rst b/docs/user/motors/genericmotor.rst new file mode 100644 index 000000000..4f763e15d --- /dev/null +++ b/docs/user/motors/genericmotor.rst @@ -0,0 +1,55 @@ +.. _genericmotor: + +GenericMotor Class Usage +====================== + +Here we explore different features of the GenericMotor class. + +Class that represents a simple motor defined mainly by its thrust curve. +There is no distinction between the propellant types (e.g. Solid, Liquid). +This class is meant for rough estimations of the motor performance, +therefore for more accurate results, use the ``SolidMotor``, ``HybridMotor`` +or ``LiquidMotor`` classes. + +Creating a Generic Motor +------------------------ + +To define a generic motor, we will need a few information about our motor: + +- The thrust source file, which is a file containing the thrust curve of the motor. + This file can be a .eng file, a .rse file, or a .csv file. See more details in + :doc:`Thrust Source Details ` +- A few physical parameters, which the most important are: + - The burn time of the motor. + - The chamber_radius; + - The chamber_height; + - The chamber_position; + - The propellant initial mass; + - The radius of the nozzle; + - The dry mass of the motor; + +The usage of the GenericMotor class is very similar to the other motor classes. See +more details in +:doc:`SolidMotor Class Usage `, +:doc:`LiquidMotor Class Usage `, +:doc:`HybridMotor Class Usage `. + +The ``load_from_eng_file method`` +----------------------------- + +The GenericMotor class has a method called ``load_from_eng_file`` that allows +the user to build a GenericMotor object only by providing the path to the .eng file. + +The parameters available in the method are the same as the ones used in the +constructor of the GenericMotor class. But the method will automatically read +the .eng file and extract the required information if the user does not +provide it. In this case, the following assumptions about the most +relevant parameters are made: + +- The chamber_radius is assumed to be the same as the motor diameter in the .eng file; +- The chamber_height is assumed to be the same as the motor length in the .eng file; +- The chamber_position is assumed to be 0; +- The propellant initial mass is assumed to be the same as the propellant mass in the .eng file; +- The nozzle_radius is assumed to be 85% of the chamber_radius; +- The dry mass is assumed to be the total mass minus the propellant mass in the .eng file; + diff --git a/docs/user/motors/motors.rst b/docs/user/motors/motors.rst index b4c4e4f8c..a6bc6ef4b 100644 --- a/docs/user/motors/motors.rst +++ b/docs/user/motors/motors.rst @@ -24,6 +24,12 @@ Motors Usage :caption: Liquid Motors Liquid Motor Usage + +.. toctree:: + :maxdepth: 3 + :caption: Generic Motors + + Generic Motor Usage .. toctree:: :maxdepth: 3 diff --git a/rocketpy/motors/motor.py b/rocketpy/motors/motor.py index 6aed1a916..07d969e3d 100644 --- a/rocketpy/motors/motor.py +++ b/rocketpy/motors/motor.py @@ -2,6 +2,7 @@ import warnings from abc import ABC, abstractmethod from functools import cached_property +from os import path import numpy as np @@ -268,7 +269,10 @@ class Function. Thrust units are Newtons. # Handle .eng file inputs self.description_eng_file = None if isinstance(thrust_source, str): - if thrust_source[-3:] == "eng": + if ( + path.exists(thrust_source) + and path.splitext(path.basename(thrust_source))[1] == ".eng" + ): _, self.description_eng_file, points = Motor.import_eng(thrust_source) thrust_source = points @@ -1214,145 +1218,6 @@ def __init__( self.prints = _MotorPrints(self) self.plots = _MotorPlots(self) - @staticmethod - def load_from_eng_file( - file_name, - nozzle_radius, - chamber_radius=None, - chamber_height=None, - chamber_position=0, - propellant_initial_mass=None, - dry_mass=None, - burn_time=None, - center_of_dry_mass_position=None, - dry_inertia=(0, 0, 0), - nozzle_position=0, - reshape_thrust_curve=False, - interpolation_method="linear", - coordinate_system_orientation="nozzle_to_combustion_chamber", - ): - """Loads motor data from a .eng file and processes it. - - Parameters - ---------- - file_name : string - Name of the .eng file. E.g. 'test.eng'. - nozzle_radius : int, float - Motor's nozzle outlet radius in meters. - chamber_radius : int, float, optional - The radius of a overall cylindrical chamber of propellant in meters. - chamber_height : int, float, optional - The height of a overall cylindrical chamber of propellant in meters. - chamber_position : int, float, optional - The position, in meters, of the centroid (half height) of the motor's - overall cylindrical chamber of propellant with respect to the motor's - coordinate system. - propellant_initial_mass : int, float, optional - The initial mass of the propellant in the motor. - dry_mass : int, float, optional - Same as in Motor class. See the :class:`Motor ` docs - burn_time: float, tuple of float, optional - Motor's burn time. - If a float is given, the burn time is assumed to be between 0 and - the given float, in seconds. - If a tuple of float is given, the burn time is assumed to be between - the first and second elements of the tuple, in seconds. - If not specified, automatically sourced as the range between the - first and last-time step of the motor's thrust curve. This can only - be used if the motor's thrust is defined by a list of points, such - as a .csv file, a .eng file or a Function instance whose source is a - list. - center_of_dry_mass_position : int, float, optional - The position, in meters, of the motor's center of mass with respect - to the motor's coordinate system when it is devoid of propellant. - If not specified, automatically sourced as the chamber position. - dry_inertia : tuple, list - Tuple or list containing the motor's dry mass inertia tensor - nozzle_position : int, float, optional - Motor's nozzle outlet position in meters, in the motor's coordinate - system. Default is 0, in which case the origin of the - coordinate system is placed at the motor's nozzle outlet. - reshape_thrust_curve : boolean, tuple, optional - If False, the original thrust curve supplied is not altered. If a - tuple is given, whose first parameter is a new burn out time and - whose second parameter is a new total impulse in Ns, the thrust - curve is reshaped to match the new specifications. May be useful - for motors whose thrust curve shape is expected to remain similar - in case the impulse and burn time varies slightly. Default is - False. Note that the Motor burn_time parameter must include the new - reshaped burn time. - interpolation_method : string, optional - Method of interpolation to be used in case thrust curve is given - coordinate_system_orientation : string, optional - Orientation of the motor's coordinate system. The coordinate system - is defined by the motor's axis of symmetry. The origin of the - coordinate system may be placed anywhere along such axis, such as - at the nozzle area, and must be kept the same for all other - positions specified. Options are "nozzle_to_combustion_chamber" and - "combustion_chamber_to_nozzle". Default is - "nozzle_to_combustion_chamber". - Returns - ------- - Generic Motor object - """ - if isinstance(file_name, str): - if file_name[-3:] == "eng": - _, description, thrust_source = Motor.import_eng(file_name) - else: - raise ValueError("File must be a .eng file.") - else: - raise ValueError("File name must be a string.") - - thrust = Function(thrust_source, "Time (s)", "Thrust (N)", "linear", "zero") - - # handle eng parameters - - if chamber_radius: - if not isinstance(chamber_radius, (int, float)): - raise ValueError("Chamber radius must be a number.") - else: - chamber_radius = float(description[1]) # get motor diameter - - if chamber_height: - if not isinstance(chamber_height, (int, float)): - raise ValueError("Chamber height must be a number.") - else: - chamber_height = float(description[2]) # get motor length - - if propellant_initial_mass: - if not isinstance(propellant_initial_mass, (int, float)): - raise ValueError("Propellant initial mass must be a number.") - else: - propellant_initial_mass = float(description[-3]) - - if dry_mass: - if not isinstance(dry_mass, (int, float)): - raise ValueError("Dry mass must be a number.") - else: - total_mass = float(description[-2]) - dry_mass = total_mass - propellant_initial_mass - - e_vel = thrust.integral(*(0.0, 4.897)) / propellant_initial_mass - print(f"exhaust velocity: {e_vel:.2f} m/s ") - - gen_motor = GenericMotor( - thrust_source=thrust, - burn_time=burn_time, - chamber_radius=chamber_radius, - chamber_height=chamber_height, - chamber_position=chamber_position, - propellant_initial_mass=propellant_initial_mass, - nozzle_radius=nozzle_radius, - dry_mass=dry_mass, - center_of_dry_mass_position=center_of_dry_mass_position, - dry_inertia=dry_inertia, - nozzle_position=nozzle_position, - reshape_thrust_curve=reshape_thrust_curve, - interpolation_method=interpolation_method, - coordinate_system_orientation=coordinate_system_orientation, - ) - return gen_motor - @cached_property def propellant_initial_mass(self): """Calculates the initial mass of the propellant. @@ -1481,6 +1346,134 @@ def propellant_I_13(self): def propellant_I_23(self): return Function(0) + @staticmethod + def load_from_eng_file( + file_name, + nozzle_radius=None, + chamber_radius=None, + chamber_height=None, + chamber_position=0, + propellant_initial_mass=None, + dry_mass=None, + burn_time=None, + center_of_dry_mass_position=None, + dry_inertia=(0, 0, 0), + nozzle_position=0, + reshape_thrust_curve=False, + interpolation_method="linear", + coordinate_system_orientation="nozzle_to_combustion_chamber", + ): + """Loads motor data from a .eng file and processes it. + + Parameters + ---------- + file_name : string + Name of the .eng file. E.g. 'test.eng'. + nozzle_radius : int, float + Motor's nozzle outlet radius in meters. + chamber_radius : int, float, optional + The radius of a overall cylindrical chamber of propellant in meters. + chamber_height : int, float, optional + The height of a overall cylindrical chamber of propellant in meters. + chamber_position : int, float, optional + The position, in meters, of the centroid (half height) of the motor's + overall cylindrical chamber of propellant with respect to the motor's + coordinate system. + propellant_initial_mass : int, float, optional + The initial mass of the propellant in the motor. + dry_mass : int, float, optional + Same as in Motor class. See the :class:`Motor ` docs + burn_time: float, tuple of float, optional + Motor's burn time. + If a float is given, the burn time is assumed to be between 0 and + the given float, in seconds. + If a tuple of float is given, the burn time is assumed to be between + the first and second elements of the tuple, in seconds. + If not specified, automatically sourced as the range between the + first and last-time step of the motor's thrust curve. This can only + be used if the motor's thrust is defined by a list of points, such + as a .csv file, a .eng file or a Function instance whose source is a + list. + center_of_dry_mass_position : int, float, optional + The position, in meters, of the motor's center of mass with respect + to the motor's coordinate system when it is devoid of propellant. + If not specified, automatically sourced as the chamber position. + dry_inertia : tuple, list + Tuple or list containing the motor's dry mass inertia tensor + nozzle_position : int, float, optional + Motor's nozzle outlet position in meters, in the motor's coordinate + system. Default is 0, in which case the origin of the + coordinate system is placed at the motor's nozzle outlet. + reshape_thrust_curve : boolean, tuple, optional + If False, the original thrust curve supplied is not altered. If a + tuple is given, whose first parameter is a new burn out time and + whose second parameter is a new total impulse in Ns, the thrust + curve is reshaped to match the new specifications. May be useful + for motors whose thrust curve shape is expected to remain similar + in case the impulse and burn time varies slightly. Default is + False. Note that the Motor burn_time parameter must include the new + reshaped burn time. + interpolation_method : string, optional + Method of interpolation to be used in case thrust curve is given + coordinate_system_orientation : string, optional + Orientation of the motor's coordinate system. The coordinate system + is defined by the motor's axis of symmetry. The origin of the + coordinate system may be placed anywhere along such axis, such as + at the nozzle area, and must be kept the same for all other + positions specified. Options are "nozzle_to_combustion_chamber" and + "combustion_chamber_to_nozzle". Default is + "nozzle_to_combustion_chamber". + + Returns + ------- + Generic Motor object + """ + if isinstance(file_name, str): + if path.splitext(path.basename(file_name))[1] == ".eng": + _, description, thrust_source = Motor.import_eng(file_name) + else: + raise ValueError("File must be a .eng file.") + else: + raise ValueError("File name must be a string.") + + thrust = Function(thrust_source, "Time (s)", "Thrust (N)", "linear", "zero") + + # handle eng parameters + if not chamber_radius: + chamber_radius = ( + float(description[1]) / 1000 + ) # get motor diameter in meters + + if not chamber_height: + chamber_height = float(description[2]) / 1000 # get motor length in meters + + if not propellant_initial_mass: + propellant_initial_mass = float(description[-3]) + + if not dry_mass: + total_mass = float(description[-2]) + dry_mass = total_mass - propellant_initial_mass + + if not nozzle_radius: + nozzle_radius = 0.85 * chamber_radius + + return GenericMotor( + thrust_source=thrust, + burn_time=burn_time, + chamber_radius=chamber_radius, + chamber_height=chamber_height, + chamber_position=chamber_position, + propellant_initial_mass=propellant_initial_mass, + nozzle_radius=nozzle_radius, + dry_mass=dry_mass, + center_of_dry_mass_position=center_of_dry_mass_position, + dry_inertia=dry_inertia, + nozzle_position=nozzle_position, + reshape_thrust_curve=reshape_thrust_curve, + interpolation_method=interpolation_method, + coordinate_system_orientation=coordinate_system_orientation, + ) + def all_info(self): """Prints out all data and graphs available about the Motor.""" # Print motor details diff --git a/tests/unit/test_genericmotor.py b/tests/unit/test_genericmotor.py index c6321ae4d..2454c76d3 100644 --- a/tests/unit/test_genericmotor.py +++ b/tests/unit/test_genericmotor.py @@ -2,6 +2,8 @@ import pytest import scipy.integrate +from rocketpy import Function, Motor + BURN_TIME = (2, 7) @@ -129,3 +131,49 @@ def test_generic_motor_inertia(generic_motor): assert generic_motor.I_11.y_array == pytest.approx(I_11) assert generic_motor.I_22.y_array == pytest.approx(I_22) assert generic_motor.I_33.y_array == pytest.approx(I_33) + + +def test_load_from_eng_file(generic_motor): + """Tests the GenericMotor.load_from_eng_file method. + + Parameters + ---------- + generic_motor : rocketpy.GenericMotor + The GenericMotor object to be used in the tests. + """ + # using cesaroni data as example + burnt_time = (0, 3.9) + dry_mass = 5.231 - 3.101 # 2.130 kg + propellant_initial_mass = 3.101 + chamber_radius = 75 / 1000 + chamber_height = 757 / 1000 + nozzle_radius = chamber_radius * 0.85 # nozzle radius is 85% of chamber radius + # Parameters from manual testing using the SolidMotor class as a reference + average_thrust = 1545.218 + total_impulse = 6026.350 + max_thrust = 2200.0 + exhaust_velocity = 1943.357 + + # importing .eng file + generic_motor = generic_motor.load_from_eng_file("data/motors/Cesaroni_M1670.eng") + + # testing relevant parameters + assert generic_motor.burn_time == burnt_time + assert generic_motor.dry_mass == dry_mass + assert generic_motor.propellant_initial_mass == propellant_initial_mass + assert generic_motor.chamber_radius == chamber_radius + assert generic_motor.chamber_height == chamber_height + assert generic_motor.chamber_position == 0 + assert generic_motor.average_thrust == pytest.approx(average_thrust) + assert generic_motor.total_impulse == pytest.approx(total_impulse) + assert generic_motor.exhaust_velocity.average(*burnt_time) == pytest.approx( + exhaust_velocity + ) + assert generic_motor.max_thrust == pytest.approx(max_thrust) + assert generic_motor.nozzle_radius == pytest.approx(nozzle_radius) + + # testing thrust curve + _, _, points = Motor.import_eng("data/motors/Cesaroni_M1670.eng") + assert generic_motor.thrust.y_array == pytest.approx( + Function(points, "Time (s)", "Thrust (N)", "linear", "zero").y_array + ) From 7e33586bceca9179d70e91d4a808ace723723533 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Sun, 8 Sep 2024 20:11:55 -0300 Subject: [PATCH 5/5] MNT: small fixes to load_from_eng PR --- CHANGELOG.md | 6 +- docs/user/motors/genericmotor.rst | 97 ++++++++++++++++++++++++------- tests/unit/test_genericmotor.py | 11 ++-- 3 files changed, 85 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c084940e1..0bc6754f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,17 +32,19 @@ Attention: The newest changes should be on top --> ### Added +- ENH: Adds GenericMotor.load_from_eng_file() method [#676](https://github.com/RocketPy-Team/RocketPy/pull/676) +- ENH: Introducing local sensitivity analysis [#575](https://github.com/RocketPy-Team/RocketPy/pull/575) - ENH: Add STFT function to Function class [#620](https://github.com/RocketPy-Team/RocketPy/pull/620) - ENH: Rocket Axis Definition [#635](https://github.com/RocketPy-Team/RocketPy/pull/635) ### Changed -- DOC: Fix documentation dependencies [#651](https://github.com/RocketPy-Team/RocketPy/pull/651) -- DOC: Fix documentation warnings [#645](https://github.com/RocketPy-Team/RocketPy/pull/645) - DOC: New Environment class docs pages [#644](https://github.com/RocketPy-Team/RocketPy/pull/644) ### Fixed +- DOC: Fix documentation dependencies [#651](https://github.com/RocketPy-Team/RocketPy/pull/651) +- DOC: Fix documentation warnings [#645](https://github.com/RocketPy-Team/RocketPy/pull/645) - BUG: Rotational EOMs Not Relative To CDM [#674](https://github.com/RocketPy-Team/RocketPy/pull/674) - BUG: Pressure ISA Extrapolation as "linear" [#675](https://github.com/RocketPy-Team/RocketPy/pull/675) - BUG: fix the Frequency Response plot of Flight class [#653](https://github.com/RocketPy-Team/RocketPy/pull/653) diff --git a/docs/user/motors/genericmotor.rst b/docs/user/motors/genericmotor.rst index 4f763e15d..5bd14d548 100644 --- a/docs/user/motors/genericmotor.rst +++ b/docs/user/motors/genericmotor.rst @@ -1,7 +1,7 @@ .. _genericmotor: GenericMotor Class Usage -====================== +======================== Here we explore different features of the GenericMotor class. @@ -16,29 +16,61 @@ Creating a Generic Motor To define a generic motor, we will need a few information about our motor: -- The thrust source file, which is a file containing the thrust curve of the motor. - This file can be a .eng file, a .rse file, or a .csv file. See more details in +- The thrust source file, which is a file containing the thrust curve of the motor. \ + This file can be a .eng file, a .rse file, or a .csv file. See more details in \ :doc:`Thrust Source Details ` - A few physical parameters, which the most important are: - The burn time of the motor. - - The chamber_radius; - - The chamber_height; - - The chamber_position; + - The combustion chamber radius; + - The combustion chamber height; + - The combustion chamber position; - The propellant initial mass; - - The radius of the nozzle; - - The dry mass of the motor; + - The nozzle radius; + - The motor dry mass. -The usage of the GenericMotor class is very similar to the other motor classes. See -more details in +The usage of the GenericMotor class is very similar to the other motor classes. +See more details in the :doc:`SolidMotor Class Usage `, -:doc:`LiquidMotor Class Usage `, -:doc:`HybridMotor Class Usage `. +:doc:`LiquidMotor Class Usage `, and +:doc:`HybridMotor Class Usage ` pages. -The ``load_from_eng_file method`` ------------------------------ -The GenericMotor class has a method called ``load_from_eng_file`` that allows -the user to build a GenericMotor object only by providing the path to the .eng file. +.. jupyter-execute:: + + from rocketpy.motors import GenericMotor + + # Define the motor parameters + motor = GenericMotor( + thrust_source = "../data/motors/Cesaroni_M1670.eng", + burn_time = 3.9, + chamber_radius = 33 / 100, + chamber_height = 600 / 1000, + chamber_position = 0, + propellant_initial_mass = 2.5, + nozzle_radius = 33 / 1000, + dry_mass = 1.815, + center_of_dry_mass_position = 0, + dry_inertia = (0.125, 0.125, 0.002), + nozzle_position = 0, + reshape_thrust_curve = False, + interpolation_method = "linear", + coordinate_system_orientation = "nozzle_to_combustion_chamber", + ) + + # Print the motor information + motor.info() + +.. note:: + + The GenericMotor is a simplified model of a rocket motor. If you need more \ + accurate results, use the ``SolidMotor``, ``HybridMotor`` or ``LiquidMotor`` classes. + + +The ``load_from_eng_file`` method +--------------------------------- + +The ``GenericMotor`` class has a method called ``load_from_eng_file`` that allows +the user to build a GenericMotor object by providing just the path to an .eng file. The parameters available in the method are the same as the ones used in the constructor of the GenericMotor class. But the method will automatically read @@ -46,10 +78,31 @@ the .eng file and extract the required information if the user does not provide it. In this case, the following assumptions about the most relevant parameters are made: -- The chamber_radius is assumed to be the same as the motor diameter in the .eng file; -- The chamber_height is assumed to be the same as the motor length in the .eng file; -- The chamber_position is assumed to be 0; -- The propellant initial mass is assumed to be the same as the propellant mass in the .eng file; -- The nozzle_radius is assumed to be 85% of the chamber_radius; -- The dry mass is assumed to be the total mass minus the propellant mass in the .eng file; +- The ``chamber_radius`` is assumed to be the same as the motor diameter in the .eng file; +- The ``chamber_height`` is assumed to be the same as the motor length in the .eng file; +- The ``chamber_position`` is assumed to be 0; +- The ``propellant_initial_mass`` is assumed to be the same as the propellant mass in the .eng file; +- The ``nozzle_radius`` is assumed to be 85% of the ``chamber_radius``; +- The ``dry_mass`` is assumed to be the total mass minus the propellant mass in the .eng file; + +As an example, we can demonstrate: + +.. jupyter-execute:: + + from rocketpy.motors import GenericMotor + + + # Load the motor from an .eng file + motor = GenericMotor.load_from_eng_file("../data/motors/Cesaroni_M1670.eng") + + # Print the motor information + motor.info() + +Although the ``load_from_eng_file`` method is very useful, it is important to +note that the user can still provide the parameters manually if needed. + +.. tip:: + + The ``load_from_eng_file`` method is a very useful tool for simulating motors \ + when the user does not have all the information required to build a ``SolidMotor`` yet. diff --git a/tests/unit/test_genericmotor.py b/tests/unit/test_genericmotor.py index 2454c76d3..67e464709 100644 --- a/tests/unit/test_genericmotor.py +++ b/tests/unit/test_genericmotor.py @@ -142,23 +142,24 @@ def test_load_from_eng_file(generic_motor): The GenericMotor object to be used in the tests. """ # using cesaroni data as example - burnt_time = (0, 3.9) + burn_time = (0, 3.9) dry_mass = 5.231 - 3.101 # 2.130 kg propellant_initial_mass = 3.101 chamber_radius = 75 / 1000 chamber_height = 757 / 1000 - nozzle_radius = chamber_radius * 0.85 # nozzle radius is 85% of chamber radius + nozzle_radius = chamber_radius * 0.85 # 85% of chamber radius + # Parameters from manual testing using the SolidMotor class as a reference average_thrust = 1545.218 total_impulse = 6026.350 max_thrust = 2200.0 exhaust_velocity = 1943.357 - # importing .eng file + # creating motor from .eng file generic_motor = generic_motor.load_from_eng_file("data/motors/Cesaroni_M1670.eng") # testing relevant parameters - assert generic_motor.burn_time == burnt_time + assert generic_motor.burn_time == burn_time assert generic_motor.dry_mass == dry_mass assert generic_motor.propellant_initial_mass == propellant_initial_mass assert generic_motor.chamber_radius == chamber_radius @@ -166,7 +167,7 @@ def test_load_from_eng_file(generic_motor): assert generic_motor.chamber_position == 0 assert generic_motor.average_thrust == pytest.approx(average_thrust) assert generic_motor.total_impulse == pytest.approx(total_impulse) - assert generic_motor.exhaust_velocity.average(*burnt_time) == pytest.approx( + assert generic_motor.exhaust_velocity.average(*burn_time) == pytest.approx( exhaust_velocity ) assert generic_motor.max_thrust == pytest.approx(max_thrust)