Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BUG: Rail Buttons Not Accepted in Add Surfaces #701

Merged
merged 10 commits into from
Oct 9, 2024
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Attention: The newest changes should be on top -->

### Changed

-
- DEP: deprecate NOAA's RuC sounding [#706](https://github.com/RocketPy-Team/RocketPy/pull/706)

### Fixed

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**
Expand Down
11 changes: 11 additions & 0 deletions docs/user/environment/1-atm-models/soundings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://rucsoundings.noaa.gov/>`_.
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
Expand Down
126 changes: 8 additions & 118 deletions rocketpy/environment/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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``,
Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -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
Expand Down
11 changes: 7 additions & 4 deletions rocketpy/environment/fetchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import re
import time
import warnings
from datetime import datetime, timedelta, timezone

import netCDF4
Expand Down Expand Up @@ -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)
Expand Down
33 changes: 19 additions & 14 deletions rocketpy/rocket/rocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -966,14 +966,30 @@ 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
AeroSurface (e.g. NoseCone, TrapezoidalFins, etc.)

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
Expand All @@ -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()
Expand Down
14 changes: 0 additions & 14 deletions tests/integration/test_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
[
Expand Down
Loading