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 optional metadata to Pyresample 2.0 AreaDefinition #464

Merged
merged 25 commits into from
Feb 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
97660d1
Start refactoring future geometry AreaDefinition to support metadata
djhoese Oct 28, 2022
d350168
Fix a few docstrings
djhoese Oct 29, 2022
2fcabc9
Fix docstring references to AreaDefinition
djhoese Oct 29, 2022
545c5c4
Add .attrs to pyresample 2.0 AreaDefinition
djhoese Oct 31, 2022
2869938
Update copyright year in pyresample/test/test_geometry/test_area.py
djhoese Nov 7, 2022
edebe75
Rewrite future area tests to use pytest
djhoese Nov 18, 2022
2a1afb7
Merge branch 'main' into feature-area-metadata
djhoese Jan 27, 2023
c4cc9bd
Port test changes from legacy tests to future area tests
djhoese Jan 27, 2023
da6abd6
Remove 'area_id' as required metadata for future AreaDefinition class
djhoese Jan 27, 2023
d624e3f
Fix future resampler tests for changes to the future AreaDefinition
djhoese Jan 27, 2023
3d3c842
Merge branch 'main' into feature-area-metadata
djhoese Feb 8, 2023
4e90852
Port new geostationary bbox tests to test_area.py module
djhoese Feb 8, 2023
1135ff5
Move some swath tests to test_geometry/test_swath.py
djhoese Feb 8, 2023
f528ca8
Consolidate swath hashing tests and other import cleanups
djhoese Feb 8, 2023
d1c40c6
Consolidate area equality tests
djhoese Feb 8, 2023
97dbf70
More area test cleanup
djhoese Feb 8, 2023
f90e95c
More area test cleanup (get_proj_coords)
djhoese Feb 8, 2023
1e2f40d
Add deprecation note to test_geometry_legacy.py docstring
djhoese Feb 8, 2023
d22502f
More area test cleanup
djhoese Feb 9, 2023
7d8b2b6
More area test cleanup
djhoese Feb 9, 2023
22c3964
More area test cleanup
djhoese Feb 9, 2023
2fb0611
Move primary swath test class to test_geometry/test_swath.py
djhoese Feb 9, 2023
8b2b32b
More swath test cleanup
djhoese Feb 9, 2023
88eeb62
More area test cleanup
djhoese Feb 9, 2023
be2bf83
Move boundary tests to geometry specific test modules
djhoese Feb 10, 2023
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
8 changes: 4 additions & 4 deletions pyresample/area_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def load_area(area_file_name, *regions):

Returns
-------
area_defs : AreaDefinition or list
area_defs : pyresample.geometry.AreaDefinition or list
If one area name is specified a single AreaDefinition object is returned.
If several area names are specified a list of AreaDefinition objects is returned

Expand Down Expand Up @@ -109,7 +109,7 @@ def load_area_from_string(area_strs, *regions):

Returns
-------
area_defs : AreaDefinition or list
area_defs : pyresample.geometry.AreaDefinition or list
If one area name is specified a single AreaDefinition object is returned.
If several area names are specified a list of AreaDefinition objects is returned
"""
Expand Down Expand Up @@ -376,7 +376,7 @@ def get_area_def(area_id, area_name, proj_id, proj4_args, width, height, area_ex
Number of pixel in y dimension
rotation: float
Rotation in degrees (negative is cw)
area_extent : list
area_extent : list | tuple
Area extent as a list of ints (LL_x, LL_y, UR_x, UR_y)

Returns
Expand Down Expand Up @@ -458,7 +458,7 @@ def create_area_def(area_id, projection, width=None, height=None, area_extent=No

Returns
-------
AreaDefinition or DynamicAreaDefinition : AreaDefinition or DynamicAreaDefinition
area : pyresample.geometry.AreaDefinition or pyresample.geometry.DynamicAreaDefinition
If shape and area_extent are found, an AreaDefinition object is returned.
If only shape or area_extent can be found, a DynamicAreaDefinition object is returned

Expand Down
6 changes: 3 additions & 3 deletions pyresample/ewa/ewa.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ def ll2cr(swath_def, area_def, fill=np.nan, copy=True):

Parameters
----------
swath_def : SwathDefinition
swath_def : pyresample.geometry.SwathDefinition
Navigation definition for swath data to remap
area_def : AreaDefinition
area_def : pyresample.geometry.AreaDefinition
Grid definition to be mapped to
fill : float, optional
Fill value used in longitude and latitude arrays
Expand Down Expand Up @@ -95,7 +95,7 @@ def fornav(cols, rows, area_def, data_in,
Column location for each input swath pixel (from `ll2cr`)
rows : numpy array
Row location for each input swath pixel (from `ll2cr`)
area_def : AreaDefinition
area_def : pyresample.geometry.AreaDefinition
Grid definition to be mapped to
data_in : numpy array or tuple of numpy arrays
Swath data to be remapped to output grid
Expand Down
1 change: 1 addition & 0 deletions pyresample/future/geometry/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from typing import Union

from .area import AreaDefinition, DynamicAreaDefinition # noqa
from .base import BaseDefinition # noqa
from .swath import CoordinateDefinition, SwathDefinition # noqa

StaticGeometry = Union[SwathDefinition, CoordinateDefinition, AreaDefinition]
106 changes: 104 additions & 2 deletions pyresample/future/geometry/area.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2021 Pyresample developers
# Copyright (c) 2021-2022 Pyresample developers
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
Expand All @@ -19,4 +19,106 @@

from __future__ import annotations

from pyresample.geometry import AreaDefinition, DynamicAreaDefinition # noqa
from typing import Optional, Union

from pyproj import CRS

from pyresample.geometry import AreaDefinition as LegacyAreaDefinition # noqa
from pyresample.geometry import ( # noqa
DynamicAreaDefinition,
get_full_geostationary_bounding_box_in_proj_coords,
get_geostationary_angle_extent,
get_geostationary_bounding_box_in_lonlats,
get_geostationary_bounding_box_in_proj_coords,
)


class AreaDefinition(LegacyAreaDefinition):
Copy link
Member

Choose a reason for hiding this comment

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

Do we want the new area def to inherit all the methods of the legacy one, or do we want to split responsibility a bit?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah I struggled with this decision. Initially this class in the future subpackage didn't have anything custom about it so it was just a basic import. I think for ease of transitioning to future we might want it to be a subclass so that isinstance checks still work? Or maybe usages of stuff like that should be fixed to be more duck-typing-friendly?

Copy link
Member

Choose a reason for hiding this comment

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

I vote for more duck typing.

Copy link
Member Author

Choose a reason for hiding this comment

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

Please read my other comments. That is the plan, but I don't want to throw that all into one PR and I also don't want to deal with all the tests failing because I didn't implement one of 50 methods on the legacy AreaDefinition class. The plan is to do this work in one of the next 2 or 3 PRs.

"""Uniformly-spaced grid of pixels on a coordinate referenced system.

Args:
crs:
Dictionary of PROJ parameters or string of PROJ or WKT parameters.
Can also be a :class:`pyproj.crs.CRS` object.
width:
x dimension in number of pixels, aka number of grid columns
height:
y dimension in number of pixels, aka number of grid rows
area_extent:
Area extent as a list (lower_left_x, lower_left_y, upper_right_x, upper_right_y)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
Area extent as a list (lower_left_x, lower_left_y, upper_right_x, upper_right_y)
Area extent (in projection coordinates) provided as a list (lower_left_x, lower_left_y, upper_right_x, upper_right_y)

attrs:
Arbitrary metadata related to the area. Some keys in this
dictionary have special meaning and may be used for various
logging or serialization processes. The primary special keys are:

* name: The identifying name for this area. This is equivalent
to 'area_id' in the legacy AreaDefinition class. It is used
in YAML serialization if provided. Defaults to an empty string
if not provided. Not providing this should not affect normal
coordinate operations.
* description: A human-readable description of the area. This is
equivalent to 'description' in the legacy AreaDefinition class.
It is used in YAML serialization if provided.

Attributes:
area_id (str):
Identifier for the area. This is a convenience for backwards
compatibility and accesses the ``.attrs['name']`` metadata.
This will be set to an empty string if not provided.
width (int):
x dimension in number of pixels, aka number of grid columns
height (int):
y dimension in number of pixels, aka number of grid rows
size (int):
Number of points in grid
area_extent_ll (tuple):
Copy link
Contributor

Choose a reason for hiding this comment

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

In the attributes, it should be listed also the area_extent .

Area extent in lons lats as a tuple (lower_left_lon, lower_left_lat, upper_right_lon, upper_right_lat)
pixel_size_x (float):
Pixel width in projection units
pixel_size_y (float):
Pixel height in projection units
upper_left_extent (tuple):
Copy link
Contributor

Choose a reason for hiding this comment

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

upper_left_extent is a misleading name. I would rather call it upper_left_corner. It's used only in AreaDefinition.from_ul_corner btw

Copy link
Member

Choose a reason for hiding this comment

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

extent is the extreme border in all extent definitions I've seen, also outside of Pytroll, so I don't see that as very misleading.

Coordinates (x, y) of upper left corner of upper left pixel in projection units
pixel_upper_left (tuple):
Copy link
Contributor

Choose a reason for hiding this comment

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

Just a thought: maybe pixel_upper_left could be renamed upper_left_centroid (see previous comment)

Coordinates (x, y) of center of upper left pixel in projection units
pixel_offset_x (float):
x offset between projection center and upper left corner of upper
left pixel in units of pixels.
pixel_offset_y (float):
y offset between projection center and upper left corner of upper
left pixel in units of pixels.
crs (CRS):
Coordinate reference system object similar to the PROJ parameters in
`proj_dict` and `proj_str`. This is the preferred attribute to use
when working with the `pyproj` library. Note, however, that this
object is not thread-safe and should not be passed between threads.
attrs (dict):
Arbitrary metadata related to the area.

"""

def __init__(
self,
crs: Union[str, int, dict, CRS],
width: int,
height: int,
area_extent: tuple[float, float, float, float],
attrs: Optional[dict] = None
):
# FUTURE: Maybe do `shape` instead of the separate height and width
mraspaud marked this conversation as resolved.
Show resolved Hide resolved
# FUTURE: Add 'to_legacy()' method
# FUTURE: Add __slots__
# FUTURE: Convert this to new class that uses a legacy area internally
# Use this to more easily deprecate usage of old properties
attrs = attrs or {}
area_id = attrs.get("name", "")
super().__init__(
area_id,
"",
area_id,
crs,
width,
height,
area_extent,
)
self.attrs = attrs
mraspaud marked this conversation as resolved.
Show resolved Hide resolved
17 changes: 17 additions & 0 deletions pyresample/future/geometry/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright (c) 2021-2022 Pyresample developers
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
"""Base geometry definitions and utility functions."""

from pyresample.geometry import BaseDefinition, get_array_hashable # noqa
75 changes: 67 additions & 8 deletions pyresample/test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
CoordinateDefinition,
SwathDefinition,
)
from pyresample.geometry import AreaDefinition as LegacyAreaDefinition
from pyresample.geometry import SwathDefinition as LegacySwathDefinition
from pyresample.test.utils import create_test_latitude, create_test_longitude

SRC_SWATH_2D_SHAPE = (50, 10)
Expand All @@ -36,6 +38,67 @@
DST_AREA_SHAPE = (80, 85)


@pytest.fixture(params=[LegacySwathDefinition, SwathDefinition],
ids=["LegacySwathDefinition", "SwathDefinition"])
def swath_class(request):
"""Get one of the currently active 'SwathDefinition' classes.

Currently only includes the legacy 'SwathDefinition' class and the future
'SwathDefinition' class in 'pyresample.future.geometry.swath'.

"""
return request.param


@pytest.fixture
def create_test_swath(swath_class):
"""Get a function for creating SwathDefinitions for testing.

Should be used as a pytest fixture and will automatically run the test
function with the legacy SwathDefinition class and the future
SwathDefinition class. If tests require a specific class they should
NOT use this fixture and instead use the exact class directly.

"""
def _create_test_swath(lons, lats, **kwargs):
return swath_class(lons, lats, **kwargs)
return _create_test_swath


@pytest.fixture(params=[LegacyAreaDefinition, AreaDefinition],
ids=["LegacyAreaDefinition", "AreaDefinition"])
def area_class(request):
"""Get one of the currently active 'AreaDefinition' classes.

Currently only includes the legacy 'AreaDefinition' class and the future
'AreaDefinition' class in 'pyresample.future.geometry.area'.

"""
return request.param


@pytest.fixture
def create_test_area(area_class):
"""Get a function for creating AreaDefinitions for testing.

Should be used as a pytest fixture and will automatically run the test
function with the legacy AreaDefinition class and the future
AreaDefinition class. If tests require a specific class they should
NOT use this fixture and instead use the exact class directly.

"""
def _create_test_area(crs, width, height, area_extent, **kwargs):
"""Create an AreaDefinition object for testing."""
args = (crs, width, height, area_extent)
if area_class is LegacyAreaDefinition:
attrs = kwargs.pop("attrs", {})
area_id = attrs.pop("name", "test_area")
args = (area_id, "", "") + args
area = area_class(*args, **kwargs)
return area
return _create_test_area


def _euro_lonlats():
lons = create_test_longitude(3.0, 12.0, SRC_SWATH_2D_SHAPE)
lats = create_test_latitude(75.0, 26.0, SRC_SWATH_2D_SHAPE)
Expand Down Expand Up @@ -117,8 +180,7 @@ def area_def_lcc_conus_1km():
"""Create an AreaDefinition with an LCC projection over CONUS (1500, 2000)."""
proj_str = "+proj=lcc +lon_0=-95 +lat_1=35.0 +lat_2=35.0 +datum=WGS84 +no_defs"
crs = CRS.from_string(proj_str)
area_def = AreaDefinition("area_def_lcc_conus", "", "",
crs, SRC_AREA_SHAPE[1], SRC_AREA_SHAPE[0],
area_def = AreaDefinition(crs, SRC_AREA_SHAPE[1], SRC_AREA_SHAPE[0],
mraspaud marked this conversation as resolved.
Show resolved Hide resolved
(-750000, -750000, 750000, 750000))
return area_def

Expand All @@ -131,7 +193,6 @@ def area_def_stere_source():

"""
return AreaDefinition(
'areaD', 'Europe (3km, HRV, VTC)', 'areaD',
{
'a': '6378144.0',
'b': '6356759.0',
Expand All @@ -141,15 +202,14 @@ def area_def_stere_source():
'proj': 'stere'
},
SRC_AREA_SHAPE[1], SRC_AREA_SHAPE[0],
[-1370912.72, -909968.64000000001, 1029087.28, 1490031.3600000001]
(-1370912.72, -909968.64000000001, 1029087.28, 1490031.3600000001),
)


@pytest.fixture(scope="session")
def area_def_stere_target():
"""Create an AreaDefinition with a polar-stereographic projection (800, 850)."""
return AreaDefinition(
'areaD', 'Europe (3km, HRV, VTC)', 'areaD',
{
'a': '6378144.0',
'b': '6356759.0',
Expand All @@ -159,23 +219,22 @@ def area_def_stere_target():
'proj': 'stere'
},
DST_AREA_SHAPE[1], DST_AREA_SHAPE[0],
[-1370912.72, -909968.64000000001, 1029087.28, 1490031.3600000001]
(-1370912.72, -909968.64000000001, 1029087.28, 1490031.3600000001)
)


@pytest.fixture(scope="session")
def area_def_lonlat_pm180_target():
"""Create an AreaDefinition with a geographic lon/lat projection with prime meridian at 180 (800, 850)."""
return AreaDefinition(
'lonlat_pm180', '', '',
{
'proj': 'longlat',
'pm': '180.0',
'datum': 'WGS84',
'no_defs': None,
},
DST_AREA_SHAPE[1], DST_AREA_SHAPE[0],
[-20.0, 20.0, 20.0, 35.0]
(-20.0, 20.0, 20.0, 35.0)
)


Expand Down
Loading