diff --git a/CHANGELOG.md b/CHANGELOG.md index aad23f5..c9e4e76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,9 +18,9 @@ See PR #62 ## [0.2.4] - December, 2025 ### Features -* Added a way to call the get_coverage method of WeatherForecast with floats for lat, long (instead of tuples only) +* Added a way to call the get_coverage method of WeatherForecast with floats for lat, lon (instead of tuples only) to get forecast at a specific location (nearest grid point). -* Added a check on the lat, long parameters of get_coverage so that the closest gridpoint is used +* Added a check on the lat, lon parameters of get_coverage so that the closest gridpoint is used * Added min and max available coordinates to get_coverage_description output * Updated tests and documentation to reflect these changes diff --git a/README.md b/README.md index 92c2d70..ea484ce 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ df_arome = arome_client.get_coverage( ], heights=[10], # Optional: height above ground level pressures=None, # Optional: pressure level - long = (-5.1413, 9.5602), # Optional: longitude. tuple (min_long, max_long) or a float for a specific location + lon = (-5.1413, 9.5602), # Optional: longitude. tuple (min_long, max_long) or a float for a specific location lat = (41.33356, 51.0889), # Optional: latitude. tuple (min_lat, max_lat) or a float for a specific location coverage_id=None, # Optional: an alternative to indicator/run/interval temp_dir=None, # Optional: Directory to store the temporary file @@ -116,7 +116,7 @@ print(description) ``` #### Geographical Coverage -The geographical coverage of forecasts can be customized using the lat and long parameters in the get_coverage method. By default, Meteole retrieves data for the entire metropolitan France. +The geographical coverage of forecasts can be customized using the lat and lon parameters in the get_coverage method. By default, Meteole retrieves data for the entire metropolitan France. #### Fetch Forecasts for Multiple Indicators The `get_combined_coverage` method allows you to retrieve weather data for multiple indicators at the same time, streamlining the process of gathering forecasts for different parameters (e.g., temperature, wind speed, etc.). For detailed guidance on using this feature, refer to this [tutorial](./tutorial/Fetch_forecast_for_multiple_indicators.ipynb). diff --git a/docs/pages/coverage_parameters.md b/docs/pages/coverage_parameters.md index ba77294..4045dd5 100644 --- a/docs/pages/coverage_parameters.md +++ b/docs/pages/coverage_parameters.md @@ -71,4 +71,4 @@ coverage_axis['pressures'] ``` ### Geographical Coverage -The geographical coverage of forecasts can be customized using the `lat` and `long` parameters. By default, Meteole retrieves data for the entire metropolitan France. +The geographical coverage of forecasts can be customized using the `lat` and `lon` parameters. By default, Meteole retrieves data for the entire metropolitan France. diff --git a/docs/pages/how_to.md b/docs/pages/how_to.md index 8d2fc78..9878db7 100644 --- a/docs/pages/how_to.md +++ b/docs/pages/how_to.md @@ -85,7 +85,7 @@ df_arome = arome_client.get_coverage( ], # Optional: prediction times (in hours) heights=[10], # Optional: height above ground level pressures=None, # Optional: pressure level - long = (-5.1413, 9.5602), # Optional: longitude. tuple (min_long, max_long) or a float for a specific location + lon = (-5.1413, 9.5602), # Optional: longitude. tuple (min_long, max_long) or a float for a specific location lat = (41.33356, 51.0889), # Optional: latitude. tuple (min_lat, max_lat) or a float for a specific location coverage_id=None, # Optional: an alternative to indicator/run/interval temp_dir=None, # Optional: Directory to store the temporary file diff --git a/src/meteole/climat.py b/src/meteole/climat.py index 4209df3..83733b5 100644 --- a/src/meteole/climat.py +++ b/src/meteole/climat.py @@ -185,7 +185,7 @@ def _distance_from_coords(lat1: float, lon1: float, lat2: float, lon2: float) -> def sort_stations_by_distance(lat: float, lon: float, stations: list[dict[str, Any]]) -> list[dict[str, Any]]: - """Sorts a list of stations by distance to a given point (lat, long) + """Sorts a list of stations by distance to a given point (lat, lon) Returns a copy of the list, sorted """ diff --git a/src/meteole/forecast.py b/src/meteole/forecast.py index 01b6a7d..eec1b8c 100644 --- a/src/meteole/forecast.py +++ b/src/meteole/forecast.py @@ -246,7 +246,8 @@ def get_coverage( self, indicator: str | None = None, lat: tuple | float = FRANCE_METRO_LATITUDES, - long: tuple | float = FRANCE_METRO_LONGITUDES, + lon: tuple | float = FRANCE_METRO_LONGITUDES, + long: tuple | float | None = None, ensemble_numbers: list[int] | None = None, heights: list[int] | None = None, pressures: list[int] | None = None, @@ -260,7 +261,7 @@ def get_coverage( Args: indicator: Indicator of a coverage to retrieve. - lat (long): Minimum and maximum latitude (longitude), or latitude (longitude) of the desired location. + lat (lon): Minimum and maximum latitude (longitude), or latitude (longitude) of the desired location. The closest grid point to the requested coordinate will be used. ensemble_numbers: For ensemble models only, numbers of the desired ensemble members. If None, defaults to the member 0. @@ -278,6 +279,17 @@ def get_coverage( Returns: pd.DataFrame: The complete run for the specified execution. """ + if long is not None: + if lon != self.FRANCE_METRO_LONGITUDES: + raise ValueError( + "Arguments `long` and `lon` cannot both be specified. Use only `lon` (longitude) in future code." + ) + warn( + ("Argument `long` is deprecated and will be removed in a future version. Use `lon` instead."), + DeprecationWarning, + stacklevel=2, + ) + lon = long # Numbers cannot be None if the model type is ENSEMBLE if self.MODEL_TYPE == "ENSEMBLE": if ensemble_numbers is None: @@ -294,11 +306,11 @@ def get_coverage( axis = self.get_coverage_description(coverage_id) - # Handle lat,long inputs (needs axis to check bounds) - user_lat, user_long = lat, long - lat, long = self._check_and_format_coords(lat, long, axis) + # Handle lat,lon inputs (needs axis to check bounds) + user_lat, user_long = lat, lon + lat, lon = self._check_and_format_coords(lat, lon, axis) logger.info(f"Using `lat={lat} (user input: {user_lat})`") - logger.info(f"Using `long={long} (user input: {user_long})`") + logger.info(f"Using `lon={lon} (user input: {user_long})`") heights = self._raise_if_invalid_or_fetch_default("heights", heights, axis["heights"]) pressures = self._raise_if_invalid_or_fetch_default("pressures", pressures, axis["pressures"]) @@ -314,7 +326,7 @@ def get_coverage( pressure=pressure if pressure != -1 else None, forecast_horizon=forecast_horizon, lat=lat, - long=long, + lon=lon, temp_dir=temp_dir, ) for forecast_horizon in forecast_horizons @@ -326,15 +338,15 @@ def get_coverage( return pd.concat(df_list, axis=0).reset_index(drop=True) def _check_and_format_coords( - self, lat: float | tuple[float, float], long: float | tuple[float, float], axis: dict[str, Any] + self, lat: float | tuple[float, float], lon: float | tuple[float, float], axis: dict[str, Any] ) -> tuple[tuple[float, float], tuple[float, float]]: - """Formats lat, long arguments passed to get_coverage: + """Formats lat, lon arguments passed to get_coverage: - Rounds all coordinates to the closest grid point - If a single float is passed, converts it to a tuple (value,value) Args: - lat, long : tuple (min,max) or float. - lat (long): Minimum and maximum latitude (longitude), or latitude (longitude) of the desired location. + lat, lon : tuple (min,max) or float. + lat (lon): Minimum and maximum latitude (longitude), or latitude (longitude) of the desired location. The closest grid point to the requested coordinate will be used. Returns: @@ -344,10 +356,10 @@ def _check_and_format_coords( min_lat, max_lat = lat, lat else: min_lat, max_lat = lat - if isinstance(long, (int, float)): - min_long, max_long = long, long + if isinstance(lon, (int, float)): + min_long, max_long = lon, lon else: - min_long, max_long = long + min_long, max_long = lon min_long, max_long = self._compute_closest_grid_point(min_long), self._compute_closest_grid_point(max_long) min_lat, max_lat = self._compute_closest_grid_point(min_lat), self._compute_closest_grid_point(max_lat) if min_lat < axis["min_latitude"]: @@ -645,7 +657,7 @@ def _get_data_single_forecast( pressure: int | None, height: int | None, lat: tuple, - long: tuple, + lon: tuple, temp_dir: str | None = None, ) -> pd.DataFrame: """(Protected) @@ -658,7 +670,7 @@ def _get_data_single_forecast( forecast_horizon (dt.timedelta): the forecast horizon (how much time ahead?) ensemble_number (int): For ensemble models only, number of the desired ensemble member. lat (tuple): minimum and maximum latitude - long (tuple): minimum and maximum longitude + lon (tuple): minimum and maximum longitude temp_dir (str | None): Directory to store the temporary file. Defaults to None. Returns: @@ -672,21 +684,21 @@ def _get_data_single_forecast( pressure=pressure, forecast_horizon_in_seconds=int(forecast_horizon.total_seconds()), lat=lat, - long=long, + lon=lon, ) df: pd.DataFrame = self._grib_bytes_to_df(grib_binary, temp_dir=temp_dir) if self.MODEL_NAME == "pearpege": - # for unclear reasons, the pearpege API does not accept lat, long + # for unclear reasons, the pearpege API does not accept lat, lon # parameters unlike the other models API. # So we retrieve all the domain and then filter the results - # for the desired lat, long in _get_data_single_forecast + # for the desired lat, lon in _get_data_single_forecast df = df.loc[ (df["latitude"] <= lat[1]) & (df["latitude"] >= lat[0]) - & (df["longitude"] <= long[1]) - & (df["longitude"] >= long[0]) + & (df["longitude"] <= lon[1]) + & (df["longitude"] >= lon[0]) ] # Drop and rename columns df.drop(columns=["surface", "valid_time"], errors="ignore", inplace=True) @@ -745,7 +757,7 @@ def _get_coverage_file( pressure: int | None = None, forecast_horizon_in_seconds: int = 0, lat: tuple = (37.5, 55.4), - long: tuple = (-12, 16), + lon: tuple = (-12, 16), ) -> bytes: """(Protected) Retrieves data for a specified model prediction. @@ -760,7 +772,7 @@ def _get_coverage_file( Defaults to 0 (current time). lat (tuple[float, float], optional): Tuple specifying the minimum and maximum latitudes. Defaults to (37.5, 55.4), covering the latitudes of France. - long (tuple[float, float], optional): Tuple specifying the minimum and maximum longitudes. + lon (tuple[float, float], optional): Tuple specifying the minimum and maximum longitudes. Defaults to (-12, 16), covering the longitudes of France. Returns: @@ -779,10 +791,10 @@ def _get_coverage_file( url = f"{self._model_base_path}/{self._entry_point.replace('xxx', f'{ensemble_number:03}')}/GetCoverage" if self.MODEL_NAME == "pearpege": - # for unclear reasons, the pearpege API does not accept lat, long + # for unclear reasons, the pearpege API does not accept lat, lon # parameters unlike the other models API. # So we retrieve all the domain and then filter the results - # for the desired lat, long in _get_data_single_forecast + # for the desired lat, lon in _get_data_single_forecast subset = [ *([f"pressure({pressure})"] if pressure is not None else []), *([f"height({height})"] if height is not None else []), @@ -794,7 +806,7 @@ def _get_coverage_file( *([f"height({height})"] if height is not None else []), f"time({forecast_horizon_in_seconds})", f"lat({lat[0]},{lat[1]})", - f"long({long[0]},{long[1]})", + f"lon({lon[0]},{lon[1]})", ] params = { @@ -843,7 +855,7 @@ def get_combined_coverage( pressures: list[int] | None = None, intervals: list[str | None] | None = None, lat: tuple = FRANCE_METRO_LATITUDES, - long: tuple = FRANCE_METRO_LONGITUDES, + lon: tuple = FRANCE_METRO_LONGITUDES, forecast_horizons: list[dt.timedelta] | None = None, temp_dir: str | None = None, ) -> pd.DataFrame: @@ -865,7 +877,7 @@ def get_combined_coverage( Must be `None` or "" for instant indicators ; otherwise, raises an exception. Defaults to 'P1D' for time-aggregated indicators. lat (tuple): The latitude range as (min_latitude, max_latitude). Defaults to FRANCE_METRO_LATITUDES. - long (tuple): The longitude range as (min_longitude, max_longitude). Defaults to FRANCE_METRO_LONGITUDES. + lon (tuple): The longitude range as (min_longitude, max_longitude). Defaults to FRANCE_METRO_LONGITUDES. forecast_horizons (list[dt.timedelta] | None): A list of forecast horizon values in dt.timedelta. Defaults to None. temp_dir (str | None): Directory to store the temporary file. Defaults to None. @@ -886,7 +898,7 @@ def get_combined_coverage( indicator_names=indicator_names, run=run, lat=lat, - long=long, + lon=lon, ensemble_numbers=ensemble_numbers, heights=heights, pressures=pressures, @@ -907,7 +919,7 @@ def _get_combined_coverage_for_single_run( pressures: list[int] | None = None, intervals: list[str | None] | None = None, lat: tuple = FRANCE_METRO_LATITUDES, - long: tuple = FRANCE_METRO_LONGITUDES, + lon: tuple = FRANCE_METRO_LONGITUDES, forecast_horizons: list[dt.timedelta] | None = None, temp_dir: str | None = None, ) -> pd.DataFrame: @@ -929,7 +941,7 @@ def _get_combined_coverage_for_single_run( Must be `None` or "" for instant indicators ; otherwise, raises an exception. Defaults to 'P1D' for time-aggregated indicators. lat (tuple): The latitude range as (min_latitude, max_latitude). Defaults to FRANCE_METRO_LATITUDES. - long (tuple): The longitude range as (min_longitude, max_longitude). Defaults to FRANCE_METRO_LONGITUDES. + lon (tuple): The longitude range as (min_longitude, max_longitude). Defaults to FRANCE_METRO_LONGITUDES. forecast_horizons (list[dt.timedelta] | None): A list of forecast horizon values (as a dt.timedelta object). Defaults to None. temp_dir (str | None): Directory to store the temporary file. Defaults to None. @@ -989,7 +1001,7 @@ def _check_params_length(params: list[Any] | None, arg_name: str) -> list[Any]: coverage_id=coverage_id, run=run, lat=lat, - long=long, + lon=lon, ensemble_numbers=[ensemble_number] if ensemble_number is not None else None, heights=[height] if height is not None else [], pressures=[pressure] if pressure is not None else [], diff --git a/tests/test_forecasts.py b/tests/test_forecasts.py index 5f34122..993a163 100644 --- a/tests/test_forecasts.py +++ b/tests/test_forecasts.py @@ -173,7 +173,7 @@ def test_get_data_single_forecast(self, mock_get_coverage_file, mock_grib_bytes_ ensemble_number=None, forecast_horizon=dt.timedelta(hours=0), lat=(37.5, 55.4), - long=(-12, 16), + lon=(-12, 16), ) self.assertTrue("data" in df.columns) @@ -200,7 +200,7 @@ def test_get_data_single_forecast_with_height( ensemble_number=None, forecast_horizon=dt.timedelta(hours=0), lat=(37.5, 55.4), - long=(-12, 16), + lon=(-12, 16), ) self.assertTrue("data_2m" in df.columns) @@ -261,7 +261,7 @@ def test_get_coverage(self, mock_get_data_single_forecast, mock_get_capabilities heights=[2], forecast_horizons=[dt.timedelta(hours=0)], lat=(37.5, 55.4), - long=(-12, 16), + lon=(-12, 16), ) mock_get_data_single_forecast.assert_called_once_with( @@ -271,7 +271,7 @@ def test_get_coverage(self, mock_get_data_single_forecast, mock_get_capabilities ensemble_number=None, forecast_horizon=dt.timedelta(hours=0), lat=(37.5, 55.4), - long=(-12, 16), + lon=(-12, 16), temp_dir=None, ) @@ -281,7 +281,7 @@ def test_get_coverage(self, mock_get_data_single_forecast, mock_get_capabilities def test_get_coverage_lat_lon( self, mock_get_data_single_forecast, mock_get_capabilities, mock_get_coverage_description ): - """Tests the different ways that a user can provide lat and long to get_coverage (float and tuple)""" + """Tests the different ways that a user can provide lat and lon to get_coverage (float and tuple)""" mock_get_data_single_forecast.return_value = pd.DataFrame( { "latitude": [1, 2, 3], @@ -309,8 +309,8 @@ def test_get_coverage_lat_lon( ) for lat, expected_lat in zip((37.5689, (37.5689, 45.00986)), ((37.57, 37.57), (37.57, 45.01))): - for long, expected_long in zip((2.568, (-1.566, 2.568)), ((2.57, 2.57), (-1.57, 2.57))): - forecast.get_coverage(coverage_id="toto", lat=lat, long=long) + for lon, expected_long in zip((2.568, (-1.566, 2.568)), ((2.57, 2.57), (-1.57, 2.57))): + forecast.get_coverage(coverage_id="toto", lat=lat, lon=lon) mock_get_data_single_forecast.assert_called_once_with( coverage_id="toto", ensemble_number=None, @@ -318,7 +318,7 @@ def test_get_coverage_lat_lon( pressure=None, forecast_horizon=dt.timedelta(hours=0), lat=expected_lat, - long=expected_long, + lon=expected_long, temp_dir=None, ) mock_get_data_single_forecast.reset_mock() @@ -456,7 +456,7 @@ def test_get_combined_coverage( pressures = [None, None] intervals = ["", "P1D"] lat = (37.5, 55.4) - long = (-12, 16) + lon = (-12, 16) forecast_horizons = [dt.timedelta(hours=0)] expected_result = pd.DataFrame( @@ -483,7 +483,7 @@ def test_get_combined_coverage( pressures=pressures, intervals=intervals, lat=lat, - long=long, + lon=lon, forecast_horizons=forecast_horizons, ) pd.testing.assert_frame_equal(result, expected_result) @@ -514,7 +514,7 @@ def test_get_combined_coverage_invalid_forecast_horizons( pressures = [None, None] intervals = ["", "P1D"] lat = (37.5, 55.4) - long = (-12, 16) + lon = (-12, 16) forecast_horizons = [dt.timedelta(hours=0)] forecast = AromeForecast( @@ -531,7 +531,7 @@ def test_get_combined_coverage_invalid_forecast_horizons( pressures=pressures, intervals=intervals, lat=lat, - long=long, + lon=lon, forecast_horizons=forecast_horizons, ) self.assertIn("are not valid for these coverage_ids", str(context.exception)) @@ -599,7 +599,7 @@ def test_get_combined_coverage_multiple_runs( pressures = [None, None] intervals = ["", "P1D"] lat = (37.5, 55.4) - long = (-12, 16) + lon = (-12, 16) forecast_horizons = [dt.timedelta(hours=0)] expected_result = pd.DataFrame( @@ -631,7 +631,7 @@ def test_get_combined_coverage_multiple_runs( pressures=pressures, intervals=intervals, lat=lat, - long=long, + lon=lon, forecast_horizons=forecast_horizons, ) pd.testing.assert_frame_equal(result, expected_result) @@ -680,7 +680,7 @@ def test_get_combined_coverage_no_heights_or_pressures( pressures = None intervals = ["", "P1D"] lat = (37.5, 55.4) - long = (-12, 16) + lon = (-12, 16) forecast_horizons = [dt.timedelta(hours=0)] expected_result = pd.DataFrame( @@ -701,7 +701,7 @@ def test_get_combined_coverage_no_heights_or_pressures( ) result = forecast.get_combined_coverage( - indicator_names, runs, heights, pressures, intervals, lat, long, forecast_horizons + indicator_names, runs, heights, pressures, intervals, lat, lon, forecast_horizons ) pd.testing.assert_frame_equal(result, expected_result) diff --git a/tutorial/Fetch_forecasts.ipynb b/tutorial/Fetch_forecasts.ipynb index 29b1c45..e2310d9 100644 --- a/tutorial/Fetch_forecasts.ipynb +++ b/tutorial/Fetch_forecasts.ipynb @@ -125,9 +125,9 @@ "# You can specify a custom location using latitude and longitude, either as tuples (min, max) or as single values for a specific location\n", "# A combination also works (e.g. a tuple for latitude and a single value for longitude)\n", "# Coordinates get rounded to the nearest grid point\n", - "client.get_coverage(random_indicator, lat=48.8566, long=2.3522)\n", + "client.get_coverage(random_indicator, lat=48.8566, lon=2.3522)\n", "\n", - "client.get_coverage(random_indicator, lat=(45, 50), long=(2, 3))" + "client.get_coverage(random_indicator, lat=(45, 50), lon=(2, 3))" ] }, { diff --git a/tutorial/Fetch_forecasts_ensemble.ipynb b/tutorial/Fetch_forecasts_ensemble.ipynb index 24f8517..e039bdd 100644 --- a/tutorial/Fetch_forecasts_ensemble.ipynb +++ b/tutorial/Fetch_forecasts_ensemble.ipynb @@ -156,7 +156,7 @@ " forecast_horizons=aro_cov_descr[\"forecast_horizons\"],\n", " heights=[2],\n", " pressures=None,\n", - " long=(1.0, 1.5),\n", + " lon=(1.0, 1.5),\n", " lat=(43, 43.5),\n", " temp_dir=None,\n", ")\n", @@ -218,7 +218,7 @@ " ensemble_numbers=range(5),\n", " heights=[2],\n", " pressures=None,\n", - " long=(1.0, 1.5),\n", + " lon=(1.0, 1.5),\n", " lat=(43, 43.5),\n", " temp_dir=None,\n", ")\n",