Skip to content

Use reproject for image registration #317

Open
wtbarnes wants to merge 14 commits into
LM-SAL:mainfrom
wtbarnes:reproject-aiaprep
Open

Use reproject for image registration #317
wtbarnes wants to merge 14 commits into
LM-SAL:mainfrom
wtbarnes:reproject-aiaprep

Conversation

@wtbarnes

@wtbarnes wtbarnes commented Nov 1, 2023

Copy link
Copy Markdown
Contributor

Fixes #384

This is a first pass at using the reproject package to perform image registration by constructing a WCS for the aligned, translated, and scaled image and reprojecting the input map into that new WCS. This also now works for submaps. Note that this does not add a dependency as reproject is an optional dependency of sunpy that we already pick up by requiring sunpy[map].

I've marked this as a draft because it still needs quite a bit of cleaning up and I'd like several pairs of eyes on it before we even think about merging (if we do at all).

There is also the issue of performance. This is markedly slower for full-disk images by an OOM.

In [22]: %%timeit
    ...: m_l15_cutout = aiapy.calibrate.prep.register_reproject(m_l1_cutout)
1.6 s ± 15.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [23]: %%timeit
    ...: m_l15 = aiapy.calibrate.prep.register_reproject(m_l1)
33.5 s ± 749 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [24]: %%timeit
    ...: m_l15 = aiapy.calibrate.prep.register(m_l1)
WARNING: SunpyUserWarning: Integer input data has been cast to float64. [sunpy.image.transform]
3.34 s ± 10.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

However, this version does have the advantage that prep on submaps is possible. Additionally, reproject offers several different algorithms for performing the actual reprojection and there are proposed performance improvements.. There is also progress being made on parallelizing the map_coordinates function with Dask which is the underlying scipy function called by reproject_interp. There's also a cupy version of map_coordinates.

All my comments above are assuming that the performance bottleneck is the map_coordinates function, i.e. the actual interpolation of the array onto the new coordinates. While this is almost certainly true, it should be confirmed with some profiling.

Some TODOs:

  • Fix up docstring
  • Fix tests--need a submap test and to remove/modify some of our existing tests.
  • Changelog
  • Quantitative comparisons with our current version of register
  • Comparisons to aiaprep in SSW?
  • Performance...

Summary by Sourcery

Switch AIA image registration to use WCS-based reprojection and make registration compatible with both full-disk maps and submaps while centralizing detector dimension handling.

New Features:

  • Support registering AIA submaps to level 1.5 using WCS-based reprojection.

Enhancements:

  • Replace rotation-based registration with a reprojection-based implementation built on sunpy.map.GenericMap.reproject_to.
  • Introduce a shared detector_dimensions utility and apply it across calibration and PSF code to standardize detector size usage.
  • Adjust PSF deconvolution infrastructure and examples to prefer a JAX-based backend instead of CuPy.

Documentation:

  • Remove the CuPy intersphinx mapping and update PSF deconvolution documentation to reference JAX instead of CuPy.

Tests:

  • Update registration tests to validate shape, WCS metadata, and level for both full-frame and submap inputs, and drop tests tied to the old transform-based implementation.

@astrofrog

Copy link
Copy Markdown

Let me know if you are interested in trying to optimize performance here - there's been a lot of dask-related work in the last couple of years in reproject, and it's now easy to do the reprojection with multi-threading for example.

Comment thread aiapy/calibrate/prep.py Outdated
@nabobalis

Copy link
Copy Markdown
Member

Let me know if you are interested in trying to optimize performance here - there's been a lot of dask-related work in the last couple of years in reproject, and it's now easy to do the reprojection with multi-threading for example.

What is the best way to make it "faster", we have a 4k by 4k image and at least the way the code is written, we do one file at a time. Is providing parallel=True enough?

@astrofrog

Copy link
Copy Markdown

What is the best way to make it "faster", we have a 4k by 4k image and at least the way the code is written, we do one file at a time. Is providing parallel=True enough?

Providing parallel=True is a start, and you can tweak the block size to see what is optimal in your case. What is the input data - dask arrays? FITS files on disk?

@nabobalis

Copy link
Copy Markdown
Member

What is the best way to make it "faster", we have a 4k by 4k image and at least the way the code is written, we do one file at a time. Is providing parallel=True enough?

Providing parallel=True is a start, and you can tweak the block size to see what is optimal in your case. What is the input data - dask arrays? FITS files on disk?

I got

    kwargs["parallel"] = kwargs.get("parallel", True)
    kwargs["block_size"] = kwargs.get("block_size", (1024, 1024))

as the fastest on my local PC.

Since its a sunpy map, its a FITS file read from astropy.io.fits but map does support dask arrays. This function is specifically for 4k by 4k or 5k by 5k 2D images only if that makes much of a difference?

@nabobalis

nabobalis commented Feb 4, 2026

Copy link
Copy Markdown
Member

Worried about optimizing for my own computer but yolo.

@astrofrog

Copy link
Copy Markdown

Could you show me a simple example of reprojection of this data I could run locally? (just in case I have suggestions for speeding up further). I'm always interested in different real-life cases.

@nabobalis

Copy link
Copy Markdown
Member

If you are ok with running the aiapy from this branch:

FITS file: https://jsoc1.stanford.edu/SUM39/D1961191728/S00000/aia.lev1_euv_12s.2026-01-19T235959Z.211.image_lev1.fits

import sunpy.map as sm
import aiapy.calibrate

m_l1 = sm.Map("~/Downloads/aia.lev1_euv_12s.2026-01-19T235959Z.211.image_lev1.fits")
m_l15 = aiapy.calibrate.register(m_l1)

Is how I testing this out.

The WCS its being reprojected too is basically one which is slightly shifted, to account for alignment issues with the telescope.

It is not too dissimilar to the idea in this example from the sunpy gallery: https://docs.sunpy.org/en/stable/generated/gallery/map_transformations/reprojection_align_aia_hmi.html

@nabobalis nabobalis force-pushed the reproject-aiaprep branch 3 times, most recently from d7e1220 to e41759f Compare February 5, 2026 01:50
Comment thread aiapy/calibrate/tests/test_prep.py
@nabobalis nabobalis force-pushed the reproject-aiaprep branch 2 times, most recently from 039a00a to 1e357a5 Compare February 5, 2026 02:14
Comment thread aiapy/calibrate/prep.py Outdated
newmap.meta["lvl_num"] = 1.5
newmap.meta["bitpix"] = -64
return newmap
wcs_l15_full_disk = astropy.wcs.WCS(header_l15_full_disk, preserve_units=True)

@nabobalis nabobalis Feb 6, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So before the PR was doing:

    # Find the bottom left corner of the map in the full-frame WCS
    blc_full_disk = pixel_to_pixel(smap.wcs, wcs_l15_full_disk, 0, 0) * u.pix
    # Calculate distance between full-frame center and bottom left corner
    # This is the location of disk center in the aligned WCS of this map
    ref_pixel = ref_pixel_full_disk - blc_full_disk
    # Construct the L1.5 WCS for this map and reproject
    wcs_l15 = astropy.wcs.WCS(
        make_fitswcs_header(
            smap.data.shape,
            ref_coord,
            reference_pixel=ref_pixel,
            scale=scale,
            rotation_matrix=np.eye(2),
        )
    )

which I have removed but as this code stands, I don't think its doing anything useful without this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The reason I originally implemented it this way is so this works on submaps. As it stands, your implementation will reproject a submap into a 4K by 4K image always. There are two options:

  1. We either need to check if the input map is a submap and raise an exception if it is
  2. restore this codepath so that it works on submaps too.

One of the main reasons I started this PR was to avoid option 1 so I'd suggest 2. If we go with that, we should double check that is the most optimal solution.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

If you need help figuring out submaps feel free to to show me a minimal example

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I have restored it back to how it was but none of the unit tests now pass.

@astrofrog

Copy link
Copy Markdown

@nabobalis - thanks for the MWE! With this example I get:

In [1]: import sunpy.map as sm
   ...: import aiapy.calibrate
   ...: 
   ...: m_l1 = sm.Map("aia.lev1_euv_12s.2026-01-19T235959Z.211.image_lev1.fits")

In [2]: %time m_l15 = aiapy.calibrate.register(m_l1)
2026-02-06 21:10:28 - reproject.common - INFO: Broadcasting is not being used
2026-02-06 21:10:28 - reproject.common - INFO: Not parallelizing along broadcasted dimension (block_size=(1024, 1024), shape_out=(4096, 4096))
2026-02-06 21:10:28 - reproject.common - INFO: Setting up output dask array with map_blocks
2026-02-06 21:10:28 - reproject.common - INFO: Computing output array directly to zarr array at /var/folders/zy/t1l3sx310d3d6p0kyxqzlrnr0000gr/T/tmpdettbo2c/b4640be3-c8d0-4d5e-ad10-56409aad4e37.zarr
2026-02-06 21:10:30 - reproject.common - INFO: Copying output zarr array into output Numpy arrays
CPU times: user 5.82 s, sys: 1.85 s, total: 7.67 s
Wall time: 2.52 s

Just to check, what kind of times are you aiming for ideally?

@nabobalis

nabobalis commented Feb 6, 2026

Copy link
Copy Markdown
Member

There is no time in mind, it's just that we want to avoid it being noticeable slower than the original version which uses affine_transfrom from scipy. The reason for this is that someone might want to call this code on say 1000 images to construct a new cube of data (used for a DEM analysis for example).

At least in my local testing on my PC, it's only 2x slower which for me is good enough. I know that someone will complain to me about it when we release it but yolo.

For comparison, register locally was taking of order ~1.5 seconds from this branch whereas main takes ~0.8 seconds

@wtbarnes wtbarnes left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I can't request changes on my own PR, but consider this review blocking. The biggest issue I see is that as now implemented, this does not work on submaps. I think we also need to be sure we aren't making this dramatically slower.

Comment thread aiapy/calibrate/prep.py Outdated
__all__ = ["correct_degradation", "degradation", "register"]


@add_common_docstring(rotation_function_names=_rotation_function_names)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is no longer relevant

Comment thread aiapy/calibrate/prep.py Outdated
Comment thread aiapy/calibrate/prep.py Outdated
Comment thread aiapy/calibrate/prep.py Outdated
Comment thread aiapy/calibrate/prep.py Outdated
Comment thread aiapy/calibrate/prep.py Outdated
Comment thread aiapy/calibrate/prep.py Outdated
Comment thread aiapy/calibrate/prep.py Outdated
newmap.meta["lvl_num"] = 1.5
newmap.meta["bitpix"] = -64
return newmap
wcs_l15_full_disk = astropy.wcs.WCS(header_l15_full_disk, preserve_units=True)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The reason I originally implemented it this way is so this works on submaps. As it stands, your implementation will reproject a submap into a 4K by 4K image always. There are two options:

  1. We either need to check if the input map is a submap and raise an exception if it is
  2. restore this codepath so that it works on submaps too.

One of the main reasons I started this PR was to avoid option 1 so I'd suggest 2. If we go with that, we should double check that is the most optimal solution.

Comment thread changelog/317.breaking.rst Outdated
@nabobalis nabobalis marked this pull request as ready for review February 10, 2026 17:34
@sourcery-ai

sourcery-ai Bot commented Feb 10, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Reimplements AIA image registration to use WCS-based reprojection via sunpy/reproject, enabling registration of submaps, centralizing detector dimensions, and cleaning up PSF/spike utilities, tests, and documentation to match the new behavior and dependencies.

Sequence diagram for the new WCS-based register image registration

sequenceDiagram
    actor User
    participant ClientCode
    participant AIA_Map as AIAMap_or_HMIMap
    participant Register as register
    participant Utils as detector_dimensions
    participant WCS as Astropy_WCS
    participant Reproject as reproject_to

    User->>ClientCode: call register(smap, missing, algorithm, kwargs)
    ClientCode->>Register: register(smap, missing, algorithm, kwargs)

    Register->>Register: check smap.processing_level
    Register-->>Register: maybe warn AIApyUserWarning

    Register->>Utils: detector_dimensions()
    Utils-->>Register: shape_full_disk

    Register->>WCS: make_fitswcs_header(shape_full_disk, ref_coord=Sun_center, reference_pixel=center_full_frame, scale=0.6arcsec, rotation_matrix=I)
    WCS-->>Register: WCS_L15_full_disk

    Register->>WCS: pixel_to_pixel(smap.wcs, WCS_L15_full_disk, 0, 0)
    WCS-->>Register: blc_full_disk

    Register->>WCS: make_fitswcs_header(smap.shape, ref_coord=Sun_center, reference_pixel=ref_pixel, scale=0.6arcsec, rotation_matrix=I)
    WCS-->>Register: WCS_L15

    Register->>Reproject: smap.reproject_to(WCS_L15, algorithm, return_footprint=False, parallel=True, block_size=(1024,1024), kwargs)
    Reproject-->>Register: smap_L15

    Register->>Register: fill NaN in smap_L15.data with missing

    Register->>Register: new_meta = deepcopy(smap.meta)
    Register->>Register: remove CROTA2 from new_meta
    Register->>Register: new_meta.update(smap_L15.meta)
    Register->>Register: set new_meta[lvl_num] = 1.5

    Register->>AIA_Map: smap._new_instance(data, new_meta, plot_settings=smap.plot_settings)
    AIA_Map-->>Register: registered_map

    Register-->>ClientCode: registered_map
    ClientCode-->>User: return registered_map
Loading

Class diagram for shared detector_dimensions and registration-related utilities

classDiagram
    class aiapy_utils {
    }

    class detector_dimensions {
        +Quantity detector_dimensions()
    }

    class register {
        +register(smap, missing, algorithm, kwargs)
    }

    class fetch_spikes {
        +fetch_spikes(smap, as_coords)
    }

    class _psf {
        +_psf(meshinfo, angles, diffraction_orders, focal_plane)
    }

    class AIAMap_or_HMIMap {
        +wcs
        +meta
        +data
        +coordinate_frame
        +plot_settings
        +processing_level
        +reproject_to(target_wcs, algorithm, return_footprint, parallel, block_size, kwargs)
        +_new_instance(data, meta, plot_settings)
    }

    aiapy_utils <|-- detector_dimensions
    aiapy_utils <|-- register
    aiapy_utils <|-- fetch_spikes
    aiapy_utils <|-- _psf

    register --> detector_dimensions : uses
    fetch_spikes --> detector_dimensions : uses
    _psf --> detector_dimensions : uses

    register --> AIAMap_or_HMIMap : reprojects
    fetch_spikes --> AIAMap_or_HMIMap : analyzes
Loading

File-Level Changes

Change Details Files
Reimplemented register() to build an explicit level-1.5 WCS and call GenericMap.reproject_to, enabling submap support and changing the registration API.
  • Replaced the previous rotate-based implementation with construction of a full-disk WCS using detector_dimensions(), SkyCoord(0,0) as the reference coordinate, 0.6 arcsec/pixel scale, and an identity rotation matrix.
  • Computed the L1.5 reference pixel for the current map by transforming the bottom-left corner into the full-disk WCS and deriving the disk-center pixel position for the current cutout.
  • Reprojected the input map to the new WCS via smap.reproject_to with configurable algorithm and passthrough kwargs, defaulting to interpolation with parallel execution and a fixed block size.
  • Filled NaNs in the reprojected data with a configurable missing value, deep-copied and merged metadata while dropping CROTA2, set lvl_num to 1.5, and returned a new map via _new_instance.
  • Relaxed input requirements by removing AIAMap/HMIMap and full-disk checks, updated the warning stacklevel, and changed the function signature from order/method to algorithm/**kwargs, dropping the cupy/scipy method handling and associated transform module.
aiapy/calibrate/prep.py
Introduced a detector_dimensions() utility and refactored code that assumed hard-coded 4096x4096 shapes to use it.
  • Added detector_dimensions() returning a 4096x4096 pixel Quantity and exported it from aiapy.utils.
  • Updated spike fetching to use detector_dimensions().value when unraveling spike positions instead of a hard-coded shape.
  • Updated PSF generation and tests to derive PSF array dimensions from detector_dimensions() rather than literal 4096 values.
aiapy/utils/utils.py
aiapy/calibrate/spikes.py
aiapy/psf/psf.py
aiapy/psf/tests/test_psf.py
Adapted calibration and PSF tests to the new registration behavior and utilities, including explicit submap registration tests and shape/metadata expectations.
  • Updated register() tests to assert that the output map uses detector_dimensions(), has centered CRPIX values, correct r_sun, level number, bitpix, unit scale, and identity rotation without relying on prior padding/cropping behavior.
  • Added a new test to verify that register() works on submaps, preserving submap shape while enforcing the same WCS and metadata properties as full-disk maps.
  • Adjusted the filesave test to assert the same WCS/metadata invariants after round-tripping a level-1.5 map to disk.
  • Removed tests that enforced AIAMap/HMIMap/full-disk-only constraints and the cupy-backed register() comparison, and changed the PSF deconvolution test to skip when jax (rather than cupy) is missing.
aiapy/calibrate/tests/test_prep.py
aiapy/psf/tests/test_deconvolve.py
Updated shared test fixtures and minor configuration to support submap tests and non-GUI plotting in a more direct way.
  • Made the matplotlib Agg backend selection unconditional at import time, removing the contextlib.suppress wrapper.
  • Changed the aia_171_map fixture to use detector_dimensions() for resampling, and added an aia_171_submap fixture that builds a submap using SkyCoord corners on a resampled map.
aiapy/conftest.py
Aligned documentation, intersphinx configuration, and changelog with the new behavior and dependency story.
  • Removed the intersphinx mapping for cupy from the Sphinx configuration, reflecting the de-emphasis of cupy-specific docs.
  • Updated the PSF deconvolution example text to refer to jax instead of cupy for GPU acceleration.
  • Added breaking-change changelog entries describing the new registration behavior and related API changes.
  • Removed the now-unused calibrate.transform module and its rotation-function-related API surface.
docs/conf.py
examples/psf_deconvolution.py
changelog/317.breaking.1.rst
changelog/317.breaking.rst
aiapy/calibrate/transform.py
aiapy/calibrate/__init__.py

Assessment against linked issues

Issue Objective Addressed Explanation
#384 Modify aiapy.calibrate.register so that for a full-disk AIA image it always returns a data array with the full detector dimensions (4096 x 4096) instead of a slightly smaller array after rotation/scaling.
#384 Update code/tests/documentation to reflect and enforce the guarantee that register returns a consistent full-frame shape instead of warning that it may not be 4096 x 4096.

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

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've found 4 issues, and left some high level feedback:

  • The new register implementation no longer sets r_sun and bitpix in the output metadata (unlike the old version), but the tests now assume these keys are present; consider explicitly restoring them alongside lvl_num to preserve the previous behavior.
  • Resetting the reproject.common logger level to WARNING inside register introduces a global side effect on logging; it would be safer to either respect any existing log level or make this behavior optional/configurable instead of hard-coding it.
  • The previous register implementation enforced AIAMap/HMIMap types and full-disk inputs, while the new one accepts arbitrary maps and submaps; if this widened scope is intentional, it might be worth double-checking that all assumptions in the WCS construction hold for non-SDO maps, or reintroducing some lightweight validation.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `register` implementation no longer sets `r_sun` and `bitpix` in the output metadata (unlike the old version), but the tests now assume these keys are present; consider explicitly restoring them alongside `lvl_num` to preserve the previous behavior.
- Resetting the `reproject.common` logger level to `WARNING` inside `register` introduces a global side effect on logging; it would be safer to either respect any existing log level or make this behavior optional/configurable instead of hard-coding it.
- The previous `register` implementation enforced AIAMap/HMIMap types and full-disk inputs, while the new one accepts arbitrary maps and submaps; if this widened scope is intentional, it might be worth double-checking that all assumptions in the WCS construction hold for non-SDO maps, or reintroducing some lightweight validation.

## Individual Comments

### Comment 1
<location> `aiapy/calibrate/prep.py:98-99` </location>
<code_context>
-    return newmap
+    kwargs["return_footprint"] = kwargs.get("return_footprint", False)
+    # This was selected as the fastest method in local testing
+    kwargs["parallel"] = kwargs.get("parallel", True)
+    kwargs["block_size"] = kwargs.get("block_size", (1024, 1024))
+    # Suppress the reproject INFO logging
+    # TODO: Make this configurable by the user?
</code_context>

<issue_to_address>
**issue (bug_risk):** Unconditionally injecting `parallel` and `block_size` into `reproject_to` kwargs may break algorithms that don’t accept them.

These options are currently added for all algorithms. Some `reproject` backends don’t accept `parallel`/`block_size` and may raise `TypeError`. Consider only passing them for algorithms that support them (e.g. `algorithm == "interpolation"`), or making them opt-in via a dedicated flag so callers can disable them when incompatible.
</issue_to_address>

### Comment 2
<location> `aiapy/calibrate/prep.py:107` </location>
<code_context>
+    smap_l15 = smap.reproject_to(wcs_l15, algorithm=algorithm, **kwargs)
+    # Fill in missing values
+    data = smap_l15.data
+    missing = np.nanmin(smap_l15.data) if missing is None else missing
+    data[np.isnan(data)] = missing
+    # Restore metadata (reproject_to only carries over the WCS keywords)
</code_context>

<issue_to_address>
**issue (bug_risk):** Using `np.nanmin` will fail when the reprojected data are all NaN.

If `smap_l15.data` is entirely NaN (e.g. off-disk cutout, bad WCS, or very tight cropping), `np.nanmin` will raise a `ValueError`. Consider handling the all-NaN case first (e.g. fallback to the original map’s minimum or a configurable default) before calling `nanmin`.
</issue_to_address>

### Comment 3
<location> `aiapy/calibrate/tests/test_prep.py:39-48` </location>
<code_context>
+def test_register_submap(aia_171_submap) -> None:
</code_context>

<issue_to_address>
**suggestion (testing):** Add explicit tests for `missing` handling and NaN filling behaviour in `register`.

The updated `register` now fills NaNs and accepts a `missing` argument, but this isn’t covered by tests. Please add tests that:

* Build a map containing NaNs and call `register` without `missing`; assert (a) the result has no NaNs and (b) the replacement value matches `np.nanmin` of the reprojected data.
* Call `register(..., missing=<sentinel>)` and assert that NaNs in the result are replaced by that sentinel.

This will protect the NaN-handling logic and the `missing` parameter contract from regressions.
</issue_to_address>

### Comment 4
<location> `changelog/317.breaking.1.rst:1` </location>
<code_context>
+Removed the need for cupy from aiapy.
</code_context>

<issue_to_address>
**issue (typo):** Capitalize "CuPy" to match the official project name.

This keeps the changelog aligned with the library’s official naming and branding.
</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.

Comment thread aiapy/calibrate/prep.py Outdated
Comment on lines +98 to +99
kwargs["parallel"] = kwargs.get("parallel", True)
kwargs["block_size"] = kwargs.get("block_size", (1024, 1024))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): Unconditionally injecting parallel and block_size into reproject_to kwargs may break algorithms that don’t accept them.

These options are currently added for all algorithms. Some reproject backends don’t accept parallel/block_size and may raise TypeError. Consider only passing them for algorithms that support them (e.g. algorithm == "interpolation"), or making them opt-in via a dedicated flag so callers can disable them when incompatible.

Comment thread aiapy/calibrate/prep.py
smap_l15 = smap.reproject_to(wcs_l15, algorithm=algorithm, **kwargs)
# Fill in missing values
data = smap_l15.data
missing = np.nanmin(smap_l15.data) if missing is None else missing

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): Using np.nanmin will fail when the reprojected data are all NaN.

If smap_l15.data is entirely NaN (e.g. off-disk cutout, bad WCS, or very tight cropping), np.nanmin will raise a ValueError. Consider handling the all-NaN case first (e.g. fallback to the original map’s minimum or a configurable default) before calling nanmin.

Comment thread aiapy/calibrate/tests/test_prep.py Outdated
Comment on lines +39 to +48
def test_register_submap(aia_171_submap) -> None:
"""
Test that we can register a submap and that the output has the same shape
as the input submap and is correctly aligned.
"""
lvl_15_submap = register(aia_171_submap)
np.testing.assert_array_equal(lvl_15_submap.data.shape, aia_171_submap.data.shape)
assert lvl_15_submap.meta["crpix1"] == lvl_15_submap.data.shape[0] / 2 + 0.5
assert lvl_15_submap.meta["crpix2"] == lvl_15_submap.data.shape[1] / 2 + 0.5
assert lvl_15_submap.meta["r_sun"] == lvl_15_submap.meta["rsun_obs"] / lvl_15_submap.meta["cdelt1"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (testing): Add explicit tests for missing handling and NaN filling behaviour in register.

The updated register now fills NaNs and accepts a missing argument, but this isn’t covered by tests. Please add tests that:

  • Build a map containing NaNs and call register without missing; assert (a) the result has no NaNs and (b) the replacement value matches np.nanmin of the reprojected data.
  • Call register(..., missing=<sentinel>) and assert that NaNs in the result are replaced by that sentinel.

This will protect the NaN-handling logic and the missing parameter contract from regressions.

Comment thread changelog/317.breaking.1.rst Outdated
Comment thread aiapy/calibrate/prep.py Outdated
Comment thread changelog/317.breaking.rst Outdated
@wtbarnes

Copy link
Copy Markdown
Contributor Author

Ok I fixed up the tests a bit to use derived properties and also

  • modified register to just set the reference pixel as the existing reference pixel if the image is not full frame. I suspect this is probably safe in most cases, but does have the consequence that a stack of images all passed to register may not have the same reference pixel. I'm not sure what the alternative is though as the expected for what the reference pixel should be in the case of a cutout passed to this function is ambiguous
  • added a test that asserts submap and register are commutative. this is currently failing and I'm not entirely sure it should pass but I've left it for now.

Co-authored-by: Will Barnes <will.t.barnes@gmail.com>
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.

aiapy.calibrate.register does not return a 4096 by 4096

3 participants