diff --git a/CHANGELOG.md b/CHANGELOG.md index cf4da0a72..ba1d36558 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,20 +32,37 @@ Attention: The newest changes should be on top --> ### Added -- + ### Changed -- + ### Fixed -- + +## [v1.6.1] - 2024-10-10 + +### Changed + +- REL: v1.6.1 [#708](https://github.com/RocketPy-Team/RocketPy/pull/708) +- DEP: deprecate NOAA's RuC sounding [#706](https://github.com/RocketPy-Team/RocketPy/pull/706) + +### Fixed + +- BUG: Fix Motor Zero Dry Mass Check [#710](https://github.com/RocketPy-Team/RocketPy/pull/710) +- BUG: Fix Environment.max_expected_height for custom atmosphere [#707](https://github.com/RocketPy-Team/RocketPy/pull/707) +- BUG: Initialize _Controller Init Parameters [#703](https://github.com/RocketPy-Team/RocketPy/pull/703) +- BUG: Rail Buttons Not Accepted in Add Surfaces [#701](https://github.com/RocketPy-Team/RocketPy/pull/701) +- BUG: Vector encoding breaks MonteCarlo export. [#704](https://github.com/RocketPy-Team/RocketPy/pull/704) +- BUG: Single Point Functions Can Not Be Defined [#700](https://github.com/RocketPy-Team/RocketPy/pull/700) +- BUG: savetxt Not Accepting lambda Functions [#698](https://github.com/RocketPy-Team/RocketPy/pull/698) ## [v1.6.0] - 2024-09-29 ### Added +- REL: v1.6.0 [#697](https://github.com/RocketPy-Team/RocketPy/pull/697) - ENH: Generic Surfaces and Generic Linear Surfaces [#680](https://github.com/RocketPy-Team/RocketPy/pull/680) - ENH: Free-Form Fins [#694](https://github.com/RocketPy-Team/RocketPy/pull/694) - ENH: Expand Polation Options for ND Functions. [#691](https://github.com/RocketPy-Team/RocketPy/pull/691) diff --git a/README.md b/README.md index c549840f7..04181badd 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ RocketPy is the next-generation trajectory simulation solution for High-Power Ro 2. **Accurate Weather Modeling** - Supports International Standard Atmosphere (1976) - - Custom atmospheric profiles and Soundings (Wyoming, NOAARuc) + - Custom atmospheric profiles and Soundings (Wyoming) - Weather forecasts, reanalysis, and ensembles for realistic scenarios 3. **Aerodynamic Models** diff --git a/docs/conf.py b/docs/conf.py index d2a0c5413..c5fbf4fad 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,7 +27,7 @@ author = "RocketPy Team" # The full version, including alpha/beta/rc tags -release = "1.6.0" +release = "1.6.1" # -- General configuration --------------------------------------------------- diff --git a/docs/user/environment/1-atm-models/soundings.rst b/docs/user/environment/1-atm-models/soundings.rst index c84243ca5..1b0ae95b5 100644 --- a/docs/user/environment/1-atm-models/soundings.rst +++ b/docs/user/environment/1-atm-models/soundings.rst @@ -41,6 +41,17 @@ Initialize a new Environment instance: NOAA's Ruc Soundings -------------------- +.. important:: + + From September 30th, 2024, this model is no longer available since NOAA has \ + discontinued the Ruc Soundings public service. The following message is \ + displayed on the website: \ + "On Monday, September 30, a number of legacy websites were permanently removed. \ + These sites were no longer being maintained and did not meet security and \ + design requirements mandated by NOAA. They were intended for research \ + purposes and are not designed for operational use, such as for commercial \ + purposes or the safety of life and property." + Another option for upper air soundings is `NOAA's Ruc Soundings `_. This service allows users to download virtual soundings from numerical weather prediction models such as GFS, RAP, and NAM, and also real soundings from the diff --git a/docs/user/installation.rst b/docs/user/installation.rst index c870cbcb8..a0a00837c 100644 --- a/docs/user/installation.rst +++ b/docs/user/installation.rst @@ -19,7 +19,7 @@ If you want to choose a specific version to guarantee compatibility, you may ins .. code-block:: shell - pip install rocketpy==1.6.0 + pip install rocketpy==1.6.1 Optional Installation Method: ``conda`` diff --git a/pyproject.toml b/pyproject.toml index 7dc3899ce..6ba78a667 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "rocketpy" -version = "1.6.0" +version = "1.6.1" description="Advanced 6-DOF trajectory simulation for High-Power Rocketry." dynamic = ["dependencies"] readme = "README.md" diff --git a/rocketpy/control/controller.py b/rocketpy/control/controller.py index 93a13ecfd..a68de1a28 100644 --- a/rocketpy/control/controller.py +++ b/rocketpy/control/controller.py @@ -84,8 +84,10 @@ def __init__( None """ self.interactive_objects = interactive_objects + self.base_controller_function = controller_function self.controller_function = self.__init_controller_function(controller_function) self.sampling_rate = sampling_rate + self.initial_observed_variables = initial_observed_variables self.name = name self.prints = _ControllerPrints(self) diff --git a/rocketpy/environment/environment.py b/rocketpy/environment/environment.py index 39ce18532..5cef60b21 100644 --- a/rocketpy/environment/environment.py +++ b/rocketpy/environment/environment.py @@ -16,7 +16,6 @@ fetch_gfs_file_return_dataset, fetch_hiresw_file_return_dataset, fetch_nam_file_return_dataset, - fetch_noaaruc_sounding, fetch_open_elevation, fetch_rap_file_return_dataset, fetch_wyoming_sounding, @@ -142,11 +141,11 @@ class Environment: Environment.atmospheric_model_type : string Describes the atmospheric model which is being used. Can only assume the following values: ``standard_atmosphere``, ``custom_atmosphere``, - ``wyoming_sounding``, ``NOAARucSounding``, ``Forecast``, ``Reanalysis``, + ``wyoming_sounding``, ``Forecast``, ``Reanalysis``, ``Ensemble``. Environment.atmospheric_model_file : string Address of the file used for the atmospheric model being used. Only - defined for ``wyoming_sounding``, ``NOAARucSounding``, ``Forecast``, + defined for ``wyoming_sounding``, ``Forecast``, ``Reanalysis``, ``Ensemble`` Environment.atmospheric_model_dict : dictionary Dictionary used to properly interpret ``netCDF`` and ``OPeNDAP`` files. @@ -1053,24 +1052,6 @@ def set_atmospheric_model( # pylint: disable=too-many-statements .. _weather.uwyo: http://weather.uwyo.edu/upperair/sounding.html - - ``NOAARucSounding``: sets pressure, temperature, wind-u - and wind-v profiles and surface elevation obtained from - an upper air sounding given by the file parameter through - an URL. This URL should point to a data webpage obtained - through NOAA's Ruc Sounding servers, which can be accessed - in `rucsoundings`_. Selecting ROABs as the - initial data source, specifying the station through it's - WMO-ID and opting for the ASCII (GSD format) button, the - following example URL opens up: - - https://rucsoundings.noaa.gov/get_raobs.cgi?data_source=RAOB&latest=latest&start_year=2019&start_month_name=Feb&start_mday=5&start_hour=12&start_min=0&n_hrs=1.0&fcst_len=shortest&airport=83779&text=Ascii%20text%20%28GSD%20format%29&hydrometeors=false&start=latest - - Any ASCII GSD format page from this server can be read, - so information from virtual soundings such as GFS and NAM - can also be imported. - - .. _rucsoundings: https://rucsoundings.noaa.gov/ - - ``windy_atmosphere``: sets pressure, temperature, wind-u and wind-v profiles and surface elevation obtained from the Windy API. See file argument to specify the model as either ``ECMWF``, @@ -1279,8 +1260,6 @@ def set_atmospheric_model( # pylint: disable=too-many-statements self.process_standard_atmosphere() elif type == "wyoming_sounding": self.process_wyoming_sounding(file) - elif type == "noaarucsounding": - self.process_noaaruc_sounding(file) elif type == "custom_atmosphere": self.process_custom_atmosphere(pressure, temperature, wind_u, wind_v) elif type == "windy": @@ -1334,7 +1313,7 @@ def process_standard_atmosphere(self): self.__set_wind_speed_function(0) # 80k meters is the limit of the standard atmosphere - self.max_expected_height = 80000 + self._max_expected_height = 80000 def process_custom_atmosphere( self, pressure=None, temperature=None, wind_u=0, wind_v=0 @@ -1411,7 +1390,7 @@ def process_custom_atmosphere( None """ # Initialize an estimate of the maximum expected atmospheric model height - max_expected_height = 1000 + max_expected_height = self.max_expected_height or 1000 # Save pressure profile if pressure is None: @@ -1455,7 +1434,7 @@ def wind_heading_func(h): # TODO: create another custom reset for heading self.__reset_wind_direction_function() self.__reset_wind_speed_function() - self.max_expected_height = max_expected_height + self._max_expected_height = max_expected_height def process_windy_atmosphere( self, model="ECMWF" @@ -1530,7 +1509,7 @@ def process_windy_atmosphere( self.__set_wind_speed_function(data_array[:, (1, 7)]) # Save maximum expected height - self.max_expected_height = max(altitude_array[0], altitude_array[-1]) + self._max_expected_height = max(altitude_array[0], altitude_array[-1]) # Get elevation data from file self.elevation = float(response["header"]["elevation"]) @@ -1668,7 +1647,7 @@ def process_wyoming_sounding(self, file): # pylint: disable=too-many-statements ) # Save maximum expected height - self.max_expected_height = data_array[-1, 1] + self._max_expected_height = data_array[-1, 1] def process_noaaruc_sounding(self, file): # pylint: disable=too-many-statements """Import and process the upper air sounding data from `NOAA @@ -1689,107 +1668,18 @@ def process_noaaruc_sounding(self, file): # pylint: disable=too-many-statements See also -------- - More details can be found at: https://rucsoundings.noaa.gov/. + This method is deprecated and will be fully deleted in version 1.8.0. Returns ------- None """ - # Request NOAA Ruc Sounding from file url - response = fetch_noaaruc_sounding(file) - - # Split response into lines - lines = response.text.split("\n") - - # Process GSD format (https://rucsoundings.noaa.gov/raob_format.html) - - # Extract elevation data - for line in lines: - # Split line into columns - columns = re.split(" +", line)[1:] - if len(columns) > 0: - if columns[0] == "1" and columns[5] != "99999": - # Save elevation - self.elevation = float(columns[5]) - else: - # No elevation data available - pass - - pressure_array = [] - barometric_height_array = [] - temperature_array = [] - wind_speed_array = [] - wind_direction_array = [] - - for line in lines: - # Split line into columns - columns = re.split(" +", line)[1:] - if len(columns) < 6: - # skip lines with less than 6 columns - continue - if columns[0] in ["4", "5", "6", "7", "8", "9"]: - # Convert columns to floats - columns = np.array(columns, dtype=float) - # Select relevant columns - altitude, pressure, temperature, wind_direction, wind_speed = columns[ - [2, 1, 3, 5, 6] - ] - # Check for missing values - if altitude == 99999: - continue - # Save values only if they are not missing - if pressure != 99999: - pressure_array.append([altitude, pressure]) - barometric_height_array.append([pressure, altitude]) - if temperature != 99999: - temperature_array.append([altitude, temperature]) - if wind_direction != 99999: - wind_direction_array.append([altitude, wind_direction]) - if wind_speed != 99999: - wind_speed_array.append([altitude, wind_speed]) - - # Convert lists to arrays - pressure_array = np.array(pressure_array) - barometric_height_array = np.array(barometric_height_array) - temperature_array = np.array(temperature_array) - wind_speed_array = np.array(wind_speed_array) - wind_direction_array = np.array(wind_direction_array) - - # Converts 10*hPa to Pa and save values - pressure_array[:, 1] = 10 * pressure_array[:, 1] - self.__set_pressure_function(pressure_array) - # Converts 10*hPa to Pa and save values - barometric_height_array[:, 0] = 10 * barometric_height_array[:, 0] - self.__set_barometric_height_function(barometric_height_array) - - # Convert C to K and save values - temperature_array[:, 1] = temperature_array[:, 1] / 10 + 273.15 - self.__set_temperature_function(temperature_array) - - # Process wind-u and wind-v - # Converts Knots to m/s - wind_speed_array[:, 1] = wind_speed_array[:, 1] * 1.852 / 3.6 - wind_heading_array = wind_direction_array[:, :] * 1 - # Convert wind direction to wind heading - wind_heading_array[:, 1] = (wind_direction_array[:, 1] + 180) % 360 - wind_u = wind_speed_array[:, :] * 1 - wind_v = wind_speed_array[:, :] * 1 - wind_u[:, 1] = wind_speed_array[:, 1] * np.sin( - np.deg2rad(wind_heading_array[:, 1]) - ) - wind_v[:, 1] = wind_speed_array[:, 1] * np.cos( - np.deg2rad(wind_heading_array[:, 1]) + warnings.warn( + "NOAA RUC models are no longer available. " + "This method is deprecated and will be fully deleted in version 1.8.0.", + DeprecationWarning, ) - - # Save wind data - self.__set_wind_direction_function(wind_direction_array) - self.__set_wind_heading_function(wind_heading_array) - self.__set_wind_speed_function(wind_speed_array) - self.__set_wind_velocity_x_function(wind_u) - self.__set_wind_velocity_y_function(wind_v) - - # Save maximum expected height - self.max_expected_height = pressure_array[-1, 0] + return file def process_forecast_reanalysis( self, file, dictionary @@ -2009,7 +1899,7 @@ def process_forecast_reanalysis( self.__set_wind_speed_function(data_array[:, (1, 7)]) # Save maximum expected height - self.max_expected_height = max(height[0], height[-1]) + self._max_expected_height = max(height[0], height[-1]) # Get elevation data from file if dictionary["surface_geopotential_height"] is not None: @@ -2354,7 +2244,7 @@ def select_ensemble_member(self, member=0): self.__set_wind_speed_function(data_array[:, (1, 7)]) # Save other attributes - self.max_expected_height = max(height[0], height[-1]) + self._max_expected_height = max(height[0], height[-1]) self.ensemble_member = member # Update air density, speed of sound and dynamic viscosity diff --git a/rocketpy/environment/fetchers.py b/rocketpy/environment/fetchers.py index 6ba566145..58537025b 100644 --- a/rocketpy/environment/fetchers.py +++ b/rocketpy/environment/fetchers.py @@ -5,6 +5,7 @@ import re import time +import warnings from datetime import datetime, timedelta, timezone import netCDF4 @@ -347,10 +348,12 @@ def fetch_noaaruc_sounding(file): ImportError If unable to load the specified file or the file content is too short. """ - response = requests.get(file) - if response.status_code != 200 or len(response.text) < 10: - raise ImportError("Unable to load " + file + ".") - return response + warnings.warn( + "The NOAA RUC soundings are deprecated since September 30th, 2024. " + "This method will be removed in version 1.8.0.", + DeprecationWarning, + ) + return file @exponential_backoff(max_attempts=5, base_delay=2, max_delay=60) diff --git a/rocketpy/mathutils/function.py b/rocketpy/mathutils/function.py index 9e6e07b9d..ba05636a5 100644 --- a/rocketpy/mathutils/function.py +++ b/rocketpy/mathutils/function.py @@ -3171,7 +3171,7 @@ def savetxt( ) # Generate the data points using the callable x = np.linspace(lower, upper, samples) - data_points = np.column_stack((x, self.source(x))) + data_points = np.column_stack((x, self(x))) else: # If the source is already an array, use it as is data_points = self.source @@ -3247,12 +3247,12 @@ def __validate_source(self, source): # pylint: disable=too-many-statements ) source_len, source_dim = source.shape - - if source_len < source_dim: - raise ValueError( - "Too few data points to define a domain. The number of rows " - "must be greater than or equal to the number of columns." - ) + if not source_len == 1: # do not check for one point Functions + if source_len < source_dim: + raise ValueError( + "Too few data points to define a domain. The number of rows " + "must be greater than or equal to the number of columns." + ) return source diff --git a/rocketpy/mathutils/vector_matrix.py b/rocketpy/mathutils/vector_matrix.py index 82b5475f1..0da44935d 100644 --- a/rocketpy/mathutils/vector_matrix.py +++ b/rocketpy/mathutils/vector_matrix.py @@ -418,6 +418,10 @@ def k(): """Returns the k vector, [0, 0, 1].""" return Vector([0, 0, 1]) + def to_dict(self): + """Returns the vector as a JSON compatible element.""" + return list(self.components) + class Matrix: """Pure Python 3x3 Matrix class for simple matrix-matrix and matrix-vector @@ -998,6 +1002,10 @@ def __repr__(self): + f" [{self.zx}, {self.zy}, {self.zz}])" ) + def to_dict(self): + """Returns the matrix as a JSON compatible element.""" + return [list(row) for row in self.components] + @staticmethod def identity(): """Returns the 3x3 identity matrix.""" diff --git a/rocketpy/motors/motor.py b/rocketpy/motors/motor.py index eae7931b4..5fa154d88 100644 --- a/rocketpy/motors/motor.py +++ b/rocketpy/motors/motor.py @@ -370,7 +370,7 @@ def dry_mass(self, dry_mass): dry_mass : float Motor dry mass in kg. """ - if dry_mass: + if dry_mass is not None: if isinstance(dry_mass, (int, float)): self._dry_mass = dry_mass else: diff --git a/rocketpy/rocket/rocket.py b/rocketpy/rocket/rocket.py index fe0c803b5..61de81f27 100644 --- a/rocketpy/rocket/rocket.py +++ b/rocketpy/rocket/rocket.py @@ -966,6 +966,22 @@ def add_motor(self, motor, position): # pylint: disable=too-many-statements self.evaluate_com_to_cdm_function() self.evaluate_nozzle_gyration_tensor() + def __add_single_surface(self, surface, position): + """Adds a single aerodynamic surface to the rocket. Makes checks for + rail buttons case, and position type. + """ + position = ( + Vector([0, 0, position]) + if not isinstance(position, (Vector, tuple, list)) + else Vector(position) + ) + if isinstance(surface, RailButtons): + self.rail_buttons = Components() + self.rail_buttons.add(surface, position) + else: + self.aerodynamic_surfaces.add(surface, position) + self.__evaluate_single_surface_cp_to_cdm(surface, position) + def add_surfaces(self, surfaces, positions): """Adds one or more aerodynamic surfaces to the rocket. The aerodynamic surface must be an instance of a class that inherits from the @@ -973,7 +989,7 @@ def add_surfaces(self, surfaces, positions): Parameters ---------- - surfaces : list, AeroSurface, NoseCone, TrapezoidalFins, EllipticalFins, Tail + surfaces : list, AeroSurface, NoseCone, TrapezoidalFins, EllipticalFins, Tail, RailButtons Aerodynamic surface to be added to the rocket. Can be a list of AeroSurface if more than one surface is to be added. positions : int, float, list, tuple, Vector @@ -996,22 +1012,11 @@ def add_surfaces(self, surfaces, positions): ------- None """ - # TODO: separate this method into smaller methods: https://github.com/RocketPy-Team/RocketPy/pull/696#discussion_r1771978422 try: for surface, position in zip(surfaces, positions): - if not isinstance(position, (Vector, tuple, list)): - position = Vector([0, 0, position]) - else: - position = Vector(position) - self.aerodynamic_surfaces.add(surface, position) - self.__evaluate_single_surface_cp_to_cdm(surface, position) + self.__add_single_surface(surface, position) except TypeError: - if not isinstance(positions, (Vector, tuple, list)): - positions = Vector([0, 0, positions]) - else: - positions = Vector(positions) - self.aerodynamic_surfaces.add(surfaces, positions) - self.__evaluate_single_surface_cp_to_cdm(surfaces, positions) + self.__add_single_surface(surfaces, positions) self.evaluate_center_of_pressure() self.evaluate_stability_margin() diff --git a/tests/integration/test_environment.py b/tests/integration/test_environment.py index 5495d2e03..17d81a76e 100644 --- a/tests/integration/test_environment.py +++ b/tests/integration/test_environment.py @@ -98,20 +98,6 @@ def test_standard_atmosphere( assert example_plain_env.prints.print_earth_details() is None -@patch("matplotlib.pyplot.show") -def test_noaaruc_atmosphere( - mock_show, example_spaceport_env -): # pylint: disable=unused-argument - url = ( - r"https://rucsoundings.noaa.gov/get_raobs.cgi?data_source=RAOB&latest=" - r"latest&start_year=2019&start_month_name=Feb&start_mday=5&start_hour=12" - r"&start_min=0&n_hrs=1.0&fcst_len=shortest&airport=83779&text=Ascii" - r"%20text%20%28GSD%20format%29&hydrometeors=false&start=latest" - ) - example_spaceport_env.set_atmospheric_model(type="NOAARucSounding", file=url) - assert example_spaceport_env.all_info() is None - - @pytest.mark.parametrize( "model_name", [