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

Reply via email to