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 functionality for download of subregion #70

Merged
merged 23 commits into from
Mar 4, 2021
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
3 changes: 2 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ With era5cli you can: ​
- select multiple variables for several months and years
- split outputs by years, producing a separate file for every year instead of merging them in one file
- download multiple files at once
- extract data for a sub-region of the globe
- download ERA5 back extension (preliminary version)

.. inclusion-marker-end-do-not-remove

| Free software: Apache Software License 2.0
| Free software: Apache Software License 2.0
| Documentation: https://era5cli.readthedocs.io
17 changes: 17 additions & 0 deletions era5cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,22 @@ def _build_parser():
''')
)

common.add_argument(
"--area", nargs=4, type=float,
required=False,
help=textwrap.dedent('''\
Coordinates in case extraction of a subregion is
requested. Specified as
ymax xmin ymin xmax with x and y in the
range -180, +180 and -90, +90, respectively
e.g. --area 90 -180 -90 180.
Requests are rounded down to two decimals.
Without specification, the whole available area
will be returned.

''')
)

mnth = argparse.ArgumentParser(add_help=False)

mnth.add_argument(
Expand Down Expand Up @@ -383,6 +399,7 @@ def _execute(args):
days=days,
hours=hours,
variables=args.variables,
area=args.area,
outputformat=args.format,
outputprefix=args.outputprefix,
period=args.command,
Expand Down
41 changes: 40 additions & 1 deletion era5cli/fetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ class Fetch:
hours to download data for.
variables: list(str)
List of variable names to download data for.
area: None, list(float)
Coordinates in case extraction of a subregion is requested.
Specified as [ymax, xmin, ymin, xmax], with x and y
in the range -180, +180 and -90, +90, respectively. Requests are
rounded down to two decimals. Without specification, the whole
available area will be returned.
outputformat: str
Type of file to download: 'netcdf' or 'grib'.
outputprefix: str
Expand Down Expand Up @@ -65,7 +71,7 @@ class Fetch:

def __init__(self, years: list, months: list, days: list,
hours: list, variables: list, outputformat: str,
outputprefix: str, period: str, ensemble: bool,
outputprefix: str, period: str, ensemble: bool, area=None,
statistics=None, synoptic=None, pressurelevels=None,
merge=False, threads=None, prelimbe=False, land=False):
"""Initialization of Fetch class."""
Expand All @@ -86,6 +92,9 @@ def __init__(self, years: list, months: list, days: list,
"""list(int): List of pressure levels."""
self.variables = variables
"""list(str): List of variables."""
self.area = area
"""list(float): Coordinates specifying the subregion that will be
extracted. Default is None for whole available area."""
self.outputformat = outputformat
"""str: File format of output file."""
self.years = years
Expand Down Expand Up @@ -157,6 +166,9 @@ def _define_outputfilename(self, var, years):
else self.outputprefix)
yearblock = f"{start}-{end}" if not start == end else f"{start}"
fname = f"{prefix}_{var}_{yearblock}_{self.period}"
if self.area:
coords = [str(int(c)) for c in self.area]
fname += '_[' + ']['.join(coords) + ']'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beware that round(1.5) = 2 while int(1.5)=1.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about something like:

lon = lambda x: f"{x}E" if x>0 else f"{abs(x)}W"
lat = lambda y:  f"{y}N" if y>0 else f"{abs(x)}S"
xmin, xmax = -10, 10
ymin, ymax = 45, 55
name = f"{lon(xmin)}-{lon(xmax)}_{lat(ymin)}-{lat(ymax)}"
--> '10W-10E_45N-55N'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks muchly! Incorporated.
Questions (to pay attention to in the new commit):

  • I added that it does not add NS/EW notation if the coordinate is 0. Is this correct? [added a test for this as well; line 223 in test_fetch.py gives an example].
  • Are they in the right order like this?

I liked having a direct relationship between the user input and the file name; that is the only thing I'm sad to lose...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nicely done 👍 . I think I'd prefer 0N and 0E over just plain zeros.

if self.ensemble:
fname += "_ensemble"
if self.statistics:
Expand Down Expand Up @@ -267,6 +279,30 @@ def _check_variable(self, variable):
"Invalid variable name: {}".format(variable)
)

def _check_area(self):
"""Confirm that area parameters are correct."""
(ymax, xmin, ymin, xmax) = self.area
if not (-90 <= ymax <= 90
and -90 <= ymin <= 90
and -180 <= xmin <= 180
and -180 <= xmax <= 180
and ymax > ymin
and xmax != xmin
):
raise ValueError(
"Provide coordinates as ymax xmin ymin xmax. "
"x must be in range -180,+180 and y must be in range -90,+90."
)

def _parse_area(self):
"""Parse area parameters to accepted coordinates."""
self._check_area()
area = [round(coord, ndigits=2) for coord in self.area]
if self.area != area:
Peter9192 marked this conversation as resolved.
Show resolved Hide resolved
print(
f"NB: coordinates {self.area} rounded down to two decimals.\n")
return area

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

Expand Down Expand Up @@ -310,6 +346,9 @@ def _build_request(self, variable, years):
self._check_levels()
request["pressure_level"] = self.pressure_levels

if self.area:
request["area"] = self._parse_area()

product_type = self._product_type()
if product_type is not None:
request["product_type"] = product_type
Expand Down
43 changes: 43 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,49 @@ def test_parse_args():
assert not args.threads
assert args.variables == ['total_precipitation']
assert args.land
assert not args.area


def test_area_argument():
"""Test if area argument is parsed correctly."""
# Test if area arguments are parsed correctly
argv = ['hourly', '--startyear', '2008',
'--variables', 'total_precipitation', '--statistics',
'--endyear', '2008', '--ensemble',
'--area', '90', '-180', '-90', '180']
args = cli._parse_args(argv)
assert args.area == [90, -180, -90, 180]

# Check that area defaults to None
argv = ['hourly', '--startyear', '2008',
'--variables', 'total_precipitation', '--statistics',
'--endyear', '2008', '--ensemble']
args = cli._parse_args(argv)
assert not args.area

# Requires four values
with pytest.raises(SystemExit):
argv = ['hourly', '--startyear', '2008',
'--variables', 'total_precipitation', '--statistics',
'--endyear', '2008', '--ensemble',
'--area', '90', '-180', '-90']
cli._parse_args(argv)

# A value cannot be missing
with pytest.raises(SystemExit):
bvreede marked this conversation as resolved.
Show resolved Hide resolved
argv = ['hourly', '--startyear', '2008',
'--variables', 'total_precipitation', '--statistics',
'--endyear', '2008', '--ensemble',
'--area', '90', '-180', '-90', '']
cli._parse_args(argv)

# Values must be numeric
with pytest.raises(SystemExit):
argv = ['hourly', '--startyear', '2008',
'--variables', 'total_precipitation', '--statistics',
'--endyear', '2008', '--ensemble',
'--area', '90', '-180', '-90', 'E']
cli._parse_args(argv)


def test_period_args():
Expand Down
73 changes: 72 additions & 1 deletion tests/test_fetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@

def initialize(outputformat='netcdf', merge=False, statistics=None,
synoptic=None, ensemble=True, pressurelevels=None,
threads=2, period='hourly', variables=['total_precipitation'],
threads=2, period='hourly', area=None,
variables=['total_precipitation'],
years=[2008, 2009], months=list(range(1, 13)),
days=list(range(1, 32)), hours=list(range(0, 24)),
prelimbe=False, land=False):
Expand All @@ -16,6 +17,7 @@ def initialize(outputformat='netcdf', merge=False, statistics=None,
months=months,
days=days,
hours=hours,
area=area,
variables=variables,
outputformat=outputformat,
outputprefix='era5',
Expand Down Expand Up @@ -201,6 +203,20 @@ def test_define_outputfilename():
fn = 'era5-land_total_precipitation_2008_hourly.nc'
assert fname == fn

era5.area = [90.0, -180.0, -90.0, 180.0]
fname = era5._define_outputfilename('total_precipitation', [2008])
fn = (
'era5-land_total_precipitation_2008_hourly_[90][-180][-90][180].nc'
)
assert fname == fn

era5.area = [90.0, -180.0, -80.999, 170.001]
fname = era5._define_outputfilename('total_precipitation', [2008])
fn = (
'era5-land_total_precipitation_2008_hourly_[90][-180][-80][170].nc'
)
assert fname == fn


def test_number_outputfiles(capsys):
"""Test function for the number of outputs."""
Expand Down Expand Up @@ -520,3 +536,58 @@ def test_more_incompatible_options():
era5 = initialize(statistics=True, ensemble=False)
with pytest.raises(ValueError):
era5._build_request('total_precipitation', [2008])


def test_area():
"""Test that area is parsed properly."""
era5 = initialize()
assert era5.area is None

era5 = initialize(area=[90, -180, -90, 180])
(name, request) = era5._build_request('total_precipitation', [2008])
assert era5.area == [90, -180, -90, 180]
assert request["area"] == [90, -180, -90, 180]

# Decimals are rounded down
era5 = initialize(area=[89.9999, -179.90, -90.0000, 179.012])
(name, request) = era5._build_request('total_precipitation', [2008])
assert request["area"] == [90.0, -179.90, -90.0, 179.01]

# ymax may not be lower than ymin
with pytest.raises(ValueError):
era5 = initialize(area=[-10, -180, 10, 180])
era5._build_request('total_precipitation', [2008])

# xmin higher than xmax should be ok
era5 = initialize(area=[90, 120, -90, -120])
(name, request) = era5._build_request('total_precipitation', [2008])
assert request["area"] == [90.0, 120.0, -90.0, -120.0]

# ymax may not equal ymin
with pytest.raises(ValueError):
era5 = initialize(area=[0, -180, 0, 180])
era5._build_request('total_precipitation', [2008])

# xmin may not equal xmax
with pytest.raises(ValueError):
era5 = initialize(area=[90, 0, -90, 0])
era5._build_request('total_precipitation', [2008])

# ymax, xmin, ymin, xmax may not be out of bounds
with pytest.raises(ValueError):
era5 = initialize(area=[1000, -180, -90, 180])
era5._build_request('total_precipitation', [2008])
with pytest.raises(ValueError):
era5 = initialize(area=[90, 1000, -90, 180])
era5._build_request('total_precipitation', [2008])
with pytest.raises(ValueError):
era5 = initialize(area=[90, -180, 1000, 180])
era5._build_request('total_precipitation', [2008])
with pytest.raises(ValueError):
era5 = initialize(area=[90, -180, -90, 1000])
era5._build_request('total_precipitation', [2008])

# Coordinate missing
with pytest.raises(ValueError):
era5 = initialize(area=[-180, 180, -90])
era5._build_request('total_precipitation', [2008])