Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-reproject for
openSUSE:Factory checked in at 2026-06-28 21:10:57
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-reproject (Old)
and /work/SRC/openSUSE:Factory/.python-reproject.new.11887 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-reproject"
Sun Jun 28 21:10:57 2026 rev:7 rq:1362185 version:0.21.0
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-reproject/python-reproject.changes
2026-06-15 19:54:14.734595254 +0200
+++
/work/SRC/openSUSE:Factory/.python-reproject.new.11887/python-reproject.changes
2026-06-28 21:12:26.362039197 +0200
@@ -1,0 +2,7 @@
+Sun Jun 28 14:45:25 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 0.21.0:
+ * Fixes to HiPS functionality
+ * Give each thread its own copy of the WCS in the dask HiPS
+
+-------------------------------------------------------------------
Old:
----
reproject-0.20.0-gh.tar.gz
New:
----
reproject-0.21.0-gh.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-reproject.spec ++++++
--- /var/tmp/diff_new_pack.DMXtR3/_old 2026-06-28 21:12:27.978093843 +0200
+++ /var/tmp/diff_new_pack.DMXtR3/_new 2026-06-28 21:12:27.978093843 +0200
@@ -26,7 +26,7 @@
%bcond_with test
%endif
Name: python-reproject%{psuffix}
-Version: 0.20.0
+Version: 0.21.0
Release: 0
Summary: Reproject astronomical images
License: BSD-3-Clause
++++++ reproject-0.20.0-gh.tar.gz -> reproject-0.21.0-gh.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/reproject-0.20.0/.github/workflows/ci_workflows.yml
new/reproject-0.21.0/.github/workflows/ci_workflows.yml
--- old/reproject-0.20.0/.github/workflows/ci_workflows.yml 2026-06-10
15:02:18.000000000 +0200
+++ new/reproject-0.21.0/.github/workflows/ci_workflows.yml 2026-06-25
15:20:33.000000000 +0200
@@ -15,7 +15,7 @@
jobs:
tests:
- uses:
OpenAstronomy/github-actions-workflows/.github/workflows/tox.yml@2835f0cacddf3f8de198db9afdb5354a5cebe0ef
# v2.6.3
+ uses:
OpenAstronomy/github-actions-workflows/.github/workflows/tox.yml@4193751d511425d4edc1d5657c24b2128d49b017
# v2.6.4
with:
envs: |
- macos: py311-test-oldestdeps
@@ -38,7 +38,7 @@
build:
needs: tests
- uses:
OpenAstronomy/github-actions-workflows/.github/workflows/publish.yml@2835f0cacddf3f8de198db9afdb5354a5cebe0ef
# v2.6.3
+ uses:
OpenAstronomy/github-actions-workflows/.github/workflows/publish.yml@4193751d511425d4edc1d5657c24b2128d49b017
# v2.6.4
with:
upload_to_pypi: false
save_artifacts: true
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/reproject-0.20.0/.github/workflows/update-changelog.yaml
new/reproject-0.21.0/.github/workflows/update-changelog.yaml
--- old/reproject-0.20.0/.github/workflows/update-changelog.yaml
2026-06-10 15:02:18.000000000 +0200
+++ new/reproject-0.21.0/.github/workflows/update-changelog.yaml
2026-06-25 15:20:33.000000000 +0200
@@ -14,7 +14,7 @@
steps:
- name: Checkout code
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #
v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #
v6.0.3
with:
ref: main
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/reproject-0.20.0/CHANGES.md
new/reproject-0.21.0/CHANGES.md
--- old/reproject-0.20.0/CHANGES.md 2026-06-10 15:02:18.000000000 +0200
+++ new/reproject-0.21.0/CHANGES.md 2026-06-25 15:20:33.000000000 +0200
@@ -1,3 +1,34 @@
+## v0.20.0 - 2026-06-10
+
+<!-- Release notes generated using configuration in .github/release.yml at
main -->
+### What's Changed
+
+#### New Features
+
+* Support 32-bit floats in reproject_adaptive by @svank in
https://github.com/astropy/reproject/pull/588
+
+#### Bug Fixes
+
+* Fix setting shape which is deprecated, and use reshape() instead by
@astrofrog in https://github.com/astropy/reproject/pull/586
+* Improve accuracy for high-resolution images in reproject_exact by @astrofrog
in https://github.com/astropy/reproject/pull/589
+
+#### Other Changes
+
+* Added Python 3.14 CI jobs by @astrofrog in
https://github.com/astropy/reproject/pull/576
+* Add a new Montage accuracy test in ICRS by @astrofrog in
https://github.com/astropy/reproject/pull/590
+* Clean up private/public API by @astrofrog in
https://github.com/astropy/reproject/pull/591
+* Migrate to new astropy-unified theme by @Cadair in
https://github.com/astropy/reproject/pull/581
+* BLD: cleanup manifest template by @neutrinoceros in
https://github.com/astropy/reproject/pull/599
+* Upgrade PyPI CI publishing to use Trusted Publishing by @lmmx in
https://github.com/astropy/reproject/pull/598
+* Enable limited API builds only when building wheels for publishing by
@astrofrog in https://github.com/astropy/reproject/pull/600
+* Fix caplog-related failures in test_reproject_parallel_broadcasting by
@astrofrog in https://github.com/astropy/reproject/pull/602
+
+### New Contributors
+
+* @lmmx made their first contribution in
https://github.com/astropy/reproject/pull/598
+
+**Full Changelog**:
https://github.com/astropy/reproject/compare/v0.19.0...v0.20.0
+
## v0.19.0 - 2025-11-14
<!-- Release notes generated using configuration in .github/release.yml at
main -->
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/reproject-0.20.0/reproject/healpix/tests/test_healpix.py
new/reproject-0.21.0/reproject/healpix/tests/test_healpix.py
--- old/reproject-0.20.0/reproject/healpix/tests/test_healpix.py
2026-06-10 15:02:18.000000000 +0200
+++ new/reproject-0.21.0/reproject/healpix/tests/test_healpix.py
2026-06-25 15:20:33.000000000 +0200
@@ -42,13 +42,15 @@
@pytest.mark.parametrize(
"nside,nested,healpix_system,image_system,dtype,order",
- itertools.product(
- [1, 2, 4, 8, 16, 32, 64],
- [True, False],
- "C",
- "C",
- ALL_DTYPES,
- ["bilinear", "nearest-neighbor"],
+ list(
+ itertools.product(
+ [1, 2, 4, 8, 16, 32, 64],
+ [True, False],
+ ["C"],
+ ["C"],
+ ALL_DTYPES,
+ ["bilinear", "nearest-neighbor"],
+ )
),
)
def test_reproject_healpix_to_image_footprint(
@@ -88,7 +90,11 @@
@pytest.mark.parametrize(
"wcsapi,nside,nested,healpix_system,image_system,dtype",
- itertools.product([True, False], [1, 2, 4, 8, 16, 32, 64], [True, False],
"C", "C", ALL_DTYPES),
+ list(
+ itertools.product(
+ [True, False], [1, 2, 4, 8, 16, 32, 64], [True, False], ["C"],
["C"], ALL_DTYPES
+ )
+ ),
)
def test_reproject_healpix_to_image_round_trip(
wcsapi, nside, nested, healpix_system, image_system, dtype
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/reproject-0.20.0/reproject/hips/_dask_array.py
new/reproject-0.21.0/reproject/hips/_dask_array.py
--- old/reproject-0.20.0/reproject/hips/_dask_array.py 2026-06-10
15:02:18.000000000 +0200
+++ new/reproject-0.21.0/reproject/hips/_dask_array.py 2026-06-25
15:20:33.000000000 +0200
@@ -1,5 +1,7 @@
+import copy
import functools
import os
+import threading
import urllib
import uuid
@@ -65,7 +67,11 @@
# TODO: here need to check consistency, maybe actually don't allow
spectral level to be passed in
self._tile_depth = int(self._properties["hips_tile_depth"])
- self._order_depth = int(self._properties["hips_order_freq"])
+ try:
+ self._order_depth = int(self._properties["hips_order_axis2"])
+ except KeyError:
+ # This property was originally called hips_order_freq, so try
that for backward-compatibility
+ self._order_depth = int(self._properties["hips_order_freq"])
if level_depth is None:
self._level_depth = self._order_depth - (self._order_spatial -
self._level_spatial)
@@ -136,6 +142,22 @@
self._blank = np.broadcast_to(np.nan, self.shape)
+ # astropy.wcs (wcslib) is not thread-safe, so when dask computes chunks
+ # in parallel each thread needs to use its own copy of the WCS.
+ self._local = threading.local()
+
+ @property
+ def _thread_wcs(self):
+ # Return a copy of the WCS that is unique to the calling thread.
+ wcs = getattr(self._local, "wcs", None)
+ if wcs is None:
+ if hasattr(self.wcs, "deepcopy"):
+ wcs = self.wcs.deepcopy()
+ else:
+ wcs = copy.deepcopy(self.wcs)
+ self._local.wcs = wcs
+ return wcs
+
def __getitem__(self, item):
for idx in range(self.ndim):
@@ -156,11 +178,14 @@
# Convert pixel coordinates to HEALPix indices
+ # astropy.wcs is not thread-safe, so use this thread's own WCS copy
+ wcs = self._thread_wcs
+
if self.ndim == 2:
- coord = self.wcs.pixel_to_world(jmid, imid)
+ coord = wcs.pixel_to_world(jmid, imid)
else:
kmid = 0.5 * (item[0].start + item[0].stop)
- coord, spectral_coord =
skycoord_first(self.wcs.pixel_to_world(jmid, imid, kmid))
+ coord, spectral_coord = skycoord_first(wcs.pixel_to_world(jmid,
imid, kmid))
if self._frame_str == "equatorial":
lon, lat = coord.ra.deg, coord.dec.deg
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/reproject-0.20.0/reproject/hips/_high_level.py
new/reproject-0.21.0/reproject/hips/_high_level.py
--- old/reproject-0.20.0/reproject/hips/_high_level.py 2026-06-10
15:02:18.000000000 +0200
+++ new/reproject-0.21.0/reproject/hips/_high_level.py 2026-06-25
15:20:33.000000000 +0200
@@ -4,7 +4,7 @@
import warnings
from concurrent.futures import ThreadPoolExecutor
from copy import deepcopy
-from datetime import datetime
+from datetime import UTC, datetime
from itertools import product
from logging import getLogger
from pathlib import Path
@@ -443,9 +443,9 @@
generated_properties = {
"creator_did": f"ivo://reproject/P/{str(uuid.uuid4())}",
- "obs_title": os.path.dirname(output_directory),
+ "obs_title": os.path.basename(os.path.normpath(output_directory)),
"hips_version": "1.4",
- "hips_release_date": datetime.now().isoformat(),
+ "hips_release_date": datetime.now(UTC).strftime("%Y-%m-%dT%H:%MZ"),
"hips_status": "public master clonableOnce",
"hips_tile_format": tile_format,
"hips_tile_width": tile_size,
@@ -461,7 +461,7 @@
generated_properties["dataproduct_type"] = "image"
else:
generated_properties["dataproduct_type"] = "spectral-cube"
- generated_properties["hips_order_freq"] = level_depth
+ generated_properties["hips_order_axis2"] = level_depth
generated_properties["hips_order_min"] = 0
generated_properties["hips_tile_depth"] = tile_depth
wav = cor_spectralcoord.to_value(u.m)
@@ -469,7 +469,7 @@
generated_properties["em_max"] = wav.max()
if tile_format == "fits":
- generated_properties["hips_pixel_bitpix"] = -64
+ generated_properties["hips_pixel_bitpix"] = -32
if not np.isinf(pixel_min) and not np.isinf(pixel_max):
generated_properties["hips_pixel_cut"] = f"{pixel_min} {pixel_max}"
@@ -541,27 +541,43 @@
if ndim == 3:
half_tile_depth = tile_depth // 2
+ if indices is None:
+ indices = list(
+ find_indices(
+ ndim=ndim,
+ output_directory=output_directory,
+ spatial_level=spatial_level,
+ level_depth=level_depth,
+ )
+ )
+
+ # If no tiles were generated at the deepest level there is nothing to
+ # build lower-resolution tiles from.
+ if len(indices) == 0:
+ warnings.warn(
+ "No tiles were generated at the deepest level, so no "
+ "lower-resolution tiles will be produced",
+ stacklevel=2,
+ )
+ return
+
for sub in range(1, spatial_level + 1):
if ndim == 2:
ilevel = spatial_level - sub
else:
- ilevel = (spatial_level - sub, level_depth - sub)
-
- if indices is None:
- indices = list(
- find_indices(
- ndim=ndim,
- output_directory=output_directory,
- spatial_level=spatial_level,
- level_depth=level_depth,
- )
- )
+ # The spectral order is tied to the spatial order and decreases by
+ # one at each step, but is clamped at 0: once the coarsest spectral
+ # order is reached, lower-resolution tiles only degrade spatially.
+ prev_spectral_level = max(0, level_depth - (sub - 1))
+ spectral_level = max(0, level_depth - sub)
+ spectral_changed = spectral_level != prev_spectral_level
+ ilevel = (spatial_level - sub, spectral_level)
# Find index of tiles to produce at lower-resolution levels
if ndim == 2:
indices = np.sort(np.unique(np.asarray(indices) // 4))
- else:
+ elif spectral_changed:
indices = sorted(
set(
[
@@ -570,6 +586,15 @@
]
)
)
+ else:
+ indices = sorted(
+ set(
+ [
+ (spatial_index // 4, spectral_index)
+ for (spatial_index, spectral_index) in indices
+ ]
+ )
+ )
make_tile_folders(level=ilevel, indices=indices,
output_directory=output_directory)
@@ -627,12 +652,21 @@
array = np.ones((tile_depth, tile_size, tile_size)) * np.nan
+ # When the spectral order is unchanged (clamped at 0) we only
+ # combine the four spatial sub-tiles and reduce spatially.
+ spec_subindices = range(2) if spectral_changed else range(1)
+
for subindex in range(4):
- for subindex_spec in range(2):
+ for subindex_spec in spec_subindices:
+
+ if spectral_changed:
+ current_spectral_index = 2 * index[1] +
subindex_spec
+ else:
+ current_spectral_index = index[1]
- current_index = (4 * index[0] + subindex, 2 * index[1]
+ subindex_spec)
+ current_index = (4 * index[0] + subindex,
current_spectral_index)
subtile_filename = tile_filename(
- level=(ilevel[0] + 1, ilevel[1] + 1),
+ level=(ilevel[0] + 1, prev_spectral_level),
index=current_index,
output_directory=output_directory,
extension=EXTENSION[tile_format],
@@ -647,11 +681,13 @@
fits_getdata_untrimmed(
subtile_filename, tile_size=tile_size,
tile_depth=tile_depth
),
- 2,
+ 2 if spectral_changed else (1, 2, 2),
func=np.nanmean,
)
- if subindex_spec == 0:
+ if not spectral_changed:
+ subtile_slice = [slice(None)]
+ elif subindex_spec == 0:
subtile_slice = [slice(None, half_tile_depth)]
else:
subtile_slice = [slice(half_tile_depth, None)]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/reproject-0.20.0/reproject/hips/_trim_utils.py
new/reproject-0.21.0/reproject/hips/_trim_utils.py
--- old/reproject-0.20.0/reproject/hips/_trim_utils.py 2026-06-10
15:02:18.000000000 +0200
+++ new/reproject-0.21.0/reproject/hips/_trim_utils.py 2026-06-25
15:20:33.000000000 +0200
@@ -19,6 +19,9 @@
imin, imax = indices[0], indices[-1]
slices.append(slice(imin, imax + 1))
fits_axis = 3 - axis
+ # Store the original (untrimmed) size so that a reader can restore
+ # the full tile, and record the size of the leading margin removed.
+ header[f"ONAXIS{fits_axis}"] = array.shape[axis]
header[f"TRIM{fits_axis}"] = imin
header[f"CRPIX{fits_axis}"] -= imin
header[f"NAXIS{fits_axis}"] = imax + 1 - imin
@@ -36,12 +39,20 @@
data = hdulist[0].data
header = hdulist[0].header
+ # Prefer the original tile size recorded at write time (ONAXISn), falling
+ # back to the requested tile dimensions for tiles written without it.
+ onaxis = (
+ int(header.get("ONAXIS3", tile_depth)),
+ int(header.get("ONAXIS2", tile_size)),
+ int(header.get("ONAXIS1", tile_size)),
+ )
+
pad_before = tuple(header.get(f"TRIM{3 - axis}", 0) for axis in range(3))
shape = data.shape
pad_after = (
- tile_depth - shape[0] - pad_before[0],
- tile_size - shape[1] - pad_before[1],
- tile_size - shape[2] - pad_before[2],
+ onaxis[0] - shape[0] - pad_before[0],
+ onaxis[1] - shape[1] - pad_before[1],
+ onaxis[2] - shape[2] - pad_before[2],
)
data = np.pad(
@@ -51,6 +62,6 @@
constant_values=np.nan,
)
- assert data.shape == (tile_depth, tile_size, tile_size)
+ assert data.shape == onaxis
return data
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/reproject-0.20.0/reproject/hips/tests/test_high_level.py
new/reproject-0.21.0/reproject/hips/tests/test_high_level.py
--- old/reproject-0.20.0/reproject/hips/tests/test_high_level.py
2026-06-10 15:02:18.000000000 +0200
+++ new/reproject-0.21.0/reproject/hips/tests/test_high_level.py
2026-06-25 15:20:33.000000000 +0200
@@ -1,15 +1,20 @@
+import os
import re
from math import prod
import numpy as np
import pytest
+from astropy.coordinates import ICRS
+from astropy.io import fits
from astropy.io.fits import Header
from astropy.wcs import WCS
from PIL import Image
from pyavm import AVM
from ... import reproject_interp
-from .._high_level import reproject_to_hips
+from .._high_level import compute_lower_resolution_tiles, reproject_to_hips
+from .._trim_utils import fits_getdata_untrimmed, fits_writeto_withtrim
+from .._utils import load_properties, tile_header_3d
EXPECTED_FILES = [
"Norder0/Dir0/Npix0.fits",
@@ -325,5 +330,164 @@
assert_files_expected(output_directory, EXPECTED_FILES_CUBE)
+def test_properties_regressions(tmp_path, simple_celestial_fits_wcs):
+ # Regression tests for the HiPS properties file:
+ # - obs_title should be the dataset directory name, not its parent
+ # - hips_pixel_bitpix should match the float32 tiles that are written
+ # - hips_release_date should follow the ISO 8601 YYYY-mm-ddTHH:MMZ form
+ output_directory = tmp_path / "some" / "nested" / "my_survey"
+
+ reproject_to_hips(
+ (np.ones((30, 40)), simple_celestial_fits_wcs),
+ coord_system_out="equatorial",
+ level=3,
+ reproject_function=reproject_interp,
+ output_directory=output_directory,
+ )
+
+ properties = load_properties(output_directory)
+
+ assert properties["obs_title"] == "my_survey"
+ assert properties["hips_pixel_bitpix"] == "-32"
+ assert re.fullmatch(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}Z",
properties["hips_release_date"])
+
+ tile = next(output_directory.glob("Norder3/*/*.fits"))
+ assert fits.getheader(tile)["BITPIX"] == -32
+
+
+def test_trimmed_tiles_store_original_size(tmp_path):
+ # Regression test: trimmed 3D tiles must record their original (untrimmed)
+ # size via ONAXIS1/2/3 so that a reader can reconstruct the full tile.
+ tile_size, tile_depth = 16, 8
+ header = tile_header_3d(
+ spatial_level=2,
+ spatial_index=0,
+ spectral_level=2,
+ spectral_index=0,
+ frame=ICRS(),
+ tile_size=tile_size,
+ tile_depth=tile_depth,
+ )
+ if isinstance(header, tuple):
+ header = header[0]
+ crpix_before = (header["CRPIX1"], header["CRPIX2"], header["CRPIX3"])
+
+ array = np.full((tile_depth, tile_size, tile_size), np.nan)
+ array[2:5, 3:10, 4:12] = 1.0 # off-centre block so every axis is trimmed
+
+ filename = str(tmp_path / "tile.fits")
+ fits_writeto_withtrim(filename, array, header)
+
+ written = fits.getheader(filename)
+ # The original full size is preserved in ONAXISn ...
+ assert (written["ONAXIS1"], written["ONAXIS2"], written["ONAXIS3"]) == (
+ tile_size,
+ tile_size,
+ tile_depth,
+ )
+ # ... while NAXISn and the reference pixel reflect the trimmed, stored data
+ assert (written["NAXIS1"], written["NAXIS2"], written["NAXIS3"]) == (8, 7,
3)
+ assert (written["TRIM1"], written["TRIM2"], written["TRIM3"]) == (4, 3, 2)
+ # The reference pixel on each axis is shifted by the leading trim
+ assert written["CRPIX1"] == crpix_before[0] - 4
+ assert written["CRPIX2"] == crpix_before[1] - 3
+ assert written["CRPIX3"] == crpix_before[2] - 2
+
+ # The reader restores the full tile, relying on ONAXISn even without hints
+ restored = fits_getdata_untrimmed(filename, tile_size=1, tile_depth=1)
+ assert restored.shape == (tile_depth, tile_size, tile_size)
+ np.testing.assert_array_equal(np.nan_to_num(restored),
np.nan_to_num(array.astype(np.float32)))
+
+
+CUBE_CLAMP_HEADER = """
+WCSAXES = 3
+CRPIX1 = 16.
+CRPIX2 = 16.
+CRPIX3 = 1.
+CDELT1 = -0.15
+CDELT2 = 0.15
+CDELT3 = {cdelt3}
+CUNIT1 = 'deg'
+CUNIT2 = 'deg'
+CUNIT3 = 'Hz'
+CTYPE1 = 'RA---SIN'
+CTYPE2 = 'DEC--SIN'
+CTYPE3 = 'FREQ-LOG'
+CRVAL1 = 50.
+CRVAL2 = 70.
+CRVAL3 = 1.0E9
+SPECSYS = 'LSRK'
+"""
+
+
+def test_reproject_to_hips3d_clamps_spectral_order(tmp_path):
+ # Regression test: when the spectral (Lmax) order is smaller than the
+ # spatial (Kmax) order, lower-resolution tiles must clamp the spectral
+ # order at 0 (never negative) and keep degrading spatially.
+ #
+ # This needs Lmax < Kmax, which means a low spectral order. Because the
+ # frequency grid spans FREQ_MIN=1e-18 to FREQ_MAX=1e38 Hz, a low order only
+ # arises for a band covering many decades, so we use a coarse spatial grid
+ # (small Kmax) together with a wide, log-sampled spectral axis (small
Lmax).
+ level, level_depth = 4, 1 # Kmax > Lmax forces the clamp
+
+ nz = 8
+ # CDELT3 for the -LOG algorithm so the nz channels span ~4 decades
+ cdelt3 = 1e9 * np.log(10**4) / (nz - 1)
+ header = Header.fromstring(CUBE_CLAMP_HEADER.format(cdelt3=cdelt3),
sep="\n")
+ cube = np.arange(nz * 32 * 32).reshape(nz, 32, 32).astype(float)
+
+ output_directory = tmp_path / "cube"
+
+ reproject_to_hips(
+ (cube, WCS(header)),
+ coord_system_out="equatorial",
+ reproject_function=reproject_interp,
+ output_directory=output_directory,
+ tile_size=16,
+ tile_depth=8,
+ level=level,
+ level_depth=level_depth,
+ )
+
+ pairs = sorted(
+ tuple(int(value) for value in path.name.replace("Norder",
"").split("_"))
+ for path in output_directory.glob("Norder*")
+ )
+
+ # Orders follow L = max(0, Lmax - (Kmax - K)), clamped at 0, down to
spatial 0
+ expected = sorted({(level - sub, max(0, level_depth - sub)) for sub in
range(level + 1)})
+ assert pairs == expected
+ assert all(spectral_order >= 0 for _, spectral_order in pairs)
+ assert min(spatial_order for spatial_order, _ in pairs) == 0
+ assert min(spectral_order for _, spectral_order in pairs) == 0
+
+ # Clamped lower-resolution tiles still carry the propagated data
+ clamped = list(output_directory.glob("Norder0_0/*/*.fits"))
+ assert clamped
+ assert np.isfinite(fits.getdata(clamped[0])).any()
+
+
+def test_compute_lower_resolution_no_tiles_warns(tmp_path):
+ # Regression test: if no tiles were generated at the deepest level, the
+ # lower-resolution step should warn and return rather than raise.
+ output_directory = tmp_path / "empty"
+ os.makedirs(output_directory / "Norder4_2")
+
+ with pytest.warns(UserWarning, match="No tiles were generated"):
+ compute_lower_resolution_tiles(
+ output_directory=output_directory,
+ ndim=3,
+ frame=ICRS(),
+ tile_format="fits",
+ tile_size=8,
+ tile_depth=4,
+ spatial_level=4,
+ level_depth=2,
+ )
+
+ assert list(output_directory.glob("Norder*")) == [output_directory /
"Norder4_2"]
+
+
# TODO: Add tests of different spectral frames
# TODO: Add test of auto level determination for all sky maps in 2D and 3D
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/reproject-0.20.0/reproject/spherical_intersect/tests/test_overlap.py
new/reproject-0.21.0/reproject/spherical_intersect/tests/test_overlap.py
--- old/reproject-0.20.0/reproject/spherical_intersect/tests/test_overlap.py
2026-06-10 15:02:18.000000000 +0200
+++ new/reproject-0.21.0/reproject/spherical_intersect/tests/test_overlap.py
2026-06-25 15:20:33.000000000 +0200
@@ -26,7 +26,7 @@
np.testing.assert_allclose(area_ratio, 1, rtol=1e-7)
[email protected](("clockwise1", "clockwise2"), product([False, True],
[False, True]))
[email protected](("clockwise1", "clockwise2"), list(product([False,
True], [False, True])))
def test_overlap_direction(clockwise1, clockwise2):
# Regression test for a bug that caused the calculation to fail if one or
# both of the polygons were clockwise
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/reproject-0.20.0/reproject/tests/test_high_level.py
new/reproject-0.21.0/reproject/tests/test_high_level.py
--- old/reproject-0.20.0/reproject/tests/test_high_level.py 2026-06-10
15:02:18.000000000 +0200
+++ new/reproject-0.21.0/reproject/tests/test_high_level.py 2026-06-25
15:20:33.000000000 +0200
@@ -122,7 +122,7 @@
@pytest.mark.filterwarnings("ignore::FutureWarning")
[email protected]("projection_type, dtype",
itertools.product(ALL_MODES, ALL_DTYPES))
[email protected]("projection_type, dtype",
list(itertools.product(ALL_MODES, ALL_DTYPES)))
def test_surface_brightness(projection_type, dtype):
header_in = fits.Header.fromstring(INPUT_HDR, sep="\n")
header_in["NAXIS"] = 2