From 3563b7acd120c215bae69d578642230d45384e44 Mon Sep 17 00:00:00 2001 From: ghiggi Date: Fri, 8 Dec 2023 20:06:05 +0100 Subject: [PATCH 1/3] Add BoundarySides class --- pyresample/boundary/__init__.py | 1 + pyresample/boundary/boundary_sides.py | 84 ++++++++++++++ .../test/test_boundary/test_boundary_sides.py | 107 ++++++++++++++++++ 3 files changed, 192 insertions(+) create mode 100644 pyresample/boundary/boundary_sides.py create mode 100644 pyresample/test/test_boundary/test_boundary_sides.py diff --git a/pyresample/boundary/__init__.py b/pyresample/boundary/__init__.py index 258952af..2e420646 100644 --- a/pyresample/boundary/__init__.py +++ b/pyresample/boundary/__init__.py @@ -17,6 +17,7 @@ # along with this program. If not, see . """The Boundary classes.""" +from pyresample.boundary.boundary_sides import BoundarySides # noqa from pyresample.boundary.legacy_boundary import ( # noqa AreaBoundary, AreaDefBoundary, diff --git a/pyresample/boundary/boundary_sides.py b/pyresample/boundary/boundary_sides.py new file mode 100644 index 00000000..9b80d12e --- /dev/null +++ b/pyresample/boundary/boundary_sides.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014-2023 Pyresample developers +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"""Define the BoundarySides class.""" + +import logging + +import numpy as np + +logger = logging.getLogger(__name__) + + +class BoundarySides: + """A class to represent the sides of an area boundary. + The sides are stored as a tuple of 4 numpy arrays, each representing the + coordinate (geographic or projected) of the vertices of the boundary side. + The sides must be stored in the order (top, right, left, bottom), + which refers to the side position with respect to the coordinate array. + The first row of the coordinate array correspond to the top side, the last row to the bottom side, + the first column to the left side and the last column to the right side. + Please note that the last vertex of each side must be equal to the first vertex of the next side. + """ + __slots__ = ['_sides'] + + def __init__(self, sides): + """Initialize the BoundarySides object.""" + if len(sides) != 4 or not all(isinstance(side, np.ndarray) and side.ndim == 1 for side in sides): + raise ValueError("Sides must be a list of four numpy arrays.") + + if not all(np.array_equal(sides[i][-1], sides[(i + 1) % 4][0]) for i in range(4)): + raise ValueError("The last element of each side must be equal to the first element of the next side.") + + self._sides = tuple(sides) # Store as a tuple + + @property + def top(self): + """Return the vertices of the top side.""" + return self._sides[0] + + @property + def right(self): + """Return the vertices of the right side.""" + return self._sides[1] + + @property + def bottom(self): + """Return the vertices of the bottom side.""" + return self._sides[2] + + @property + def left(self): + """Return the vertices of the left side.""" + return self._sides[3] + + @property + def vertices(self): + """Return the vertices of the concatenated sides. + Note that the last element of each side is discarded to avoid duplicates. + """ + return np.concatenate([side[:-1] for side in self._sides]) + + def __iter__(self): + """Return an iterator over the sides.""" + return iter(self._sides) + + def __getitem__(self, index): + """Return the side at the given index.""" + if not isinstance(index, int) or not 0 <= index < 4: + raise IndexError("Index must be an integer from 0 to 3.") + return self._sides[index] \ No newline at end of file diff --git a/pyresample/test/test_boundary/test_boundary_sides.py b/pyresample/test/test_boundary/test_boundary_sides.py new file mode 100644 index 00000000..aed24f98 --- /dev/null +++ b/pyresample/test/test_boundary/test_boundary_sides.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# pyresample, Resampling of remote sensing image data in python +# +# Copyright (C) 2010-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 . +"""Test the BoundarySides objects.""" + +import numpy as np +import pytest + +from pyresample.boundary import BoundarySides + + +class TestBoundarySides: + """Test suite for the BoundarySides class with 1D numpy arrays for sides.""" + + def test_initialization_valid_input(self): + """Test initialization with valid 1D numpy array inputs.""" + sides = [np.array([1, 2, 3]), # top + np.array([3, 4, 5]), # right + np.array([5, 6, 7]), # bottom + np.array([7, 8, 1])] # left + boundary = BoundarySides(sides) + assert all(np.array_equal(boundary[i], sides[i]) for i in range(4)) + + def test_initialization_invalid_input(self): + """Test initialization with invalid inputs, such as wrong number of sides or non-1D arrays.""" + with pytest.raises(ValueError): + BoundarySides([np.array([1, 2]), # Invalid number of sides + np.array([2, 3])]) + + with pytest.raises(ValueError): + BoundarySides([np.array([1, 2]), # Non-1D arrays + np.array([[2, 3], [4, 5]]), + np.array([5, 6]), + np.array([6, 7])]) + + with pytest.raises(ValueError): + BoundarySides([np.array([1, 2]), # Invalid side connection + np.array([3, 4]), + np.array([4, 6]), + np.array([6, 1])]) + + def test_property_accessors(self): + """Test property accessors with 1D numpy arrays.""" + sides = [np.array([1, 2, 3]), # top + np.array([3, 4, 5]), # right + np.array([5, 6, 7]), # bottom + np.array([7, 8, 1])] # left + boundary = BoundarySides(sides) + assert np.array_equal(boundary.top, sides[0]) + assert np.array_equal(boundary.right, sides[1]) + assert np.array_equal(boundary.bottom, sides[2]) + assert np.array_equal(boundary.left, sides[3]) + + def test_vertices_property(self): + """Test the vertices property with concatenated 1D numpy arrays.""" + sides = [np.array([1, 2, 3]), # top + np.array([3, 4, 5]), # right + np.array([5, 6, 7]), # bottom + np.array([7, 8, 1])] # left + boundary = BoundarySides(sides) + expected_vertices = np.array([1, 2, 3, 4, 5, 6, 7, 8]) + assert np.array_equal(boundary.vertices, expected_vertices) + + def test_iteration(self): + """Test iteration over the 1D numpy array sides.""" + sides = [np.array([1, 2, 3]), # top + np.array([3, 4, 5]), # right + np.array([5, 6, 7]), # bottom + np.array([7, 8, 1])] # left + boundary = BoundarySides(sides) + for i, side in enumerate(boundary): + assert np.array_equal(side, sides[i]) + + def test_indexing_valid(self): + """Test valid indexing with 1D numpy arrays.""" + sides = [np.array([1, 2, 3]), # top + np.array([3, 4, 5]), # right + np.array([5, 6, 7]), # bottom + np.array([7, 8, 1])] # left + boundary = BoundarySides(sides) + for i in range(4): + assert np.array_equal(boundary[i], sides[i]) + + def test_indexing_invalid(self): + """Test indexing with invalid indices.""" + sides = [np.array([1, 2, 3]), # top + np.array([3, 4, 5]), # right + np.array([5, 6, 7]), # bottom + np.array([7, 8, 1])] # left + boundary = BoundarySides(sides) + with pytest.raises(IndexError): + boundary[4] # Invalid index \ No newline at end of file From 47e2004a1517a92f92eaf34a9323306351713262 Mon Sep 17 00:00:00 2001 From: ghiggi Date: Fri, 8 Dec 2023 20:08:06 +0100 Subject: [PATCH 2/3] Fix precommit issues --- pyresample/boundary/boundary_sides.py | 4 +++- pyresample/test/test_boundary/test_boundary_sides.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pyresample/boundary/boundary_sides.py b/pyresample/boundary/boundary_sides.py index 9b80d12e..e52499ea 100644 --- a/pyresample/boundary/boundary_sides.py +++ b/pyresample/boundary/boundary_sides.py @@ -26,6 +26,7 @@ class BoundarySides: """A class to represent the sides of an area boundary. + The sides are stored as a tuple of 4 numpy arrays, each representing the coordinate (geographic or projected) of the vertices of the boundary side. The sides must be stored in the order (top, right, left, bottom), @@ -69,6 +70,7 @@ def left(self): @property def vertices(self): """Return the vertices of the concatenated sides. + Note that the last element of each side is discarded to avoid duplicates. """ return np.concatenate([side[:-1] for side in self._sides]) @@ -81,4 +83,4 @@ def __getitem__(self, index): """Return the side at the given index.""" if not isinstance(index, int) or not 0 <= index < 4: raise IndexError("Index must be an integer from 0 to 3.") - return self._sides[index] \ No newline at end of file + return self._sides[index] diff --git a/pyresample/test/test_boundary/test_boundary_sides.py b/pyresample/test/test_boundary/test_boundary_sides.py index aed24f98..58051b5d 100644 --- a/pyresample/test/test_boundary/test_boundary_sides.py +++ b/pyresample/test/test_boundary/test_boundary_sides.py @@ -104,4 +104,4 @@ def test_indexing_invalid(self): np.array([7, 8, 1])] # left boundary = BoundarySides(sides) with pytest.raises(IndexError): - boundary[4] # Invalid index \ No newline at end of file + boundary[4] # Invalid index From a2e9fb18222033f802c4c9c805fceda1ee4d9318 Mon Sep 17 00:00:00 2001 From: Gionata Ghiggi Date: Tue, 12 Dec 2023 09:16:13 +0100 Subject: [PATCH 3/3] Update pyresample/boundary/boundary_sides.py Co-authored-by: David Hoese --- pyresample/boundary/boundary_sides.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyresample/boundary/boundary_sides.py b/pyresample/boundary/boundary_sides.py index e52499ea..9722d90d 100644 --- a/pyresample/boundary/boundary_sides.py +++ b/pyresample/boundary/boundary_sides.py @@ -37,7 +37,7 @@ class BoundarySides: """ __slots__ = ['_sides'] - def __init__(self, sides): + def __init__(self, sides: tuple[ArrayLike, ArrayLike, ArrayLike, ArrayLike]): """Initialize the BoundarySides object.""" if len(sides) != 4 or not all(isinstance(side, np.ndarray) and side.ndim == 1 for side in sides): raise ValueError("Sides must be a list of four numpy arrays.")