Skip to content

Conversation

@ZachHoppinen
Copy link

@ZachHoppinen ZachHoppinen commented Jan 21, 2026

This PR adds in 3 utility functions, rearranges the py-resample ravel/rearrange logic to handle nd array, and adds in tests of the utility functions used on 2d, 3d, and 4d arrays.

This PR address the fact that currently calling geocode on any array with more than 3d dimension fails. #1299

Reminders

  • Fix Geocode 4D dataset #1299
  • Pass Pre-commit check (green)
  • Pass Codacy code review (green)
  • Pass Circle CI test (green)
  • Make sure that your code follows our style. Use the other functions/files as a basis.
  • If modifying functionality, describe changes to function behavior and arguments in a comment below the function declaration.
  • If adding new functionality, add a detailed description to the documentation and/or an example.

Summary by Sourcery

Extend pyresample-based geocoding to support arbitrarily high-dimensional input arrays by generalizing spatial dimension handling and reshaping logic.

New Features:

  • Add utilities to move spatial (row, col) dimensions to/from the front of arrays for resampling workflows.
  • Add utilities to flatten and restore non-spatial dimensions around pyresample calls while preserving original shapes.

Bug Fixes:

  • Fix failure when calling geocode on arrays with more than three dimensions by correctly handling non-spatial dimensions in pyresample resampling.

Enhancements:

  • Guard scipy-based resampling to explicitly reject input arrays with more than three dimensions, directing users to pyresample instead.

Tests:

  • Add unit tests covering the new spatial-dimension and flatten/restore utilities for 2D through 5D arrays, including round-trip and invalid-shape scenarios.

@welcome
Copy link

welcome bot commented Jan 21, 2026

💖 Thanks for opening this pull request! Please check out our contributing guidelines. 💖
Keep in mind that all new features should be documented. It helps to write the comments next to the code or below your functions describing all arguments, and return types before writing the code. This will help you think about your code design and usually results in better code.

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Jan 21, 2026

Reviewer's Guide

Refactors pyresample geocoding to support arbitrary ND input arrays by introducing reusable spatial-dimension utilities, wiring them into run_resample, and adding focused unit tests for the new helpers and shape round‑trips.

Sequence diagram for updated pyresample run_resample ND array handling

sequenceDiagram
    actor Caller
    participant Resample
    participant utils0
    participant PyResample as run_pyresample

    Caller->>Resample: run_resample(src_data, box_ind, print_msg)
    alt software is pyresample
        Resample->>utils0: move_spatial_dimension(src_data, to_front=True)
        utils0-->>Resample: src_data_spatial_first
        Resample->>Resample: non_spatial_shape = src_data_spatial_first.shape[2:]
        Resample->>utils0: flatten_for_resample(src_data_spatial_first)
        utils0-->>Resample: src_data_flat
        Resample->>PyResample: run_pyresample(src_data_flat, **kwargs)
        PyResample-->>Resample: dest_data_flat
        Resample->>Resample: rows, cols = dest_data_flat.shape[:2]
        Resample->>utils0: restore_from_resample(dest_data_flat, rows, cols, non_spatial_shape)
        utils0-->>Resample: dest_data_spatial_first
        Resample->>utils0: move_spatial_dimension(dest_data_spatial_first, to_front=False)
        utils0-->>Resample: dest_data_original_layout
        Resample-->>Caller: dest_data_original_layout
    else software is scipy
        alt src_data ndim > 3
            Resample-->>Caller: raise NotImplementedError
        else src_data ndim <= 3
            Resample->>Resample: run_regular_grid_interpolator(...)
            Resample-->>Caller: dest_data
        end
    end
Loading

Class diagram for updated resample utilities and run_resample workflow

classDiagram
    class Resample {
        +string software
        +string interp_method
        +run_resample(src_data, box_ind, print_msg)
        +run_pyresample(src_data, **kwargs)
        +run_regular_grid_interpolator(src_data, interp_method, fill_value, **kwargs)
    }

    class utils0 {
        +move_spatial_dimension(data, to_front)
        +flatten_for_resample(data)
        +restore_from_resample(data, rows, cols, non_spatial_shape)
    }

    Resample ..> utils0 : uses

    class move_spatial_dimension {
        +np.ndarray move_spatial_dimension(data, to_front)
        -np.ndarray data
        -bool to_front
    }

    class flatten_for_resample {
        +np.ndarray flatten_for_resample(data)
        -np.ndarray data
    }

    class restore_from_resample {
        +np.ndarray restore_from_resample(data, rows, cols, non_spatial_shape)
        -np.ndarray data
        -int rows
        -int cols
        -tuple non_spatial_shape
    }

    utils0 <.. move_spatial_dimension : defines
    utils0 <.. flatten_for_resample : defines
    utils0 <.. restore_from_resample : defines

    Resample ..> move_spatial_dimension : move spatial dims front/back
    Resample ..> flatten_for_resample : flatten nonspatial dims
    Resample ..> restore_from_resample : restore nonspatial dims
Loading

File-Level Changes

Change Details Files
Introduce reusable utilities to move spatial dimensions and flatten/restore non-spatial dimensions for pyresample.
  • Add move_spatial_dimension() to move (row, col) axes between the front and back of arrays with >=2 dimensions using np.moveaxis.
  • Add flatten_for_resample() to assume spatial axes at the front and collapse all trailing non-spatial dimensions into a single dimension, no-op for <=2D.
  • Add restore_from_resample() to reshape resampled data back to the original non-spatial shape given (rows, cols, non_spatial_shape), raising on incompatible sizes.
src/mintpy/utils/utils0.py
Rework pyresample resampling pipeline to use the new utilities and properly handle >3D data while explicitly rejecting >3D for scipy-based resampling.
  • In run_resample(), always move spatial dimensions (rows, cols) from the end to the front before pyresample using move_spatial_dimension().
  • Capture the original non-spatial shape, flatten it into a single trailing dimension via flatten_for_resample(), and after resampling restore the original non-spatial dimensions with restore_from_resample().
  • Move spatial axes back to the end after restoration so the API still returns data shaped like the input, and add a NotImplementedError for scipy-based resampling when src_data has more than 3 dimensions.
src/mintpy/objects/resample.py
Add tests to validate the new utility functions across 2D–5D shapes and round-trip behavior.
  • Add unit tests for flatten_for_resample() on 2D–4D arrays and restore_from_resample() on 2D–4D arrays, including a negative test where the non-spatial shape is inconsistent with the data size.
  • Add tests that exercise move_spatial_dimension() front/back moves and verify round-trip correctness for 2D–5D shapes with both to_front=True and False.
  • Stub out geocoding tests for 3D and 4D data to be implemented later.
tests/test_geocoding.py

Assessment against linked issues

Issue Objective Addressed Explanation
#1299 Enable geocode.py (via the resampling backend) to geocode 4D datasets such as timeseriesDecorCov.h5 without triggering the pyresample 'Mismatch between geometry and dataset' error, and more generally support datasets with >3 dimensions.

Possibly linked issues

  • Geocode 4D dataset #1299: PR generalizes pyresample reshaping so geocode.py can process 4D datasets like timeseriesDecorCov.h5 without errors.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 3 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `src/mintpy/utils/utils0.py:1102-1076` </location>
<code_context>
+    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 #####################################
</code_context>

<issue_to_address>
**suggestion:** Consider simplifying `restore_from_resample` by deriving rows/cols from `data` instead of passing them in

Since callers always compute `rows, cols` from `dest_data.shape[:2]` and `restore_from_resample` only uses them for reshaping, it can instead read `rows, cols = data.shape[:2]` internally and just take `non_spatial_shape`. This reduces the API surface and removes the risk of `rows/cols` drifting from the actual input shape.

Suggested implementation:

```python
def restore_from_resample(data, 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.
    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 non_spatial_shape:
        data = data.reshape(rows, cols, *non_spatial_shape)
    else:
        data = data.reshape(rows, cols)
    return data

```

All call sites of `restore_from_resample` must be updated to match the new signature. Wherever it is currently called like:

`restore_from_resample(data, rows, cols, non_spatial_shape)`

it should be changed to:

`restore_from_resample(data, non_spatial_shape)`

and any now-unused local variables `rows` and `cols` that were only computed for this call can be removed.
</issue_to_address>

### Comment 2
<location> `tests/test_geocoding.py:7-8` </location>
<code_context>
+# 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():
</code_context>

<issue_to_address>
**issue (testing):** Replace placeholder geocode 3D test with an integration test that proves the >2D pyresample path works

Right now this test is just a stub, so it doesn’t actually exercise the 3D geocode/pyresample path described in the PR. Please turn it into an integration-style test that creates a minimal `resample` instance (or uses an existing fixture) configured for `pyresample`, passes in a small 3D array (e.g., `(time, rows, cols)`), runs `run_resample`, and asserts the call succeeds and the output shape is correct for a 3D input. If feasible, also check a simple value-preservation/transform case (e.g., identity LUT) to show the new utility logic is wired correctly and the regression from #1299 is covered.
</issue_to_address>

### Comment 3
<location> `tests/test_geocoding.py:10-8` </location>
<code_context>
+def test_geocode_3d():
+    pass
+
+def test_geocode_4d():
+    pass
+    # res_obj = resample(lut_file=lookupFile,
+    #                 src_file=file,
</code_context>

<issue_to_address>
**issue (testing):** Add a 4D geocode integration test to confirm the new ND handling fixes #1299

Given this PR is meant to fix failures for arrays with >3 dimensions, this is the key scenario to cover end-to-end.

Please implement this test to:
- Build a small 4D input array (e.g. `(time, band, rows, cols)`).
- Run it through `run_resample` using `pyresample`.
- Assert that:
  - No exception is raised.
  - The output shape/layout matches expectations.
  - Non-spatial axes (e.g. time/band) preserve their ordering and are not swapped or collapsed.

Without this, we don’t actually verify that the ND ravel/rearrange logic fixes the original 4D failure (#1299).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@scottstanie
Copy link
Contributor

If you run pre-commit install in your dev environment, you can see what errors will pop up before the CI has to yell

@ZachHoppinen
Copy link
Author

Thanks Scott! That is very helpful in fixing those files. I guess I should have read the readme closer...

@ZachHoppinen
Copy link
Author

I tested this as a workflow using

import numpy as np
from mintpy.utils import attribute as attr, readfile, utils as ut, writefile
from mintpy.objects.resample import resample

infile = '/Users/zmhoppinen/Documents/nga/data/alos/utqiagvik/25/mintpy/2024/timeseriesDecorCov.h5'
dsName = 'timeseries'
src_box = None
data = readfile.read(infile,
                        datasetName=dsName,
                        box=src_box,
                        print_msg=False)[0]

lookupFile = '/Users/zmhoppinen/Documents/nga/data/alos/utqiagvik/25/mintpy/2024/inputs/geometryGeo.h5'
res_obj = resample(lut_file=lookupFile,
                    src_file=infile,
                    SNWE=None,
                    lalo_step=None)
res_obj.open()
res_obj.prepare()

Happy to provide those files for testing if we need to do more testing than the workflow tests run already in the CI/CD.

@ZachHoppinen ZachHoppinen changed the title Convering 3d pyresampling ravel/rearrange logic to handle nd arrays. Converting 3d pyresampling ravel/rearrange logic to handle nd arrays. Jan 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Geocode 4D dataset

2 participants