1- # pylint: disable=too-many-public-methods, too-many-instance-attributes
1+ # pylint: disable=too-many-public-methods, too-many-instance-attributes, too-many-lines
22import bisect
33import json
44import logging
5+ import os
56import re
67import warnings
78from collections import namedtuple
1314
1415from rocketpy .environment .fetchers import (
1516 fetch_aigfs_file_return_dataset ,
17+ fetch_atmospheric_data_from_meteomatics ,
1618 fetch_atmospheric_data_from_windy ,
1719 fetch_gefs_ensemble ,
1820 fetch_gfs_file_return_dataset ,
@@ -145,8 +147,8 @@ class Environment:
145147 Environment.atmospheric_model_type : string
146148 Describes the atmospheric model which is being used. Can only assume the
147149 following values: ``standard_atmosphere``, ``custom_atmosphere``,
148- ``wyoming_sounding``, ``windy``, ``forecast ``, ``reanalysis ``,
149- ``ensemble``.
150+ ``wyoming_sounding``, ``windy``, ``meteomatics ``, ``forecast ``,
151+ ``reanalysis``, `` ensemble``.
150152 Environment.atmospheric_model_file : string
151153 Address of the file used for the atmospheric model being used. Only
152154 defined for ``wyoming_sounding``, ``windy``, ``forecast``,
@@ -1188,6 +1190,8 @@ def set_atmospheric_model( # pylint: disable=too-many-statements
11881190 wind_u = 0 ,
11891191 wind_v = 0 ,
11901192 pressure_conversion_factor = None ,
1193+ username = None ,
1194+ password = None ,
11911195 ):
11921196 """Define the atmospheric model for this Environment.
11931197
@@ -1196,15 +1200,18 @@ def set_atmospheric_model( # pylint: disable=too-many-statements
11961200 type : string
11971201 Atmospheric model selector (case-insensitive). Accepted values are
11981202 ``"standard_atmosphere"``, ``"wyoming_sounding"``, ``"windy"``,
1199- ``"forecast"``, ``"reanalysis"``, ``"ensemble"`` and
1200- ``"custom_atmosphere"``.
1203+ ``"forecast"``, ``"reanalysis"``, ``"ensemble"``,
1204+ ``"custom_atmosphere"`` and ``"meteomatics"`` .
12011205 file : string | netCDF4.Dataset, optional
12021206 Data source or model shortcut. Meaning depends on ``type``:
12031207
12041208 - ``"standard_atmosphere"`` and ``"custom_atmosphere"``: ignored.
12051209 - ``"wyoming_sounding"``: URL of the sounding text page.
12061210 - ``"windy"``: one of ``"ECMWF"``, ``"GFS"``, ``"ICON"`` or
12071211 ``"ICONEU"``.
1212+ - ``"meteomatics"``: the Meteomatics weather model to query, such
1213+ as ``"mix"`` (the default when omitted). See the Meteomatics
1214+ documentation for the models available to your account.
12081215 - ``"forecast"``: local path, OPeNDAP URL, open
12091216 ``netCDF4.Dataset``, or one of ``"AIGFS"``, ``"GFS"``,
12101217 ``"NAM"``, ``"RAP"``, ``"HRRR"`` or ``"HIRESW"`` for the
@@ -1290,6 +1297,14 @@ def set_atmospheric_model( # pylint: disable=too-many-statements
12901297 model name (e.g. ERA5/ECMWF/MERRA2 reanalysis files commonly use hPa,
12911298 while online GFS/NAM/RAP/HRRR forecast models use Pa) or, if
12921299 unavailable, by reading the pressure unit attribute from the file.
1300+ username : string, optional
1301+ Meteomatics account username. Only used when ``type`` is
1302+ ``"meteomatics"``. If None (the default), the value is read from the
1303+ ``METEOMATICS_USERNAME`` environment variable.
1304+ password : string, optional
1305+ Meteomatics account password. Only used when ``type`` is
1306+ ``"meteomatics"``. If None (the default), the value is read from the
1307+ ``METEOMATICS_PASSWORD`` environment variable.
12931308
12941309 Returns
12951310 -------
@@ -1338,6 +1353,10 @@ def set_atmospheric_model( # pylint: disable=too-many-statements
13381353 self .process_custom_atmosphere (pressure , temperature , wind_u , wind_v )
13391354 case "windy" :
13401355 self .process_windy_atmosphere (file )
1356+ case "meteomatics" :
1357+ self .process_meteomatics_atmosphere (
1358+ model = file , username = username , password = password
1359+ )
13411360 case "forecast" | "reanalysis" | "ensemble" :
13421361 # Capture the user-supplied names before __validate_dictionary
13431362 # converts them to dicts, so they can drive auto-detection.
@@ -1736,6 +1755,181 @@ def __parse_windy_file(self, response, time_index, pressure_levels):
17361755 wind_v_array ,
17371756 )
17381757
1758+ def process_meteomatics_atmosphere ( # pylint: disable=too-many-statements
1759+ self ,
1760+ model = "mix" ,
1761+ username = None ,
1762+ password = None ,
1763+ min_altitude = 10 ,
1764+ max_altitude = 12000 ,
1765+ wind_resolution = 20 ,
1766+ temperature_pressure_resolution = 10 ,
1767+ query_limit = 10 ,
1768+ ):
1769+ """Process data from the Meteomatics API to retrieve a vertical
1770+ atmospheric profile at the launch site.
1771+
1772+ The Meteomatics API is queried for temperature, pressure and both wind
1773+ components at several altitudes above ground level, which are then
1774+ converted to profiles above sea level using the ``Environment``
1775+ elevation. Authentication uses a personal username and password; when
1776+ not provided, they are read from the ``METEOMATICS_USERNAME`` and
1777+ ``METEOMATICS_PASSWORD`` environment variables.
1778+
1779+ Parameters
1780+ ----------
1781+ model : str, optional
1782+ The Meteomatics weather model to query. Default is ``"mix"``. Your
1783+ account may not have access to every model.
1784+ username : str, optional
1785+ Meteomatics account username. Defaults to the
1786+ ``METEOMATICS_USERNAME`` environment variable.
1787+ password : str, optional
1788+ Meteomatics account password. Defaults to the
1789+ ``METEOMATICS_PASSWORD`` environment variable.
1790+ min_altitude : float, optional
1791+ Lowest altitude above ground level (in meters) to query. Default is
1792+ 10.
1793+ max_altitude : float, optional
1794+ Highest altitude above ground level (in meters) to query. Default
1795+ is 12000. The API errors if it lies outside the model's supported
1796+ range.
1797+ wind_resolution : int, optional
1798+ Number of altitude levels used for the wind components. Default is
1799+ 20.
1800+ temperature_pressure_resolution : int, optional
1801+ Number of altitude levels used for temperature and pressure.
1802+ Default is 10.
1803+ query_limit : int, optional
1804+ Maximum number of parameters requested at once. Parameters are
1805+ grouped accordingly to respect the account's per-request limit.
1806+ Default is 10.
1807+
1808+ Raises
1809+ ------
1810+ ValueError
1811+ If credentials are missing, if no launch date is set, or if the API
1812+ returns no usable data.
1813+ """
1814+ model = model if isinstance (model , str ) else "mix"
1815+ username = username or os .environ .get ("METEOMATICS_USERNAME" )
1816+ password = password or os .environ .get ("METEOMATICS_PASSWORD" )
1817+ if not username or not password :
1818+ raise ValueError (
1819+ "Meteomatics requires a username and password. Provide them via "
1820+ "the 'username' and 'password' arguments of set_atmospheric_model, "
1821+ "or set the METEOMATICS_USERNAME and METEOMATICS_PASSWORD "
1822+ "environment variables."
1823+ )
1824+ if getattr (self , "datetime_date" , None ) is None :
1825+ raise ValueError (
1826+ "A launch date is required to use the Meteomatics atmospheric "
1827+ "model. Provide it when creating the Environment or via set_date()."
1828+ )
1829+
1830+ profiles = fetch_atmospheric_data_from_meteomatics (
1831+ username = username ,
1832+ password = password ,
1833+ latitude = self .latitude ,
1834+ longitude = self .longitude ,
1835+ date = self .datetime_date ,
1836+ model = model ,
1837+ min_altitude = min_altitude ,
1838+ max_altitude = max_altitude ,
1839+ wind_resolution = wind_resolution ,
1840+ temperature_pressure_resolution = temperature_pressure_resolution ,
1841+ query_limit = query_limit ,
1842+ )
1843+
1844+ if self .elevation == 0 :
1845+ warnings .warn (
1846+ "The Environment elevation is 0 m (possibly unset), so "
1847+ "Meteomatics heights above ground level are being treated as "
1848+ "heights above sea level. If the launch site is not at sea "
1849+ "level, set the elevation (e.g. Environment(elevation=...) or "
1850+ "set_elevation('Open-Elevation')) before this call for an "
1851+ "accurate profile." ,
1852+ UserWarning ,
1853+ stacklevel = 2 ,
1854+ )
1855+
1856+ def to_profile_array (profile ):
1857+ """Convert an {AGL height: value} mapping into a sorted
1858+ (ASL height, value) array, dropping any missing value."""
1859+ heights = sorted (h for h , value in profile .items () if value is not None )
1860+ return np .array (
1861+ [(h + self .elevation , profile [h ]) for h in heights ], dtype = float
1862+ )
1863+
1864+ pressure_array = to_profile_array (profiles ["pressure" ])
1865+ temperature_array = to_profile_array (profiles ["temperature" ])
1866+
1867+ # Wind u and v share the same altitude grid; keep only common levels.
1868+ wind_heights = sorted (
1869+ h
1870+ for h in set (profiles ["wind_u" ]) & set (profiles ["wind_v" ])
1871+ if profiles ["wind_u" ][h ] is not None and profiles ["wind_v" ][h ] is not None
1872+ )
1873+ # Each profile needs at least two levels: a single-point Function cannot
1874+ # be evaluated at its own node (it raises IndexError downstream), so a
1875+ # collapsed grid must fail here with an actionable message instead.
1876+ if min (len (pressure_array ), len (temperature_array ), len (wind_heights )) < 2 :
1877+ raise ValueError (
1878+ "Meteomatics did not return enough usable atmospheric data: at "
1879+ "least two valid altitude levels are required for pressure, "
1880+ "temperature and wind. Check the requested model, the altitude "
1881+ "range (min_altitude and max_altitude must be far enough apart "
1882+ "that the sampled levels do not collapse to a single height), "
1883+ "and your account permissions."
1884+ )
1885+
1886+ wind_u_values = np .array ([profiles ["wind_u" ][h ] for h in wind_heights ])
1887+ wind_v_values = np .array ([profiles ["wind_v" ][h ] for h in wind_heights ])
1888+ wind_asl_heights = np .array (wind_heights , dtype = float ) + self .elevation
1889+ wind_u_array = np .column_stack ((wind_asl_heights , wind_u_values ))
1890+ wind_v_array = np .column_stack ((wind_asl_heights , wind_v_values ))
1891+
1892+ wind_speed_array = calculate_wind_speed (wind_u_values , wind_v_values )
1893+ wind_heading_array = calculate_wind_heading (wind_u_values , wind_v_values )
1894+ wind_direction_array = convert_wind_heading_to_direction (wind_heading_array )
1895+
1896+ # Save atmospheric data
1897+ self .__set_pressure_function (pressure_array )
1898+ self .__set_barometric_height_function (pressure_array [:, (1 , 0 )])
1899+ self .__set_temperature_function (temperature_array )
1900+ self .__set_wind_velocity_x_function (wind_u_array )
1901+ self .__set_wind_velocity_y_function (wind_v_array )
1902+ self .__set_wind_heading_function (
1903+ np .column_stack ((wind_asl_heights , wind_heading_array ))
1904+ )
1905+ self .__set_wind_direction_function (
1906+ np .column_stack ((wind_asl_heights , wind_direction_array ))
1907+ )
1908+ self .__set_wind_speed_function (
1909+ np .column_stack ((wind_asl_heights , wind_speed_array ))
1910+ )
1911+
1912+ # Save maximum expected height
1913+ self ._max_expected_height = float (
1914+ max (pressure_array [- 1 , 0 ], temperature_array [- 1 , 0 ], wind_asl_heights [- 1 ])
1915+ )
1916+
1917+ # Save model info metadata (single point in space and time)
1918+ self .atmospheric_model_init_date = self .datetime_date
1919+ self .atmospheric_model_end_date = self .datetime_date
1920+ self .atmospheric_model_interval = 0
1921+ self .atmospheric_model_init_lat = self .latitude
1922+ self .atmospheric_model_end_lat = self .latitude
1923+ self .atmospheric_model_init_lon = self .longitude
1924+ self .atmospheric_model_end_lon = self .longitude
1925+
1926+ # Save debugging data
1927+ self .wind_us = wind_u_values
1928+ self .wind_vs = wind_v_values
1929+ self .temperatures = temperature_array [:, 1 ]
1930+ self .pressures = pressure_array [:, 1 ]
1931+ self .height = wind_asl_heights
1932+
17391933 def process_wyoming_sounding (self , file ): # pylint: disable=too-many-statements
17401934 """Import and process the upper air sounding data from `Wyoming
17411935 Upper Air Soundings` database given by the url in file. Sets
@@ -3004,7 +3198,13 @@ def from_dict(cls, data): # pylint: disable=too-many-statements
30043198 env .elevation = data ["elevation" ]
30053199 env .max_expected_height = data ["max_expected_height" ]
30063200
3007- if atmospheric_model in ("windy" , "forecast" , "reanalysis" , "ensemble" ):
3201+ if isinstance (atmospheric_model , str ) and atmospheric_model .lower () in (
3202+ "windy" ,
3203+ "meteomatics" ,
3204+ "forecast" ,
3205+ "reanalysis" ,
3206+ "ensemble" ,
3207+ ):
30083208 env .atmospheric_model_init_date = data ["atmospheric_model_init_date" ]
30093209 env .atmospheric_model_end_date = data ["atmospheric_model_end_date" ]
30103210 env .atmospheric_model_interval = data ["atmospheric_model_interval" ]
0 commit comments