From cff324c3ea3fc3c97dbcf44ebb370ffa653e95bf Mon Sep 17 00:00:00 2001 From: Zachary Hoppinen Date: Wed, 21 Jan 2026 13:43:30 -0700 Subject: [PATCH 01/12] Convering 3d pyresampling ravel/rearrange login to handle nd arrays. Add in tests --- .gitignore | 3 + src/mintpy/objects/resample.py | 29 +++-- src/mintpy/utils/utils0.py | 53 ++++++++++ tests/test_geocoding.py | 188 +++++++++++++++++++++++++++++++++ 4 files changed, 266 insertions(+), 7 deletions(-) create mode 100644 tests/test_geocoding.py diff --git a/.gitignore b/.gitignore index ae17ef32a..e5f3a990f 100644 --- a/.gitignore +++ b/.gitignore @@ -133,3 +133,6 @@ dmypy.json # Pyre type checker .pyre/ + +# vscode +.vscode/ \ No newline at end of file diff --git a/src/mintpy/objects/resample.py b/src/mintpy/objects/resample.py index ce7b503af..e80dff8ee 100644 --- a/src/mintpy/objects/resample.py +++ b/src/mintpy/objects/resample.py @@ -205,10 +205,18 @@ def run_resample(self, src_data, box_ind=0, print_msg=True): ) if self.software == 'pyresample': - # move 1st/time dimension to the last + + # Move spatial dimensions (row, col) from the end to the front # so that rows/cols axis are the first, as required by pyresample - if len(src_data.shape) == 3: - src_data = np.moveaxis(src_data, 0, -1) + src_data = ut.move_spatial_dimension(src_data, to_front=True) + + # save non-spatial shape for restoring later + # () for 2d data + non_spatial_shape = src_data.shape[2:] + + # and ravel all non-spatial dimensions to shape (row, col, dn1 * dn2 * ...) + # will return just return data for 2d data + src_data = ut.flatten_for_resample(src_data) # resample source data into target data dest_data = self.run_pyresample( @@ -220,9 +228,14 @@ def run_resample(self, src_data, box_ind=0, print_msg=True): **kwargs, ) - # move 1st/time dimension back - if len(dest_data.shape) == 3: - dest_data = np.moveaxis(dest_data, -1, 0) + rows, cols = dest_data.shape[:2] + + # Restore original non-spatial dimensions + dest_data = ut.restore_from_resample(dest_data, rows, cols, non_spatial_shape) + + # Move spatial dimensions (row, col) from the front back to the end + # Restores (..., rows, cols) for any number of leading dimensions + dest_data = ut.move_spatial_dimension(dest_data, to_front=False) else: vprint(f'{self.interp_method} resampling using scipy.interpolate.RegularGridInterpolator ...') @@ -233,6 +246,8 @@ def run_resample(self, src_data, box_ind=0, print_msg=True): prog_bar.update(i+1) dest_data[i, :, :] = self.run_regular_grid_interpolator(src_data=src_data[i, :, :], **kwargs) prog_bar.close() + elif len(src_data.shape) > 3: + raise NotImplementedError('scipy resampling for >3D data is NOT implemented yet. Use pyresample instead.') else: dest_data = self.run_regular_grid_interpolator(src_data=src_data, **kwargs) @@ -839,4 +854,4 @@ def run_regular_grid_interpolator(self, src_data, interp_method='nearest', fill_ # interpolate output matrix geo_data[self.interp_mask] = interp_func(self.dest_pts) - return geo_data + return geo_data \ No newline at end of file diff --git a/src/mintpy/utils/utils0.py b/src/mintpy/utils/utils0.py index 2d36f0d12..af2116158 100644 --- a/src/mintpy/utils/utils0.py +++ b/src/mintpy/utils/utils0.py @@ -1062,7 +1062,60 @@ def circle_index(atr, circle_par): return idx +def move_spatial_dimension(data, to_front=True): + """Move spatial dimensions (row, col) to front or back for pyresample + + Parameters: + data - np.ndarray, shape (..., rows, cols) or (rows, cols) + to_front - bool + True -> move spatial dims (row, col) to axes (0, 1) + False -> move spatial dims (row, col) back to the end + Returns: data - 2D/3D np.array, output data with moved spatial dimensions + """ + if data.ndim <= 2: + return data + + if to_front: + return np.moveaxis(data, [-2, -1], [0, 1]) + else: + return np.moveaxis(data, [0, 1], [-2, -1]) + +def flatten_for_resample(data): + """ + Flatten non-spatial dimensions for pyresample. + This assumes spatial dimensions are the first two dimensions. + + Parameters: data - np.ndarray, input data with spatial dimensions at the front + + Returns: src_data - np.ndarray, reshaped data with spatial dimensions at the + front and non-spatial dimensions flattened + """ + if data.ndim <= 2: + return data + + rows, cols = data.shape[:2] + # multiple dimensions. We now need to ravel all non-spatial dimensions + data = data.reshape(rows, cols, -1) + return data + +def restore_from_resample(data, rows, cols, non_spatial_shape): + """ + Restore non-spatial dimensions after pyresample. + This assumes spatial dimensions are the first two dimensions. + + Parameters: data - np.ndarray, input data with spatial dimensions at the front + rows - int, number of rows in spatial dimensions + cols - int, number of columns in spatial dimensions + non_spatial_shape - tuple, original shape of non-spatial dimensions + Returns: data - np.ndarray, reshaped data with original non-spatial dimensions + """ + + if len(non_spatial_shape) > 0: + data = data.reshape(rows, cols, *non_spatial_shape) + else: + data = data.reshape(rows, cols) + return data #################################### User Interaction ##################################### def yes_or_no(question): diff --git a/tests/test_geocoding.py b/tests/test_geocoding.py new file mode 100644 index 000000000..74d39dfb1 --- /dev/null +++ b/tests/test_geocoding.py @@ -0,0 +1,188 @@ +import pytest + +import numpy as np +# from mintpy.objects.resample import resample +from mintpy.utils.utils0 import move_spatial_dimension, flatten_for_resample, restore_from_resample + +def test_geocode_3d(): + pass + +def test_geocode_4d(): + pass + # res_obj = resample(lut_file=lookupFile, + # src_file=file, + # SNWE=None, + # lalo_step=None) + # res_obj.open() + # res_obj.prepare() + + # data = np.random.rand(3, 4, 20, 30) + + # res_obj.run_resample(src_data=data, box_ind=0) + +def test_flatten_for_resample_2d(): + data = np.random.rand(20, 30) + + out = flatten_for_resample(data) + + assert out.shape == (20, 30) + np.testing.assert_array_equal(out, data) + +def test_flatten_for_resample_3d(): + data = np.random.rand(20, 30, 5) + + out = flatten_for_resample(data) + + assert out.shape == (20, 30, 5) + np.testing.assert_array_equal(out, data) + +def test_flatten_for_resample_4d(): + data = np.random.rand(20, 30, 5, 4) + + out = flatten_for_resample(data) + + assert out.shape == (20, 30, 20) # 5 * 4 + np.testing.assert_array_equal(out.reshape(20, 30, 5, 4), data) + +def test_restore_from_resample_2d(): + rows, cols = 20, 30 + data = np.random.rand(rows, cols) + + out = restore_from_resample( + data, + rows=rows, + cols=cols, + non_spatial_shape=() + ) + + assert out.shape == (rows, cols) + np.testing.assert_array_equal(out, data) + +def test_restore_from_resample_3d(): + rows, cols = 20, 30 + non_spatial_shape = (5,) + data = np.random.rand(rows, cols, 5) + + out = restore_from_resample( + data, + rows=rows, + cols=cols, + non_spatial_shape=non_spatial_shape + ) + + assert out.shape == (20, 30, 5) + np.testing.assert_array_equal(out, data) + +def test_restore_from_resample_4d(): + rows, cols = 20, 30 + non_spatial_shape = (5, 4) + data = np.random.rand(rows, cols, 20) # 5 * 4 + + out = restore_from_resample( + data, + rows=rows, + cols=cols, + non_spatial_shape=non_spatial_shape + ) + + assert out.shape == (20, 30, 5, 4) + np.testing.assert_array_equal(out.reshape(20, 30, 20), data) + +@pytest.mark.parametrize( + "shape", + [ + (20, 30), # 2D + (20, 30, 5), # 3D + (20, 30, 5, 4), # 4D + (20, 30, 2, 3, 4), # 5D + ], +) +def test_flatten_restore_roundtrip(shape): + data = np.random.rand(*shape) + rows, cols = shape[:2] + non_spatial_shape = shape[2:] + + flat = flatten_for_resample(data) + restored = restore_from_resample( + flat, + rows=rows, + cols=cols, + non_spatial_shape=non_spatial_shape + ) + + np.testing.assert_array_equal(restored, data) + +def test_restore_from_resample_invalid_shape(): + rows, cols = 20, 30 + data = np.random.rand(rows, cols, 10) + + with pytest.raises(ValueError): + restore_from_resample( + data, + rows=rows, + cols=cols, + non_spatial_shape=(3, 4) # 12 != 10 + ) + +def test_move_spatial_dimension_2d_front_and_back(): + # shape: (time, row, col) + arr = np.zeros((20, 30)) + + front = move_spatial_dimension(arr, to_front=True) + assert front.shape == (20, 30) + + back = move_spatial_dimension(front, to_front=False) + assert back.shape == arr.shape + + # ensure data integrity + np.testing.assert_array_equal(back, arr) + +def test_move_spatial_dimension_3d_front_and_back(): + # shape: (time, row, col) + arr = np.zeros((5, 20, 30)) + + front = move_spatial_dimension(arr, to_front=True) + assert front.shape == (20, 30, 5) + + back = move_spatial_dimension(front, to_front=False) + assert back.shape == arr.shape + + # ensure data integrity + np.testing.assert_array_equal(back, arr) + +def test_move_spatial_dimension_4d_front_and_back(): + # shape: (time, band, row, col) + arr = np.random.rand(3, 4, 20, 30) + + front = move_spatial_dimension(arr, to_front=True) + assert front.shape == (20, 30, 3, 4) + + back = move_spatial_dimension(front, to_front=False) + assert back.shape == arr.shape + + np.testing.assert_array_equal(back, arr) + +@pytest.mark.parametrize( + "shape", + [ + (20, 30), # 2D + (20, 30, 5), # 3D + (20, 30, 5, 4), # 4D + (20, 30, 2, 3, 4), # 5D + ], +) +@pytest.mark.parametrize("to_front", [True, False]) +def test_move_spatial_dimension_roundtrip(shape, to_front): + data = np.random.rand(*shape) + + # Move spatial dimensions + moved = move_spatial_dimension(data, to_front=to_front) + + # Invert the move + restored = move_spatial_dimension(moved, to_front=not to_front) + + # Shape should match original + assert restored.shape == data.shape + + # Values should match original exactly + np.testing.assert_array_equal(restored, data) From a823efc27137b9a50a679b92ae589c0703334bf6 Mon Sep 17 00:00:00 2001 From: Zachary Hoppinen Date: Wed, 21 Jan 2026 13:57:35 -0700 Subject: [PATCH 02/12] simply restore_from_resample to just find spatial dims from data isntead of function args. Remove end->end tests that aren't really possible with the path inputs of mintpy --- src/mintpy/objects/resample.py | 4 +--- src/mintpy/utils/utils0.py | 23 +++++++++++++++-------- tests/test_geocoding.py | 16 ---------------- 3 files changed, 16 insertions(+), 27 deletions(-) diff --git a/src/mintpy/objects/resample.py b/src/mintpy/objects/resample.py index e80dff8ee..02059c392 100644 --- a/src/mintpy/objects/resample.py +++ b/src/mintpy/objects/resample.py @@ -227,11 +227,9 @@ def run_resample(self, src_data, box_ind=0, print_msg=True): nprocs=self.nprocs, **kwargs, ) - - rows, cols = dest_data.shape[:2] # Restore original non-spatial dimensions - dest_data = ut.restore_from_resample(dest_data, rows, cols, non_spatial_shape) + dest_data = ut.restore_from_resample(dest_data, non_spatial_shape) # Move spatial dimensions (row, col) from the front back to the end # Restores (..., rows, cols) for any number of leading dimensions diff --git a/src/mintpy/utils/utils0.py b/src/mintpy/utils/utils0.py index af2116158..5ebf59b40 100644 --- a/src/mintpy/utils/utils0.py +++ b/src/mintpy/utils/utils0.py @@ -1099,19 +1099,26 @@ def flatten_for_resample(data): data = data.reshape(rows, cols, -1) return data -def restore_from_resample(data, rows, cols, non_spatial_shape): +def restore_from_resample(data, non_spatial_shape): """ - Restore non-spatial dimensions after pyresample. + Restore non-spatial dimensions after pyresample. This assumes spatial dimensions are the first two dimensions. - Parameters: data - np.ndarray, input data with spatial dimensions at the front - rows - int, number of rows in spatial dimensions - cols - int, number of columns in spatial dimensions - non_spatial_shape - tuple, original shape of non-spatial dimensions - Returns: data - np.ndarray, reshaped data with original non-spatial dimensions + Parameters + ---------- + data : np.ndarray + Input data with spatial dimensions at the front. + non_spatial_shape : tuple + Original shape of non-spatial dimensions. + + Returns + ------- + np.ndarray + Reshaped data with original non-spatial dimensions restored. """ + rows, cols = data.shape[:2] - if len(non_spatial_shape) > 0: + if non_spatial_shape: data = data.reshape(rows, cols, *non_spatial_shape) else: data = data.reshape(rows, cols) diff --git a/tests/test_geocoding.py b/tests/test_geocoding.py index 74d39dfb1..d797a97bf 100644 --- a/tests/test_geocoding.py +++ b/tests/test_geocoding.py @@ -4,22 +4,6 @@ # from mintpy.objects.resample import resample from mintpy.utils.utils0 import move_spatial_dimension, flatten_for_resample, restore_from_resample -def test_geocode_3d(): - pass - -def test_geocode_4d(): - pass - # res_obj = resample(lut_file=lookupFile, - # src_file=file, - # SNWE=None, - # lalo_step=None) - # res_obj.open() - # res_obj.prepare() - - # data = np.random.rand(3, 4, 20, 30) - - # res_obj.run_resample(src_data=data, box_ind=0) - def test_flatten_for_resample_2d(): data = np.random.rand(20, 30) From 52db046f6ab01aaff5ed7b653770c34cec82be15 Mon Sep 17 00:00:00 2001 From: Zachary Hoppinen Date: Wed, 21 Jan 2026 13:59:38 -0700 Subject: [PATCH 03/12] add in single newline for CI/CD end-of-file fixer --- src/mintpy/objects/resample.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mintpy/objects/resample.py b/src/mintpy/objects/resample.py index 02059c392..19fda214a 100644 --- a/src/mintpy/objects/resample.py +++ b/src/mintpy/objects/resample.py @@ -852,4 +852,5 @@ def run_regular_grid_interpolator(self, src_data, interp_method='nearest', fill_ # interpolate output matrix geo_data[self.interp_mask] = interp_func(self.dest_pts) - return geo_data \ No newline at end of file + return geo_data + \ No newline at end of file From 7f1000b17aa7b1547fb8f7b790ea657b69e38bdb Mon Sep 17 00:00:00 2001 From: Zachary Hoppinen Date: Wed, 21 Jan 2026 14:01:07 -0700 Subject: [PATCH 04/12] and another newline space at the end of the .gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index e5f3a990f..b4256e5b7 100644 --- a/.gitignore +++ b/.gitignore @@ -135,4 +135,4 @@ dmypy.json .pyre/ # vscode -.vscode/ \ No newline at end of file +.vscode/ From 7ce13436f538ee417a273b59c066a435dd218511 Mon Sep 17 00:00:00 2001 From: Zachary Hoppinen Date: Wed, 21 Jan 2026 14:02:36 -0700 Subject: [PATCH 05/12] remove tab at end of resample file... --- src/mintpy/objects/resample.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/mintpy/objects/resample.py b/src/mintpy/objects/resample.py index 19fda214a..98938e8c4 100644 --- a/src/mintpy/objects/resample.py +++ b/src/mintpy/objects/resample.py @@ -853,4 +853,3 @@ def run_regular_grid_interpolator(self, src_data, interp_method='nearest', fill_ geo_data[self.interp_mask] = interp_func(self.dest_pts) return geo_data - \ No newline at end of file From 9e21aeb2e7b68835f3c8727f16743514cfd5b4c6 Mon Sep 17 00:00:00 2001 From: Zachary Hoppinen Date: Wed, 21 Jan 2026 14:05:59 -0700 Subject: [PATCH 06/12] remove row/col key words from all tests --- tests/test_geocoding.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/tests/test_geocoding.py b/tests/test_geocoding.py index d797a97bf..71f858007 100644 --- a/tests/test_geocoding.py +++ b/tests/test_geocoding.py @@ -34,8 +34,6 @@ def test_restore_from_resample_2d(): out = restore_from_resample( data, - rows=rows, - cols=cols, non_spatial_shape=() ) @@ -49,8 +47,6 @@ def test_restore_from_resample_3d(): out = restore_from_resample( data, - rows=rows, - cols=cols, non_spatial_shape=non_spatial_shape ) @@ -64,8 +60,6 @@ def test_restore_from_resample_4d(): out = restore_from_resample( data, - rows=rows, - cols=cols, non_spatial_shape=non_spatial_shape ) @@ -83,14 +77,11 @@ def test_restore_from_resample_4d(): ) def test_flatten_restore_roundtrip(shape): data = np.random.rand(*shape) - rows, cols = shape[:2] non_spatial_shape = shape[2:] flat = flatten_for_resample(data) restored = restore_from_resample( flat, - rows=rows, - cols=cols, non_spatial_shape=non_spatial_shape ) @@ -103,8 +94,6 @@ def test_restore_from_resample_invalid_shape(): with pytest.raises(ValueError): restore_from_resample( data, - rows=rows, - cols=cols, non_spatial_shape=(3, 4) # 12 != 10 ) From 84cbecd684ef4f62545c35396138305c45e949bb Mon Sep 17 00:00:00 2001 From: Zachary Hoppinen Date: Thu, 22 Jan 2026 10:19:39 -0700 Subject: [PATCH 07/12] remove trailing white space in resample.py --- src/mintpy/objects/resample.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mintpy/objects/resample.py b/src/mintpy/objects/resample.py index 98938e8c4..156dc384b 100644 --- a/src/mintpy/objects/resample.py +++ b/src/mintpy/objects/resample.py @@ -209,7 +209,7 @@ def run_resample(self, src_data, box_ind=0, print_msg=True): # Move spatial dimensions (row, col) from the end to the front # so that rows/cols axis are the first, as required by pyresample src_data = ut.move_spatial_dimension(src_data, to_front=True) - + # save non-spatial shape for restoring later # () for 2d data non_spatial_shape = src_data.shape[2:] From 1c94e90e05967d1ca24565eadf1dce846cec5698 Mon Sep 17 00:00:00 2001 From: Zachary Hoppinen Date: Thu, 22 Jan 2026 10:23:58 -0700 Subject: [PATCH 08/12] cleaning remaining trailing white spaces --- src/mintpy/objects/resample.py | 2 +- src/mintpy/utils/utils0.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mintpy/objects/resample.py b/src/mintpy/objects/resample.py index 156dc384b..ef1debe29 100644 --- a/src/mintpy/objects/resample.py +++ b/src/mintpy/objects/resample.py @@ -227,7 +227,7 @@ def run_resample(self, src_data, box_ind=0, print_msg=True): nprocs=self.nprocs, **kwargs, ) - + # Restore original non-spatial dimensions dest_data = ut.restore_from_resample(dest_data, non_spatial_shape) diff --git a/src/mintpy/utils/utils0.py b/src/mintpy/utils/utils0.py index 5ebf59b40..7d35bb42d 100644 --- a/src/mintpy/utils/utils0.py +++ b/src/mintpy/utils/utils0.py @@ -1082,11 +1082,11 @@ def move_spatial_dimension(data, to_front=True): def flatten_for_resample(data): """ - Flatten non-spatial dimensions for pyresample. + Flatten non-spatial dimensions for pyresample. This assumes spatial dimensions are the first two dimensions. Parameters: data - np.ndarray, input data with spatial dimensions at the front - + Returns: src_data - np.ndarray, reshaped data with spatial dimensions at the front and non-spatial dimensions flattened """ From a8412c1a86077b68511b990338ed1dcf21106fbc Mon Sep 17 00:00:00 2001 From: Zachary Hoppinen Date: Thu, 22 Jan 2026 10:37:08 -0700 Subject: [PATCH 09/12] remove unused import in tests and sorting --- tests/test_geocoding.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_geocoding.py b/tests/test_geocoding.py index 71f858007..b64c30cd1 100644 --- a/tests/test_geocoding.py +++ b/tests/test_geocoding.py @@ -1,8 +1,7 @@ import pytest -import numpy as np -# from mintpy.objects.resample import resample from mintpy.utils.utils0 import move_spatial_dimension, flatten_for_resample, restore_from_resample +import numpy as np def test_flatten_for_resample_2d(): data = np.random.rand(20, 30) From 354dd633f830d6159a9f9c43eacc9808e5ac5060 Mon Sep 17 00:00:00 2001 From: Zachary Hoppinen Date: Thu, 22 Jan 2026 11:38:57 -0700 Subject: [PATCH 10/12] sorting imports in tests.... --- tests/test_geocoding.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/test_geocoding.py b/tests/test_geocoding.py index b64c30cd1..a19b5c744 100644 --- a/tests/test_geocoding.py +++ b/tests/test_geocoding.py @@ -1,7 +1,11 @@ +import numpy as np import pytest -from mintpy.utils.utils0 import move_spatial_dimension, flatten_for_resample, restore_from_resample -import numpy as np +from mintpy.utils.utils0 import ( + flatten_for_resample, + move_spatial_dimension, + restore_from_resample, +) def test_flatten_for_resample_2d(): data = np.random.rand(20, 30) From d9117d0ce303ed79d8112ac094f1ea9dcece6950 Mon Sep 17 00:00:00 2001 From: Zachary Hoppinen Date: Thu, 22 Jan 2026 11:44:32 -0700 Subject: [PATCH 11/12] using scott's advice to fix pre-commit errors of extra new line in imports --- tests/test_geocoding.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_geocoding.py b/tests/test_geocoding.py index a19b5c744..eebf4adab 100644 --- a/tests/test_geocoding.py +++ b/tests/test_geocoding.py @@ -7,6 +7,7 @@ restore_from_resample, ) + def test_flatten_for_resample_2d(): data = np.random.rand(20, 30) From 613f9e17b5216955063e01ec15213c19b00b1406 Mon Sep 17 00:00:00 2001 From: Zachary Hoppinen Date: Thu, 22 Jan 2026 12:35:06 -0700 Subject: [PATCH 12/12] remove changes to .gitignore --- .gitignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index b4256e5b7..ae17ef32a 100644 --- a/.gitignore +++ b/.gitignore @@ -133,6 +133,3 @@ dmypy.json # Pyre type checker .pyre/ - -# vscode -.vscode/