diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ab36ca79..7ff32bd8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,12 +32,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added +- ENH: adds `Function.remove_outliers` method [#554](https://github.com/RocketPy-Team/RocketPy/pull/554) ### Changed - +- ENH: Optional argument to show the plot in Function.compare_plots [#563](https://github.com/RocketPy-Team/RocketPy/pull/563) ### Fixed - +- BUG: export_eng 'Motor' method would not work for liquid motors. [#559](https://github.com/RocketPy-Team/RocketPy/pull/559) ## [v1.2.1] - 2024-02-22 diff --git a/rocketpy/mathutils/function.py b/rocketpy/mathutils/function.py index e8b6a9318..cefed044d 100644 --- a/rocketpy/mathutils/function.py +++ b/rocketpy/mathutils/function.py @@ -1144,6 +1144,51 @@ def low_pass_filter(self, alpha, file_path=None): title=self.title, ) + def remove_outliers_iqr(self, threshold=1.5): + """Remove outliers from the Function source using the interquartile + range method. The Function should have an array-like source. + + Parameters + ---------- + threshold : float, optional + Threshold for the interquartile range method. Default is 1.5. + + Returns + ------- + Function + The Function with the outliers removed. + + References + ---------- + [1] https://en.wikipedia.org/wiki/Outlier#Tukey's_fences + """ + + if callable(self.source): + raise TypeError( + "Cannot remove outliers if the source is a callable object." + + " The Function.source should be array-like." + ) + + x = self.x_array + y = self.y_array + y_q1 = np.percentile(y, 25) + y_q3 = np.percentile(y, 75) + y_iqr = y_q3 - y_q1 + y_lower = y_q1 - threshold * y_iqr + y_upper = y_q3 + threshold * y_iqr + + y_filtered = y[(y >= y_lower) & (y <= y_upper)] + x_filtered = x[(y >= y_lower) & (y <= y_upper)] + + return Function( + source=np.column_stack((x_filtered, y_filtered)), + inputs=self.__inputs__, + outputs=self.__outputs__, + interpolation=self.__interpolation__, + extrapolation=self.__extrapolation__, + title=self.title, + ) + # Define all presentation methods def __call__(self, *args): """Plot the Function if no argument is given. If an @@ -1474,6 +1519,7 @@ def compare_plots( force_data=False, force_points=False, return_object=False, + show=True, ): """Plots N 1-Dimensional Functions in the same plot, from a lower limit to an upper limit, by sampling the Functions several times in @@ -1481,38 +1527,43 @@ def compare_plots( Parameters ---------- - plot_list : list + plot_list : list[Tuple[Function,str]] List of Functions or list of tuples in the format (Function, label), where label is a string which will be displayed in the legend. - lower : scalar, optional - The lower limit of the interval in which the Functions are to be - plotted. The default value for function type Functions is 0. By - contrast, if the Functions given are defined by a dataset, the - default value is the lowest value of the datasets. - upper : scalar, optional - The upper limit of the interval in which the Functions are to be - plotted. The default value for function type Functions is 10. By - contrast, if the Functions given are defined by a dataset, the - default value is the highest value of the datasets. + lower : float, optional + This represents the lower limit of the interval for plotting the + Functions. If the Functions are defined by a dataset, the smallest + value from the dataset is used. If no value is provided (None), and + the Functions are of Function type, 0 is used as the default. + upper : float, optional + This represents the upper limit of the interval for plotting the + Functions. If the Functions are defined by a dataset, the largest + value from the dataset is used. If no value is provided (None), and + the Functions are of Function type, 10 is used as the default. samples : int, optional The number of samples in which the functions will be evaluated for plotting it, which draws lines between each evaluated point. The default value is 1000. - title : string, optional + title : str, optional Title of the plot. Default value is an empty string. - xlabel : string, optional + xlabel : str, optional X-axis label. Default value is an empty string. - ylabel : string, optional + ylabel : str, optional Y-axis label. Default value is an empty string. - force_data : Boolean, optional + force_data : bool, optional If Function is given by an interpolated dataset, setting force_data to True will plot all points, as a scatter, in the dataset. Default value is False. - force_points : Boolean, optional + force_points : bool, optional Setting force_points to True will plot all points, as a scatter, in which the Function was evaluated to plot it. Default value is False. + return_object : bool, optional + If True, returns the figure and axis objects. Default value is + False. + show : bool, optional + If True, shows the plot. Default value is True. Returns ------- @@ -1586,7 +1637,8 @@ def compare_plots( plt.xlabel(xlabel) plt.ylabel(ylabel) - plt.show() + if show: + plt.show() if return_object: return fig, ax diff --git a/rocketpy/motors/motor.py b/rocketpy/motors/motor.py index 0285a1203..6c2242a9f 100644 --- a/rocketpy/motors/motor.py +++ b/rocketpy/motors/motor.py @@ -1012,31 +1012,36 @@ def export_eng(self, file_name, motor_name): None """ # Open file - file = open(file_name, "w") - - # Write first line - file.write( - motor_name - + " {:3.1f} {:3.1f} 0 {:2.3} {:2.3} RocketPy\n".format( - 2000 * self.grain_outer_radius, - 1000 - * self.grain_number - * (self.grain_initial_height + self.grain_separation), - self.propellant_initial_mass, - self.propellant_initial_mass, - ) - ) + with open(file_name, "w") as file: + # Write first line + def get_attr_value(obj, attr_name, multiplier=1): + return multiplier * getattr(obj, attr_name, 0) + + grain_outer_radius = get_attr_value(self, "grain_outer_radius", 2000) + grain_number = get_attr_value(self, "grain_number", 1000) + grain_initial_height = get_attr_value(self, "grain_initial_height") + grain_separation = get_attr_value(self, "grain_separation") + + grain_total = grain_number * (grain_initial_height + grain_separation) + + if grain_outer_radius == 0 or grain_total == 0: + warnings.warn( + "The motor object doesn't have some grain-related attributes. " + "Using zeros to write to file." + ) - # Write thrust curve data points - for time, thrust in self.thrust.source[1:-1, :]: - # time, thrust = item - file.write("{:.4f} {:.3f}\n".format(time, thrust)) + file.write( + f"{motor_name} {grain_outer_radius:3.1f} {grain_total:3.1f} 0 " + f"{self.propellant_initial_mass:2.3} " + f"{self.propellant_initial_mass:2.3} RocketPy\n" + ) - # Write last line - file.write("{:.4f} {:.3f}\n".format(self.thrust.source[-1, 0], 0)) + # Write thrust curve data points + for time, thrust in self.thrust.source[1:-1, :]: + file.write(f"{time:.4f} {thrust:.3f}\n") - # Close file - file.close() + # Write last line + file.write(f"{self.thrust.source[-1, 0]:.4f} {0:.3f}\n") return None diff --git a/tests/conftest.py b/tests/conftest.py index 9afcbbdd9..4766b570a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,33 +1,19 @@ -import datetime - -import numericalunits -import numpy as np import pytest -from rocketpy import ( - CylindricalTank, - Environment, - EnvironmentAnalysis, - Flight, - Fluid, - Function, - GenericMotor, - HybridMotor, - LevelBasedTank, - LiquidMotor, - MassBasedTank, - NoseCone, - Parachute, - RailButtons, - Rocket, - SolidMotor, - SphericalTank, - Tail, - TrapezoidalFins, - UllageBasedTank, -) - -# Pytest configuration +pytest_plugins = [ + "tests.fixtures.environment.environment_fixtures", + "tests.fixtures.flight.flight_fixtures", + "tests.fixtures.function.function_fixtures", + "tests.fixtures.motor.liquid_fixtures", + "tests.fixtures.motor.hybrid_fixtures", + "tests.fixtures.motor.solid_motor_fixtures", + "tests.fixtures.motor.tanks_fixtures", + "tests.fixtures.motor.generic_motor_fixtures", + "tests.fixtures.parachutes.parachute_fixtures", + "tests.fixtures.rockets.rocket_fixtures", + "tests.fixtures.surfaces.surface_fixtures", + "tests.fixtures.units.numerical_fixtures", +] def pytest_addoption(parser): @@ -71,1245 +57,3 @@ def pytest_collection_modifyitems(config, items): for item in items: if "slow" in item.keywords: item.add_marker(skip_slow) - - -# Fixtures -## Motors and rockets - - -@pytest.fixture -def cesaroni_m1670(): # old name: solid_motor - """Create a simple object of the SolidMotor class to be used in the tests. - This is the same motor that has been used in the getting started guide for - years. - - Returns - ------- - rocketpy.SolidMotor - A simple object of the SolidMotor class - """ - example_motor = SolidMotor( - thrust_source="data/motors/Cesaroni_M1670.eng", - burn_time=3.9, - dry_mass=1.815, - dry_inertia=(0.125, 0.125, 0.002), - center_of_dry_mass_position=0.317, - nozzle_position=0, - grain_number=5, - grain_density=1815, - nozzle_radius=33 / 1000, - throat_radius=11 / 1000, - grain_separation=5 / 1000, - grain_outer_radius=33 / 1000, - grain_initial_height=120 / 1000, - grains_center_of_mass_position=0.397, - grain_initial_inner_radius=15 / 1000, - interpolation_method="linear", - coordinate_system_orientation="nozzle_to_combustion_chamber", - ) - return example_motor - - -@pytest.fixture -def cesaroni_m1670_shifted(): # old name: solid_motor - """Create a simple object of the SolidMotor class to be used in the tests. - This is the same motor that has been used in the getting started guide for - years. The difference relies in the thrust_source, which was shifted for - testing purposes. - - Returns - ------- - rocketpy.SolidMotor - A simple object of the SolidMotor class - """ - example_motor = SolidMotor( - thrust_source="tests/fixtures/motor/Cesaroni_M1670_shifted.eng", - burn_time=3.9, - dry_mass=1.815, - dry_inertia=(0.125, 0.125, 0.002), - center_of_dry_mass_position=0.317, - nozzle_position=0, - grain_number=5, - grain_density=1815, - nozzle_radius=33 / 1000, - throat_radius=11 / 1000, - grain_separation=5 / 1000, - grain_outer_radius=33 / 1000, - grain_initial_height=120 / 1000, - grains_center_of_mass_position=0.397, - grain_initial_inner_radius=15 / 1000, - interpolation_method="linear", - coordinate_system_orientation="nozzle_to_combustion_chamber", - reshape_thrust_curve=(5, 3000), - ) - return example_motor - - -@pytest.fixture -def calisto_motorless(): - """Create a simple object of the Rocket class to be used in the tests. This - is the same rocket that has been used in the getting started guide for years - but without a motor. - - Returns - ------- - rocketpy.Rocket - A simple object of the Rocket class - """ - calisto = Rocket( - radius=0.0635, - mass=14.426, - inertia=(6.321, 6.321, 0.034), - power_off_drag="data/calisto/powerOffDragCurve.csv", - power_on_drag="data/calisto/powerOnDragCurve.csv", - center_of_mass_without_motor=0, - coordinate_system_orientation="tail_to_nose", - ) - return calisto - - -@pytest.fixture -def calisto(calisto_motorless, cesaroni_m1670): # old name: rocket - """Create a simple object of the Rocket class to be used in the tests. This - is the same rocket that has been used in the getting started guide for - years. The Calisto rocket is the Projeto Jupiter's project launched at the - 2019 Spaceport America Cup. - - Parameters - ---------- - calisto_motorless : rocketpy.Rocket - An object of the Rocket class. This is a pytest fixture too. - cesaroni_m1670 : rocketpy.SolidMotor - An object of the SolidMotor class. This is a pytest fixture too. - - Returns - ------- - rocketpy.Rocket - A simple object of the Rocket class - """ - calisto = calisto_motorless - calisto.add_motor(cesaroni_m1670, position=-1.373) - return calisto - - -@pytest.fixture -def calisto_nose_to_tail(cesaroni_m1670): - """Create a simple object of the Rocket class to be used in the tests. This - is the same as the calisto fixture, but with the coordinate system - orientation set to "nose_to_tail" instead of "tail_to_nose". This allows to - check if the coordinate system orientation is being handled correctly in - the code. - - Parameters - ---------- - cesaroni_m1670 : rocketpy.SolidMotor - An object of the SolidMotor class. This is a pytest fixture too. - - Returns - ------- - rocketpy.Rocket - The Calisto rocket with the coordinate system orientation set to - "nose_to_tail". Rail buttons are already set, as well as the motor. - """ - calisto = Rocket( - radius=0.0635, - mass=14.426, - inertia=(6.321, 6.321, 0.034), - power_off_drag="data/calisto/powerOffDragCurve.csv", - power_on_drag="data/calisto/powerOnDragCurve.csv", - center_of_mass_without_motor=0, - coordinate_system_orientation="nose_to_tail", - ) - calisto.add_motor(cesaroni_m1670, position=1.373) - calisto.set_rail_buttons( - upper_button_position=-0.082, - lower_button_position=0.618, - angular_position=45, - ) - return calisto - - -@pytest.fixture -def calisto_liquid_modded(calisto_motorless, liquid_motor): - """Create a simple object of the Rocket class to be used in the tests. This - is an example of the Calisto rocket with a liquid motor. - - Parameters - ---------- - calisto_motorless : rocketpy.Rocket - An object of the Rocket class. This is a pytest fixture too. - liquid_motor : rocketpy.LiquidMotor - - Returns - ------- - rocketpy.Rocket - A simple object of the Rocket class - """ - calisto = calisto_motorless - calisto.add_motor(liquid_motor, position=-1.373) - return calisto - - -@pytest.fixture -def calisto_hybrid_modded(calisto_motorless, hybrid_motor): - """Create a simple object of the Rocket class to be used in the tests. This - is an example of the Calisto rocket with a hybrid motor. - - Parameters - ---------- - calisto_motorless : rocketpy.Rocket - An object of the Rocket class. This is a pytest fixture too. - hybrid_motor : rocketpy.HybridMotor - - Returns - ------- - rocketpy.Rocket - A simple object of the Rocket class - """ - calisto = calisto_motorless - calisto.add_motor(hybrid_motor, position=-1.373) - return calisto - - -@pytest.fixture -def calisto_robust( - calisto, - calisto_nose_cone, - calisto_tail, - calisto_trapezoidal_fins, - calisto_rail_buttons, - calisto_main_chute, - calisto_drogue_chute, -): - """Create an object class of the Rocket class to be used in the tests. This - is the same Calisto rocket that was defined in the calisto fixture, but with - all the aerodynamic surfaces and parachutes added. This avoids repeating the - same code in all tests. - - Parameters - ---------- - calisto : rocketpy.Rocket - An object of the Rocket class. This is a pytest fixture too. - calisto_nose_cone : rocketpy.NoseCone - The nose cone of the Calisto rocket. This is a pytest fixture too. - calisto_tail : rocketpy.Tail - The boat tail of the Calisto rocket. This is a pytest fixture too. - calisto_trapezoidal_fins : rocketpy.TrapezoidalFins - The trapezoidal fins of the Calisto rocket. This is a pytest fixture - calisto_rail_buttons : rocketpy.RailButtons - The rail buttons of the Calisto rocket. This is a pytest fixture too. - calisto_main_chute : rocketpy.Parachute - The main parachute of the Calisto rocket. This is a pytest fixture too. - calisto_drogue_chute : rocketpy.Parachute - The drogue parachute of the Calisto rocket. This is a pytest fixture - - Returns - ------- - rocketpy.Rocket - An object of the Rocket class - """ - # we follow this format: calisto.add_surfaces(surface, position) - calisto.add_surfaces(calisto_nose_cone, 1.160) - calisto.add_surfaces(calisto_tail, -1.313) - calisto.add_surfaces(calisto_trapezoidal_fins, -1.168) - # calisto.add_surfaces(calisto_rail_buttons, -1.168) - # TODO: if I use the line above, the calisto won't have rail buttons attribute - # we need to apply a check in the add_surfaces method to set the rail buttons - calisto.set_rail_buttons( - upper_button_position=0.082, - lower_button_position=-0.618, - angular_position=45, - ) - calisto.parachutes.append(calisto_main_chute) - calisto.parachutes.append(calisto_drogue_chute) - return calisto - - -@pytest.fixture -def calisto_air_brakes_clamp_on(calisto_robust, controller_function): - """Create an object class of the Rocket class to be used in the tests. This - is the same Calisto rocket that was defined in the calisto_robust fixture, - but with air brakes added, with clamping. - - Parameters - ---------- - calisto_robust : rocketpy.Rocket - An object of the Rocket class. This is a pytest fixture. - controller_function : function - A function that controls the air brakes. This is a pytest fixture. - - Returns - ------- - rocketpy.Rocket - An object of the Rocket class - """ - calisto = calisto_robust - # remove parachutes - calisto.parachutes = [] - calisto.add_air_brakes( - drag_coefficient_curve="data/calisto/air_brakes_cd.csv", - controller_function=controller_function, - sampling_rate=10, - clamp=True, - ) - return calisto - - -@pytest.fixture -def calisto_air_brakes_clamp_off(calisto_robust, controller_function): - """Create an object class of the Rocket class to be used in the tests. This - is the same Calisto rocket that was defined in the calisto_robust fixture, - but with air brakes added, without clamping. - - Parameters - ---------- - calisto_robust : rocketpy.Rocket - An object of the Rocket class. This is a pytest fixture. - controller_function : function - A function that controls the air brakes. This is a pytest fixture. - - Returns - ------- - rocketpy.Rocket - An object of the Rocket class - """ - calisto = calisto_robust - # remove parachutes - calisto.parachutes = [] - calisto.add_air_brakes( - drag_coefficient_curve="data/calisto/air_brakes_cd.csv", - controller_function=controller_function, - sampling_rate=10, - clamp=False, - ) - return calisto - - -@pytest.fixture -def pressurant_fluid(): - """An example of a pressurant fluid as N2 gas at - 273.15K and 30MPa. - - Returns - ------- - rocketpy.Fluid - An object of the Fluid class. - """ - return Fluid(name="N2", density=300) - - -@pytest.fixture -def fuel_pressurant(): - """An example of a pressurant fluid as N2 gas at - 273.15K and 2MPa. - - Returns - ------- - rocketpy.Fluid - An object of the Fluid class. - """ - return Fluid(name="N2", density=25) - - -@pytest.fixture -def oxidizer_pressurant(): - """An example of a pressurant fluid as N2 gas at - 273.15K and 3MPa. - - Returns - ------- - rocketpy.Fluid - An object of the Fluid class. - """ - return Fluid(name="N2", density=35) - - -@pytest.fixture -def fuel_fluid(): - """An example of propane as fuel fluid at - 273.15K and 2MPa. - - Returns - ------- - rocketpy.Fluid - An object of the Fluid class. - """ - return Fluid(name="Propane", density=500) - - -@pytest.fixture -def oxidizer_fluid(): - """An example of liquid oxygen as oxidizer fluid at - 100K and 3MPa. - - Returns - ------- - rocketpy.Fluid - An object of the Fluid class. - """ - return Fluid(name="O2", density=1000) - - -@pytest.fixture -def pressurant_tank(pressurant_fluid): - """An example of a pressurant cylindrical tank with spherical - caps. - - Parameters - ---------- - pressurant_fluid : rocketpy.Fluid - Pressurizing fluid. This is a pytest fixture. - - Returns - ------- - rocketpy.MassBasedTank - An object of the CylindricalTank class. - """ - geometry = CylindricalTank(0.135 / 2, 0.981, spherical_caps=True) - pressurant_tank = MassBasedTank( - name="Pressure Tank", - geometry=geometry, - liquid_mass=0, - flux_time=(8, 20), - gas_mass="data/SEBLM/pressurantMassFiltered.csv", - gas=pressurant_fluid, - liquid=pressurant_fluid, - ) - - return pressurant_tank - - -@pytest.fixture -def fuel_tank(fuel_fluid, fuel_pressurant): - """An example of a fuel cylindrical tank with spherical - caps. - - Parameters - ---------- - fuel_fluid : rocketpy.Fluid - Fuel fluid of the tank. This is a pytest fixture. - fuel_pressurant : rocketpy.Fluid - Pressurizing fluid of the fuel tank. This is a pytest - fixture. - - Returns - ------- - rocketpy.UllageBasedTank - """ - geometry = CylindricalTank(0.0744, 0.8068, spherical_caps=True) - ullage = ( - -Function("data/SEBLM/test124_Propane_Volume.csv") * 1e-3 - + geometry.total_volume - ) - fuel_tank = UllageBasedTank( - name="Propane Tank", - flux_time=(8, 20), - geometry=geometry, - liquid=fuel_fluid, - gas=fuel_pressurant, - ullage=ullage, - ) - - return fuel_tank - - -@pytest.fixture -def oxidizer_tank(oxidizer_fluid, oxidizer_pressurant): - """An example of a oxidizer cylindrical tank with spherical - caps. - - Parameters - ---------- - oxidizer_fluid : rocketpy.Fluid - Oxidizer fluid of the tank. This is a pytest fixture. - oxidizer_pressurant : rocketpy.Fluid - Pressurizing fluid of the oxidizer tank. This is a pytest - fixture. - - Returns - ------- - rocketpy.UllageBasedTank - """ - geometry = CylindricalTank(0.0744, 0.8068, spherical_caps=True) - ullage = ( - -Function("data/SEBLM/test124_Lox_Volume.csv") * 1e-3 + geometry.total_volume - ) - oxidizer_tank = UllageBasedTank( - name="Lox Tank", - flux_time=(8, 20), - geometry=geometry, - liquid=oxidizer_fluid, - gas=oxidizer_pressurant, - ullage=ullage, - ) - - return oxidizer_tank - - -@pytest.fixture -def liquid_motor(pressurant_tank, fuel_tank, oxidizer_tank): - """An example of a liquid motor with pressurant, fuel and oxidizer tanks. - - Parameters - ---------- - pressurant_tank : rocketpy.MassBasedTank - Tank that contains pressurizing fluid. This is a pytest fixture. - fuel_tank : rocketpy.UllageBasedTank - Tank that contains the motor fuel. This is a pytest fixture. - oxidizer_tank : rocketpy.UllageBasedTank - Tank that contains the motor oxidizer. This is a pytest fixture. - - Returns - ------- - rocketpy.LiquidMotor - """ - liquid_motor = LiquidMotor( - thrust_source="data/SEBLM/test124_Thrust_Curve.csv", - burn_time=(8, 20), - dry_mass=10, - dry_inertia=(5, 5, 0.2), - center_of_dry_mass_position=0, - nozzle_position=-1.364, - nozzle_radius=0.069 / 2, - ) - liquid_motor.add_tank(pressurant_tank, position=2.007) - liquid_motor.add_tank(fuel_tank, position=-1.048) - liquid_motor.add_tank(oxidizer_tank, position=0.711) - - return liquid_motor - - -@pytest.fixture -def spherical_oxidizer_tank(oxidizer_fluid, oxidizer_pressurant): - """An example of a oxidizer spherical tank. - - Parameters - ---------- - oxidizer_fluid : rocketpy.Fluid - Oxidizer fluid of the tank. This is a pytest fixture. - oxidizer_pressurant : rocketpy.Fluid - Pressurizing fluid of the oxidizer tank. This is a pytest - fixture. - - Returns - ------- - rocketpy.UllageBasedTank - """ - geometry = SphericalTank(0.05) - liquid_level = Function(lambda t: 0.1 * np.exp(-t / 2) - 0.05) - oxidizer_tank = LevelBasedTank( - name="Lox Tank", - flux_time=10, - geometry=geometry, - liquid=oxidizer_fluid, - gas=oxidizer_pressurant, - liquid_height=liquid_level, - ) - - return oxidizer_tank - - -@pytest.fixture -def hybrid_motor(spherical_oxidizer_tank): - """An example of a hybrid motor with spherical oxidizer - tank and fuel grains. - - Parameters - ---------- - spherical_oxidizer_tank : rocketpy.LevelBasedTank - Example Tank that contains the motor oxidizer. This is a - pytest fixture. - - Returns - ------- - rocketpy.HybridMotor - """ - motor = HybridMotor( - thrust_source=lambda t: 2000 - 100 * t, - burn_time=10, - center_of_dry_mass_position=0, - dry_inertia=(4, 4, 0.1), - dry_mass=8, - grain_density=1700, - grain_number=4, - grain_initial_height=0.1, - grain_separation=0, - grain_initial_inner_radius=0.04, - grain_outer_radius=0.1, - nozzle_position=-0.4, - nozzle_radius=0.07, - grains_center_of_mass_position=-0.1, - ) - - motor.add_tank(spherical_oxidizer_tank, position=0.3) - - return motor - - -@pytest.fixture -def generic_motor(): - """An example of a generic motor for low accuracy simulations. - - Returns - ------- - rocketpy.GenericMotor - """ - motor = GenericMotor( - burn_time=(2, 7), - thrust_source=lambda t: 2000 - 100 * (t - 2), - chamber_height=0.5, - chamber_radius=0.075, - chamber_position=-0.25, - propellant_initial_mass=5.0, - nozzle_position=-0.5, - nozzle_radius=0.075, - dry_mass=8.0, - dry_inertia=(0.2, 0.2, 0.08), - ) - - return motor - - -## AeroSurfaces - - -@pytest.fixture -def calisto_nose_cone(): - """The nose cone of the Calisto rocket. - - Returns - ------- - rocketpy.NoseCone - The nose cone of the Calisto rocket. - """ - return NoseCone( - length=0.55829, - kind="vonkarman", - base_radius=0.0635, - rocket_radius=0.0635, - name="calisto_nose_cone", - ) - - -@pytest.fixture -def calisto_tail(): - """The boat tail of the Calisto rocket. - - Returns - ------- - rocketpy.Tail - The boat tail of the Calisto rocket. - """ - return Tail( - top_radius=0.0635, - bottom_radius=0.0435, - length=0.060, - rocket_radius=0.0635, - name="calisto_tail", - ) - - -@pytest.fixture -def calisto_trapezoidal_fins(): - """The trapezoidal fins of the Calisto rocket. - - Returns - ------- - rocketpy.TrapezoidalFins - The trapezoidal fins of the Calisto rocket. - """ - return TrapezoidalFins( - n=4, - span=0.100, - root_chord=0.120, - tip_chord=0.040, - rocket_radius=0.0635, - name="calisto_trapezoidal_fins", - cant_angle=0, - sweep_length=None, - sweep_angle=None, - airfoil=None, - ) - - -@pytest.fixture -def calisto_rail_buttons(): - """The rail buttons of the Calisto rocket. - - Returns - ------- - rocketpy.RailButtons - The rail buttons of the Calisto rocket. - """ - return RailButtons( - buttons_distance=0.7, - angular_position=45, - name="Rail Buttons", - ) - - -## Parachutes - - -@pytest.fixture -def calisto_drogue_parachute_trigger(): - """The trigger for the drogue parachute of the Calisto rocket. - - Returns - ------- - function - The trigger for the drogue parachute of the Calisto rocket. - """ - - def drogue_trigger(p, h, y): - # activate drogue when vertical velocity is negative - return True if y[5] < 0 else False - - return drogue_trigger - - -@pytest.fixture -def calisto_main_parachute_trigger(): - """The trigger for the main parachute of the Calisto rocket. - - Returns - ------- - function - The trigger for the main parachute of the Calisto rocket. - """ - - def main_trigger(p, h, y): - # activate main when vertical velocity is <0 and altitude is below 800m - return True if y[5] < 0 and h < 800 else False - - return main_trigger - - -@pytest.fixture -def calisto_main_chute(calisto_main_parachute_trigger): - """The main parachute of the Calisto rocket. - - Parameters - ---------- - calisto_main_parachute_trigger : function - The trigger for the main parachute of the Calisto rocket. This is a - pytest fixture too. - - Returns - ------- - rocketpy.Parachute - The main parachute of the Calisto rocket. - """ - return Parachute( - name="calisto_main_chute", - cd_s=10.0, - trigger=calisto_main_parachute_trigger, - sampling_rate=105, - lag=1.5, - noise=(0, 8.3, 0.5), - ) - - -@pytest.fixture -def calisto_drogue_chute(calisto_drogue_parachute_trigger): - """The drogue parachute of the Calisto rocket. - - Parameters - ---------- - calisto_drogue_parachute_trigger : function - The trigger for the drogue parachute of the Calisto rocket. This is a - pytest fixture too. - - Returns - ------- - rocketpy.Parachute - The drogue parachute of the Calisto rocket. - """ - return Parachute( - name="calisto_drogue_chute", - cd_s=1.0, - trigger=calisto_drogue_parachute_trigger, - sampling_rate=105, - lag=1.5, - noise=(0, 8.3, 0.5), - ) - - -## Flights - - -@pytest.fixture -def flight_calisto(calisto, example_env): # old name: flight - """A rocketpy.Flight object of the Calisto rocket. This uses the calisto - without the aerodynamic surfaces and parachutes. The environment is the - simplest possible, with no parameters set. - - Parameters - ---------- - calisto : rocketpy.Rocket - An object of the Rocket class. This is a pytest fixture too. - example_env : rocketpy.Environment - An object of the Environment class. This is a pytest fixture too. - - Returns - ------- - rocketpy.Flight - A rocketpy.Flight object of the Calisto rocket in the simplest possible - conditions. - """ - return Flight( - environment=example_env, - rocket=calisto, - rail_length=5.2, - inclination=85, - heading=0, - terminate_on_apogee=False, - ) - - -@pytest.fixture -def flight_calisto_nose_to_tail(calisto_nose_to_tail, example_env): - """A rocketpy.Flight object of the Calisto rocket. This uses the calisto - with "nose_to_tail" coordinate system orientation, just as described in the - calisto_nose_to_tail fixture. - - Parameters - ---------- - calisto_nose_to_tail : rocketpy.Rocket - An object of the Rocket class. This is a pytest fixture too. - example_env : rocketpy.Environment - An object of the Environment class. This is a pytest fixture too. - - Returns - ------- - rocketpy.Flight - The Calisto rocket with the coordinate system orientation set to - "nose_to_tail". - """ - return Flight( - environment=example_env, - rocket=calisto_nose_to_tail, - rail_length=5.2, - inclination=85, - heading=0, - terminate_on_apogee=False, - ) - - -@pytest.fixture -def flight_calisto_robust(calisto_robust, example_env_robust): - """A rocketpy.Flight object of the Calisto rocket. This uses the calisto - with the aerodynamic surfaces and parachutes. The environment is a bit more - complex than the one in the flight_calisto fixture. This time the latitude, - longitude and elevation are set, as well as the datum and the date. The - location refers to the Spaceport America Cup launch site, while the date is - set to tomorrow at noon. - - Parameters - ---------- - calisto_robust : rocketpy.Rocket - An object of the Rocket class. This is a pytest fixture too. - example_env_robust : rocketpy.Environment - An object of the Environment class. This is a pytest fixture too. - - Returns - ------- - rocketpy.Flight - A rocketpy.Flight object of the Calisto rocket in a more complex - condition. - """ - return Flight( - environment=example_env_robust, - rocket=calisto_robust, - rail_length=5.2, - inclination=85, - heading=0, - terminate_on_apogee=False, - ) - - -@pytest.fixture -def flight_calisto_custom_wind(calisto_robust, example_env_robust): - """A rocketpy.Flight object of the Calisto rocket. This uses the calisto - with the aerodynamic surfaces and parachutes. The environment is a bit more - complex than the one in the flight_calisto_robust fixture. Now the wind is - set to 5m/s (x direction) and 2m/s (y direction), constant with altitude. - - Parameters - ---------- - calisto_robust : rocketpy.Rocket - An object of the Rocket class. This is a pytest fixture too. - example_env_robust : rocketpy.Environment - An object of the Environment class. This is a pytest fixture too. - - Returns - ------- - rocketpy.Flight - - """ - env = example_env_robust - env.set_atmospheric_model( - type="custom_atmosphere", - temperature=300, - wind_u=[(0, 5), (4000, 5)], - wind_v=[(0, 2), (4000, 2)], - ) - return Flight( - environment=env, - rocket=calisto_robust, - rail_length=5.2, - inclination=85, - heading=0, - terminate_on_apogee=False, - ) - - -@pytest.fixture -def flight_calisto_air_brakes(calisto_air_brakes_clamp_on, example_env): - """A rocketpy.Flight object of the Calisto rocket. This uses the calisto - with the aerodynamic surfaces and air brakes. The environment is the - simplest possible, with no parameters set. The air brakes are set to clamp - the deployment level. - - Parameters - ---------- - calisto_air_brakes_clamp_on : rocketpy.Rocket - An object of the Rocket class. - example_env : rocketpy.Environment - An object of the Environment class. - - Returns - ------- - rocketpy.Flight - A rocketpy.Flight object of the Calisto rocket in a more complex - condition. - """ - return Flight( - rocket=calisto_air_brakes_clamp_on, - environment=example_env, - rail_length=5.2, - inclination=85, - heading=0, - time_overshoot=False, - terminate_on_apogee=True, - ) - - -## Dimensionless motors and rockets - - -@pytest.fixture -def m(): - """Create a simple object of the numericalunits.m class to be used in the - tests. This allows to avoid repeating the same code in all tests. - - Returns - ------- - numericalunits.m - A simple object of the numericalunits.m class - """ - return numericalunits.m - - -@pytest.fixture -def kg(): - """Create a simple object of the numericalunits.kg class to be used in the - tests. This allows to avoid repeating the same code in all tests. - - Returns - ------- - numericalunits.kg - A simple object of the numericalunits.kg class - """ - return numericalunits.kg - - -@pytest.fixture -def dimensionless_cesaroni_m1670(kg, m): # old name: dimensionless_motor - """The dimensionless version of the Cesaroni M1670 motor. This is the same - motor as defined in the cesaroni_m1670 fixture, but with all the parameters - converted to dimensionless values. This allows to check if the dimensions - are being handled correctly in the code. - - Parameters - ---------- - kg : numericalunits.kg - An object of the numericalunits.kg class. This is a pytest - m : numericalunits.m - An object of the numericalunits.m class. This is a pytest - - Returns - ------- - rocketpy.SolidMotor - An object of the SolidMotor class - """ - example_motor = SolidMotor( - thrust_source="data/motors/Cesaroni_M1670.eng", - burn_time=3.9, - dry_mass=1.815 * kg, - dry_inertia=( - 0.125 * (kg * m**2), - 0.125 * (kg * m**2), - 0.002 * (kg * m**2), - ), - center_of_dry_mass_position=0.317 * m, - grain_number=5, - grain_separation=5 / 1000 * m, - grain_density=1815 * (kg / m**3), - grain_outer_radius=33 / 1000 * m, - grain_initial_inner_radius=15 / 1000 * m, - grain_initial_height=120 / 1000 * m, - nozzle_radius=33 / 1000 * m, - throat_radius=11 / 1000 * m, - interpolation_method="linear", - grains_center_of_mass_position=0.397 * m, - nozzle_position=0 * m, - coordinate_system_orientation="nozzle_to_combustion_chamber", - ) - return example_motor - - -@pytest.fixture # old name: dimensionless_rocket -def dimensionless_calisto(kg, m, dimensionless_cesaroni_m1670): - """The dimensionless version of the Calisto rocket. This is the same rocket - as defined in the calisto fixture, but with all the parameters converted to - dimensionless values. This allows to check if the dimensions are being - handled correctly in the code. - - Parameters - ---------- - kg : numericalunits.kg - An object of the numericalunits.kg class. This is a pytest fixture too. - m : numericalunits.m - An object of the numericalunits.m class. This is a pytest fixture too. - dimensionless_cesaroni_m1670 : rocketpy.SolidMotor - The dimensionless version of the Cesaroni M1670 motor. This is a pytest - fixture too. - - Returns - ------- - rocketpy.Rocket - An object of the Rocket class - """ - example_rocket = Rocket( - radius=0.0635 * m, - mass=14.426 * kg, - inertia=(6.321 * (kg * m**2), 6.321 * (kg * m**2), 0.034 * (kg * m**2)), - power_off_drag="data/calisto/powerOffDragCurve.csv", - power_on_drag="data/calisto/powerOnDragCurve.csv", - center_of_mass_without_motor=0 * m, - coordinate_system_orientation="tail_to_nose", - ) - example_rocket.add_motor(dimensionless_cesaroni_m1670, position=(-1.373) * m) - return example_rocket - - -## Environment - - -@pytest.fixture -def example_env(): - """Create a simple object of the Environment class to be used in the tests. - This allows to avoid repeating the same code in all tests. The environment - set here is the simplest possible, with no parameters set. - - Returns - ------- - rocketpy.Environment - The simplest object of the Environment class - """ - return Environment() - - -@pytest.fixture -def example_env_robust(): - """Create an object of the Environment class to be used in the tests. This - allows to avoid repeating the same code in all tests. The environment set - here is a bit more complex than the one in the example_env fixture. This - time the latitude, longitude and elevation are set, as well as the datum and - the date. The location refers to the Spaceport America Cup launch site, - while the date is set to tomorrow at noon. - - Returns - ------- - rocketpy.Environment - An object of the Environment class - """ - env = Environment( - latitude=32.990254, - longitude=-106.974998, - elevation=1400, - datum="WGS84", - ) - tomorrow = datetime.date.today() + datetime.timedelta(days=1) - env.set_date((tomorrow.year, tomorrow.month, tomorrow.day, 12)) - return env - - -@pytest.fixture -def env_analysis(): - """Create a simple object of the Environment Analysis class to be used in - the tests. This allows to avoid repeating the same code in all tests. - - Returns - ------- - EnvironmentAnalysis - A simple object of the Environment Analysis class - """ - env_analysis = EnvironmentAnalysis( - start_date=datetime.datetime(2019, 10, 23), - end_date=datetime.datetime(2021, 10, 23), - latitude=39.3897, - longitude=-8.28896388889, - start_hour=6, - end_hour=18, - surface_data_file="./data/weather/EuroC_single_level_reanalysis_2002_2021.nc", - pressure_level_data_file="./data/weather/EuroC_pressure_levels_reanalysis_2001-2021.nc", - timezone=None, - unit_system="metric", - forecast_date=None, - forecast_args=None, - max_expected_altitude=None, - ) - - return env_analysis - - -## Functions - - -@pytest.fixture -def linear_func(): - """Create a linear function based on a list of points. The function - represents y = x and may be used on different tests. - - Returns - ------- - Function - A linear function representing y = x. - """ - return Function( - [[0, 0], [1, 1], [2, 2], [3, 3]], - ) - - -@pytest.fixture -def linearly_interpolated_func(): - """Create a linearly interpolated function based on a list of points. - - Returns - ------- - Function - Linearly interpolated Function, with constant extrapolation - """ - return Function( - [[0, 0], [1, 7], [2, -3], [3, -1], [4, 3]], - interpolation="linear", - extrapolation="constant", - ) - - -@pytest.fixture -def spline_interpolated_func(): - """Create a spline interpolated function based on a list of points. - - Returns - ------- - Function - Spline interpolated, with natural extrapolation - """ - return Function( - [[0, 0], [1, 7], [2, -3], [3, -1], [4, 3]], - interpolation="spline", - extrapolation="natural", - ) - - -@pytest.fixture -def func_from_csv(): - """Create a function based on a csv file. The csv file contains the - coordinates of the E473 airfoil at 10e6 degrees, but anything else could be - used here as long as it is a csv file. - - Returns - ------- - rocketpy.Function - A function based on a csv file. - """ - func = Function( - source="tests/fixtures/airfoils/e473-10e6-degrees.csv", - ) - return func - - -@pytest.fixture -def func_2d_from_csv(): - """Create a 2d function based on a csv file. - - Returns - ------- - rocketpy.Function - A function based on a csv file. - """ - # Do not define any of the optional parameters so that the tests can check - # if the defaults are being used correctly. - func = Function( - source="tests/fixtures/function/2d.csv", - ) - return func - - -## Controller -@pytest.fixture -def controller_function(): - """Create a controller function that updates the air brakes deployment level - based on the altitude and vertical velocity of the rocket. This is the same - controller function that is used in the air brakes example in the - documentation. - - Returns - ------- - function - A controller function - """ - - def controller_function( - time, sampling_rate, state, state_history, observed_variables, air_brakes - ): - z = state[2] - vz = state[5] - previous_vz = state_history[-1][5] - if time < 3.9: - return None - if z < 1500: - air_brakes.deployment_level = 0 - else: - new_deployment_level = ( - air_brakes.deployment_level + 0.1 * vz + 0.01 * previous_vz**2 - ) - if new_deployment_level > air_brakes.deployment_level + 0.2 / sampling_rate: - new_deployment_level = air_brakes.deployment_level + 0.2 / sampling_rate - elif ( - new_deployment_level < air_brakes.deployment_level - 0.2 / sampling_rate - ): - new_deployment_level = air_brakes.deployment_level - 0.2 / sampling_rate - else: - new_deployment_level = air_brakes.deployment_level - air_brakes.deployment_level = new_deployment_level - - return controller_function - - -@pytest.fixture -def lambda_quad_func(): - """Create a lambda function based on a string. - - Returns - ------- - Function - A lambda function based on a string. - """ - func = lambda x: x**2 - return Function( - source=func, - ) diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fixtures/environment/__init__.py b/tests/fixtures/environment/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fixtures/environment/environment_fixtures.py b/tests/fixtures/environment/environment_fixtures.py new file mode 100644 index 000000000..54bc8bcce --- /dev/null +++ b/tests/fixtures/environment/environment_fixtures.py @@ -0,0 +1,73 @@ +import datetime + +import pytest + +from rocketpy import Environment, EnvironmentAnalysis + + +@pytest.fixture +def example_env(): + """Create a simple object of the Environment class to be used in the tests. + This allows to avoid repeating the same code in all tests. The environment + set here is the simplest possible, with no parameters set. + + Returns + ------- + rocketpy.Environment + The simplest object of the Environment class + """ + return Environment() + + +@pytest.fixture +def example_env_robust(): + """Create an object of the Environment class to be used in the tests. This + allows to avoid repeating the same code in all tests. The environment set + here is a bit more complex than the one in the example_env fixture. This + time the latitude, longitude and elevation are set, as well as the datum and + the date. The location refers to the Spaceport America Cup launch site, + while the date is set to tomorrow at noon. + + Returns + ------- + rocketpy.Environment + An object of the Environment class + """ + env = Environment( + latitude=32.990254, + longitude=-106.974998, + elevation=1400, + datum="WGS84", + ) + tomorrow = datetime.date.today() + datetime.timedelta(days=1) + env.set_date((tomorrow.year, tomorrow.month, tomorrow.day, 12)) + return env + + +@pytest.fixture +def env_analysis(): + """Create a simple object of the Environment Analysis class to be used in + the tests. This allows to avoid repeating the same code in all tests. + + Returns + ------- + EnvironmentAnalysis + A simple object of the Environment Analysis class + """ + env_analysis = EnvironmentAnalysis( + start_date=datetime.datetime(2019, 10, 23), + end_date=datetime.datetime(2021, 10, 23), + latitude=39.3897, + longitude=-8.28896388889, + start_hour=6, + end_hour=18, + surface_data_file="./data/weather/EuroC_single_level_reanalysis_2002_2021.nc", + pressure_level_data_file="./data/weather/EuroC_pressure_levels_reanalysis_2001-2021.nc", + timezone=None, + unit_system="metric", + forecast_date=None, + forecast_args=None, + max_expected_altitude=None, + ) + + return env_analysis diff --git a/tests/fixtures/flight/__init__.py b/tests/fixtures/flight/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fixtures/flight/flight_fixtures.py b/tests/fixtures/flight/flight_fixtures.py new file mode 100644 index 000000000..a82ce8ba1 --- /dev/null +++ b/tests/fixtures/flight/flight_fixtures.py @@ -0,0 +1,160 @@ +import pytest + +from rocketpy import Flight + + +@pytest.fixture +def flight_calisto(calisto, example_env): # old name: flight + """A rocketpy.Flight object of the Calisto rocket. This uses the calisto + without the aerodynamic surfaces and parachutes. The environment is the + simplest possible, with no parameters set. + + Parameters + ---------- + calisto : rocketpy.Rocket + An object of the Rocket class. This is a pytest fixture too. + example_env : rocketpy.Environment + An object of the Environment class. This is a pytest fixture too. + + Returns + ------- + rocketpy.Flight + A rocketpy.Flight object of the Calisto rocket in the simplest possible + conditions. + """ + return Flight( + environment=example_env, + rocket=calisto, + rail_length=5.2, + inclination=85, + heading=0, + terminate_on_apogee=False, + ) + + +@pytest.fixture +def flight_calisto_nose_to_tail(calisto_nose_to_tail, example_env): + """A rocketpy.Flight object of the Calisto rocket. This uses the calisto + with "nose_to_tail" coordinate system orientation, just as described in the + calisto_nose_to_tail fixture. + + Parameters + ---------- + calisto_nose_to_tail : rocketpy.Rocket + An object of the Rocket class. This is a pytest fixture too. + example_env : rocketpy.Environment + An object of the Environment class. This is a pytest fixture too. + + Returns + ------- + rocketpy.Flight + The Calisto rocket with the coordinate system orientation set to + "nose_to_tail". + """ + return Flight( + environment=example_env, + rocket=calisto_nose_to_tail, + rail_length=5.2, + inclination=85, + heading=0, + terminate_on_apogee=False, + ) + + +@pytest.fixture +def flight_calisto_robust(calisto_robust, example_env_robust): + """A rocketpy.Flight object of the Calisto rocket. This uses the calisto + with the aerodynamic surfaces and parachutes. The environment is a bit more + complex than the one in the flight_calisto fixture. This time the latitude, + longitude and elevation are set, as well as the datum and the date. The + location refers to the Spaceport America Cup launch site, while the date is + set to tomorrow at noon. + + Parameters + ---------- + calisto_robust : rocketpy.Rocket + An object of the Rocket class. This is a pytest fixture too. + example_env_robust : rocketpy.Environment + An object of the Environment class. This is a pytest fixture too. + + Returns + ------- + rocketpy.Flight + A rocketpy.Flight object of the Calisto rocket in a more complex + condition. + """ + return Flight( + environment=example_env_robust, + rocket=calisto_robust, + rail_length=5.2, + inclination=85, + heading=0, + terminate_on_apogee=False, + ) + + +@pytest.fixture +def flight_calisto_custom_wind(calisto_robust, example_env_robust): + """A rocketpy.Flight object of the Calisto rocket. This uses the calisto + with the aerodynamic surfaces and parachutes. The environment is a bit more + complex than the one in the flight_calisto_robust fixture. Now the wind is + set to 5m/s (x direction) and 2m/s (y direction), constant with altitude. + + Parameters + ---------- + calisto_robust : rocketpy.Rocket + An object of the Rocket class. This is a pytest fixture too. + example_env_robust : rocketpy.Environment + An object of the Environment class. This is a pytest fixture too. + + Returns + ------- + rocketpy.Flight + + """ + env = example_env_robust + env.set_atmospheric_model( + type="custom_atmosphere", + temperature=300, + wind_u=[(0, 5), (4000, 5)], + wind_v=[(0, 2), (4000, 2)], + ) + return Flight( + environment=env, + rocket=calisto_robust, + rail_length=5.2, + inclination=85, + heading=0, + terminate_on_apogee=False, + ) + + +@pytest.fixture +def flight_calisto_air_brakes(calisto_air_brakes_clamp_on, example_env): + """A rocketpy.Flight object of the Calisto rocket. This uses the calisto + with the aerodynamic surfaces and air brakes. The environment is the + simplest possible, with no parameters set. The air brakes are set to clamp + the deployment level. + + Parameters + ---------- + calisto_air_brakes_clamp_on : rocketpy.Rocket + An object of the Rocket class. + example_env : rocketpy.Environment + An object of the Environment class. + + Returns + ------- + rocketpy.Flight + A rocketpy.Flight object of the Calisto rocket in a more complex + condition. + """ + return Flight( + rocket=calisto_air_brakes_clamp_on, + environment=example_env, + rail_length=5.2, + inclination=85, + heading=0, + time_overshoot=False, + terminate_on_apogee=True, + ) diff --git a/tests/fixtures/function/__init__.py b/tests/fixtures/function/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fixtures/function/function_fixtures.py b/tests/fixtures/function/function_fixtures.py new file mode 100644 index 000000000..566e4d115 --- /dev/null +++ b/tests/fixtures/function/function_fixtures.py @@ -0,0 +1,140 @@ +import pytest + +from rocketpy import Function + + +@pytest.fixture +def linear_func(): + """Create a linear function based on a list of points. The function + represents y = x and may be used on different tests. + + Returns + ------- + Function + A linear function representing y = x. + """ + return Function( + [[0, 0], [1, 1], [2, 2], [3, 3]], + ) + + +@pytest.fixture +def linearly_interpolated_func(): + """Create a linearly interpolated function based on a list of points. + + Returns + ------- + Function + Linearly interpolated Function, with constant extrapolation + """ + return Function( + [[0, 0], [1, 7], [2, -3], [3, -1], [4, 3]], + interpolation="linear", + extrapolation="constant", + ) + + +@pytest.fixture +def spline_interpolated_func(): + """Create a spline interpolated function based on a list of points. + + Returns + ------- + Function + Spline interpolated, with natural extrapolation + """ + return Function( + [[0, 0], [1, 7], [2, -3], [3, -1], [4, 3]], + interpolation="spline", + extrapolation="natural", + ) + + +@pytest.fixture +def func_from_csv(): + """Create a function based on a csv file. The csv file contains the + coordinates of the E473 airfoil at 10e6 degrees, but anything else could be + used here as long as it is a csv file. + + Returns + ------- + rocketpy.Function + A function based on a csv file. + """ + func = Function( + source="tests/fixtures/airfoils/e473-10e6-degrees.csv", + ) + return func + + +@pytest.fixture +def func_2d_from_csv(): + """Create a 2d function based on a csv file. + + Returns + ------- + rocketpy.Function + A function based on a csv file. + """ + # Do not define any of the optional parameters so that the tests can check + # if the defaults are being used correctly. + func = Function( + source="tests/fixtures/function/2d.csv", + ) + return func + + +## Controller +@pytest.fixture +def controller_function(): + """Create a controller function that updates the air brakes deployment level + based on the altitude and vertical velocity of the rocket. This is the same + controller function that is used in the air brakes example in the + documentation. + + Returns + ------- + function + A controller function + """ + + def controller_function( + time, sampling_rate, state, state_history, observed_variables, air_brakes + ): + z = state[2] + vz = state[5] + previous_vz = state_history[-1][5] + if time < 3.9: + return None + if z < 1500: + air_brakes.deployment_level = 0 + else: + new_deployment_level = ( + air_brakes.deployment_level + 0.1 * vz + 0.01 * previous_vz**2 + ) + if new_deployment_level > air_brakes.deployment_level + 0.2 / sampling_rate: + new_deployment_level = air_brakes.deployment_level + 0.2 / sampling_rate + elif ( + new_deployment_level < air_brakes.deployment_level - 0.2 / sampling_rate + ): + new_deployment_level = air_brakes.deployment_level - 0.2 / sampling_rate + else: + new_deployment_level = air_brakes.deployment_level + air_brakes.deployment_level = new_deployment_level + + return controller_function + + +@pytest.fixture +def lambda_quad_func(): + """Create a lambda function based on a string. + + Returns + ------- + Function + A lambda function based on a string. + """ + func = lambda x: x**2 + return Function( + source=func, + ) diff --git a/tests/fixtures/hybrid/__init__.py b/tests/fixtures/hybrid/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fixtures/hybrid/hybrid_fixtures.py b/tests/fixtures/hybrid/hybrid_fixtures.py new file mode 100644 index 000000000..9c3d8c8dc --- /dev/null +++ b/tests/fixtures/hybrid/hybrid_fixtures.py @@ -0,0 +1,275 @@ +import numpy as np +import pytest + +from rocketpy import ( + CylindricalTank, + Fluid, + Function, + HybridMotor, + LevelBasedTank, + LiquidMotor, + MassBasedTank, + SphericalTank, + UllageBasedTank, +) + + +@pytest.fixture +def pressurant_fluid(): + """An example of a pressurant fluid as N2 gas at + 273.15K and 30MPa. + + Returns + ------- + rocketpy.Fluid + An object of the Fluid class. + """ + return Fluid(name="N2", density=300) + + +@pytest.fixture +def fuel_pressurant(): + """An example of a pressurant fluid as N2 gas at + 273.15K and 2MPa. + + Returns + ------- + rocketpy.Fluid + An object of the Fluid class. + """ + return Fluid(name="N2", density=25) + + +@pytest.fixture +def oxidizer_pressurant(): + """An example of a pressurant fluid as N2 gas at + 273.15K and 3MPa. + + Returns + ------- + rocketpy.Fluid + An object of the Fluid class. + """ + return Fluid(name="N2", density=35) + + +@pytest.fixture +def fuel_fluid(): + """An example of propane as fuel fluid at + 273.15K and 2MPa. + + Returns + ------- + rocketpy.Fluid + An object of the Fluid class. + """ + return Fluid(name="Propane", density=500) + + +@pytest.fixture +def oxidizer_fluid(): + """An example of liquid oxygen as oxidizer fluid at + 100K and 3MPa. + + Returns + ------- + rocketpy.Fluid + An object of the Fluid class. + """ + return Fluid(name="O2", density=1000) + + +@pytest.fixture +def pressurant_tank(pressurant_fluid): + """An example of a pressurant cylindrical tank with spherical + caps. + + Parameters + ---------- + pressurant_fluid : rocketpy.Fluid + Pressurizing fluid. This is a pytest fixture. + + Returns + ------- + rocketpy.MassBasedTank + An object of the CylindricalTank class. + """ + geometry = CylindricalTank(0.135 / 2, 0.981, spherical_caps=True) + pressurant_tank = MassBasedTank( + name="Pressure Tank", + geometry=geometry, + liquid_mass=0, + flux_time=(8, 20), + gas_mass="data/SEBLM/pressurantMassFiltered.csv", + gas=pressurant_fluid, + liquid=pressurant_fluid, + ) + + return pressurant_tank + + +@pytest.fixture +def fuel_tank(fuel_fluid, fuel_pressurant): + """An example of a fuel cylindrical tank with spherical + caps. + + Parameters + ---------- + fuel_fluid : rocketpy.Fluid + Fuel fluid of the tank. This is a pytest fixture. + fuel_pressurant : rocketpy.Fluid + Pressurizing fluid of the fuel tank. This is a pytest + fixture. + + Returns + ------- + rocketpy.UllageBasedTank + """ + geometry = CylindricalTank(0.0744, 0.8068, spherical_caps=True) + ullage = ( + -Function("data/SEBLM/test124_Propane_Volume.csv") * 1e-3 + + geometry.total_volume + ) + fuel_tank = UllageBasedTank( + name="Propane Tank", + flux_time=(8, 20), + geometry=geometry, + liquid=fuel_fluid, + gas=fuel_pressurant, + ullage=ullage, + ) + + return fuel_tank + + +@pytest.fixture +def oxidizer_tank(oxidizer_fluid, oxidizer_pressurant): + """An example of a oxidizer cylindrical tank with spherical + caps. + + Parameters + ---------- + oxidizer_fluid : rocketpy.Fluid + Oxidizer fluid of the tank. This is a pytest fixture. + oxidizer_pressurant : rocketpy.Fluid + Pressurizing fluid of the oxidizer tank. This is a pytest + fixture. + + Returns + ------- + rocketpy.UllageBasedTank + """ + geometry = CylindricalTank(0.0744, 0.8068, spherical_caps=True) + ullage = ( + -Function("data/SEBLM/test124_Lox_Volume.csv") * 1e-3 + geometry.total_volume + ) + oxidizer_tank = UllageBasedTank( + name="Lox Tank", + flux_time=(8, 20), + geometry=geometry, + liquid=oxidizer_fluid, + gas=oxidizer_pressurant, + ullage=ullage, + ) + + return oxidizer_tank + + +@pytest.fixture +def liquid_motor(pressurant_tank, fuel_tank, oxidizer_tank): + """An example of a liquid motor with pressurant, fuel and oxidizer tanks. + + Parameters + ---------- + pressurant_tank : rocketpy.MassBasedTank + Tank that contains pressurizing fluid. This is a pytest fixture. + fuel_tank : rocketpy.UllageBasedTank + Tank that contains the motor fuel. This is a pytest fixture. + oxidizer_tank : rocketpy.UllageBasedTank + Tank that contains the motor oxidizer. This is a pytest fixture. + + Returns + ------- + rocketpy.LiquidMotor + """ + liquid_motor = LiquidMotor( + thrust_source="data/SEBLM/test124_Thrust_Curve.csv", + burn_time=(8, 20), + dry_mass=10, + dry_inertia=(5, 5, 0.2), + center_of_dry_mass_position=0, + nozzle_position=-1.364, + nozzle_radius=0.069 / 2, + ) + liquid_motor.add_tank(pressurant_tank, position=2.007) + liquid_motor.add_tank(fuel_tank, position=-1.048) + liquid_motor.add_tank(oxidizer_tank, position=0.711) + + return liquid_motor + + +@pytest.fixture +def spherical_oxidizer_tank(oxidizer_fluid, oxidizer_pressurant): + """An example of a oxidizer spherical tank. + + Parameters + ---------- + oxidizer_fluid : rocketpy.Fluid + Oxidizer fluid of the tank. This is a pytest fixture. + oxidizer_pressurant : rocketpy.Fluid + Pressurizing fluid of the oxidizer tank. This is a pytest + fixture. + + Returns + ------- + rocketpy.UllageBasedTank + """ + geometry = SphericalTank(0.05) + liquid_level = Function(lambda t: 0.1 * np.exp(-t / 2) - 0.05) + oxidizer_tank = LevelBasedTank( + name="Lox Tank", + flux_time=10, + geometry=geometry, + liquid=oxidizer_fluid, + gas=oxidizer_pressurant, + liquid_height=liquid_level, + ) + + return oxidizer_tank + + +@pytest.fixture +def hybrid_motor(spherical_oxidizer_tank): + """An example of a hybrid motor with spherical oxidizer + tank and fuel grains. + + Parameters + ---------- + spherical_oxidizer_tank : rocketpy.LevelBasedTank + Example Tank that contains the motor oxidizer. This is a + pytest fixture. + + Returns + ------- + rocketpy.HybridMotor + """ + motor = HybridMotor( + thrust_source=lambda t: 2000 - 100 * t, + burn_time=10, + center_of_dry_mass_position=0, + dry_inertia=(4, 4, 0.1), + dry_mass=8, + grain_density=1700, + grain_number=4, + grain_initial_height=0.1, + grain_separation=0, + grain_initial_inner_radius=0.04, + grain_outer_radius=0.1, + nozzle_position=-0.4, + nozzle_radius=0.07, + grains_center_of_mass_position=-0.1, + ) + + motor.add_tank(spherical_oxidizer_tank, position=0.3) + + return motor diff --git a/tests/fixtures/motor/__init__.py b/tests/fixtures/motor/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fixtures/motor/generic_motor_fixtures.py b/tests/fixtures/motor/generic_motor_fixtures.py new file mode 100644 index 000000000..925716cb5 --- /dev/null +++ b/tests/fixtures/motor/generic_motor_fixtures.py @@ -0,0 +1,30 @@ +import pytest + +from rocketpy import GenericMotor + +# Fixtures +## Motors and rockets + + +@pytest.fixture +def generic_motor(): + """An example of a generic motor for low accuracy simulations. + + Returns + ------- + rocketpy.GenericMotor + """ + motor = GenericMotor( + burn_time=(2, 7), + thrust_source=lambda t: 2000 - 100 * (t - 2), + chamber_height=0.5, + chamber_radius=0.075, + chamber_position=-0.25, + propellant_initial_mass=5.0, + nozzle_position=-0.5, + nozzle_radius=0.075, + dry_mass=8.0, + dry_inertia=(0.2, 0.2, 0.08), + ) + + return motor diff --git a/tests/fixtures/motor/hybrid_fixtures.py b/tests/fixtures/motor/hybrid_fixtures.py new file mode 100644 index 000000000..923a640b1 --- /dev/null +++ b/tests/fixtures/motor/hybrid_fixtures.py @@ -0,0 +1,40 @@ +import pytest + +from rocketpy import HybridMotor + + +@pytest.fixture +def hybrid_motor(spherical_oxidizer_tank): + """An example of a hybrid motor with spherical oxidizer + tank and fuel grains. + + Parameters + ---------- + spherical_oxidizer_tank : rocketpy.LevelBasedTank + Example Tank that contains the motor oxidizer. This is a + pytest fixture. + + Returns + ------- + rocketpy.HybridMotor + """ + motor = HybridMotor( + thrust_source=lambda t: 2000 - 100 * t, + burn_time=10, + center_of_dry_mass_position=0, + dry_inertia=(4, 4, 0.1), + dry_mass=8, + grain_density=1700, + grain_number=4, + grain_initial_height=0.1, + grain_separation=0, + grain_initial_inner_radius=0.04, + grain_outer_radius=0.1, + nozzle_position=-0.4, + nozzle_radius=0.07, + grains_center_of_mass_position=-0.1, + ) + + motor.add_tank(spherical_oxidizer_tank, position=0.3) + + return motor diff --git a/tests/fixtures/motor/liquid_fixtures.py b/tests/fixtures/motor/liquid_fixtures.py new file mode 100644 index 000000000..85a17e3a4 --- /dev/null +++ b/tests/fixtures/motor/liquid_fixtures.py @@ -0,0 +1,101 @@ +import pytest + +from rocketpy import Fluid, LiquidMotor + + +@pytest.fixture +def pressurant_fluid(): + """An example of a pressurant fluid as N2 gas at + 273.15K and 30MPa. + + Returns + ------- + rocketpy.Fluid + An object of the Fluid class. + """ + return Fluid(name="N2", density=300) + + +@pytest.fixture +def fuel_pressurant(): + """An example of a pressurant fluid as N2 gas at + 273.15K and 2MPa. + + Returns + ------- + rocketpy.Fluid + An object of the Fluid class. + """ + return Fluid(name="N2", density=25) + + +@pytest.fixture +def oxidizer_pressurant(): + """An example of a pressurant fluid as N2 gas at + 273.15K and 3MPa. + + Returns + ------- + rocketpy.Fluid + An object of the Fluid class. + """ + return Fluid(name="N2", density=35) + + +@pytest.fixture +def fuel_fluid(): + """An example of propane as fuel fluid at + 273.15K and 2MPa. + + Returns + ------- + rocketpy.Fluid + An object of the Fluid class. + """ + return Fluid(name="Propane", density=500) + + +@pytest.fixture +def oxidizer_fluid(): + """An example of liquid oxygen as oxidizer fluid at + 100K and 3MPa. + + Returns + ------- + rocketpy.Fluid + An object of the Fluid class. + """ + return Fluid(name="O2", density=1000) + + +@pytest.fixture +def liquid_motor(pressurant_tank, fuel_tank, oxidizer_tank): + """An example of a liquid motor with pressurant, fuel and oxidizer tanks. + + Parameters + ---------- + pressurant_tank : rocketpy.MassBasedTank + Tank that contains pressurizing fluid. This is a pytest fixture. + fuel_tank : rocketpy.UllageBasedTank + Tank that contains the motor fuel. This is a pytest fixture. + oxidizer_tank : rocketpy.UllageBasedTank + Tank that contains the motor oxidizer. This is a pytest fixture. + + Returns + ------- + rocketpy.LiquidMotor + """ + liquid_motor = LiquidMotor( + thrust_source="data/SEBLM/test124_Thrust_Curve.csv", + burn_time=(8, 20), + dry_mass=10, + dry_inertia=(5, 5, 0.2), + center_of_dry_mass_position=0, + nozzle_position=-1.364, + nozzle_radius=0.069 / 2, + ) + liquid_motor.add_tank(pressurant_tank, position=2.007) + liquid_motor.add_tank(fuel_tank, position=-1.048) + liquid_motor.add_tank(oxidizer_tank, position=0.711) + + return liquid_motor diff --git a/tests/fixtures/motor/solid_motor_fixtures.py b/tests/fixtures/motor/solid_motor_fixtures.py new file mode 100644 index 000000000..587d5e970 --- /dev/null +++ b/tests/fixtures/motor/solid_motor_fixtures.py @@ -0,0 +1,119 @@ +import pytest + +from rocketpy import SolidMotor + +# Fixtures +## Motors and rockets + + +@pytest.fixture +def cesaroni_m1670(): # old name: solid_motor + """Create a simple object of the SolidMotor class to be used in the tests. + This is the same motor that has been used in the getting started guide for + years. + + Returns + ------- + rocketpy.SolidMotor + A simple object of the SolidMotor class + """ + example_motor = SolidMotor( + thrust_source="data/motors/Cesaroni_M1670.eng", + burn_time=3.9, + dry_mass=1.815, + dry_inertia=(0.125, 0.125, 0.002), + center_of_dry_mass_position=0.317, + nozzle_position=0, + grain_number=5, + grain_density=1815, + nozzle_radius=33 / 1000, + throat_radius=11 / 1000, + grain_separation=5 / 1000, + grain_outer_radius=33 / 1000, + grain_initial_height=120 / 1000, + grains_center_of_mass_position=0.397, + grain_initial_inner_radius=15 / 1000, + interpolation_method="linear", + coordinate_system_orientation="nozzle_to_combustion_chamber", + ) + return example_motor + + +@pytest.fixture +def cesaroni_m1670_shifted(): # old name: solid_motor + """Create a simple object of the SolidMotor class to be used in the tests. + This is the same motor that has been used in the getting started guide for + years. The difference relies in the thrust_source, which was shifted for + testing purposes. + + Returns + ------- + rocketpy.SolidMotor + A simple object of the SolidMotor class + """ + example_motor = SolidMotor( + thrust_source="tests/fixtures/motor/Cesaroni_M1670_shifted.eng", + burn_time=3.9, + dry_mass=1.815, + dry_inertia=(0.125, 0.125, 0.002), + center_of_dry_mass_position=0.317, + nozzle_position=0, + grain_number=5, + grain_density=1815, + nozzle_radius=33 / 1000, + throat_radius=11 / 1000, + grain_separation=5 / 1000, + grain_outer_radius=33 / 1000, + grain_initial_height=120 / 1000, + grains_center_of_mass_position=0.397, + grain_initial_inner_radius=15 / 1000, + interpolation_method="linear", + coordinate_system_orientation="nozzle_to_combustion_chamber", + reshape_thrust_curve=(5, 3000), + ) + return example_motor + + +@pytest.fixture +def dimensionless_cesaroni_m1670(kg, m): # old name: dimensionless_motor + """The dimensionless version of the Cesaroni M1670 motor. This is the same + motor as defined in the cesaroni_m1670 fixture, but with all the parameters + converted to dimensionless values. This allows to check if the dimensions + are being handled correctly in the code. + + Parameters + ---------- + kg : numericalunits.kg + An object of the numericalunits.kg class. This is a pytest + m : numericalunits.m + An object of the numericalunits.m class. This is a pytest + + Returns + ------- + rocketpy.SolidMotor + An object of the SolidMotor class + """ + example_motor = SolidMotor( + thrust_source="data/motors/Cesaroni_M1670.eng", + burn_time=3.9, + dry_mass=1.815 * kg, + dry_inertia=( + 0.125 * (kg * m**2), + 0.125 * (kg * m**2), + 0.002 * (kg * m**2), + ), + center_of_dry_mass_position=0.317 * m, + grain_number=5, + grain_separation=5 / 1000 * m, + grain_density=1815 * (kg / m**3), + grain_outer_radius=33 / 1000 * m, + grain_initial_inner_radius=15 / 1000 * m, + grain_initial_height=120 / 1000 * m, + nozzle_radius=33 / 1000 * m, + throat_radius=11 / 1000 * m, + interpolation_method="linear", + grains_center_of_mass_position=0.397 * m, + nozzle_position=0 * m, + coordinate_system_orientation="nozzle_to_combustion_chamber", + ) + return example_motor diff --git a/tests/fixtures/motor/tanks_fixtures.py b/tests/fixtures/motor/tanks_fixtures.py new file mode 100644 index 000000000..4238e1e1e --- /dev/null +++ b/tests/fixtures/motor/tanks_fixtures.py @@ -0,0 +1,137 @@ +import numpy as np +import pytest + +from rocketpy import ( + CylindricalTank, + Function, + LevelBasedTank, + MassBasedTank, + SphericalTank, + UllageBasedTank, +) + + +@pytest.fixture +def pressurant_tank(pressurant_fluid): + """An example of a pressurant cylindrical tank with spherical + caps. + + Parameters + ---------- + pressurant_fluid : rocketpy.Fluid + Pressurizing fluid. This is a pytest fixture. + + Returns + ------- + rocketpy.MassBasedTank + An object of the CylindricalTank class. + """ + geometry = CylindricalTank(0.135 / 2, 0.981, spherical_caps=True) + pressurant_tank = MassBasedTank( + name="Pressure Tank", + geometry=geometry, + liquid_mass=0, + flux_time=(8, 20), + gas_mass="data/SEBLM/pressurantMassFiltered.csv", + gas=pressurant_fluid, + liquid=pressurant_fluid, + ) + + return pressurant_tank + + +@pytest.fixture +def fuel_tank(fuel_fluid, fuel_pressurant): + """An example of a fuel cylindrical tank with spherical + caps. + + Parameters + ---------- + fuel_fluid : rocketpy.Fluid + Fuel fluid of the tank. This is a pytest fixture. + fuel_pressurant : rocketpy.Fluid + Pressurizing fluid of the fuel tank. This is a pytest + fixture. + + Returns + ------- + rocketpy.UllageBasedTank + """ + geometry = CylindricalTank(0.0744, 0.8068, spherical_caps=True) + ullage = ( + -Function("data/SEBLM/test124_Propane_Volume.csv") * 1e-3 + + geometry.total_volume + ) + fuel_tank = UllageBasedTank( + name="Propane Tank", + flux_time=(8, 20), + geometry=geometry, + liquid=fuel_fluid, + gas=fuel_pressurant, + ullage=ullage, + ) + + return fuel_tank + + +@pytest.fixture +def oxidizer_tank(oxidizer_fluid, oxidizer_pressurant): + """An example of a oxidizer cylindrical tank with spherical + caps. + + Parameters + ---------- + oxidizer_fluid : rocketpy.Fluid + Oxidizer fluid of the tank. This is a pytest fixture. + oxidizer_pressurant : rocketpy.Fluid + Pressurizing fluid of the oxidizer tank. This is a pytest + fixture. + + Returns + ------- + rocketpy.UllageBasedTank + """ + geometry = CylindricalTank(0.0744, 0.8068, spherical_caps=True) + ullage = ( + -Function("data/SEBLM/test124_Lox_Volume.csv") * 1e-3 + geometry.total_volume + ) + oxidizer_tank = UllageBasedTank( + name="Lox Tank", + flux_time=(8, 20), + geometry=geometry, + liquid=oxidizer_fluid, + gas=oxidizer_pressurant, + ullage=ullage, + ) + + return oxidizer_tank + + +@pytest.fixture +def spherical_oxidizer_tank(oxidizer_fluid, oxidizer_pressurant): + """An example of a oxidizer spherical tank. + + Parameters + ---------- + oxidizer_fluid : rocketpy.Fluid + Oxidizer fluid of the tank. This is a pytest fixture. + oxidizer_pressurant : rocketpy.Fluid + Pressurizing fluid of the oxidizer tank. This is a pytest + fixture. + + Returns + ------- + rocketpy.UllageBasedTank + """ + geometry = SphericalTank(0.05) + liquid_level = Function(lambda t: 0.1 * np.exp(-t / 2) - 0.05) + oxidizer_tank = LevelBasedTank( + name="Lox Tank", + flux_time=10, + geometry=geometry, + liquid=oxidizer_fluid, + gas=oxidizer_pressurant, + liquid_height=liquid_level, + ) + + return oxidizer_tank diff --git a/tests/fixtures/parachutes/__init__.py b/tests/fixtures/parachutes/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fixtures/parachutes/parachute_fixtures.py b/tests/fixtures/parachutes/parachute_fixtures.py new file mode 100644 index 000000000..10dbd36d1 --- /dev/null +++ b/tests/fixtures/parachutes/parachute_fixtures.py @@ -0,0 +1,87 @@ +import pytest + +from rocketpy import Parachute + + +@pytest.fixture +def calisto_drogue_parachute_trigger(): + """The trigger for the drogue parachute of the Calisto rocket. + + Returns + ------- + function + The trigger for the drogue parachute of the Calisto rocket. + """ + + def drogue_trigger(p, h, y): + # activate drogue when vertical velocity is negative + return True if y[5] < 0 else False + + return drogue_trigger + + +@pytest.fixture +def calisto_main_parachute_trigger(): + """The trigger for the main parachute of the Calisto rocket. + + Returns + ------- + function + The trigger for the main parachute of the Calisto rocket. + """ + + def main_trigger(p, h, y): + # activate main when vertical velocity is <0 and altitude is below 800m + return True if y[5] < 0 and h < 800 else False + + return main_trigger + + +@pytest.fixture +def calisto_main_chute(calisto_main_parachute_trigger): + """The main parachute of the Calisto rocket. + + Parameters + ---------- + calisto_main_parachute_trigger : function + The trigger for the main parachute of the Calisto rocket. This is a + pytest fixture too. + + Returns + ------- + rocketpy.Parachute + The main parachute of the Calisto rocket. + """ + return Parachute( + name="calisto_main_chute", + cd_s=10.0, + trigger=calisto_main_parachute_trigger, + sampling_rate=105, + lag=1.5, + noise=(0, 8.3, 0.5), + ) + + +@pytest.fixture +def calisto_drogue_chute(calisto_drogue_parachute_trigger): + """The drogue parachute of the Calisto rocket. + + Parameters + ---------- + calisto_drogue_parachute_trigger : function + The trigger for the drogue parachute of the Calisto rocket. This is a + pytest fixture too. + + Returns + ------- + rocketpy.Parachute + The drogue parachute of the Calisto rocket. + """ + return Parachute( + name="calisto_drogue_chute", + cd_s=1.0, + trigger=calisto_drogue_parachute_trigger, + sampling_rate=105, + lag=1.5, + noise=(0, 8.3, 0.5), + ) diff --git a/tests/fixtures/rockets/__init__.py b/tests/fixtures/rockets/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fixtures/rockets/rocket_fixtures.py b/tests/fixtures/rockets/rocket_fixtures.py new file mode 100644 index 000000000..3a1c82988 --- /dev/null +++ b/tests/fixtures/rockets/rocket_fixtures.py @@ -0,0 +1,278 @@ +import pytest + +from rocketpy import Rocket + + +@pytest.fixture +def calisto_motorless(): + """Create a simple object of the Rocket class to be used in the tests. This + is the same rocket that has been used in the getting started guide for years + but without a motor. + + Returns + ------- + rocketpy.Rocket + A simple object of the Rocket class + """ + calisto = Rocket( + radius=0.0635, + mass=14.426, + inertia=(6.321, 6.321, 0.034), + power_off_drag="data/calisto/powerOffDragCurve.csv", + power_on_drag="data/calisto/powerOnDragCurve.csv", + center_of_mass_without_motor=0, + coordinate_system_orientation="tail_to_nose", + ) + return calisto + + +@pytest.fixture +def calisto(calisto_motorless, cesaroni_m1670): # old name: rocket + """Create a simple object of the Rocket class to be used in the tests. This + is the same rocket that has been used in the getting started guide for + years. The Calisto rocket is the Projeto Jupiter's project launched at the + 2019 Spaceport America Cup. + + Parameters + ---------- + calisto_motorless : rocketpy.Rocket + An object of the Rocket class. This is a pytest fixture too. + cesaroni_m1670 : rocketpy.SolidMotor + An object of the SolidMotor class. This is a pytest fixture too. + + Returns + ------- + rocketpy.Rocket + A simple object of the Rocket class + """ + calisto = calisto_motorless + calisto.add_motor(cesaroni_m1670, position=-1.373) + return calisto + + +@pytest.fixture +def calisto_nose_to_tail(cesaroni_m1670): + """Create a simple object of the Rocket class to be used in the tests. This + is the same as the calisto fixture, but with the coordinate system + orientation set to "nose_to_tail" instead of "tail_to_nose". This allows to + check if the coordinate system orientation is being handled correctly in + the code. + + Parameters + ---------- + cesaroni_m1670 : rocketpy.SolidMotor + An object of the SolidMotor class. This is a pytest fixture too. + + Returns + ------- + rocketpy.Rocket + The Calisto rocket with the coordinate system orientation set to + "nose_to_tail". Rail buttons are already set, as well as the motor. + """ + calisto = Rocket( + radius=0.0635, + mass=14.426, + inertia=(6.321, 6.321, 0.034), + power_off_drag="data/calisto/powerOffDragCurve.csv", + power_on_drag="data/calisto/powerOnDragCurve.csv", + center_of_mass_without_motor=0, + coordinate_system_orientation="nose_to_tail", + ) + calisto.add_motor(cesaroni_m1670, position=1.373) + calisto.set_rail_buttons( + upper_button_position=-0.082, + lower_button_position=0.618, + angular_position=45, + ) + return calisto + + +@pytest.fixture +def calisto_liquid_modded(calisto_motorless, liquid_motor): + """Create a simple object of the Rocket class to be used in the tests. This + is an example of the Calisto rocket with a liquid motor. + + Parameters + ---------- + calisto_motorless : rocketpy.Rocket + An object of the Rocket class. This is a pytest fixture too. + liquid_motor : rocketpy.LiquidMotor + + Returns + ------- + rocketpy.Rocket + A simple object of the Rocket class + """ + calisto = calisto_motorless + calisto.add_motor(liquid_motor, position=-1.373) + return calisto + + +@pytest.fixture +def calisto_hybrid_modded(calisto_motorless, hybrid_motor): + """Create a simple object of the Rocket class to be used in the tests. This + is an example of the Calisto rocket with a hybrid motor. + + Parameters + ---------- + calisto_motorless : rocketpy.Rocket + An object of the Rocket class. This is a pytest fixture too. + hybrid_motor : rocketpy.HybridMotor + + Returns + ------- + rocketpy.Rocket + A simple object of the Rocket class + """ + calisto = calisto_motorless + calisto.add_motor(hybrid_motor, position=-1.373) + return calisto + + +@pytest.fixture +def calisto_robust( + calisto, + calisto_nose_cone, + calisto_tail, + calisto_trapezoidal_fins, + calisto_rail_buttons, + calisto_main_chute, + calisto_drogue_chute, +): + """Create an object class of the Rocket class to be used in the tests. This + is the same Calisto rocket that was defined in the calisto fixture, but with + all the aerodynamic surfaces and parachutes added. This avoids repeating the + same code in all tests. + + Parameters + ---------- + calisto : rocketpy.Rocket + An object of the Rocket class. This is a pytest fixture too. + calisto_nose_cone : rocketpy.NoseCone + The nose cone of the Calisto rocket. This is a pytest fixture too. + calisto_tail : rocketpy.Tail + The boat tail of the Calisto rocket. This is a pytest fixture too. + calisto_trapezoidal_fins : rocketpy.TrapezoidalFins + The trapezoidal fins of the Calisto rocket. This is a pytest fixture + calisto_rail_buttons : rocketpy.RailButtons + The rail buttons of the Calisto rocket. This is a pytest fixture too. + calisto_main_chute : rocketpy.Parachute + The main parachute of the Calisto rocket. This is a pytest fixture too. + calisto_drogue_chute : rocketpy.Parachute + The drogue parachute of the Calisto rocket. This is a pytest fixture + + Returns + ------- + rocketpy.Rocket + An object of the Rocket class + """ + # we follow this format: calisto.add_surfaces(surface, position) + calisto.add_surfaces(calisto_nose_cone, 1.160) + calisto.add_surfaces(calisto_tail, -1.313) + calisto.add_surfaces(calisto_trapezoidal_fins, -1.168) + # calisto.add_surfaces(calisto_rail_buttons, -1.168) + # TODO: if I use the line above, the calisto won't have rail buttons attribute + # we need to apply a check in the add_surfaces method to set the rail buttons + calisto.set_rail_buttons( + upper_button_position=0.082, + lower_button_position=-0.618, + angular_position=45, + ) + calisto.parachutes.append(calisto_main_chute) + calisto.parachutes.append(calisto_drogue_chute) + return calisto + + +@pytest.fixture +def calisto_air_brakes_clamp_on(calisto_robust, controller_function): + """Create an object class of the Rocket class to be used in the tests. This + is the same Calisto rocket that was defined in the calisto_robust fixture, + but with air brakes added, with clamping. + + Parameters + ---------- + calisto_robust : rocketpy.Rocket + An object of the Rocket class. This is a pytest fixture. + controller_function : function + A function that controls the air brakes. This is a pytest fixture. + + Returns + ------- + rocketpy.Rocket + An object of the Rocket class + """ + calisto = calisto_robust + # remove parachutes + calisto.parachutes = [] + calisto.add_air_brakes( + drag_coefficient_curve="data/calisto/air_brakes_cd.csv", + controller_function=controller_function, + sampling_rate=10, + clamp=True, + ) + return calisto + + +@pytest.fixture +def calisto_air_brakes_clamp_off(calisto_robust, controller_function): + """Create an object class of the Rocket class to be used in the tests. This + is the same Calisto rocket that was defined in the calisto_robust fixture, + but with air brakes added, without clamping. + + Parameters + ---------- + calisto_robust : rocketpy.Rocket + An object of the Rocket class. This is a pytest fixture. + controller_function : function + A function that controls the air brakes. This is a pytest fixture. + + Returns + ------- + rocketpy.Rocket + An object of the Rocket class + """ + calisto = calisto_robust + # remove parachutes + calisto.parachutes = [] + calisto.add_air_brakes( + drag_coefficient_curve="data/calisto/air_brakes_cd.csv", + controller_function=controller_function, + sampling_rate=10, + clamp=False, + ) + return calisto + + +@pytest.fixture # old name: dimensionless_rocket +def dimensionless_calisto(kg, m, dimensionless_cesaroni_m1670): + """The dimensionless version of the Calisto rocket. This is the same rocket + as defined in the calisto fixture, but with all the parameters converted to + dimensionless values. This allows to check if the dimensions are being + handled correctly in the code. + + Parameters + ---------- + kg : numericalunits.kg + An object of the numericalunits.kg class. This is a pytest fixture too. + m : numericalunits.m + An object of the numericalunits.m class. This is a pytest fixture too. + dimensionless_cesaroni_m1670 : rocketpy.SolidMotor + The dimensionless version of the Cesaroni M1670 motor. This is a pytest + fixture too. + + Returns + ------- + rocketpy.Rocket + An object of the Rocket class + """ + example_rocket = Rocket( + radius=0.0635 * m, + mass=14.426 * kg, + inertia=(6.321 * (kg * m**2), 6.321 * (kg * m**2), 0.034 * (kg * m**2)), + power_off_drag="data/calisto/powerOffDragCurve.csv", + power_on_drag="data/calisto/powerOnDragCurve.csv", + center_of_mass_without_motor=0 * m, + coordinate_system_orientation="tail_to_nose", + ) + example_rocket.add_motor(dimensionless_cesaroni_m1670, position=(-1.373) * m) + return example_rocket diff --git a/tests/fixtures/surfaces/__init__.py b/tests/fixtures/surfaces/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fixtures/surfaces/surface_fixtures.py b/tests/fixtures/surfaces/surface_fixtures.py new file mode 100644 index 000000000..10595205f --- /dev/null +++ b/tests/fixtures/surfaces/surface_fixtures.py @@ -0,0 +1,78 @@ +import pytest + +from rocketpy import NoseCone, RailButtons, Tail, TrapezoidalFins + + +@pytest.fixture +def calisto_nose_cone(): + """The nose cone of the Calisto rocket. + + Returns + ------- + rocketpy.NoseCone + The nose cone of the Calisto rocket. + """ + return NoseCone( + length=0.55829, + kind="vonkarman", + base_radius=0.0635, + rocket_radius=0.0635, + name="calisto_nose_cone", + ) + + +@pytest.fixture +def calisto_tail(): + """The boat tail of the Calisto rocket. + + Returns + ------- + rocketpy.Tail + The boat tail of the Calisto rocket. + """ + return Tail( + top_radius=0.0635, + bottom_radius=0.0435, + length=0.060, + rocket_radius=0.0635, + name="calisto_tail", + ) + + +@pytest.fixture +def calisto_trapezoidal_fins(): + """The trapezoidal fins of the Calisto rocket. + + Returns + ------- + rocketpy.TrapezoidalFins + The trapezoidal fins of the Calisto rocket. + """ + return TrapezoidalFins( + n=4, + span=0.100, + root_chord=0.120, + tip_chord=0.040, + rocket_radius=0.0635, + name="calisto_trapezoidal_fins", + cant_angle=0, + sweep_length=None, + sweep_angle=None, + airfoil=None, + ) + + +@pytest.fixture +def calisto_rail_buttons(): + """The rail buttons of the Calisto rocket. + + Returns + ------- + rocketpy.RailButtons + The rail buttons of the Calisto rocket. + """ + return RailButtons( + buttons_distance=0.7, + angular_position=45, + name="Rail Buttons", + ) diff --git a/tests/fixtures/units/__init__.py b/tests/fixtures/units/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fixtures/units/numerical_fixtures.py b/tests/fixtures/units/numerical_fixtures.py new file mode 100644 index 000000000..8bc6883cf --- /dev/null +++ b/tests/fixtures/units/numerical_fixtures.py @@ -0,0 +1,28 @@ +import numericalunits +import pytest + + +@pytest.fixture +def m(): + """Create a simple object of the numericalunits.m class to be used in the + tests. This allows to avoid repeating the same code in all tests. + + Returns + ------- + numericalunits.m + A simple object of the numericalunits.m class + """ + return numericalunits.m + + +@pytest.fixture +def kg(): + """Create a simple object of the numericalunits.kg class to be used in the + tests. This allows to avoid repeating the same code in all tests. + + Returns + ------- + numericalunits.kg + A simple object of the numericalunits.kg class + """ + return numericalunits.kg diff --git a/tests/unit/test_function.py b/tests/unit/test_function.py index 1455212d9..8bcefb818 100644 --- a/tests/unit/test_function.py +++ b/tests/unit/test_function.py @@ -276,3 +276,33 @@ def test_set_discrete_based_on_model_non_mutator(linear_func): assert isinstance(func, Function) assert discretized_func.source.shape == (4, 2) assert callable(func.source) + + +@pytest.mark.parametrize( + "x, y, expected_x, expected_y", + [ + ( + np.array([1, 2, 3, 4, 5, 6]), + np.array([10, 20, 30, 40, 50000, 60]), + np.array([1, 2, 3, 4, 6]), + np.array([10, 20, 30, 40, 60]), + ), + ], +) +def test_remove_outliers_iqr(x, y, expected_x, expected_y): + """Test the function remove_outliers_iqr which is expected to remove + outliers from the data based on the Interquartile Range (IQR) method. + """ + func = Function(source=np.column_stack((x, y))) + filtered_func = func.remove_outliers_iqr(threshold=1.5) + + # Check if the outliers are removed + assert np.array_equal(filtered_func.x_array, expected_x) + assert np.array_equal(filtered_func.y_array, expected_y) + + # Check if the other attributes are preserved + assert filtered_func.__inputs__ == func.__inputs__ + assert filtered_func.__outputs__ == func.__outputs__ + assert filtered_func.__interpolation__ == func.__interpolation__ + assert filtered_func.__extrapolation__ == func.__extrapolation__ + assert filtered_func.title == func.title