From e5c75334b08313de1a2bfbeda15dd22571120187 Mon Sep 17 00:00:00 2001 From: MateusStano Date: Tue, 9 Apr 2024 17:59:22 +0200 Subject: [PATCH 01/11] ENH: use mass_flow_rate instead of differentiate --- rocketpy/simulation/flight.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rocketpy/simulation/flight.py b/rocketpy/simulation/flight.py index 77632d2f2..6959c1698 100644 --- a/rocketpy/simulation/flight.py +++ b/rocketpy/simulation/flight.py @@ -1645,8 +1645,8 @@ def u_dot_generalized(self, t, u, post_processing=False): # Retrieve necessary quantities rho = self.env.density.get_value_opt(z) total_mass = self.rocket.total_mass.get_value_opt(t) - total_mass_dot = self.rocket.total_mass.differentiate(t) - total_mass_ddot = self.rocket.total_mass.differentiate(t, order=2) + total_mass_dot = self.rocket.motor.total_mass_flow_rate(t) + total_mass_ddot = self.rocket.motor.total_mass_flow_rate.differentiate(t) ## CM position vector and time derivatives relative to CDM in body frame r_CM_z = ( -1 From e2a94c8577bf2f2bcfe19a167793cac6ea3b5f2b Mon Sep 17 00:00:00 2001 From: MateusStano Date: Tue, 9 Apr 2024 18:03:08 +0200 Subject: [PATCH 02/11] TST: fix tests --- tests/test_flight.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_flight.py b/tests/test_flight.py index db882e185..4fb4036eb 100644 --- a/tests/test_flight.py +++ b/tests/test_flight.py @@ -604,12 +604,12 @@ def test_max_values(flight_calisto_robust): calculated by hand, it was just copied from the test results. This is because the expected values are not easy to calculate by hand, and the results are not expected to change. If the results change, the test will - fail, and the expected values must be updated. If if want to update the - values, always double check if the results are really correct. Acceptable - reasons for changes in the results are: 1) changes in the code that - improve the accuracy of the simulation, 2) a bug was found and fixed. Keep - in mind that other tests may be more accurate than this one, for example, - the acceptance tests, which are based on the results of real flights. + fail, and the expected values must be updated. If the values are updated, + always double check if the results are really correct. Acceptable reasons + for changes in the results are: 1) changes in the code that improve the + accuracy of the simulation, 2) a bug was found and fixed. Keep in mind that + other tests may be more accurate than this one, for example, the acceptance + tests, which are based on the results of real flights. Parameters ---------- @@ -622,7 +622,7 @@ def test_max_values(flight_calisto_robust): assert pytest.approx(105.2774, abs=atol) == test.max_acceleration_power_on assert pytest.approx(105.2774, abs=atol) == test.max_acceleration assert pytest.approx(0.85999, abs=atol) == test.max_mach_number - assert pytest.approx(285.90240, abs=atol) == test.max_speed + assert pytest.approx(285.94948, abs=atol) == test.max_speed def test_rail_buttons_forces(flight_calisto_custom_wind): From 8ee2c95f6961e94f93ef569303588c643647778f Mon Sep 17 00:00:00 2001 From: MateusStano Date: Wed, 17 Apr 2024 22:16:59 +0200 Subject: [PATCH 03/11] ENH: create rocket total_mass_flow_rate attribute --- rocketpy/rocket/rocket.py | 4 ++++ rocketpy/simulation/flight.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/rocketpy/rocket/rocket.py b/rocketpy/rocket/rocket.py index aaf585091..c074afc8d 100644 --- a/rocketpy/rocket/rocket.py +++ b/rocketpy/rocket/rocket.py @@ -81,6 +81,9 @@ class Rocket: Function of time expressing the total mass of the rocket, defined as the sum of the propellant mass and the rocket mass without propellant. + Rocket.total_mass_flow_rate : Function + Time derivative of rocket's total mass in kg/s as a function + of time as obtained by the thrust source of the added motor. Rocket.thrust_to_weight : Function Function of time expressing the motor thrust force divided by rocket weight. The gravitational acceleration is assumed as 9.80665 m/s^2. @@ -764,6 +767,7 @@ def add_motor(self, motor, position): self.motor.center_of_dry_mass_position * _ + self.motor_position ) self.nozzle_position = self.motor.nozzle_position * _ + self.motor_position + self.total_mass_flow_rate = self.motor.total_mass_flow_rate self.evaluate_dry_mass() self.evaluate_total_mass() self.evaluate_center_of_dry_mass() diff --git a/rocketpy/simulation/flight.py b/rocketpy/simulation/flight.py index 6959c1698..badbc31e4 100644 --- a/rocketpy/simulation/flight.py +++ b/rocketpy/simulation/flight.py @@ -1645,8 +1645,8 @@ def u_dot_generalized(self, t, u, post_processing=False): # Retrieve necessary quantities rho = self.env.density.get_value_opt(z) total_mass = self.rocket.total_mass.get_value_opt(t) - total_mass_dot = self.rocket.motor.total_mass_flow_rate(t) - total_mass_ddot = self.rocket.motor.total_mass_flow_rate.differentiate(t) + total_mass_dot = self.rocket.total_mass_flow_rate.get_value_opt(t) + total_mass_ddot = self.rocket.total_mass_flow_rate.differentiate(t) ## CM position vector and time derivatives relative to CDM in body frame r_CM_z = ( -1 From 6b26f6153d070d269a872314f85d70f7a6d25443 Mon Sep 17 00:00:00 2001 From: MateusStano Date: Wed, 17 Apr 2024 22:19:16 +0200 Subject: [PATCH 04/11] DEV: changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8408aa368..b52d7d01d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed +- BUG: Swap rocket.total_mass.differentiate for motor.total_mass_flow rate [#585](https://github.com/RocketPy-Team/RocketPy/pull/585) - BUG: export_eng 'Motor' method would not work for liquid motors. [#559](https://github.com/RocketPy-Team/RocketPy/pull/559) ## [v1.2.2] - 2024-03-22 From afb3b3227262c33bd49579db87fc85a482f41204 Mon Sep 17 00:00:00 2001 From: dyu056 Date: Fri, 26 Jan 2024 00:35:36 +1300 Subject: [PATCH 05/11] Completed changes --- rocketpy/environment/environment_analysis.py | 10 ++++---- rocketpy/tools.py | 25 +------------------- 2 files changed, 6 insertions(+), 29 deletions(-) diff --git a/rocketpy/environment/environment_analysis.py b/rocketpy/environment/environment_analysis.py index 0ed638091..20f896f51 100644 --- a/rocketpy/environment/environment_analysis.py +++ b/rocketpy/environment/environment_analysis.py @@ -8,6 +8,11 @@ import numpy as np import pytz +try: + from functools import cached_property +except ImportError: + from ..tools import cached_property + from ..mathutils.function import Function from ..plots.environment_analysis_plots import _EnvironmentAnalysisPlots from ..prints.environment_analysis_prints import _EnvironmentAnalysisPrints @@ -22,11 +27,6 @@ from ..units import convert_units from .environment import Environment -try: - from functools import cached_property -except ImportError: - from ..tools import cached_property - # TODO: the average_wind_speed_profile_by_hour and similar methods could be more abstract than currently are diff --git a/rocketpy/tools.py b/rocketpy/tools.py index cb3094343..a6e9f08d9 100644 --- a/rocketpy/tools.py +++ b/rocketpy/tools.py @@ -7,6 +7,7 @@ import pytz from cftime import num2pydate from packaging import version as packaging_version +from functools import cached_property _NOT_FOUND = object() @@ -14,30 +15,6 @@ INSTALL_MAPPING = {"IPython": "ipython"} -class cached_property: - def __init__(self, func): - self.func = func - self.attrname = None - self.__doc__ = func.__doc__ - - def __set_name__(self, owner, name): - self.attrname = name - - def __get__(self, instance, owner=None): - if instance is None: - return self - if self.attrname is None: - raise TypeError( - "Cannot use cached_property instance without calling __set_name__ on it." - ) - cache = instance.__dict__ - val = cache.get(self.attrname, _NOT_FOUND) - if val is _NOT_FOUND: - val = self.func(instance) - cache[self.attrname] = val - return val - - def tuple_handler(value): """Transforms the input value into a tuple that represents a range. If the input is an input or float, From bb0f46a267a79787f4af32b2c7e5cf9fcaf1c90d Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Thu, 18 Apr 2024 21:41:38 -0400 Subject: [PATCH 06/11] MNT: refactors cached_property imports --- rocketpy/environment/environment_analysis.py | 6 +----- rocketpy/mathutils/function.py | 6 +----- rocketpy/mathutils/vector_matrix.py | 3 +-- rocketpy/motors/hybrid_motor.py | 7 ++----- rocketpy/motors/liquid_motor.py | 7 +------ rocketpy/motors/motor.py | 6 +----- rocketpy/motors/solid_motor.py | 7 ++----- rocketpy/motors/tank_geometry.py | 5 +---- rocketpy/plots/flight_plots.py | 7 ++----- rocketpy/rocket/aero_surface.py | 1 - rocketpy/tools.py | 3 --- 11 files changed, 12 insertions(+), 46 deletions(-) diff --git a/rocketpy/environment/environment_analysis.py b/rocketpy/environment/environment_analysis.py index 20f896f51..da6fde364 100644 --- a/rocketpy/environment/environment_analysis.py +++ b/rocketpy/environment/environment_analysis.py @@ -3,16 +3,12 @@ import datetime import json from collections import defaultdict +from functools import cached_property import netCDF4 import numpy as np import pytz -try: - from functools import cached_property -except ImportError: - from ..tools import cached_property - from ..mathutils.function import Function from ..plots.environment_analysis_plots import _EnvironmentAnalysisPlots from ..prints.environment_analysis_prints import _EnvironmentAnalysisPrints diff --git a/rocketpy/mathutils/function.py b/rocketpy/mathutils/function.py index cefed044d..eda903bcc 100644 --- a/rocketpy/mathutils/function.py +++ b/rocketpy/mathutils/function.py @@ -7,6 +7,7 @@ import warnings from collections.abc import Iterable from copy import deepcopy +from functools import cached_property from inspect import signature from pathlib import Path @@ -14,11 +15,6 @@ import numpy as np from scipy import integrate, linalg, optimize -try: - from functools import cached_property -except ImportError: - from ..tools import cached_property - NUMERICAL_TYPES = (float, int, complex, np.ndarray, np.integer, np.floating) diff --git a/rocketpy/mathutils/vector_matrix.py b/rocketpy/mathutils/vector_matrix.py index 50d827659..332e1b680 100644 --- a/rocketpy/mathutils/vector_matrix.py +++ b/rocketpy/mathutils/vector_matrix.py @@ -1,8 +1,7 @@ from cmath import isclose +from functools import cached_property from itertools import product -from rocketpy.tools import cached_property - class Vector: """Pure python basic R3 vector class designed for simple operations. diff --git a/rocketpy/motors/hybrid_motor.py b/rocketpy/motors/hybrid_motor.py index 6f0849cd0..557333fe7 100644 --- a/rocketpy/motors/hybrid_motor.py +++ b/rocketpy/motors/hybrid_motor.py @@ -1,3 +1,5 @@ +from functools import cached_property + from rocketpy.tools import parallel_axis_theorem_from_com from ..mathutils.function import Function, funcify_method, reset_funcified_methods @@ -7,11 +9,6 @@ from .motor import Motor from .solid_motor import SolidMotor -try: - from functools import cached_property -except ImportError: - from ..tools import cached_property - class HybridMotor(Motor): """Class to specify characteristics and useful operations for Hybrid diff --git a/rocketpy/motors/liquid_motor.py b/rocketpy/motors/liquid_motor.py index 01f728473..7314e11ba 100644 --- a/rocketpy/motors/liquid_motor.py +++ b/rocketpy/motors/liquid_motor.py @@ -1,4 +1,4 @@ -import warnings +from functools import cached_property import numpy as np @@ -13,11 +13,6 @@ from ..prints.liquid_motor_prints import _LiquidMotorPrints from .motor import Motor -try: - from functools import cached_property -except ImportError: - from ..tools import cached_property - class LiquidMotor(Motor): """Class to specify characteristics and useful operations for Liquid diff --git a/rocketpy/motors/motor.py b/rocketpy/motors/motor.py index 3834f4a15..9429da88e 100644 --- a/rocketpy/motors/motor.py +++ b/rocketpy/motors/motor.py @@ -1,6 +1,7 @@ import re import warnings from abc import ABC, abstractmethod +from functools import cached_property import numpy as np @@ -9,11 +10,6 @@ from ..prints.motor_prints import _MotorPrints from ..tools import parallel_axis_theorem_from_com, tuple_handler -try: - from functools import cached_property -except ImportError: - from ..tools import cached_property - class Motor(ABC): """Abstract class to specify characteristics and useful operations for diff --git a/rocketpy/motors/solid_motor.py b/rocketpy/motors/solid_motor.py index 8b1c2362e..db3527a95 100644 --- a/rocketpy/motors/solid_motor.py +++ b/rocketpy/motors/solid_motor.py @@ -1,3 +1,5 @@ +from functools import cached_property + import numpy as np from scipy import integrate @@ -6,11 +8,6 @@ from ..prints.solid_motor_prints import _SolidMotorPrints from .motor import Motor -try: - from functools import cached_property -except ImportError: - from ..tools import cached_property - class SolidMotor(Motor): """Class to specify characteristics and useful operations for solid motors. diff --git a/rocketpy/motors/tank_geometry.py b/rocketpy/motors/tank_geometry.py index f1940cbea..2eb7bd27e 100644 --- a/rocketpy/motors/tank_geometry.py +++ b/rocketpy/motors/tank_geometry.py @@ -11,10 +11,7 @@ cache = lru_cache(maxsize=None) -try: - from functools import cached_property -except ImportError: - from ..tools import cached_property +from functools import cached_property class TankGeometry: diff --git a/rocketpy/plots/flight_plots.py b/rocketpy/plots/flight_plots.py index 4ae5141c9..21266a1f3 100644 --- a/rocketpy/plots/flight_plots.py +++ b/rocketpy/plots/flight_plots.py @@ -1,11 +1,8 @@ +from functools import cached_property + import matplotlib.pyplot as plt import numpy as np -try: - from functools import cached_property -except ImportError: - from ..tools import cached_property - class _FlightPlots: """Class that holds plot methods for Flight class. diff --git a/rocketpy/rocket/aero_surface.py b/rocketpy/rocket/aero_surface.py index c5d154f3e..d41240ac9 100644 --- a/rocketpy/rocket/aero_surface.py +++ b/rocketpy/rocket/aero_surface.py @@ -1,6 +1,5 @@ import warnings from abc import ABC, abstractmethod -from functools import cached_property import numpy as np from scipy.optimize import fsolve diff --git a/rocketpy/tools.py b/rocketpy/tools.py index a6e9f08d9..acd0f4c27 100644 --- a/rocketpy/tools.py +++ b/rocketpy/tools.py @@ -7,9 +7,6 @@ import pytz from cftime import num2pydate from packaging import version as packaging_version -from functools import cached_property - -_NOT_FOUND = object() # Mapping of module name and the name of the package that should be installed INSTALL_MAPPING = {"IPython": "ipython"} From 684b9979b1d194fcb94e5c4da9bcf006f3c831ad Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Thu, 18 Apr 2024 21:47:19 -0400 Subject: [PATCH 07/11] DEV: Adds PR 587 to the CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b52d7d01d..7103426dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Changed +- DEP: delete deprecated rocketpy.tools.cached_property [#587](https://github.com/RocketPy-Team/RocketPy/pull/587) - DOC: Improvements of Environment docstring phrasing [#565](https://github.com/RocketPy-Team/RocketPy/pull/565) - MNT: Refactor flight prints module [#579](https://github.com/RocketPy-Team/RocketPy/pull/579) - DOC: Convert CompareFlights example notebooks to .rst files [#576](https://github.com/RocketPy-Team/RocketPy/pull/576) From ccd78af7d29e4036d92ffb731e2fca4c38212377 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Fri, 19 Apr 2024 12:07:17 -0400 Subject: [PATCH 08/11] ENH: Add exponential backoff decorator to tools.py --- rocketpy/tools.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/rocketpy/tools.py b/rocketpy/tools.py index cb3094343..861a2e3b3 100644 --- a/rocketpy/tools.py +++ b/rocketpy/tools.py @@ -1,6 +1,8 @@ +import functools import importlib import importlib.metadata import re +import time from bisect import bisect_left import numpy as np @@ -382,6 +384,25 @@ def check_requirement_version(module_name, version): return True +def exponential_backoff(max_attempts, base_delay=1, max_delay=60): + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + delay = base_delay + for i in range(max_attempts): + try: + return func(*args, **kwargs) + except Exception as e: + if i == max_attempts - 1: + raise e from None + delay = min(delay * 2, max_delay) + time.sleep(delay) + + return wrapper + + return decorator + + def parallel_axis_theorem_from_com(com_inertia_moment, mass, distance): """Calculates the moment of inertia of a object relative to a new axis using the parallel axis theorem. The new axis is parallel to and at a distance From d0fbcd72e049618ed14f21a9355d88eee98c3792 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Fri, 19 Apr 2024 12:07:45 -0400 Subject: [PATCH 09/11] MNT: Refactor code to use the exponential_backoff decorator --- rocketpy/environment/environment.py | 198 ++++++++++++++++------------ 1 file changed, 115 insertions(+), 83 deletions(-) diff --git a/rocketpy/environment/environment.py b/rocketpy/environment/environment.py index b72b2cb38..5a845a173 100644 --- a/rocketpy/environment/environment.py +++ b/rocketpy/environment/environment.py @@ -3,7 +3,7 @@ import re import warnings from collections import namedtuple -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import numpy as np import numpy.ma as ma @@ -13,6 +13,7 @@ from ..mathutils.function import Function, funcify_method from ..plots.environment_plots import _EnvironmentPlots from ..prints.environment_prints import _EnvironmentPrints +from ..tools import exponential_backoff try: import netCDF4 @@ -680,18 +681,9 @@ def set_elevation(self, elevation="Open-Elevation"): # self.elevation = elev - elif self.latitude != None and self.longitude != None: - try: - print("Fetching elevation from open-elevation.com...") - request_url = "https://api.open-elevation.com/api/v1/lookup?locations={:f},{:f}".format( - self.latitude, self.longitude - ) - response = requests.get(request_url) - results = response.json()["results"] - self.elevation = results[0]["elevation"] - print("Elevation received:", self.elevation) - except: - raise RuntimeError("Unable to reach Open-Elevation API servers.") + elif self.latitude is not None and self.longitude is not None: + self.elevation = self.__fetch_open_elevation() + print("Elevation received: ", self.elevation) else: raise ValueError( "Latitude and longitude must be set to use" @@ -1303,26 +1295,8 @@ def set_atmospheric_model( "v_wind": "vgrdprs", } # Attempt to get latest forecast - time_attempt = datetime.utcnow() - success = False - attempt_count = 0 - while not success and attempt_count < 10: - time_attempt -= timedelta(hours=6 * attempt_count) - file = "https://nomads.ncep.noaa.gov/dods/gens_bc/gens{:04d}{:02d}{:02d}/gep_all_{:02d}z".format( - time_attempt.year, - time_attempt.month, - time_attempt.day, - 6 * (time_attempt.hour // 6), - ) - try: - self.process_ensemble(file, dictionary) - success = True - except OSError: - attempt_count += 1 - if not success: - raise RuntimeError( - "Unable to load latest weather data for GEFS through " + file - ) + self.__fetch_gefs_ensemble(dictionary) + elif file == "CMC": # Define dictionary dictionary = { @@ -1338,27 +1312,7 @@ def set_atmospheric_model( "u_wind": "ugrdprs", "v_wind": "vgrdprs", } - # Attempt to get latest forecast - time_attempt = datetime.utcnow() - success = False - attempt_count = 0 - while not success and attempt_count < 10: - time_attempt -= timedelta(hours=12 * attempt_count) - file = "https://nomads.ncep.noaa.gov/dods/cmcens/cmcens{:04d}{:02d}{:02d}/cmcens_all_{:02d}z".format( - time_attempt.year, - time_attempt.month, - time_attempt.day, - 12 * (time_attempt.hour // 12), - ) - try: - self.process_ensemble(file, dictionary) - success = True - except OSError: - attempt_count += 1 - if not success: - raise RuntimeError( - "Unable to load latest weather data for CMC through " + file - ) + self.__fetch_cmc_ensemble(dictionary) # Process other forecasts or reanalysis else: # Check if default dictionary was requested @@ -1650,20 +1604,7 @@ def process_windy_atmosphere(self, model="ECMWF"): model. """ - # Process the model string - model = model.lower() - if model[-1] == "u": # case iconEu - model = "".join([model[:4], model[4].upper(), model[4 + 1 :]]) - # Load data from Windy.com: json file - url = f"https://node.windy.com/forecast/meteogram/{model}/{self.latitude}/{self.longitude}/?step=undefined" - try: - response = requests.get(url).json() - except: - if model == "iconEu": - raise ValueError( - "Could not get a valid response for Icon-EU from Windy. Check if the latitude and longitude coordinates set are inside Europe.", - ) - raise + response = self.__fetch_atmospheric_data_from_windy(model) # Determine time index from model time_array = np.array(response["data"]["hours"]) @@ -1824,18 +1765,7 @@ def process_wyoming_sounding(self, file): None """ # Request Wyoming Sounding from file url - response = requests.get(file) - if response.status_code != 200: - raise ImportError("Unable to load " + file + ".") - if len(re.findall("Can't get .+ Observations at", response.text)): - raise ValueError( - re.findall("Can't get .+ Observations at .+", response.text)[0] - + " Check station number and date." - ) - if response.text == "Invalid OUTPUT: specified\n": - raise ValueError( - "Invalid OUTPUT: specified. Make sure the output is Text: List." - ) + response = self.__fetch_wyoming_sounding(file) # Process Wyoming Sounding by finding data table and station info response_split_text = re.split("(<.{0,1}PRE>)", response.text) @@ -1961,9 +1891,7 @@ def process_noaaruc_sounding(self, file): None """ # Request NOAA Ruc Sounding from file url - response = requests.get(file) - if response.status_code != 200 or len(response.text) < 10: - raise ImportError("Unable to load " + file + ".") + response = self.__fetch_noaaruc_sounding(file) # Split response into lines lines = response.text.split("\n") @@ -3538,6 +3466,110 @@ def set_earth_geometry(self, datum): f"The reference system {datum} for Earth geometry " "is not recognized." ) + # Auxiliary functions - Fetching Data from 3rd party APIs + + @exponential_backoff(max_attempts=3, base_delay=1, max_delay=60) + def __fetch_open_elevation(self): + print("Fetching elevation from open-elevation.com...") + request_url = ( + "https://api.open-elevation.com/api/v1/lookup?locations" + f"={self.latitude},{self.longitude}" + ) + try: + response = requests.get(request_url) + except Exception as e: + raise RuntimeError("Unable to reach Open-Elevation API servers.") + results = response.json()["results"] + return results[0]["elevation"] + + @exponential_backoff(max_attempts=5, base_delay=2, max_delay=60) + def __fetch_atmospheric_data_from_windy(self, model): + model = model.lower() + if model[-1] == "u": # case iconEu + model = "".join([model[:4], model[4].upper(), model[4 + 1 :]]) + url = ( + f"https://node.windy.com/forecast/meteogram/{model}/" + f"{self.latitude}/{self.longitude}/?step=undefined" + ) + try: + response = requests.get(url).json() + except Exception as e: + if model == "iconEu": + raise ValueError( + "Could not get a valid response for Icon-EU from Windy. " + "Check if the coordinates are set inside Europe." + ) + return response + + @exponential_backoff(max_attempts=5, base_delay=2, max_delay=60) + def __fetch_wyoming_sounding(self, file): + response = requests.get(file) + if response.status_code != 200: + raise ImportError(f"Unable to load {file}.") + if len(re.findall("Can't get .+ Observations at", response.text)): + raise ValueError( + re.findall("Can't get .+ Observations at .+", response.text)[0] + + " Check station number and date." + ) + if response.text == "Invalid OUTPUT: specified\n": + raise ValueError( + "Invalid OUTPUT: specified. Make sure the output is Text: List." + ) + return response + + @exponential_backoff(max_attempts=5, base_delay=2, max_delay=60) + def __fetch_noaaruc_sounding(self, file): + response = requests.get(file) + if response.status_code != 200 or len(response.text) < 10: + raise ImportError("Unable to load " + file + ".") + + @exponential_backoff(max_attempts=5, base_delay=2, max_delay=60) + def __fetch_gefs_ensemble(self, dictionary): + time_attempt = datetime.now(tz=timezone.utc) + success = False + attempt_count = 0 + while not success and attempt_count < 10: + time_attempt -= timedelta(hours=6 * attempt_count) + file = ( + f"https://nomads.ncep.noaa.gov/dods/gens_bc/gens" + f"{time_attempt.year:04d}{time_attempt.month:02d}" + f"{time_attempt.day:02d}/" + f"gep_all_{6 * (time_attempt.hour // 6):02d}z" + ) + try: + self.process_ensemble(file, dictionary) + success = True + except OSError: + attempt_count += 1 + if not success: + raise RuntimeError( + "Unable to load latest weather data for GEFS through " + file + ) + + @exponential_backoff(max_attempts=5, base_delay=2, max_delay=60) + def __fetch_cmc_ensemble(self, dictionary): + # Attempt to get latest forecast + time_attempt = datetime.now(tz=timezone.utc) + success = False + attempt_count = 0 + while not success and attempt_count < 10: + time_attempt -= timedelta(hours=12 * attempt_count) + file = ( + f"https://nomads.ncep.noaa.gov/dods/cmcens/" + f"cmcens{time_attempt.year:04d}{time_attempt.month:02d}" + f"{time_attempt.day:02d}/" + f"cmcens_all_{12 * (time_attempt.hour // 12):02d}z" + ) + try: + self.process_ensemble(file, dictionary) + success = True + except OSError: + attempt_count += 1 + if not success: + raise RuntimeError( + "Unable to load latest weather data for CMC through " + file + ) + # Auxiliary functions - Geodesic Coordinates @staticmethod From a8a185eca86883a71580a68e6030c760e5cbd8d5 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Fri, 19 Apr 2024 12:08:58 -0400 Subject: [PATCH 10/11] TST: Fix tests with Environment class after exponential_backoff decorator was used --- tests/fixtures/environment/environment_fixtures.py | 5 +++-- tests/test_environment.py | 10 ++-------- tests/unit/test_environment.py | 2 +- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/tests/fixtures/environment/environment_fixtures.py b/tests/fixtures/environment/environment_fixtures.py index 8949f9973..851be3203 100644 --- a/tests/fixtures/environment/environment_fixtures.py +++ b/tests/fixtures/environment/environment_fixtures.py @@ -1,6 +1,7 @@ from datetime import datetime, timedelta import pytest + from rocketpy import Environment, EnvironmentAnalysis @@ -54,8 +55,8 @@ def env_analysis(): EnvironmentAnalysis """ env_analysis = EnvironmentAnalysis( - start_date=datetime.datetime(2019, 10, 23), - end_date=datetime.datetime(2021, 10, 23), + start_date=datetime(2019, 10, 23), + end_date=datetime(2021, 10, 23), latitude=39.3897, longitude=-8.28896388889, start_hour=6, diff --git a/tests/test_environment.py b/tests/test_environment.py index 5fa0e2c45..7349d512b 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -1,5 +1,4 @@ import datetime -import time from unittest.mock import patch import pytest @@ -64,13 +63,8 @@ def test_wyoming_sounding_atmosphere(mock_show, example_plain_env): # "file" option, instead of receiving the URL as a string. URL = "http://weather.uwyo.edu/cgi-bin/sounding?region=samer&TYPE=TEXT%3ALIST&YEAR=2019&MONTH=02&FROM=0500&TO=0512&STNM=83779" # give it at least 5 times to try to download the file - for i in range(5): - try: - example_plain_env.set_atmospheric_model(type="wyoming_sounding", file=URL) - break - except: - time.sleep(1) # wait 1 second before trying again - pass + example_plain_env.set_atmospheric_model(type="wyoming_sounding", file=URL) + assert example_plain_env.all_info() == None assert abs(example_plain_env.pressure(0) - 93600.0) < 1e-8 assert ( diff --git a/tests/unit/test_environment.py b/tests/unit/test_environment.py index ac25533eb..8d676f426 100644 --- a/tests/unit/test_environment.py +++ b/tests/unit/test_environment.py @@ -1,10 +1,10 @@ +import json import os import numpy as np import numpy.ma as ma import pytest import pytz -import json from rocketpy import Environment From b8990644616801a9c25e91ae141f2cacf8d7501c Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Fri, 19 Apr 2024 12:13:23 -0400 Subject: [PATCH 11/11] DEV: adds PR 588 to the CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b52d7d01d..9fe5afb0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added +- ENH: Exponential backoff decorator (fix #449) [#588](https://github.com/RocketPy-Team/RocketPy/pull/588) - ENH: Add new stability margin properties to Flight class [#572](https://github.com/RocketPy-Team/RocketPy/pull/572) - ENH: adds `Function.remove_outliers` method [#554](https://github.com/RocketPy-Team/RocketPy/pull/554)