From fd8de9714677adec0166adda43fd5952e7c6700d Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Sun, 20 Nov 2022 10:44:46 +0100 Subject: [PATCH 01/22] TST: adding tests for aerosurfaces methods --- tests/test_rocket.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_rocket.py b/tests/test_rocket.py index 6b7295358..0c19a04e0 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") @@ -81,7 +82,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") @@ -160,6 +170,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") @@ -410,6 +421,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( From 68e97261f6b45688e1f76e18071c84a501cf77d9 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Sun, 20 Nov 2022 10:45:02 +0100 Subject: [PATCH 02/22] BUG: correct wrong f string tail method --- rocketpy/AeroSurfaces.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocketpy/AeroSurfaces.py b/rocketpy/AeroSurfaces.py index 9d3c718ab..17661c8a9 100644 --- a/rocketpy/AeroSurfaces.py +++ b/rocketpy/AeroSurfaces.py @@ -1458,7 +1458,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() From ba547493b81d2c92c645a64a202998af0f7ef37e Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Sun, 20 Nov 2022 10:46:56 +0100 Subject: [PATCH 03/22] BUG: fix Environment.exportEnvironment() --- rocketpy/Environment.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/rocketpy/Environment.py b/rocketpy/Environment.py index 35e846f07..35519fcae 100644 --- a/rocketpy/Environment.py +++ b/rocketpy/Environment.py @@ -3329,7 +3329,7 @@ def allInfoReturned(self): 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 + again in further simulations by using the customAtmosphere atmospheric model. Parameters ---------- @@ -3340,9 +3340,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, @@ -3354,8 +3358,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(), From 65fe9d737ca0505f04e87533049e0cfa8d2155ee Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Sun, 20 Nov 2022 10:47:45 +0100 Subject: [PATCH 04/22] MAINT: specifying input types in utmToGeodesic --- rocketpy/Environment.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/rocketpy/Environment.py b/rocketpy/Environment.py index 35519fcae..850cd5c93 100644 --- a/rocketpy/Environment.py +++ b/rocketpy/Environment.py @@ -2974,6 +2974,7 @@ def info(self): plt.subplots_adjust(wspace=0.5) plt.show() + return None def allInfo(self): """Prints out all data and graphs available about the Environment. @@ -3216,7 +3217,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 @@ -3340,6 +3341,9 @@ 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 + try: atmosphericModelFile = self.atmosphericModelFile atmosphericModelDict = self.atmosphericModelDict @@ -3522,7 +3526,7 @@ def geodesicToUtm(self, lat, lon, datum): return x, y, utmZone, utmLetter, hemis, EW - def utmToGeodesic(self, x, y, utmZone, hemis, datum): + def utmToGeodesic(self, x: float, y: float, utmZone: int, hemis: str, datum: str): """Function to convert UTM coordinates to geodesic coordinates (i.e. latitude and longitude). The latitude should be between -80° and 84° @@ -3539,7 +3543,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 @@ -3547,9 +3551,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": From d591d529282897323fa9897ccb3357e8abbfa2ea Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Sun, 20 Nov 2022 10:48:41 +0100 Subject: [PATCH 05/22] MAINT: specifying input and retuning Nones --- rocketpy/EnvironmentAnalysis.py | 77 ++++++++++++++++++++++++--------- 1 file changed, 56 insertions(+), 21 deletions(-) diff --git a/rocketpy/EnvironmentAnalysis.py b/rocketpy/EnvironmentAnalysis.py index 8eeacbc0f..4ae226a79 100644 --- a/rocketpy/EnvironmentAnalysis.py +++ b/rocketpy/EnvironmentAnalysis.py @@ -1237,7 +1237,7 @@ def calculate_record_min_surface_100m_wind_speed(self): self.record_min_surface_100m_wind_speed = np.min(self.surface_100m_wind_speed) return self.record_min_surface_100m_wind_speed - def plot_wind_gust_distribution(self): + def plot_wind_gust_distribution(self) -> None: """Get all values of wind gust speed (for every date and hour available) and plot a single distribution. Expected result is a Weibull distribution. """ @@ -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( @@ -1932,7 +1937,7 @@ def plot_wind_rose( ax.yaxis.set_major_formatter(mtick.PercentFormatter(decimals=0)) return ax - def plot_average_day_wind_rose_specific_hour(self, hour, fig=None): + def plot_average_day_wind_rose_specific_hour(self, hour, fig=None) -> None: """Plot a specific hour of the average windrose Parameters @@ -1958,7 +1963,9 @@ def plot_average_day_wind_rose_specific_hour(self, hour, fig=None): ) plt.show() - def plot_average_day_wind_rose_all_hours(self): + return None + + def plot_average_day_wind_rose_all_hours(self) -> None: """Plot wind roses for all hours of a day, in a grid like plot.""" # Get days and hours days = list(self.surfaceDataDict.keys()) @@ -2032,8 +2039,11 @@ 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"): + def animate_average_wind_rose( + self, figsize: tuple = (8, 8), filename: str = "wind_rose.gif" + ) -> widgets.Image: """Animates the wind_rose of an average day. The inputs of a wind_rose are the location of the place where we want to analyze, (x,y,z). The data is assembled by hour, which means, the windrose of a specific hour is @@ -2099,7 +2109,7 @@ def animate_average_wind_rose(self, figsize=(8, 8), filename="wind_rose.gif"): height=fig_height, ) - def plot_wind_gust_distribution_over_average_day(self): + def plot_wind_gust_distribution_over_average_day(self) -> None: """Plots shown in the animation of how the wind gust distribution varies throughout the day.""" # Gather animation data average_wind_gust_at_given_hour = {} @@ -2169,7 +2179,9 @@ def plot_wind_gust_distribution_over_average_day(self): fig.supylabel("Probability") plt.show() - def animate_wind_gust_distribution_over_average_day(self): + return None + + def animate_wind_gust_distribution_over_average_day(self) -> HTML: """Animation of how the wind gust distribution varies throughout the day.""" # Gather animation data wind_gusts_at_given_hour = {} @@ -2252,8 +2264,8 @@ def update(frame): return HTML(animation.to_jshtml()) def plot_sustained_surface_wind_speed_distribution_over_average_day( - self, windSpeedLimit=False - ): + self, windSpeedLimit: bool = False + ) -> None: """Plots shown in the animation of how the sustained surface wind speed distribution varies throughout the day.""" # Gather animation data average_wind_speed_at_given_hour = {} @@ -2347,9 +2359,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 + self, windSpeedLimit: bool = False + ) -> HTML: + # 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 = {} @@ -2457,7 +2472,7 @@ def update(frame): return HTML(animation.to_jshtml()) @property - def altitude_AGL_range(self): + def altitude_AGL_range(self) -> tuple: min_altitude = 0 if self.maxExpectedAltitude == None: max_altitudes = [ @@ -2470,7 +2485,7 @@ def altitude_AGL_range(self): max_altitude = self.maxExpectedAltitude return min_altitude, max_altitude - def process_temperature_profile_over_average_day(self): + def process_temperature_profile_over_average_day(self) -> None: """Compute the average temperature profile for each available hour of a day, over all days in the dataset.""" altitude_list = np.linspace(*self.altitude_AGL_range, 100) @@ -2503,7 +2518,9 @@ def process_temperature_profile_over_average_day(self): average_temperature_profile_at_given_hour ) - def process_pressure_profile_over_average_day(self): + return None + + def process_pressure_profile_over_average_day(self) -> None: """Compute the average pressure profile for each available hour of a day, over all days in the dataset.""" altitude_list = np.linspace(*self.altitude_AGL_range, 100) @@ -2536,7 +2553,9 @@ def process_pressure_profile_over_average_day(self): average_pressure_profile_at_given_hour ) - def process_wind_speed_profile_over_average_day(self): + return None + + def process_wind_speed_profile_over_average_day(self) -> None: """Compute the average wind profile for each available hour of a day, over all days in the dataset.""" altitude_list = np.linspace(*self.altitude_AGL_range, 100) @@ -2570,7 +2589,9 @@ 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 - def process_wind_velocity_x_profile_over_average_day(self): + return None + + def process_wind_velocity_x_profile_over_average_day(self) -> None: """Compute the average windVelocityX profile for each available hour of a day, over all days in the dataset.""" altitude_list = np.linspace(*self.altitude_AGL_range, 100) @@ -2602,8 +2623,9 @@ 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): + def process_wind_velocity_y_profile_over_average_day(self) -> None: """Compute the average windVelocityY profile for each available hour of a day, over all days in the dataset.""" altitude_list = np.linspace(*self.altitude_AGL_range, 100) @@ -2635,8 +2657,11 @@ 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): + def plot_wind_profile_over_average_day( + self, clear_range_limits: bool = False + ) -> None: """Creates a grid of plots with the wind profile over the average day.""" self.process_wind_speed_profile_over_average_day() @@ -2706,7 +2731,9 @@ def plot_wind_profile_over_average_day(self, clear_range_limits=False): fig.supylabel(f"Altitude AGL ({self.unit_system['length']})") plt.show() - def process_wind_heading_profile_over_average_day(self): + return None + + def process_wind_heading_profile_over_average_day(self) -> None: """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) average_wind_velocity_X_profile_at_given_hour = {} @@ -2774,7 +2801,11 @@ def process_wind_heading_profile_over_average_day(self): average_wind_heading_profile_at_given_hour ) - def plot_wind_heading_profile_over_average_day(self, clear_range_limits=False): + return None + + def plot_wind_heading_profile_over_average_day( + self, clear_range_limits: bool = False + ) -> None: """Creates a grid of plots with the wind heading profile over the average day.""" self.process_wind_heading_profile_over_average_day() @@ -2836,7 +2867,9 @@ def plot_wind_heading_profile_over_average_day(self, clear_range_limits=False): fig.supylabel(f"Altitude AGL ({self.unit_system['length']})") plt.show() - def animate_wind_profile_over_average_day(self, clear_range_limits=False): + return None + + def animate_wind_profile_over_average_day(self, clear_range_limits: bool =False) -> HTML: """Animation of how wind profile evolves throughout an average day.""" self.process_wind_speed_profile_over_average_day() @@ -2905,7 +2938,7 @@ def update(frame): plt.close(fig) return HTML(animation.to_jshtml()) - def animate_wind_heading_profile_over_average_day(self, clear_range_limits=False): + def animate_wind_heading_profile_over_average_day(self, clear_range_limits: bool =False) -> HTML: """Animation of how wind heading profile evolves throughout an average day.""" self.process_wind_heading_profile_over_average_day() @@ -3110,6 +3143,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 From a014dba971c53250900b53b74f2051dbee527a47 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Sun, 20 Nov 2022 10:49:14 +0100 Subject: [PATCH 06/22] TST: Add additional cases to test_environment --- tests/test_environment.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/tests/test_environment.py b/tests/test_environment.py index 078a23cd9..852c5d18d 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 @pytest.fixture @@ -74,8 +77,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") @@ -155,11 +160,16 @@ def test_era5_atmosphere(mock_show): assert Env.allInfo() == None +# TODO: utmToGeodesic + + @pytest.mark.slow @patch("matplotlib.pyplot.show") def test_gefs_atmosphere(mock_show, example_env_robust): example_env_robust.setAtmosphericModel(type="Ensemble", file="GEFS") assert example_env_robust.allInfo() == None + assert isinstance(example_env.allPlotInfoReturned(), dict) + assert isinstance(example_env.allInfoReturned(), dict) @pytest.mark.slow @@ -205,3 +215,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 From 40baf0b4a6fb9d4a4f62ab039e04ef9603ec3b82 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Sun, 20 Nov 2022 10:49:36 +0100 Subject: [PATCH 07/22] TST: Create test for environment analysis --- tests/test_environment_anaysis.py | 149 ++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 tests/test_environment_anaysis.py diff --git a/tests/test_environment_anaysis.py b/tests/test_environment_anaysis.py new file mode 100644 index 000000000..6848190f1 --- /dev/null +++ b/tests/test_environment_anaysis.py @@ -0,0 +1,149 @@ +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}) + +# Create a simple object of the Environment Analysis class +@pytest.fixture +def env(): + """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 = 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 + + +def test_allInfo(env): + """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 : EnvironmentAnalysis + A simple object of the Environment Analysis class + + Returns + ------- + None + """ + assert env.allInfo() == None + + +@patch("matplotlib.pyplot.show") +def test_distribution_plots(mock_show, env): + """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 : EnvironmentAnalysis + A simple object of the EnvironmentAnalysis class. + + Returns + ------- + None + """ + + # Check distribution plots + assert env.plot_wind_gust_distribution() == None + assert env.plot_surface10m_wind_speed_distribution() == None + assert env.plot_wind_gust_distribution_over_average_day() == None + assert env.plot_sustained_surface_wind_speed_distribution_over_average_day() == None + + +@patch("matplotlib.pyplot.show") +def test_average_plots(mock_show, env): + """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 : EnvironmentAnalysis + A simple object of the EnvironmentAnalysis class. + + Returns + ------- + None + """ + # Check "average" plots + assert env.plot_average_temperature_along_day() == None + assert env.plot_average_surface10m_wind_speed_along_day(False) == None + assert env.plot_average_surface10m_wind_speed_along_day(True) == None + assert env.plot_average_sustained_surface100m_wind_speed_along_day() == None + assert env.plot_average_day_wind_rose_all_hours() == None + assert env.plot_average_day_wind_rose_specific_hour(12) == None + assert env.plot_average_day_wind_rose_specific_hour(12) == None + + +@patch("matplotlib.pyplot.show") +def test_profile_plots(mock_show, env): + # Check profile plots + assert env.plot_wind_heading_profile_over_average_day() == None + assert env.plot_average_wind_heading_profile(clear_range_limits=False) == None + assert env.plot_average_wind_heading_profile(clear_range_limits=True) == None + assert env.plot_average_wind_speed_profile(clear_range_limits=False) == None + assert env.plot_average_wind_speed_profile(clear_range_limits=True) == None + assert env.plot_average_pressure_profile(clear_range_limits=False) == None + assert env.plot_average_pressure_profile(clear_range_limits=True) == None + assert env.plot_wind_profile_over_average_day() == None + + +@patch("matplotlib.pyplot.show") +def test_animation_plots(mock_show, env): + + # Check animation plots + assert isinstance(env.animate_average_wind_rose(), widgets.Image) + assert isinstance(env.animate_wind_gust_distribution_over_average_day(), HTML) + assert isinstance(env.animate_wind_heading_profile_over_average_day(), HTML) + assert isinstance(env.animate_wind_profile_over_average_day(), HTML) + assert isinstance( + env.animate_sustained_surface_wind_speed_distribution_over_average_day(), + HTML, + ) + + +def test_exports(env): + + assert env.exportMeanProfiles() == None + assert env.save("EnvAnalysisDict") == None + + env2 = copy.deepcopy(env) + env2.load("EnvAnalysisDict") + assert env2.allInfo() == None + + # Delete file created by save method + os.remove("EnvAnalysisDict") From 2199195c727e01dc4c8eee3cdd22be1619194190 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Sun, 20 Nov 2022 10:49:50 +0100 Subject: [PATCH 08/22] TST: Create tests for Function class --- tests/test_function.py | 123 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 tests/test_function.py diff --git a/tests/test_function.py b/tests/test_function.py new file mode 100644 index 000000000..a498e7745 --- /dev/null +++ b/tests/test_function.py @@ -0,0 +1,123 @@ +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}) + + +@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 + + +# Test Function creation from .csv file +def test_function_from_csv(func_from_csv): + func = func_from_csv + # Assert func is zero at 0 but with 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): + 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 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 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 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 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 integral + assert np.isclose(linear_func.integral(0, 1), 0.5, atol=1e-6) From 1218179d165f1276d48e0947b93d9e784e28cacf Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Sun, 20 Nov 2022 10:50:37 +0100 Subject: [PATCH 09/22] MAINT: removing undesired comments --- rocketpy/Environment.py | 3 --- tests/test_environment.py | 3 --- 2 files changed, 6 deletions(-) diff --git a/rocketpy/Environment.py b/rocketpy/Environment.py index 850cd5c93..03314851b 100644 --- a/rocketpy/Environment.py +++ b/rocketpy/Environment.py @@ -3341,9 +3341,6 @@ 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 - try: atmosphericModelFile = self.atmosphericModelFile atmosphericModelDict = self.atmosphericModelDict diff --git a/tests/test_environment.py b/tests/test_environment.py index 852c5d18d..c66ce0b71 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -160,9 +160,6 @@ def test_era5_atmosphere(mock_show): assert Env.allInfo() == None -# TODO: utmToGeodesic - - @pytest.mark.slow @patch("matplotlib.pyplot.show") def test_gefs_atmosphere(mock_show, example_env_robust): From a847089228fa2aaa77d795529093da8c8511c4f5 Mon Sep 17 00:00:00 2001 From: Lint Action Date: Sun, 20 Nov 2022 10:35:51 +0000 Subject: [PATCH 10/22] Fix code style issues with Black --- rocketpy/EnvironmentAnalysis.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/rocketpy/EnvironmentAnalysis.py b/rocketpy/EnvironmentAnalysis.py index 4ae226a79..f1814ba1c 100644 --- a/rocketpy/EnvironmentAnalysis.py +++ b/rocketpy/EnvironmentAnalysis.py @@ -2869,7 +2869,9 @@ def plot_wind_heading_profile_over_average_day( return None - def animate_wind_profile_over_average_day(self, clear_range_limits: bool =False) -> HTML: + def animate_wind_profile_over_average_day( + self, clear_range_limits: bool = False + ) -> HTML: """Animation of how wind profile evolves throughout an average day.""" self.process_wind_speed_profile_over_average_day() @@ -2938,7 +2940,9 @@ def update(frame): plt.close(fig) return HTML(animation.to_jshtml()) - def animate_wind_heading_profile_over_average_day(self, clear_range_limits: bool =False) -> HTML: + def animate_wind_heading_profile_over_average_day( + self, clear_range_limits: bool = False + ) -> HTML: """Animation of how wind heading profile evolves throughout an average day.""" self.process_wind_heading_profile_over_average_day() From 23d98d48887f5029458465e4640d19565297a521 Mon Sep 17 00:00:00 2001 From: Guilherme Date: Sat, 31 Dec 2022 08:30:28 +0100 Subject: [PATCH 11/22] MAINT: revert type hint and improve docs --- rocketpy/Environment.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/rocketpy/Environment.py b/rocketpy/Environment.py index 03314851b..8c6e27712 100644 --- a/rocketpy/Environment.py +++ b/rocketpy/Environment.py @@ -3329,9 +3329,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 simulations 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 @@ -3523,7 +3524,7 @@ def geodesicToUtm(self, lat, lon, datum): return x, y, utmZone, utmLetter, hemis, EW - def utmToGeodesic(self, x: float, y: float, utmZone: int, hemis: str, datum: str): + def utmToGeodesic(self, x, y, utmZone, hemis, datum): """Function to convert UTM coordinates to geodesic coordinates (i.e. latitude and longitude). The latitude should be between -80° and 84° From 916cc50fd6e3b76580cff92026b9aa575f39012d Mon Sep 17 00:00:00 2001 From: Guilherme Date: Sat, 31 Dec 2022 08:32:02 +0100 Subject: [PATCH 12/22] ENH: moving fixtures to the conftest.py --- tests/conftest.py | 87 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 84 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 298425648..3c4599050 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,9 @@ -import pytest -from rocketpy import SolidMotor -from rocketpy import Rocket +import datetime + import numericalunits +import pytest + +from rocketpy import Environment, Function, Rocket, SolidMotor def pytest_addoption(parser): @@ -98,6 +100,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 From 49d4b75a13c652875b7019ba0aac5440e017349d Mon Sep 17 00:00:00 2001 From: Guilherme Date: Sat, 31 Dec 2022 08:33:00 +0100 Subject: [PATCH 13/22] MAINT: improve docstrings for tests --- tests/test_environment_anaysis.py | 152 +++++++++++++++++------------- tests/test_function.py | 81 ++++++++++------ 2 files changed, 138 insertions(+), 95 deletions(-) diff --git a/tests/test_environment_anaysis.py b/tests/test_environment_anaysis.py index 6848190f1..cc230bd5f 100644 --- a/tests/test_environment_anaysis.py +++ b/tests/test_environment_anaysis.py @@ -12,55 +12,26 @@ plt.rcParams.update({"figure.max_open_warning": 0}) -# Create a simple object of the Environment Analysis class -@pytest.fixture -def env(): - """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 = 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 - - -def test_allInfo(env): +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 : EnvironmentAnalysis + env_analysis : EnvironmentAnalysis A simple object of the Environment Analysis class Returns ------- None """ - assert env.allInfo() == None + assert env_analysis.allInfo() == None @patch("matplotlib.pyplot.show") -def test_distribution_plots(mock_show, env): +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 @@ -68,7 +39,7 @@ def test_distribution_plots(mock_show, env): Parameters ---------- - env : EnvironmentAnalysis + env_analysis : EnvironmentAnalysis A simple object of the EnvironmentAnalysis class. Returns @@ -77,14 +48,17 @@ def test_distribution_plots(mock_show, env): """ # Check distribution plots - assert env.plot_wind_gust_distribution() == None - assert env.plot_surface10m_wind_speed_distribution() == None - assert env.plot_wind_gust_distribution_over_average_day() == None - assert env.plot_sustained_surface_wind_speed_distribution_over_average_day() == None + 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 + ) @patch("matplotlib.pyplot.show") -def test_average_plots(mock_show, env): +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 @@ -92,7 +66,7 @@ def test_average_plots(mock_show, env): Parameters ---------- - env : EnvironmentAnalysis + env_analysis : EnvironmentAnalysis A simple object of the EnvironmentAnalysis class. Returns @@ -100,48 +74,94 @@ def test_average_plots(mock_show, env): None """ # Check "average" plots - assert env.plot_average_temperature_along_day() == None - assert env.plot_average_surface10m_wind_speed_along_day(False) == None - assert env.plot_average_surface10m_wind_speed_along_day(True) == None - assert env.plot_average_sustained_surface100m_wind_speed_along_day() == None - assert env.plot_average_day_wind_rose_all_hours() == None - assert env.plot_average_day_wind_rose_specific_hour(12) == None - assert env.plot_average_day_wind_rose_specific_hour(12) == None + 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 @patch("matplotlib.pyplot.show") -def test_profile_plots(mock_show, env): +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.plot_wind_heading_profile_over_average_day() == None - assert env.plot_average_wind_heading_profile(clear_range_limits=False) == None - assert env.plot_average_wind_heading_profile(clear_range_limits=True) == None - assert env.plot_average_wind_speed_profile(clear_range_limits=False) == None - assert env.plot_average_wind_speed_profile(clear_range_limits=True) == None - assert env.plot_average_pressure_profile(clear_range_limits=False) == None - assert env.plot_average_pressure_profile(clear_range_limits=True) == None - assert env.plot_wind_profile_over_average_day() == None + 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 @patch("matplotlib.pyplot.show") -def test_animation_plots(mock_show, env): +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.animate_average_wind_rose(), widgets.Image) - assert isinstance(env.animate_wind_gust_distribution_over_average_day(), HTML) - assert isinstance(env.animate_wind_heading_profile_over_average_day(), HTML) - assert isinstance(env.animate_wind_profile_over_average_day(), HTML) + 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.animate_sustained_surface_wind_speed_distribution_over_average_day(), + env_analysis.animate_sustained_surface_wind_speed_distribution_over_average_day(), HTML, ) -def test_exports(env): +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.exportMeanProfiles() == None - assert env.save("EnvAnalysisDict") == None + assert env_analysis.exportMeanProfiles() == None + assert env_analysis.save("EnvAnalysisDict") == None - env2 = copy.deepcopy(env) + env2 = copy.deepcopy(env_analysis) env2.load("EnvAnalysisDict") assert env2.allInfo() == None diff --git a/tests/test_function.py b/tests/test_function.py index a498e7745..5e56f12c3 100644 --- a/tests/test_function.py +++ b/tests/test_function.py @@ -9,37 +9,16 @@ plt.rcParams.update({"figure.max_open_warning": 0}) -@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 - - # Test Function creation from .csv file def test_function_from_csv(func_from_csv): - func = func_from_csv - # Assert func is zero at 0 but with tolerance + """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)" @@ -48,6 +27,13 @@ def test_function_from_csv(func_from_csv): 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" @@ -59,6 +45,13 @@ def test_getters(func_from_csv): 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"] @@ -72,6 +65,15 @@ def test_setters(func_from_csv): @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 @@ -88,7 +90,14 @@ def test_plots(mock_show, func_from_csv): 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" @@ -101,7 +110,14 @@ def test_interpolation_methods(linear_func): 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" @@ -119,5 +135,12 @@ def test_extrapolation_methods(linear_func): 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) From cc1dc0469c1c4eb17e2846813345ea3ef17b740c Mon Sep 17 00:00:00 2001 From: Guilherme Date: Sat, 31 Dec 2022 08:33:40 +0100 Subject: [PATCH 14/22] TST: Improved tests for info returned --- tests/test_environment.py | 61 ++++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/tests/test_environment.py b/tests/test_environment.py index c66ce0b71..affff2702 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -9,28 +9,6 @@ from rocketpy import Environment -@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 - - def test_env_set_date(example_env): tomorrow = datetime.date.today() + datetime.timedelta(days=1) example_env.setDate((tomorrow.year, tomorrow.month, tomorrow.day, 12)) @@ -165,8 +143,43 @@ def test_era5_atmosphere(mock_show): def test_gefs_atmosphere(mock_show, example_env_robust): example_env_robust.setAtmosphericModel(type="Ensemble", file="GEFS") assert example_env_robust.allInfo() == None - assert isinstance(example_env.allPlotInfoReturned(), dict) - assert isinstance(example_env.allInfoReturned(), dict) + + +@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 From bd7d88ff3c1cc8dfd5558a30a430e2a90d83941a Mon Sep 17 00:00:00 2001 From: Guilherme Date: Sat, 31 Dec 2022 08:33:57 +0100 Subject: [PATCH 15/22] MAINT: closing and cleaning files after tests --- tests/test_flight.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_flight.py b/tests/test_flight.py index ef665bb9f..025ed111b 100644 --- a/tests/test_flight.py +++ b/tests/test_flight.py @@ -1,4 +1,5 @@ import datetime +import os from unittest.mock import patch import matplotlib as plt @@ -508,6 +509,10 @@ def test_export_data(): test_1 = np.loadtxt("test_export_data_1.csv", delimiter=",") test_2 = np.loadtxt("test_export_data_2.csv", delimiter=",") + # Clean up the files that were created + os.remove("test_export_data_1.csv") + os.remove("test_export_data_2.csv") + # Check if basic exported content matches data assert np.allclose(test_flight.x[:, 0], test_1[:, 0], atol=1e-5) == True assert np.allclose(test_flight.x[:, 1], test_1[:, 1], atol=1e-5) == True @@ -596,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] @@ -611,6 +617,11 @@ def test_export_KML(): lat.append(float(coords[i + 1])) z.append(float(coords[i + 2])) + # Close the files + test_1.close() + # Clean up the files that were created + os.remove("test_export_data_1.kml") + assert np.allclose(test_flight.latitude[:, 1], lat, atol=1e-3) == True assert np.allclose(test_flight.longitude[:, 1], lon, atol=1e-3) == True assert np.allclose(test_flight.z[:, 1], z, atol=1e-3) == True From db990f9e2f04a4181b87bd4f015b7d335c2c8503 Mon Sep 17 00:00:00 2001 From: Lint Action Date: Sat, 31 Dec 2022 07:35:43 +0000 Subject: [PATCH 16/22] Fix code style issues with Black --- tests/test_flight.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_flight.py b/tests/test_flight.py index 92ddc01e5..744a29902 100644 --- a/tests/test_flight.py +++ b/tests/test_flight.py @@ -618,7 +618,6 @@ def test_export_KML(): lat.append(float(coords[i + 1])) z.append(float(coords[i + 2])) - # Delete temporary test file test_1.close() os.remove("test_export_data_1.kml") From 4bb94f6832deca480e30a7672d19f7952df95223 Mon Sep 17 00:00:00 2001 From: MateusStano Date: Tue, 3 Jan 2023 21:44:24 -0300 Subject: [PATCH 17/22] maint: removed anotations --- rocketpy/EnvironmentAnalysis.py | 52 +++++++++++++-------------------- 1 file changed, 21 insertions(+), 31 deletions(-) diff --git a/rocketpy/EnvironmentAnalysis.py b/rocketpy/EnvironmentAnalysis.py index f1814ba1c..c94a8e54b 100644 --- a/rocketpy/EnvironmentAnalysis.py +++ b/rocketpy/EnvironmentAnalysis.py @@ -1237,7 +1237,7 @@ def calculate_record_min_surface_100m_wind_speed(self): self.record_min_surface_100m_wind_speed = np.min(self.surface_100m_wind_speed) return self.record_min_surface_100m_wind_speed - def plot_wind_gust_distribution(self) -> None: + def plot_wind_gust_distribution(self): """Get all values of wind gust speed (for every date and hour available) and plot a single distribution. Expected result is a Weibull distribution. """ @@ -1937,7 +1937,7 @@ def plot_wind_rose( ax.yaxis.set_major_formatter(mtick.PercentFormatter(decimals=0)) return ax - def plot_average_day_wind_rose_specific_hour(self, hour, fig=None) -> None: + def plot_average_day_wind_rose_specific_hour(self, hour, fig=None): """Plot a specific hour of the average windrose Parameters @@ -1965,7 +1965,7 @@ def plot_average_day_wind_rose_specific_hour(self, hour, fig=None) -> None: return None - def plot_average_day_wind_rose_all_hours(self) -> 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 days = list(self.surfaceDataDict.keys()) @@ -2041,9 +2041,7 @@ def plot_average_day_wind_rose_all_hours(self) -> None: plt.show() return None - def animate_average_wind_rose( - self, figsize: tuple = (8, 8), filename: str = "wind_rose.gif" - ) -> widgets.Image: + 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 are the location of the place where we want to analyze, (x,y,z). The data is assembled by hour, which means, the windrose of a specific hour is @@ -2109,7 +2107,7 @@ def animate_average_wind_rose( height=fig_height, ) - def plot_wind_gust_distribution_over_average_day(self) -> None: + def plot_wind_gust_distribution_over_average_day(self): """Plots shown in the animation of how the wind gust distribution varies throughout the day.""" # Gather animation data average_wind_gust_at_given_hour = {} @@ -2181,7 +2179,7 @@ def plot_wind_gust_distribution_over_average_day(self) -> None: return None - def animate_wind_gust_distribution_over_average_day(self) -> HTML: + def animate_wind_gust_distribution_over_average_day(self): """Animation of how the wind gust distribution varies throughout the day.""" # Gather animation data wind_gusts_at_given_hour = {} @@ -2264,8 +2262,8 @@ def update(frame): return HTML(animation.to_jshtml()) def plot_sustained_surface_wind_speed_distribution_over_average_day( - self, windSpeedLimit: bool = False - ) -> None: + self, windSpeedLimit=False + ): """Plots shown in the animation of how the sustained surface wind speed distribution varies throughout the day.""" # Gather animation data average_wind_speed_at_given_hour = {} @@ -2362,8 +2360,8 @@ def plot_sustained_surface_wind_speed_distribution_over_average_day( return None def animate_sustained_surface_wind_speed_distribution_over_average_day( - self, windSpeedLimit: bool = False - ) -> HTML: + self, windSpeedLimit=False + ): # 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 @@ -2472,7 +2470,7 @@ def update(frame): return HTML(animation.to_jshtml()) @property - def altitude_AGL_range(self) -> tuple: + def altitude_AGL_range(self): min_altitude = 0 if self.maxExpectedAltitude == None: max_altitudes = [ @@ -2485,7 +2483,7 @@ def altitude_AGL_range(self) -> tuple: max_altitude = self.maxExpectedAltitude return min_altitude, max_altitude - def process_temperature_profile_over_average_day(self) -> None: + def process_temperature_profile_over_average_day(self): """Compute the average temperature profile for each available hour of a day, over all days in the dataset.""" altitude_list = np.linspace(*self.altitude_AGL_range, 100) @@ -2520,7 +2518,7 @@ def process_temperature_profile_over_average_day(self) -> None: return None - def process_pressure_profile_over_average_day(self) -> 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.""" altitude_list = np.linspace(*self.altitude_AGL_range, 100) @@ -2555,7 +2553,7 @@ def process_pressure_profile_over_average_day(self) -> None: return None - def process_wind_speed_profile_over_average_day(self) -> 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.""" altitude_list = np.linspace(*self.altitude_AGL_range, 100) @@ -2591,7 +2589,7 @@ def process_wind_speed_profile_over_average_day(self) -> None: return None - def process_wind_velocity_x_profile_over_average_day(self) -> 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.""" altitude_list = np.linspace(*self.altitude_AGL_range, 100) @@ -2625,7 +2623,7 @@ def process_wind_velocity_x_profile_over_average_day(self) -> None: ) return None - def process_wind_velocity_y_profile_over_average_day(self) -> 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 days in the dataset.""" altitude_list = np.linspace(*self.altitude_AGL_range, 100) @@ -2659,9 +2657,7 @@ def process_wind_velocity_y_profile_over_average_day(self) -> None: ) return None - def plot_wind_profile_over_average_day( - self, clear_range_limits: bool = False - ) -> 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.""" self.process_wind_speed_profile_over_average_day() @@ -2733,7 +2729,7 @@ def plot_wind_profile_over_average_day( return None - def process_wind_heading_profile_over_average_day(self) -> 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) average_wind_velocity_X_profile_at_given_hour = {} @@ -2803,9 +2799,7 @@ def process_wind_heading_profile_over_average_day(self) -> None: return None - def plot_wind_heading_profile_over_average_day( - self, clear_range_limits: bool = False - ) -> 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() @@ -2869,9 +2863,7 @@ def plot_wind_heading_profile_over_average_day( return None - def animate_wind_profile_over_average_day( - self, clear_range_limits: bool = False - ) -> HTML: + 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() @@ -2940,9 +2932,7 @@ def update(frame): plt.close(fig) return HTML(animation.to_jshtml()) - def animate_wind_heading_profile_over_average_day( - self, clear_range_limits: bool = False - ) -> HTML: + def animate_wind_heading_profile_over_average_day(self, clear_range_limits=False): """Animation of how wind heading profile evolves throughout an average day.""" self.process_wind_heading_profile_over_average_day() From b352ac74be38f42b81857ac56b75422211b006ad Mon Sep 17 00:00:00 2001 From: MateusStano Date: Tue, 3 Jan 2023 21:46:29 -0300 Subject: [PATCH 18/22] enh: added envanalysis import --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index faa989ddc..f0a12e010 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,7 @@ import numericalunits import pytest -from rocketpy import Environment, Function, Rocket, SolidMotor +from rocketpy import Environment, EnvironmentAnalysis, Function, Rocket, SolidMotor def pytest_addoption(parser): From 51c7cbff2cb2327faec4846fc35497670eee1788 Mon Sep 17 00:00:00 2001 From: MateusStano Date: Tue, 3 Jan 2023 21:46:52 -0300 Subject: [PATCH 19/22] maint: sort imports --- tests/test_flight.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_flight.py b/tests/test_flight.py index 744a29902..bb1b1a51e 100644 --- a/tests/test_flight.py +++ b/tests/test_flight.py @@ -1,4 +1,3 @@ -import os import datetime import os from unittest.mock import patch From 65a28f71e14b3f2d21e01d7c2742cafb7a0b44ce Mon Sep 17 00:00:00 2001 From: MateusStano Date: Tue, 3 Jan 2023 21:49:24 -0300 Subject: [PATCH 20/22] bug: fix Function np.inf call with int or float defined Functions --- rocketpy/Function.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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): From 847394739924f53c15f0c919a9ed58e0902af462 Mon Sep 17 00:00:00 2001 From: Guilherme Date: Wed, 4 Jan 2023 10:06:49 +0100 Subject: [PATCH 21/22] TST: set EnvAnalysis tests as slow option --- tests/test_environment_anaysis.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_environment_anaysis.py b/tests/test_environment_anaysis.py index cc230bd5f..3a352062f 100644 --- a/tests/test_environment_anaysis.py +++ b/tests/test_environment_anaysis.py @@ -13,6 +13,7 @@ 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. @@ -30,6 +31,7 @@ def test_allInfo(env_analysis): 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 @@ -57,6 +59,7 @@ def test_distribution_plots(mock_show, env_analysis): ) +@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 @@ -85,6 +88,7 @@ def test_average_plots(mock_show, env_analysis): 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 @@ -116,6 +120,7 @@ def test_profile_plots(mock_show, env_analysis): 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 @@ -146,6 +151,7 @@ def test_animation_plots(mock_show, env_analysis): ) +@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 From dda3ce580ce2bf224c7abb94ee5955cefe78108e Mon Sep 17 00:00:00 2001 From: Lint Action Date: Tue, 10 Jan 2023 17:41:03 +0000 Subject: [PATCH 22/22] Fix code style issues with Black --- tests/conftest.py | 1 + tests/test_environment.py | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 9c849281f..4d7bea0d4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,6 +5,7 @@ from rocketpy import Environment, EnvironmentAnalysis, Function, Rocket, SolidMotor + def pytest_addoption(parser): parser.addoption( "--runslow", action="store_true", default=False, help="run slow tests" diff --git a/tests/test_environment.py b/tests/test_environment.py index 98cbf91da..affff2702 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -8,6 +8,7 @@ from rocketpy import Environment + def test_env_set_date(example_env): tomorrow = datetime.date.today() + datetime.timedelta(days=1) example_env.setDate((tomorrow.year, tomorrow.month, tomorrow.day, 12))