mirror of
https://git.victorphan.net/basketballcantho/CSIROBoeingPhase5-Vietnam.git
synced 2026-08-05 05:43:10 +07:00
412 lines
14 KiB
Python
412 lines
14 KiB
Python
"""
|
|
Functions to retrieve ERA5 gridded climate data.
|
|
|
|
Updated Apr 2020 to directly access Zarr format data in PDS
|
|
|
|
Previous code for downloading and loading netcdf adpated from scripts by Andrew Cherry and Brian Killough.
|
|
"""
|
|
|
|
import os
|
|
import datetime
|
|
import numpy as np
|
|
import xarray as xr
|
|
import fsspec
|
|
from datacube.utils.geometry import assign_crs
|
|
|
|
# # only used for netcdf access
|
|
# from dateutil.parser import parse
|
|
# import boto3
|
|
# import botocore
|
|
# import warnings
|
|
|
|
|
|
ERA5_VARS = ['air_pressure_at_mean_sea_level',
|
|
'air_temperature_at_2_metres',
|
|
'air_temperature_at_2_metres_1hour_Maximum',
|
|
'air_temperature_at_2_metres_1hour_Minimum',
|
|
'dew_point_temperature_at_2_metres',
|
|
'eastward_wind_at_100_metres',
|
|
'eastward_wind_at_10_metres',
|
|
'integral_wrt_time_of_surface_direct_downwelling_shortwave_flux_in_air_1hour_Accumulation',
|
|
'lwe_thickness_of_surface_snow_amount',
|
|
'northward_wind_at_100_metres',
|
|
'northward_wind_at_10_metres',
|
|
'precipitation_amount_1hour_Accumulation',
|
|
'sea_surface_temperature',
|
|
'snow_density',
|
|
'surface_air_pressure']
|
|
|
|
|
|
def load_era5(
|
|
var, lat, lon, time,
|
|
reduce_func=None,
|
|
resample="1D",
|
|
):
|
|
"""
|
|
Download and return an ERA5 variable for a defined time window.
|
|
|
|
Parameters
|
|
----------
|
|
var : string
|
|
Name of the ERA5 climate variable to download, e.g "air_temperature_at_2_metres"
|
|
|
|
lat: tuple or list
|
|
Latitude range for query.
|
|
|
|
lon: tuple or list
|
|
Longitude range for query.
|
|
|
|
time: string or datetime object or a list or tuple of strings or datetime objects
|
|
Used to define starting and end date dates of the time window.
|
|
|
|
reduce_func: numpy function
|
|
lets you specify a function to apply to each day's worth of data.
|
|
The default is np.mean, which computes daily average. To get a sum, use np.sum.
|
|
|
|
resample: string
|
|
Temporal resampling frequency to be used for xarray's resample function.
|
|
The default is '1D', which is daily.
|
|
Since this is applied on monthly ERA5 data, maximum resampling period is '1M'.
|
|
|
|
Returns
|
|
-------
|
|
A lazy-loaded xarray dataset containing an ERA5 variable for the selected region and time window.
|
|
|
|
"""
|
|
|
|
# constrain query to available variables
|
|
assert var in ERA5_VARS, "var must be one of [{}] (got {})".format(
|
|
",".join(ERA5_VARS), var
|
|
)
|
|
|
|
# set default reduction function
|
|
if reduce_func is None:
|
|
reduce_func = np.mean
|
|
|
|
# process date range
|
|
if type(time) in [list, tuple]:
|
|
date_from = np.datetime64(min(time)).astype('datetime64[D]')
|
|
date_to = (np.datetime64(max(time))+1).astype('datetime64[D]')-np.timedelta64(1,'D')
|
|
elif type(time) in [str, np.datetime64]:
|
|
date_from = np.datetime64(time).astype('datetime64[D]')
|
|
date_to = (np.datetime64(time)+1).astype('datetime64[D]')-np.timedelta64(1,'D')
|
|
else:
|
|
raise(ValueError)
|
|
|
|
# actual lat lon ranges will be infered from nearest match to data
|
|
lat_range = None
|
|
lon_range = None
|
|
|
|
datasets = []
|
|
# Loop through month and year to access ERA5 zarr
|
|
month = date_from.astype('datetime64[M]')
|
|
while month <= date_to.astype('datetime64[M]'):
|
|
url = f"s3://era5-pds/zarr/{month.astype(object).year:04}/{month.astype(object).month:02}/data/{var}.zarr"
|
|
ds = xr.open_zarr(fsspec.get_mapper(url, anon=True,
|
|
client_kwargs={'region_name':'us-east-1'}),
|
|
consolidated=True)
|
|
|
|
# re-order along longitude to go from -180 to 180 if needed
|
|
if min(lon) < 0:
|
|
ds = ds.assign_coords({"lon": (((ds.lon + 180) % 360) - 180)})
|
|
ds = ds.reindex({"lon": np.sort(ds.lon)})
|
|
|
|
if lat_range is None:
|
|
# find the nearest lat lon boundary points
|
|
test = ds.sel(lat=list(lat), lon=list(lon), method="nearest")
|
|
# define the lat/lon grid
|
|
lat_range = slice(test.lat.max().values, test.lat.min().values)
|
|
lon_range = slice(test.lon.min().values, test.lon.max().values)
|
|
|
|
if "time0" in ds.dims:
|
|
ds = ds.rename({"time0": "time"})
|
|
if "time1" in ds.dims:
|
|
ds = ds.rename(
|
|
{"time1": "time"}
|
|
) # This should INTENTIONALLY error if both times are defined
|
|
|
|
output = ds[[var]].sel(lat=lat_range, lon=lon_range, time=slice(date_from, date_to)).resample(time=resample).reduce(reduce_func)
|
|
output.attrs = ds.attrs
|
|
for v in output.data_vars:
|
|
output[v].attrs = ds[v].attrs
|
|
|
|
datasets.append(output)
|
|
month += np.timedelta64(1,'M')
|
|
|
|
return assign_crs(xr.combine_by_coords(datasets), 'EPSG:4326')
|
|
|
|
|
|
# # older version of scripts to download and use netcdf
|
|
|
|
# ERA5_VARS_NC = [
|
|
# "air_pressure_at_mean_sea_level",
|
|
# "air_temperature_at_2_metres",
|
|
# "air_temperature_at_2_metres_1hour_Maximum",
|
|
# "air_temperature_at_2_metres_1hour_Minimum",
|
|
# "dew_point_temperature_at_2_metres",
|
|
# "eastward_wind_at_100_metres",
|
|
# "eastward_wind_at_10_metres",
|
|
# "integral_wrt_time_of_surface_direct_downwelling_shortwave_flux_in_air_1hour_Accumulation",
|
|
# "lwe_thickness_of_surface_snow_amount",
|
|
# "northward_wind_at_100_metres",
|
|
# "northward_wind_at_10_metres",
|
|
# "precipitation_amount_1hour_Accumulation",
|
|
# "sea_surface_temperature",
|
|
# "sea_surface_wave_from_direction",
|
|
# "sea_surface_wave_mean_period",
|
|
# "significant_height_of_wind_and_swell_waves",
|
|
# "snow_density",
|
|
# "surface_air_pressure",
|
|
# ]
|
|
|
|
|
|
# def get_era5_daily(
|
|
# var,
|
|
# date_from_arg,
|
|
# date_to_arg=None,
|
|
# reduce_func=None,
|
|
# cache_dir="era5",
|
|
# resample="1D",
|
|
# ):
|
|
# """
|
|
# Download and return an ERA5 variable for a defined time window.
|
|
|
|
# Parameters
|
|
# ----------
|
|
# var : string
|
|
# Name of the ERA5 climate variable to download, e.g "air_temperature_at_2_metres"
|
|
|
|
# date_from_arg: string or datetime object
|
|
# Starting date of the time window.
|
|
|
|
# date_to_arg: string or datetime object
|
|
# End date of the time window. If not supplied, set to be the same as starting date.
|
|
|
|
# reduce_func: numpy function
|
|
# lets you specify a function to apply to each day's worth of data.
|
|
# The default is np.mean, which computes daily average. To get a sum, use np.sum.
|
|
|
|
# cache_dir: sting
|
|
# Path to save downloaded ERA5 data. The path will be created if not already exists.
|
|
# The default is 'era5'.
|
|
|
|
# resample: string
|
|
# Temporal resampling frequency to be used for xarray's resample function.
|
|
# The default is '1D', which is daily.
|
|
# Since ERA5 data is provided as one file per month, maximum resampling period is '1M'.
|
|
|
|
# Returns
|
|
# -------
|
|
# A lazy-loaded xarray dataset containing an ERA5 variable for the selected time window.
|
|
|
|
# """
|
|
|
|
# # Massage input data
|
|
# assert var in ERA5_VARS_NC, "var must be one of [{}] (got {})".format(
|
|
# ",".join(ERA5_VARS_NC), var
|
|
# )
|
|
# if not os.path.exists(cache_dir):
|
|
# os.mkdir(cache_dir)
|
|
# if reduce_func is None:
|
|
# reduce_func = np.mean
|
|
# if type(date_from_arg) == str:
|
|
# date_from_arg = parse(date_from_arg)
|
|
# if type(date_to_arg) == str:
|
|
# date_to_arg = parse(date_to_arg)
|
|
# if date_to_arg is None:
|
|
# date_to_arg = date_from_arg
|
|
# # Make sure our dates are in the correct order
|
|
# from_date = min(date_from_arg, date_to_arg)
|
|
# to_date = max(date_from_arg, date_to_arg)
|
|
# # Download ERA5 files to local cache if they don't already exist
|
|
# client = None # Boto client (if needed)
|
|
# local_files = [] # Will hold list of local filenames
|
|
# Y, M = from_date.year, from_date.month # Loop vars
|
|
# loop_end = to_date.year * 12 + to_date.month # Loop sentinel
|
|
# while Y * 12 + M <= loop_end:
|
|
# local_file = os.path.join(
|
|
# cache_dir, "{Y:04}_{M:02}_{var}.nc".format(Y=Y, M=M, var=var)
|
|
# )
|
|
# data_key = "{Y:04}/{M:02}/data/{var}.nc".format(Y=Y, M=M, var=var)
|
|
# if not os.path.isfile(
|
|
# local_file
|
|
# ): # check if file already exists (TODO: move to temp?) (TODO: catch failed download)
|
|
# if client is None:
|
|
# client = boto3.client(
|
|
# "s3",
|
|
# config=botocore.client.Config(signature_version=botocore.UNSIGNED),
|
|
# )
|
|
# client.download_file("era5-pds", data_key, local_file)
|
|
# local_files.append(local_file)
|
|
# if M == 12:
|
|
# Y += 1
|
|
# M = 1
|
|
# else:
|
|
# M += 1
|
|
# # Load and merge the locally-cached ERA5 data from the list of filenames
|
|
# date_slice = slice(
|
|
# str(from_date.date()), str(to_date.date())
|
|
# ) # I do this to INCLUDE the whole end date, not just 00:00
|
|
|
|
# def prepro(ds):
|
|
# if "time0" in ds.dims:
|
|
# ds = ds.rename({"time0": "time"})
|
|
# if "time1" in ds.dims:
|
|
# ds = ds.rename(
|
|
# {"time1": "time"}
|
|
# ) # This should INTENTIONALLY error if both times are defined
|
|
# ds = ds[[var]]
|
|
# output = ds.sel(time=date_slice).resample(time=resample).reduce(reduce_func)
|
|
# output.attrs = ds.attrs
|
|
# for v in output.data_vars:
|
|
# output[v].attrs = ds[v].attrs
|
|
# return output
|
|
|
|
# return xr.open_mfdataset(
|
|
# local_files,
|
|
# combine="by_coords",
|
|
# compat="equals",
|
|
# preprocess=prepro,
|
|
# parallel=True,
|
|
# )
|
|
|
|
|
|
# def era5_area_crop(ds, lat, lon):
|
|
# """
|
|
# Crop a dataset containing EAR5 variables to a location.
|
|
# The output spatial grid will either include input grid points within lat/lon boundaries or the nearest point if none is within the search location.
|
|
|
|
# Parameters
|
|
# ----------
|
|
# ds : xarray dataset
|
|
# A dataset containing ERA5 variables of interest.
|
|
|
|
# lat: tuple or list
|
|
# Latitude range for query.
|
|
|
|
# lon: tuple or list
|
|
# Longitude range for query.
|
|
|
|
# Returns
|
|
# -------
|
|
# An xarray dataset containing ERA5 variables for the selected location.
|
|
|
|
# """
|
|
|
|
# # Handle single value lat/lon args by wrapping them in lists
|
|
# try:
|
|
# min(lat)
|
|
# except TypeError:
|
|
# lat = [lat]
|
|
# try:
|
|
# min(lon)
|
|
# except TypeError:
|
|
# lon = [lon]
|
|
# if min(lon) < 0:
|
|
# # re-order along longitude to go from -180 to 180
|
|
# ds = ds.assign_coords({"lon": (((ds.lon + 180) % 360) - 180)})
|
|
# ds = ds.reindex({"lon": np.sort(ds.lon)})
|
|
# # Issue warnings if args outside range.
|
|
# if min(lat) < ds.lat.min() or max(lat) > ds.lat.max():
|
|
# warnings.warn(
|
|
# "Lats must be in range {} .. {}. Got: {}".format(
|
|
# ds.lat.min().values, ds.lat.max().values, lat
|
|
# )
|
|
# )
|
|
# if min(lon) < ds.lon.min() or max(lon) > ds.lon.max():
|
|
# warnings.warn(
|
|
# "Lons must be in range {} .. {}. Got: {}".format(
|
|
# ds.lon.min().values, ds.lon.max().values, lon
|
|
# )
|
|
# )
|
|
# # Find existing coords between min&max
|
|
# lats = ds.lat[np.logical_and(ds.lat >= min(lat), ds.lat <= max(lat))].values
|
|
# # If there was nothing between, just plan to grab closest
|
|
# if len(lats) == 0:
|
|
# lats = np.unique(ds.lat.sel(lat=np.array(lat), method="nearest"))
|
|
# lons = ds.lon[np.logical_and(ds.lon >= min(lon), ds.lon <= max(lon))].values
|
|
# if len(lons) == 0:
|
|
# lons = np.unique(ds.lon.sel(lon=np.array(lon), method="nearest"))
|
|
# # crop and keep attrs
|
|
# output = ds.sel(lat=lats, lon=lons)
|
|
# output.attrs = ds.attrs
|
|
# for var in output.data_vars:
|
|
# output[var].attrs = ds[var].attrs
|
|
# return output
|
|
|
|
|
|
# def era5_area_nearest(ds, lat, lon):
|
|
# """
|
|
# Crop a dataset containing EAR5 variables to a location.
|
|
# The output spatial grid is snapped to the nearest input grid points.
|
|
|
|
# Parameters
|
|
# ----------
|
|
# ds : xarray dataset
|
|
# A dataset containing ERA5 variables of interest.
|
|
|
|
# lat: tuple or list
|
|
# Latitude range for query.
|
|
|
|
# lon: tuple or list
|
|
# Longitude range for query.
|
|
|
|
# Returns
|
|
# -------
|
|
# An xarray dataset containing ERA5 variables for the selected location.
|
|
|
|
# """
|
|
|
|
# if min(lon) < 0:
|
|
# # re-order along longitude to go from -180 to 180
|
|
# ds = ds.assign_coords({"lon": (((ds.lon + 180) % 360) - 180)})
|
|
# ds = ds.reindex({"lon": np.sort(ds.lon)})
|
|
|
|
# # find the nearest lat lon boundary points
|
|
# test = ds.sel(lat=lat, lon=lon, method="nearest")
|
|
# # define the lat/lon grid
|
|
# lat_range = slice(test.lat.max().values, test.lat.min().values)
|
|
# lon_range = slice(test.lon.min().values, test.lon.max().values)
|
|
# # crop and keep attrs
|
|
# output = ds.sel(lat=lat_range, lon=lon_range)
|
|
# output.attrs = ds.attrs
|
|
# for var in output.data_vars:
|
|
# output[var].attrs = ds[var].attrs
|
|
# return output
|
|
|
|
|
|
# def load_era5_netcdf(var, lat, lon, time, grid="nearest", **kwargs):
|
|
# """
|
|
# Returns a ERA5 variable for a selected location and time window.
|
|
|
|
# Parameters
|
|
# ----------
|
|
# var : string
|
|
# Name of the ERA5 climate variable to download, e.g "air_temperature_at_2_metres"
|
|
|
|
# lat: tuple or list
|
|
# Latitude range for query.
|
|
|
|
# lon: tuple or list
|
|
# Longitude range for query.
|
|
|
|
# time: tuple or list
|
|
# Time range for query.
|
|
|
|
# grid: string
|
|
# Option for output spatial gridding.
|
|
# The default is 'nearest', for which output spatial grid is snapped to the nearest ERA5 input grid points.
|
|
# Alternatively, output spatial grid will either include input grid points within lat/lon boundaries or the nearest point if none is within the search location.
|
|
|
|
# Returns
|
|
# -------
|
|
# An xarray dataset containing the variable for the selected location and time window.
|
|
|
|
# """
|
|
|
|
# ds = get_era5_daily(var, time[0], time[1], **kwargs)
|
|
# if grid == "nearest":
|
|
# return era5_area_nearest(ds, lat, lon).compute()
|
|
# else:
|
|
# return era5_area_crop(ds, lat, lon).compute()
|