-
Notifications
You must be signed in to change notification settings - Fork 11
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
Region: explicit W/E/S/N bounds and cyclic longitude support; Longitude class and longitude module #266
Conversation
aospy/test/test_region.py
Outdated
@@ -3,7 +3,7 @@ | |||
import xarray as xr | |||
|
|||
from aospy import Region | |||
from aospy.region import _get_land_mask | |||
from aospy.region import _arr_to_0360_lon, _get_land_mask, _lon_to_0360 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
F401 'aospy.region._arr_to_0360_lon' imported but unused
F401 'aospy.region._lon_to_0360' imported but unused
aospy/test/test_region.py
Outdated
# def test_mask_var_retain_orig_lons(data_for_reg_calcs, lons_orig, | ||
# reg_lon_bounds): | ||
# # Each 'reg_lon_bounds' was chosen so that, with the corresponding | ||
# # 'lons_orig', the result will match the 'expected_data' (for convenience) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
E501 line too long (80 > 79 characters)
Yeah don't edit the the .ipynb file directly :). The easiest thing is to just open it in Jupyter, make the changes, and save it. |
aospy/test/test_region.py
Outdated
@@ -3,7 +3,12 @@ | |||
import xarray as xr | |||
|
|||
from aospy import Region | |||
from aospy.region import _get_land_mask | |||
from aospy.region import ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
F401 'aospy.region._lon_to_0360' imported but unused
F401 'aospy.region._arr_to_0360_lon' imported but unused
The I don't see an obvious way that what I introduced caused this, although this is the first time we're seeing it. I just added a try/except guard to see if that solves it. |
Hmm, now these have been replaced by even less intuitive ones, e.g. here It seems like the example objects within the CI environments have somehow been corrupted. Maybe due to the change in Region call signature? In contrast, these tests are all passing on my local machine. |
I think those were always there -- see L1828-L1873 in the old build for example. The tear down errors were just side effects of the results never being computed/saved to files. Xarray recently released a new version (version 0.10.3); which version are you on locally? |
Good call. I was on 10.2; after updating to 10.3 I can reproduce. I'll open a separate issue to track this. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still need to look at the code, but I read through the docstrings (which are in general very nice!) and noticed a few minor things that I didn't want to forget.
aospy/region.py
Outdated
boundaries are more complicated than a single lat-lon rectangle, | ||
use `mask_bounds` instead. | ||
west_bound, east_bound, south_bound, north_bound : scalar, optional | ||
The western, eastern southern, and northern boundaries, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: add a comma between "eastern" and "southern."
(i.e. it crosses the Prime Meridian for data with longitudes spanning | ||
0 to 360), it must be defined as the union of two sections extending to | ||
the east and to the west of the Prime Meridian. | ||
Note that longitudes spanning (-180, 180), (0, 360), or any other range |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should (0, 360)
be (180, 540)
to correspond with the example after the colon?
aospy/region.py
Outdated
>>> globe = Region(name='globe', lat_bounds=(-90, 90), | ||
... lon_bounds=(0, 360), do_land_mask=False) | ||
>>> globe = Region(name='globe', west_bound=0, east_bound=360, | ||
... south_bound=0, north_bound=360, do_land_mask=False) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm guessing for this example south_bound=-90
and north_bound=90
should be used.
Thanks! FYI I've realized after this commit that the logic I implemented is incomplete (i.e. wrong)...I'm working on a better solution and will ping you when it's ready for a proper review. |
@@ -1,6 +1,6 @@ | |||
"""Subpackage comprising various utility functions used elsewhere in aospy.""" | |||
from . import io | |||
from . import longitude |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
F401 '.longitude' imported but unused
@@ -1,6 +1,6 @@ | |||
"""Subpackage comprising various utility functions used elsewhere in aospy.""" | |||
from . import io | |||
from . import longitude | |||
from .longitude import Longitude |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
F401 '.longitude.Longitude' imported but unused
OK, c.f. #272, I have re-done the logic using the new utils.longitude module and Longitude class. Remaining items to address for my own organizational purposes:
|
C.f. my comment #267 (comment), I think after all that dealing with non-cyclic longitudes is probably outside of the scope of this PR (despite that being partly the original motivation). Frankly, I am almost out of spare cycles for the next few weeks, and I want to get what's here so far in. So we would return to the cyclic points in conjunction with how we treat region boundaries in a future PR. @spencerkclark does that sound OK to you? Sorry that I turned this one into such a long and winding road. Either way, I'd appreciate a review of this PR when you get the chance. Thanks! |
Totally fine! I'll try and give things a review over the next few days. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @spencerahill, this is going to be a big upgrade for our region handling. I have mostly cosmetic comments, the one exception being one on the comparison of Longitude
objects to DataArrays.
aospy/utils/longitude.py
Outdated
if isinstance(other, Longitude): | ||
cond = (self.hemisphere == other.hemisphere and | ||
self.longitude == other.longitude) | ||
if cond: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this if
-else
block is equivalent to simply returning cond
.
aospy/test/test_utils_longitude.py
Outdated
(Longitude(0), Longitude(0)), | ||
(Longitude('0E'), 0), | ||
(Longitude('0E'), 720), | ||
(Longitude('0E'), [0, 720])]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here and for the other operators, should we test arrays on either the right or left side of the operator? For example:
@pytest.mark.parametrize(
('obj1', 'obj2'),
[(Longitude('100W'), Longitude('100W')),
...
(Longitude('0E'), [0, 720]),
([0, 720], Longitude('0E'))])
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would also be good to add test coverage for use with DataArrays.
aospy/utils/longitude.py
Outdated
else: | ||
return False | ||
else: | ||
return np.equal(other, self) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here and in the other operators we need special logic if other
is a DataArray; as written this currently doesn't work:
In [1]: import xarray as xr
In [2]: from longitude import Longitude
In [3]: Longitude(0.) == xr.DataArray([0., 720])
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-3-85150267fe77> in <module>()
----> 1 Longitude(0.) == xr.DataArray([0., 720])
/Users/spencerclark/Downloads/lon_sah2/longitude.py in func_other_to_lon(self, other)
72 def other_to_lon(func):
73 def func_other_to_lon(self, other):
---> 74 return func(self, _maybe_cast_to_lon(other))
75 return func_other_to_lon
76
/Users/spencerclark/Downloads/lon_sah2/longitude.py in __eq__(self, other)
136 return False
137 else:
--> 138 return np.equal(other, self)
139
140 @other_to_lon
TypeError: operand type(s) all returned NotImplemented from __array_ufunc__(<ufunc 'equal'>, '__call__', <xarray.DataArray (dim_0: 2)>
array([ 0., 720.])
Dimensions without coordinates: dim_0, Longitude('0.0E')): 'DataArray', 'Longitude'
I think the following should fix things:
if isinstance(other, xr.DataArray):
return xr.apply_ufunc(np.equal, other, self)
else:
return np.equal(other, self)
def __str__(self): | ||
return 'Geographical region "' + self.name + '"' | ||
|
||
__repr__ = __str__ | ||
def _make_mask(self, data, lon_str=LON_STR, lat_str=LAT_STR): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the reasoning behind having this function take lon_str
and lat_str
as arguments? As part of the data-loading process don't we automatically rename the longitude and latitude coordinates LON_STR
and LAT_STR
? Is it that you are envisioning using these functions outside of aospy
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's right. This gets called by mask_var
, which in turn gets called by ts
and its siblings, which I want to make available to non-aospy data.
aospy/region.py
Outdated
"south, north). Value given: " | ||
"{}".format(rect_bounds)) | ||
else: | ||
lons = _lon_bounds_to_lon_obj(rect_bounds[:2]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even though it's an extra line, I have a slight preference for using tuple unpacking to extract the bounds here before converting to Longitude objects (for clarity):
west_bound, east_bound, south_bound, north_bound = rect_bounds
lons = _lon_bounds_to_lon_obj([west_bound, east_bound])
bounds.append(tuple(lons + [south_bound, north_bound]))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to switch to a namedtuple, which will make this logic cleaner
aospy/test/test_region.py
Outdated
region = Region(name='test', mask_bounds=bounds_in) | ||
assert isinstance(region.mask_bounds, tuple) | ||
assert len(region.mask_bounds) == 2 | ||
for n, bounds in enumerate(region.mask_bounds): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Slight preference for using zip
and tuple unpacking here rather than enumerate
and indexing the bounds_in
list/tuples:
for (west, east, south, north), bounds in zip(bounds_in, region.mask_bounds):
assert isinstance(bounds, tuple)
assert np.all(bounds == (Longitude(west), Longitude(east), south, north))
aospy/test/test_region.py
Outdated
(1, 2, 3)]) | ||
|
||
|
||
# def test_make_mask_latlon_rect(data_for_reg_calcs): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason behind these commented-out tests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, forgot to remove these
@spencerkclark thanks, your comments all look good. Will address + flesh out the docs. |
aospy/region.py
Outdated
SFC_AREA_STR, | ||
YEAR_STR | ||
) | ||
from .utils.longitude import Longitude, _maybe_cast_to_lon |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
F401 '.utils.longitude.Longitude' imported but unused
OK, I think this is ready to go. Will merge once tests pass (EDIT and add a What's New). Thanks @spencerkclark for the invaluable help. |
Closes #229
Still need to implement the cyclic longitudes logic we converged on in #229; the new logic that's currently in
region.py
that touches this will be updated, as will the related tests.Still also need tests and to finish implementation of support for non-aospy data. But wanted to open this PR to get a stake in the ground.
@spencerkclark running the tests on my local machine, the tutorial notebook test failed because I didn't update the
Region
definitions in the notebook to the new call signature. But now I can't remember how we generated the tutorial notebook. Do you recall? I could manipulate the .ipynb file directly, but this seems fraught.