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

Add support for ERA5-land #67

Merged
merged 29 commits into from
Feb 19, 2021
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
1791e5e
Add variable info for ERA5 land
Peter9192 Feb 2, 2021
4ed451f
Pass test and add ERA5land info to tests
Peter9192 Feb 2, 2021
42f427c
added variables to ERA5 land list
bvreede Feb 2, 2021
e05f1ee
added --land to cli
bvreede Feb 5, 2021
36540a8
add era5land variables to reference
bvreede Feb 5, 2021
b0746a3
add era5land variables to info
bvreede Feb 5, 2021
796739b
add land variable to Fetch class
bvreede Feb 5, 2021
6a1f54f
add -land to outputfilename
bvreede Feb 5, 2021
32452c1
adjust construction of dataset name and api request with land
bvreede Feb 5, 2021
b820fee
add land options to choice of product type
bvreede Feb 8, 2021
03f5b6c
Adjusted tests with land addition and fixed code errors
bvreede Feb 8, 2021
a9dcfe3
fix test_vars for land support
bvreede Feb 15, 2021
722456e
add test for _build_request with land
bvreede Feb 15, 2021
cb17d07
fixed typo in variable name
bvreede Feb 15, 2021
9e43fe8
adjust to flake8 requests
bvreede Feb 15, 2021
8cc5c9b
Add filename test for era5 land
Peter9192 Feb 16, 2021
1cac322
Reorganize tests for product type and add tests for land
Peter9192 Feb 16, 2021
d5d7115
Refactor the product type function
Peter9192 Feb 16, 2021
554d01d
Add test for check levels
Peter9192 Feb 16, 2021
2b268b1
fix flake8
Peter9192 Feb 16, 2021
730246d
Add test for _check_variables
Peter9192 Feb 16, 2021
ff2f03a
Add tests for incompatible options
Peter9192 Feb 16, 2021
2b5552a
Add test for ERA5-land period discrepancy
Peter9192 Feb 16, 2021
83805cd
Test fail for land-only variable but no --land flag
Peter9192 Feb 16, 2021
f792f08
Add tests for _build_name
Peter9192 Feb 16, 2021
5de09d9
Make xfail test point to new issue
Peter9192 Feb 16, 2021
9509e23
add tests to confirm parsing of --land
bvreede Feb 18, 2021
7c8c856
set hourly period explicitly in test
bvreede Feb 19, 2021
1c1cc84
remove TODO in favor of issue #71
bvreede Feb 19, 2021
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
26 changes: 24 additions & 2 deletions era5cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,22 @@ def _build_parser():
downloads data from the preliminary back
extension. Note that when "--prelimbe" is used,
"--startyear" and "--endyear" should be set
between 1950 and 1978.
between 1950 and 1978. --prelimbe is incompatible
with --land.

''')
)

common.add_argument(
"--land", action="store_true", default=False,
help=textwrap.dedent('''\
Download data from ERA5-Land.
Providing the "--land" argument
downloads data from the ERA5-Land dataset.
Note that the ERA5-Land dataset starts in
1981.
--land is incompatible with the use of
--prelimbe and --ensemble.

''')
)
Expand Down Expand Up @@ -261,6 +276,8 @@ def _build_parser():
"2Dvars" for all available single level or 2D
variables \n
"3Dvars" for all available 3D variables \n
"land" for all available variables in
ERA5-land \n
Enter variable name (e.g. "total_precipitation")
or pressure level (e.g. "825") to show if the
variable or level is available and in which list.
Expand Down Expand Up @@ -300,6 +317,10 @@ def _construct_year_list(args):
assert 1950 <= year <= 1978, (
'year should be between 1950 and 1978'
)
elif args.land:
assert 1981 <= year <= datetime.now().year, (
'for ERA5-Land, year should be between 1981 and present'
)
else:
assert 1979 <= year <= datetime.now().year, (
'year should be between 1979 and present'
Expand Down Expand Up @@ -335,7 +356,7 @@ def _set_period_args(args):
assert args.ensemble, (
"Statistics can only be computed over an ensemble, "
"add --ensemble or remove --statistics."
)
)
days = args.days
hours = args.hours
else:
Expand Down Expand Up @@ -372,6 +393,7 @@ def _execute(args):
threads=args.threads,
merge=args.merge,
prelimbe=args.prelimbe,
land=args.land
)
era5.fetch(dryrun=args.dryrun)
return True
Expand Down
191 changes: 120 additions & 71 deletions era5cli/fetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,17 +59,20 @@ class Fetch:
be written to stdout.
prelimbe: bool
Whether to download the preliminary back extension (1950-1978).
land: bool
Whether to download ERA5-Land data.
"""

def __init__(self, years: list, months: list, days: list,
hours: list, variables: list, outputformat: str,
outputprefix: str, period: str, ensemble: bool,
statistics=None, synoptic=None, pressurelevels=None,
merge=False, threads=None, prelimbe=False):
merge=False, threads=None, prelimbe=False, land=False):
"""Initialization of Fetch class."""
self.months = era5cli.utils._zpad_months(months)
"""list(str): List of zero-padded strings of months
(e.g. ['01', '02',..., '12'])."""
# TODO make sure these docstrings are consistent with cli docs
nielsdrost marked this conversation as resolved.
Show resolved Hide resolved
if period == 'monthly':
self.days = None
else:
Expand Down Expand Up @@ -112,6 +115,9 @@ def __init__(self, years: list, months: list, days: list,
self.prelimbe = prelimbe
"""bool: Whether to select from the ERA5 preliminary back
extension which supports years from 1950 to 1978"""
self.land = land
"""bool: Whether to download from the ERA5-Land
dataset."""

def fetch(self, dryrun=False):
"""Split calls and fetch results.
Expand Down Expand Up @@ -146,23 +152,19 @@ def _extension(self):

def _define_outputfilename(self, var, years):
"""Define output filename."""
start_year = years[0]
end_year = years[-1]
if not end_year or (end_year == start_year):
fname = ("{}_{}_{}_{}".format(
self.outputprefix, var,
start_year, self.period))
else:
fname = ("{}_{}_{}-{}_{}".format(
self.outputprefix, var,
start_year, end_year, self.period))
start, end = years[0], years[-1]

prefix = (f"{self.outputprefix}-land" if self.land
else self.outputprefix)
yearblock = f"{start}-{end}" if not start == end else f"{start}"
fname = f"{prefix}_{var}_{yearblock}_{self.period}"
if self.ensemble:
fname += "_ensemble"
if self.statistics:
fname += "_statistics"
if self.synoptic:
fname += "_synoptic"
fname += ".{}".format(self.ext)
fname += f".{self.ext}"
return fname

def _split_variable(self):
Expand Down Expand Up @@ -193,81 +195,128 @@ def _split_variable_yr(self):

def _product_type(self):
"""Construct the product type name from the options."""
producttype = ""

if self.ensemble:
producttype += "ensemble_members"
elif not self.ensemble:
producttype += "reanalysis"

if self.period == "monthly" and not self.prelimbe:
producttype = "monthly_averaged_" + producttype
if self.synoptic:
producttype += "_by_hour_of_day"
elif self.period == "monthly" and self.prelimbe:
if self.ensemble:
producttype = "members-"
elif not self.ensemble:
producttype = "reanalysis-"
if self.synoptic:
producttype += "synoptic-monthly-means"
elif not self.synoptic:
producttype += "monthly-means-of-daily-means"
elif self.period == "hourly" and self.ensemble and self.statistics:
producttype = [
if self.period == 'hourly' and self.ensemble and self.statistics:
# The only configuration to return a list
return [
"ensemble_members",
"ensemble_mean",
"ensemble_spread",
]

if self.land and self.period == "hourly":
# The only configuration to return None
return None

# Default flow
if self.ensemble:
producttype = "ensemble_members"
else:
producttype = "reanalysis"

if self.period == "hourly":
return producttype

producttype = "monthly_averaged_" + producttype
if self.synoptic:
producttype += "_by_hour_of_day"

if not self.prelimbe:
return producttype

# Prelimbe has deviating product types for monthly data
if self.ensemble:
producttype = "members-"
else:
producttype = "reanalysis-"
if self.synoptic:
producttype += "synoptic-monthly-means"
else:
producttype += "monthly-means-of-daily-means"
return producttype

def _build_request(self, variable, years):
"""Build the download request for the retrieve method of cdsapi."""
name = "reanalysis-era5-"
request = {'variable': variable,
'year': years,
'product_type': self._product_type(),
'month': self.months,
'time': self.hours,
'format': self.outputformat}
def _check_levels(self):
"""Retrieve pressure level info for request"""
if not self.pressure_levels:
raise ValueError(
"Requested 3D variable(s), but no pressure levels specified."
"Aborting."
)
if not all(level in ref.PLEVELS for level in self.pressure_levels):
raise ValueError(
f"Invalid pressure levels. Allowed values are: {ref.PLEVELS}"
)

# variable is pressure level variable
if variable in ref.PLVARS:
try:
if all(level in ref.PLEVELS for level in self.pressure_levels):
name += "pressure-levels"
request["pressure_level"] = self.pressure_levels
else:
raise ValueError(
"Invalid pressure levels. Allowed values are: {}"
.format(ref.PLEVELS))
except TypeError:
def _check_variable(self, variable):
"""Check variable available and compatible with other inputs."""
# if land then the variable must be in era5 land
if self.land:
if variable not in ref.ERA5_LAND_VARS:
raise ValueError(
"Invalid pressure levels. Allowed values are: {}"
.format(ref.PLEVELS))
# variable is single level variable
f"Variable {variable} is not available in ERA5-Land.\n"
f"Choose from {ref.ERA5_LAND_VARS}"
)
elif variable in ref.PLVARS+ref.SLVARS:
if self.period == "monthly":
if variable in ref.MISSING_MONTHLY_VARS:
header = ("There is no monthly data available for the "
"following variables:\n")
raise ValueError(era5cli.utils._print_multicolumn(
header,
ref.MISSING_MONTHLY_VARS))
else:
raise ValueError(
"Invalid variable name: {}".format(variable)
)

def _build_name(self, variable):
"""Build up name of dataset to use"""

name = "reanalysis-era5"

if self.land:
name += "-land"
elif variable in ref.PLVARS:
name += "-pressure-levels"
elif variable in ref.SLVARS:
name += "single-levels"
# variable is unknown
name += "-single-levels"
else:
raise ValueError('Invalid variable name: {}'.format(variable))
raise ValueError(
"Invalid variable name: {}".format(variable)
)

if self.period == "monthly":
name += "-monthly-means"
if variable in ref.MISSING_MONTHLY_VARS:
header = ("There is no monthly data available for the "
"following variables:\n")
raise ValueError(era5cli.utils._print_multicolumn(
header,
ref.MISSING_MONTHLY_VARS))
elif self.period == "hourly":
# Add day list to request if applicable
if self.days:
request["day"] = self.days

if self.prelimbe:
if self.land:
raise ValueError(
"Back extension not (yet) available for ERA5-Land."
)
name += "-preliminary-back-extension"
return name

def _build_request(self, variable, years):
"""Build the download request for the retrieve method of cdsapi."""
self._check_variable(variable)

name = self._build_name(variable)

request = {'variable': variable,
'year': years,
'month': self.months,
'time': self.hours,
'format': self.outputformat}

if "pressure-levels" in name:
self._check_levels()
request["pressure_level"] = self.pressure_levels

product_type = self._product_type()
if product_type is not None:
request["product_type"] = product_type

if self.period == "hourly":
request["day"] = self.days

return(name, request)

Expand All @@ -287,7 +336,7 @@ def _getdata(self, variables: list, years: list, outputfile: str):
"It can take some time before downloading starts, ",
"please do not kill this process in the meantime.",
os.linesep
)
)
connection = cdsapi.Client()
print("".join(queueing_message)) # print queueing message
connection.retrieve(name, request, outputfile)
Expand Down
15 changes: 9 additions & 6 deletions era5cli/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ class Info:
----------
infoname: str
Name of information that needs to be printed. Supported are
'levels', '2Dvars', '3Dvars' and any variable or pressure level
defined in era5cli.inputref
'levels', '2Dvars', '3Dvars', 'ERA5land', and any variable or
pressure level defined in era5cli.inputref

Raises
------
Expand All @@ -33,10 +33,11 @@ def __init__(self, infoname: str):
"""list: List with information to be printed."""
self.infotype = "list"
except KeyError:
self.infotype = []
for valname, vallist in ref.REFDICT.items():
if self.infoname in vallist:
self.infotype = valname
if self.infotype is None:
self.infotype.append(valname)
if len(self.infotype) == 0:
raise ValueError('Unknown value for reference argument.')

def list(self):
Expand All @@ -53,13 +54,15 @@ def vars(self):

Print in which list the given variable occurs.
"""
print("{} is in the list: {}".format(self.infoname, self.infotype))
lists = ', '.join(self.infotype)
print("{} is in the list(s): {}".format(self.infoname, lists))

def _define_table_header(self):
"""Define table header."""
hdict = {
'levels': 'pressure levels',
'2Dvars': '2D variables',
'3Dvars': '3D variables'
'3Dvars': '3D variables',
'land': 'ERA5-land variables'
}
self.header = "Available {}:".format(hdict[self.infoname])
Loading