Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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).
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/coverage_parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 1 addition & 1 deletion docs/pages/how_to.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/meteole/climat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""

Expand Down
76 changes: 44 additions & 32 deletions src/meteole/forecast.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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.
Expand All @@ -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:
Expand All @@ -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"])
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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"]:
Expand Down Expand Up @@ -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)
Expand All @@ -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:
Expand All @@ -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)
Expand Down Expand Up @@ -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.
Expand All @@ -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:
Expand All @@ -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 []),
Expand All @@ -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 = {
Expand Down Expand Up @@ -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:
Expand All @@ -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.

Expand All @@ -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,
Expand All @@ -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:
Expand All @@ -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.

Expand Down Expand Up @@ -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 [],
Expand Down
Loading