diff --git a/rocketpy/AeroSurfaces.py b/rocketpy/AeroSurfaces.py index 68fb2bfb2..6f8be379b 100644 --- a/rocketpy/AeroSurfaces.py +++ b/rocketpy/AeroSurfaces.py @@ -1466,7 +1466,7 @@ def geometricInfo(self): def aerodynamicInfo(self): print(f"\nTail name: {self.name}") - print(f"Tail Center of Pressure Position in Local Coordinates: {self.cp:.3f} m") + print(f"Tail Center of Pressure Position in Local Coordinates: {self.cp} m") print(f"Tail Lift Coefficient Slope: {self.clalpha:.3f} 1/rad") print("Tail Lift Coefficient as a function of Alpha and Mach:") self.cl() diff --git a/rocketpy/Environment.py b/rocketpy/Environment.py index d68bf552c..23463e1e5 100644 --- a/rocketpy/Environment.py +++ b/rocketpy/Environment.py @@ -3027,7 +3027,54 @@ def info(self): # Plot graphs print("\n\nAtmospheric Model Plots") - self.plots.atmospheric_model() + # Create height grid + grid = np.linspace(self.elevation, self.maxExpectedHeight) + + # Create figure + plt.figure(figsize=(9, 4.5)) + + # Create wind speed and wind direction subplot + ax1 = plt.subplot(121) + ax1.plot( + [self.windSpeed(i) for i in grid], grid, "#ff7f0e", label="Speed of Sound" + ) + ax1.set_xlabel("Wind Speed (m/s)", color="#ff7f0e") + ax1.tick_params("x", colors="#ff7f0e") + ax1up = ax1.twiny() + ax1up.plot( + [self.windDirection(i) for i in grid], + grid, + color="#1f77b4", + label="Density", + ) + ax1up.set_xlabel("Wind Direction (°)", color="#1f77b4") + ax1up.tick_params("x", colors="#1f77b4") + ax1up.set_xlim(0, 360) + ax1.set_ylabel("Height Above Sea Level (m)") + ax1.grid(True) + + # Create density and speed of sound subplot + ax2 = plt.subplot(122) + ax2.plot( + [self.speedOfSound(i) for i in grid], + grid, + "#ff7f0e", + label="Speed of Sound", + ) + ax2.set_xlabel("Speed of Sound (m/s)", color="#ff7f0e") + ax2.tick_params("x", colors="#ff7f0e") + ax2up = ax2.twiny() + ax2up.plot( + [self.density(i) for i in grid], grid, color="#1f77b4", label="Density" + ) + ax2up.set_xlabel("Density (kg/m³)", color="#1f77b4") + ax2up.tick_params("x", colors="#1f77b4") + ax2.set_ylabel("Height Above Sea Level (m)") + ax2.grid(True) + + plt.subplots_adjust(wspace=0.5) + plt.show() + return None def allInfo(self): """Prints out all data and graphs available about the Environment. @@ -3046,7 +3093,7 @@ def allInfo(self): return None - def allPlotInfoReturned(self): + def allPlotInfoReturned(self) -> dict: """Returns a dictionary with all plot information available about the Environment. Parameters @@ -3158,9 +3205,10 @@ def allInfoReturned(self): return info def exportEnvironment(self, filename="environment"): - """Export important attributes of Environment class so it can be used - again in further siulations by using the customAtmosphere atmospheric - model. + """Export important attributes of Environment class to a .json file, + saving all the information needed to recreate the same environment using + customAtmosphere. + Parameters ---------- filename @@ -3170,9 +3218,13 @@ def exportEnvironment(self, filename="environment"): None """ - # TODO: in the future, allow the user to select which format will be used (json, csv, etc.). Default must be JSON. - # TODO: add self.exportEnvDictionary to the documentation - # TODO: find a way to documennt the workaround I've used on ma.getdata(self... + try: + atmosphericModelFile = self.atmosphericModelFile + atmosphericModelDict = self.atmosphericModelDict + except AttributeError: + atmosphericModelFile = "" + atmosphericModelDict = "" + self.exportEnvDictionary = { "railLength": self.rL, "gravity": self.g, @@ -3184,8 +3236,8 @@ def exportEnvironment(self, filename="environment"): "timeZone": self.timeZone, "maxExpectedHeight": float(self.maxExpectedHeight), "atmosphericModelType": self.atmosphericModelType, - "atmosphericModelFile": self.atmosphericModelFile, - "atmosphericModelDict": self.atmosphericModelDict, + "atmosphericModelFile": atmosphericModelFile, + "atmosphericModelDict": atmosphericModelDict, "atmosphericModelPressureProfile": ma.getdata( self.pressure.getSource() ).tolist(), @@ -3365,7 +3417,7 @@ def utmToGeodesic(self, x, y, utmZone, hemis, datum): hemis : string Equals to "S" for southern hemisphere and "N" for Northern hemisphere datum : string - The desired reference ellipsoide model, the following options are + The desired reference ellipsoid model, the following options are available: "SAD69", "WGS84", "NAD83", and "SIRGAS2000". The default is "SIRGAS2000", then this model will be used if the user make some typing mistake @@ -3373,9 +3425,9 @@ def utmToGeodesic(self, x, y, utmZone, hemis, datum): Returns ------- lat: float - latitude of the analysed point + latitude of the analyzed point lon: float - latitude of the analysed point + latitude of the analyzed point """ if hemis == "N": diff --git a/rocketpy/EnvironmentAnalysis.py b/rocketpy/EnvironmentAnalysis.py index 8eeacbc0f..c94a8e54b 100644 --- a/rocketpy/EnvironmentAnalysis.py +++ b/rocketpy/EnvironmentAnalysis.py @@ -1410,6 +1410,7 @@ def plot_average_temperature_along_day(self): plt.grid(alpha=0.25) plt.legend() plt.show() + return None def calculate_average_sustained_surface10m_wind_along_day(self): """Computes average sustained wind speed progression throughout the @@ -1509,6 +1510,7 @@ def plot_average_surface10m_wind_speed_along_day(self, windSpeedLimit=False): plt.grid(alpha=0.25) plt.legend() plt.show() + return None def calculate_average_sustained_surface100m_wind_along_day(self): """Computes average sustained wind speed progression throughout the @@ -1672,6 +1674,7 @@ def plot_average_wind_speed_profile(self, clear_range_limits=False): plt.legend() plt.xlim(0, max(np.percentile(wind_speed_profiles, 50 + 49.85, axis=0))) plt.show() + return None def plot_average_wind_heading_profile(self, clear_range_limits=False): """Average wind heading for all datetimes available.""" @@ -1764,6 +1767,7 @@ def plot_average_wind_heading_profile(self, clear_range_limits=False): plt.title("Average Wind heading Profile") plt.legend() plt.show() + return None def process_wind_speed_and_direction_data_for_average_day(self): """Process the wind_speed and wind_direction data to generate lists of all the wind_speeds recorded @@ -1892,6 +1896,7 @@ def plot_average_pressure_profile(self, clear_range_limits=False): plt.legend() plt.xlim(0, max(np.percentile(pressure_profiles, 50 + 49.85, axis=0))) plt.show() + return None @staticmethod def plot_wind_rose( @@ -1958,6 +1963,8 @@ def plot_average_day_wind_rose_specific_hour(self, hour, fig=None): ) plt.show() + return None + def plot_average_day_wind_rose_all_hours(self): """Plot wind roses for all hours of a day, in a grid like plot.""" # Get days and hours @@ -2032,6 +2039,7 @@ def plot_average_day_wind_rose_all_hours(self): f"Wind Roses ({self.unit_system['wind_speed']})", fontsize=20, x=0.5, y=1 ) plt.show() + return None def animate_average_wind_rose(self, figsize=(8, 8), filename="wind_rose.gif"): """Animates the wind_rose of an average day. The inputs of a wind_rose @@ -2169,6 +2177,8 @@ def plot_wind_gust_distribution_over_average_day(self): fig.supylabel("Probability") plt.show() + return None + def animate_wind_gust_distribution_over_average_day(self): """Animation of how the wind gust distribution varies throughout the day.""" # Gather animation data @@ -2347,9 +2357,12 @@ def plot_sustained_surface_wind_speed_distribution_over_average_day( fig.supylabel("Probability") plt.show() + return None + def animate_sustained_surface_wind_speed_distribution_over_average_day( self, windSpeedLimit=False - ): # TODO: getting weird results since the 0.3 on y axis is not parametrized + ): + # TODO: getting weird results since the 0.3 on y axis is not parametrized """Animation of how the sustained surface wind speed distribution varies throughout the day.""" # Gather animation data surface_wind_speeds_at_given_hour = {} @@ -2503,6 +2516,8 @@ def process_temperature_profile_over_average_day(self): average_temperature_profile_at_given_hour ) + return None + def process_pressure_profile_over_average_day(self): """Compute the average pressure profile for each available hour of a day, over all days in the dataset.""" @@ -2536,6 +2551,8 @@ def process_pressure_profile_over_average_day(self): average_pressure_profile_at_given_hour ) + return None + def process_wind_speed_profile_over_average_day(self): """Compute the average wind profile for each available hour of a day, over all days in the dataset.""" @@ -2570,6 +2587,8 @@ def process_wind_speed_profile_over_average_day(self): self.max_average_wind_at_altitude = max_wind self.average_wind_profile_at_given_hour = average_wind_profile_at_given_hour + return None + def process_wind_velocity_x_profile_over_average_day(self): """Compute the average windVelocityX profile for each available hour of a day, over all days in the dataset.""" @@ -2602,6 +2621,7 @@ def process_wind_velocity_x_profile_over_average_day(self): self.average_windVelocityX_profile_at_given_hour = ( average_windVelocityX_profile_at_given_hour ) + return None def process_wind_velocity_y_profile_over_average_day(self): """Compute the average windVelocityY profile for each available hour of a day, over all @@ -2635,6 +2655,7 @@ def process_wind_velocity_y_profile_over_average_day(self): self.average_windVelocityY_profile_at_given_hour = ( average_windVelocityY_profile_at_given_hour ) + return None def plot_wind_profile_over_average_day(self, clear_range_limits=False): """Creates a grid of plots with the wind profile over the average day.""" @@ -2706,6 +2727,8 @@ def plot_wind_profile_over_average_day(self, clear_range_limits=False): fig.supylabel(f"Altitude AGL ({self.unit_system['length']})") plt.show() + return None + def process_wind_heading_profile_over_average_day(self): """Compute the average wind velocities (both X and Y components) profile for each available hour of a day, over all days in the dataset.""" altitude_list = np.linspace(*self.altitude_AGL_range, 100) @@ -2774,6 +2797,8 @@ def process_wind_heading_profile_over_average_day(self): average_wind_heading_profile_at_given_hour ) + return None + def plot_wind_heading_profile_over_average_day(self, clear_range_limits=False): """Creates a grid of plots with the wind heading profile over the average day.""" self.process_wind_heading_profile_over_average_day() @@ -2836,6 +2861,8 @@ def plot_wind_heading_profile_over_average_day(self, clear_range_limits=False): fig.supylabel(f"Altitude AGL ({self.unit_system['length']})") plt.show() + return None + def animate_wind_profile_over_average_day(self, clear_range_limits=False): """Animation of how wind profile evolves throughout an average day.""" self.process_wind_speed_profile_over_average_day() @@ -3110,6 +3137,8 @@ def allInfo(self): f"Percentage of Days Without Clouds: {100*self.percentage_of_days_with_no_cloud_coverage:.1f} %" ) + return None + def exportMeanProfiles(self, filename="export_env_analysis"): """ Exports the mean profiles of the weather data to a file in order to it diff --git a/rocketpy/Function.py b/rocketpy/Function.py index f0d64e60b..986c6de96 100644 --- a/rocketpy/Function.py +++ b/rocketpy/Function.py @@ -152,7 +152,7 @@ def setSource(self, source): temp = 1 * source def source(x): - return 0 * x + temp + return temp # Handle callable source or number source if callable(source): diff --git a/tests/conftest.py b/tests/conftest.py index be47393c2..4d7bea0d4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,7 @@ import numericalunits import pytest -from rocketpy import Environment, Rocket, SolidMotor +from rocketpy import Environment, EnvironmentAnalysis, Function, Rocket, SolidMotor def pytest_addoption(parser): @@ -101,6 +101,85 @@ def dimensionless_rocket(kg, m, dimensionless_solid_motor): return example_rocket +@pytest.fixture +def example_env(): + Env = Environment(railLength=5, datum="WGS84") + return Env + + +@pytest.fixture +def example_env_robust(): + Env = Environment( + railLength=5, + latitude=32.990254, + longitude=-106.974998, + elevation=1400, + datum="WGS84", + ) + tomorrow = datetime.date.today() + datetime.timedelta(days=1) + Env.setDate( + (tomorrow.year, tomorrow.month, tomorrow.day, 12) + ) # Hour given in UTC time + return Env + + +# Create a simple object of the Environment Analysis class +@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, + surfaceDataFile="./data/weather/EuroC_single_level_reanalysis_2002_2021.nc", + pressureLevelDataFile="./data/weather/EuroC_pressure_levels_reanalysis_2001-2021.nc", + timezone=None, + unit_system="metric", + forecast_date=None, + forecast_args=None, + maxExpectedAltitude=None, + ) + + return env_analysis + + +@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 func_from_csv(): + func = Function( + source="tests/fixtures/airfoils/e473-10e6-degrees.csv", + inputs=["Scalar"], + outputs=["Scalar"], + interpolation="linear", + extrapolation="linear", + ) + return func + + def pytest_collection_modifyitems(config, items): if config.getoption("--runslow"): # --runslow given in cli: do not skip slow tests diff --git a/tests/test_environment.py b/tests/test_environment.py index 20f826275..affff2702 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -1,9 +1,12 @@ import datetime +import os from unittest.mock import patch +import numpy as np import pytest import pytz -from rocketpy import Environment, Flight, Rocket, SolidMotor + +from rocketpy import Environment def test_env_set_date(example_env): @@ -52,8 +55,10 @@ def test_set_topographic_profile(example_env): @patch("matplotlib.pyplot.show") def test_standard_atmosphere(mock_show, example_env): example_env.setAtmosphericModel(type="StandardAtmosphere") + assert example_env.info() == None assert example_env.allInfo() == None assert example_env.pressure(0) == 101325.0 + assert example_env.printEarthDetails() == None @patch("matplotlib.pyplot.show") @@ -140,6 +145,43 @@ def test_gefs_atmosphere(mock_show, example_env_robust): assert example_env_robust.allInfo() == None +@patch("matplotlib.pyplot.show") +def test_info_returns(mock_show, example_env): + + returned_plots = example_env.allPlotInfoReturned() + returned_infos = example_env.allInfoReturned() + expected_info = { + "grav": 9.80665, + "launch_rail_length": 5, + "elevation": 0, + "modelType": "StandardAtmosphere", + "modelTypeMaxExpectedHeight": 80000, + "windSpeed": 0, + "windDirection": 0, + "windHeading": 0, + "surfacePressure": 1013.25, + "surfaceTemperature": 288.15, + "surfaceAirDensity": 1.225000018124288, + "surfaceSpeedOfSound": 340.293988026089, + "lat": 0, + "lon": 0, + } + expected_plots_keys = [ + "grid", + "windSpeed", + "windDirection", + "speedOfSound", + "density", + "windVelX", + "windVelY", + "pressure", + "temperature", + ] + assert list(returned_infos.keys()) == list(expected_info.keys()) + assert list(returned_infos.values()) == list(expected_info.values()) + assert list(returned_plots.keys()) == expected_plots_keys + + @pytest.mark.slow @patch("matplotlib.pyplot.show") def test_cmc_atmosphere(mock_show, example_env_robust): @@ -183,3 +225,16 @@ def test_hiresw_ensemble_atmosphere(mock_show, example_env_robust): dictionary=HIRESW_dictionary, ) assert example_env_robust.allInfo() == None + + +def test_export_environment(example_env_robust): + assert example_env_robust.exportEnvironment(filename="environment") == None + os.remove("environment.json") + + +def test_utmToGeodesic(example_env_robust): + lat, lon = example_env_robust.utmToGeodesic( + x=315468.64, y=3651938.65, utmZone=13, hemis="N", datum="WGS84" + ) + assert np.isclose(lat, 32.99025, atol=1e-5) == True + assert np.isclose(lon, -106.9750, atol=1e-5) == True diff --git a/tests/test_environment_anaysis.py b/tests/test_environment_anaysis.py new file mode 100644 index 000000000..3a352062f --- /dev/null +++ b/tests/test_environment_anaysis.py @@ -0,0 +1,175 @@ +import copy +import datetime +import os +from unittest.mock import patch + +import ipywidgets as widgets +import matplotlib as plt +import pytest +from IPython.display import HTML + +from rocketpy import EnvironmentAnalysis + +plt.rcParams.update({"figure.max_open_warning": 0}) + + +@pytest.mark.slow +def test_allInfo(env_analysis): + """Test the EnvironmentAnalysis.allInfo() method, which already invokes + several other methods. It is a good way to test the whole class in a first view. + However, if it fails, it is hard to know which method is failing. + + Parameters + ---------- + env_analysis : EnvironmentAnalysis + A simple object of the Environment Analysis class + + Returns + ------- + None + """ + assert env_analysis.allInfo() == None + + +@pytest.mark.slow +@patch("matplotlib.pyplot.show") +def test_distribution_plots(mock_show, env_analysis): + """Tests the distribution plots method of the EnvironmentAnalysis class. It + only checks if the method runs without errors. It does not check if the + plots are correct, as this would require a lot of work and would be + difficult to maintain. + + Parameters + ---------- + env_analysis : EnvironmentAnalysis + A simple object of the EnvironmentAnalysis class. + + Returns + ------- + None + """ + + # Check distribution plots + assert env_analysis.plot_wind_gust_distribution() == None + assert env_analysis.plot_surface10m_wind_speed_distribution() == None + assert env_analysis.plot_wind_gust_distribution_over_average_day() == None + assert ( + env_analysis.plot_sustained_surface_wind_speed_distribution_over_average_day() + == None + ) + + +@pytest.mark.slow +@patch("matplotlib.pyplot.show") +def test_average_plots(mock_show, env_analysis): + """Tests the average plots method of the EnvironmentAnalysis class. It + only checks if the method runs without errors. It does not check if the + plots are correct, as this would require a lot of work and would be + difficult to maintain. + + Parameters + ---------- + env_analysis : EnvironmentAnalysis + A simple object of the EnvironmentAnalysis class. + + Returns + ------- + None + """ + # Check "average" plots + assert env_analysis.plot_average_temperature_along_day() == None + assert env_analysis.plot_average_surface10m_wind_speed_along_day(False) == None + assert env_analysis.plot_average_surface10m_wind_speed_along_day(True) == None + assert ( + env_analysis.plot_average_sustained_surface100m_wind_speed_along_day() == None + ) + assert env_analysis.plot_average_day_wind_rose_all_hours() == None + assert env_analysis.plot_average_day_wind_rose_specific_hour(12) == None + assert env_analysis.plot_average_day_wind_rose_specific_hour(12) == None + + +@pytest.mark.slow +@patch("matplotlib.pyplot.show") +def test_profile_plots(mock_show, env_analysis): + """Check the profile plots method of the EnvironmentAnalysis class. It + only checks if the method runs without errors. It does not check if the + plots are correct, as this would require a lot of work and would be + difficult to maintain. + + Parameters + ---------- + mock_show : Mock + Mock of the matplotlib.pyplot.show() method + env_analysis : EnvironmentAnalysis + A simple object of the EnvironmentAnalysis class. + """ + # Check profile plots + assert env_analysis.plot_wind_heading_profile_over_average_day() == None + assert ( + env_analysis.plot_average_wind_heading_profile(clear_range_limits=False) == None + ) + assert ( + env_analysis.plot_average_wind_heading_profile(clear_range_limits=True) == None + ) + assert ( + env_analysis.plot_average_wind_speed_profile(clear_range_limits=False) == None + ) + assert env_analysis.plot_average_wind_speed_profile(clear_range_limits=True) == None + assert env_analysis.plot_average_pressure_profile(clear_range_limits=False) == None + assert env_analysis.plot_average_pressure_profile(clear_range_limits=True) == None + assert env_analysis.plot_wind_profile_over_average_day() == None + + +@pytest.mark.slow +@patch("matplotlib.pyplot.show") +def test_animation_plots(mock_show, env_analysis): + """Check the animation plots method of the EnvironmentAnalysis class. It + only checks if the method runs without errors. It does not check if the + plots are correct, as this would require a lot of work and would be + difficult to maintain. + + Parameters + ---------- + mock_show : Mock + Mock of the matplotlib.pyplot.show() method + env_analysis : EnvironmentAnalysis + A simple object of the EnvironmentAnalysis class. + """ + + # Check animation plots + assert isinstance(env_analysis.animate_average_wind_rose(), widgets.Image) + assert isinstance( + env_analysis.animate_wind_gust_distribution_over_average_day(), HTML + ) + assert isinstance( + env_analysis.animate_wind_heading_profile_over_average_day(), HTML + ) + assert isinstance(env_analysis.animate_wind_profile_over_average_day(), HTML) + assert isinstance( + env_analysis.animate_sustained_surface_wind_speed_distribution_over_average_day(), + HTML, + ) + + +@pytest.mark.slow +def test_exports(env_analysis): + """Check the export methods of the EnvironmentAnalysis class. It + only checks if the method runs without errors. It does not check if the + files are correct, as this would require a lot of work and would be + difficult to maintain. + + Parameters + ---------- + env_analysis : EnvironmentAnalysis + A simple object of the EnvironmentAnalysis class. + """ + + assert env_analysis.exportMeanProfiles() == None + assert env_analysis.save("EnvAnalysisDict") == None + + env2 = copy.deepcopy(env_analysis) + env2.load("EnvAnalysisDict") + assert env2.allInfo() == None + + # Delete file created by save method + os.remove("EnvAnalysisDict") diff --git a/tests/test_flight.py b/tests/test_flight.py index 87b13115d..bb1b1a51e 100644 --- a/tests/test_flight.py +++ b/tests/test_flight.py @@ -1,5 +1,5 @@ -import os import datetime +import os from unittest.mock import patch import matplotlib as plt @@ -601,6 +601,7 @@ def test_export_KML(): # Load exported files and fixtures and compare them test_1 = open("test_export_data_1.kml", "r") + for row in test_1: if row[:29] == " ": r = row[29:-15] diff --git a/tests/test_function.py b/tests/test_function.py new file mode 100644 index 000000000..5e56f12c3 --- /dev/null +++ b/tests/test_function.py @@ -0,0 +1,146 @@ +from unittest.mock import patch + +import matplotlib as plt +import numpy as np +import pytest + +from rocketpy import Function + +plt.rcParams.update({"figure.max_open_warning": 0}) + + +# Test Function creation from .csv file +def test_function_from_csv(func_from_csv): + """Test the Function class creation from a .csv file. + + Parameters + ---------- + func_from_csv : rocketpy.Function + A Function object created from a .csv file. + """ + # Assert the function is zero at 0 but with a certain tolerance + assert np.isclose(func_from_csv(0), 0.0, atol=1e-6) + # Check the __str__ method + assert func_from_csv.__str__() == "Function from R1 to R1 : (Scalar) → (Scalar)" + # Check the __repr__ method + assert func_from_csv.__repr__() == "Function from R1 to R1 : (Scalar) → (Scalar)" + + +def test_getters(func_from_csv): + """Test the different getters of the Function class. + + Parameters + ---------- + func_from_csv : rocketpy.Function + A Function object created from a .csv file. + """ + assert func_from_csv.getInputs() == ["Scalar"] + assert func_from_csv.getOutputs() == ["Scalar"] + assert func_from_csv.getInterpolationMethod() == "linear" + assert func_from_csv.getExtrapolationMethod() == "linear" + assert np.isclose(func_from_csv.getValue(0), 0.0, atol=1e-6) + assert np.isclose(func_from_csv.getValueOpt_deprecated(0), 0.0, atol=1e-6) + assert np.isclose(func_from_csv.getValueOpt(0), 0.0, atol=1e-6) + assert np.isclose(func_from_csv.getValueOpt2(0), 0.0, atol=1e-6) + + +def test_setters(func_from_csv): + """Test the different setters of the Function class. + + Parameters + ---------- + func_from_csv : rocketpy.Function + A Function object created from a .csv file. + """ + # Test set methods + func_from_csv.setInputs(["Scalar2"]) + assert func_from_csv.getInputs() == ["Scalar2"] + func_from_csv.setOutputs(["Scalar2"]) + assert func_from_csv.getOutputs() == ["Scalar2"] + func_from_csv.setInterpolation("linear") + assert func_from_csv.getInterpolationMethod() == "linear" + func_from_csv.setExtrapolation("linear") + assert func_from_csv.getExtrapolationMethod() == "linear" + + +@patch("matplotlib.pyplot.show") +def test_plots(mock_show, func_from_csv): + """Test different plot methods of the Function class. + + Parameters + ---------- + mock_show : Mock + Mock of the matplotlib.pyplot.show method. + func_from_csv : rocketpy.Function + A Function object created from a .csv file. + """ + # Test plot methods + assert func_from_csv.plot() == None + # Test comparePlots + func2 = Function( + source="tests/fixtures/airfoils/e473-10e6-degrees.csv", + inputs=["Scalar"], + outputs=["Scalar"], + interpolation="linear", + extrapolation="linear", + ) + assert ( + func_from_csv.comparePlots([func_from_csv, func2], returnObject=False) == None + ) + + +def test_interpolation_methods(linear_func): + """Test some of the interpolation methods of the Function class. Methods + not tested here are already being called in other tests. + + Parameters + ---------- + linear_func : rocketpy.Function + A Function object created from a list of values. + """ + # Test Akima + linear_func.setInterpolation("akima") + assert linear_func.getInterpolationMethod() == "akima" + assert np.isclose(linear_func.getValue(0), 0.0, atol=1e-6) + + # Test polynomial + linear_func.setInterpolation("polynomial") + assert linear_func.getInterpolationMethod() == "polynomial" + assert np.isclose(linear_func.getValue(0), 0.0, atol=1e-6) + + +def test_extrapolation_methods(linear_func): + """Test some of the extrapolation methods of the Function class. Methods + not tested here are already being called in other tests. + + Parameters + ---------- + linear_func : rocketpy.Function + A Function object created from a list of values. + """ + # Test zero + linear_func.setExtrapolation("zero") + assert linear_func.getExtrapolationMethod() == "zero" + assert np.isclose(linear_func.getValue(-1), 0, atol=1e-6) + + # Test constant + linear_func.setExtrapolation("constant") + assert linear_func.getExtrapolationMethod() == "constant" + assert np.isclose(linear_func.getValue(-1), 0, atol=1e-6) + + # Test natural + linear_func.setExtrapolation("natural") + assert linear_func.getExtrapolationMethod() == "natural" + assert np.isclose(linear_func.getValue(-1), -1, atol=1e-6) + + +def test_integral(linear_func): + """Test the integral method of the Function class. + + Parameters + ---------- + linear_func : rocketpy.Function + A Function object created from a list of values. + """ + # Test integral + assert np.isclose(linear_func.integral(0, 1), 0.5, atol=1e-6) diff --git a/tests/test_rocket.py b/tests/test_rocket.py index 91dfd61dd..79401184f 100644 --- a/tests/test_rocket.py +++ b/tests/test_rocket.py @@ -4,6 +4,7 @@ import pytest from rocketpy import Rocket, SolidMotor +from rocketpy.AeroSurfaces import NoseCone @patch("matplotlib.pyplot.show") @@ -82,7 +83,16 @@ def mainTrigger(p, y): static_margin = test_rocket.staticMargin(0) + # Check if allInfo and static_method methods are working properly assert test_rocket.allInfo() == None or not abs(static_margin - 2.05) < 0.01 + # Check if NoseCone allInfo() is working properly + assert NoseCone.allInfo() == None + # Check if FinSet allInfo() is working properly + assert FinSet.allInfo() == None + # Check if Tail allInfo() is working properly + assert Tail.allInfo() == None + # Check if draw method is working properly + assert FinSet.draw() == None @patch("matplotlib.pyplot.show") @@ -249,6 +259,7 @@ def mainTrigger(p, y): static_margin = test_rocket.staticMargin(0) assert test_rocket.allInfo() == None or not abs(static_margin - 2.30) < 0.01 + assert FinSet.draw() == None @patch("matplotlib.pyplot.show") @@ -500,6 +511,9 @@ def test_add_trapezoidal_fins_sweep_length( # Check rocket's center of pressure (just double checking) assert translate - rocket.cpPosition == pytest.approx(expected_cpz_cm, 0.01) + # Check if AeroSurfaces.__getitem__() works + assert isinstance(rocket.aerodynamicSurfaces.__getitem__(0)[0], NoseCone) + def test_add_fins_assert_cp_cm_plus_fins(rocket, dimensionless_rocket, m): rocket.addTrapezoidalFins(