Skip to content

Complex input to the _nanfuncs reductions loses its imaginary part on non-NumPy backends #1016

Description

@mwcraig

On every non-NumPy backend, the reductions in ccdproc._nanfuncs cast complex input to the namespace's default real dtype before reducing, so the imaginary part is discarded (jax, dask) or the call is rejected (array-api-strict). NumPy input never sees this path.

Mechanism

_nanfuncs._setup (reached by nansum, nanmean, nanstd, nanmedian, nanmad and median) calls _promote_to_real, which passes only "real floating" dtypes through unchanged and casts everything else, including complex, to the default real floating dtype:

def _promote_to_real(x, xp, device):
"""
Promote integer and boolean ``x`` to the namespace's default real
floating dtype; a real floating ``x``, including float32, passes
through unchanged.
Parameters
----------
x : array
Input array.
xp : array namespace
Namespace to use.
device : device
Device on which to resolve the default real floating dtype.
Returns
-------
array
``x``, promoted if necessary.
"""
if xp.isdtype(x.dtype, "real floating"):
return x
# Promote to the namespace's default real dtype rather than hardcoding
# float64: jax without JAX_ENABLE_X64 has no float64 and warns when one
# is requested, which pytest's filterwarnings turns into an error.
info = xp.__array_namespace_info__()
return xp.astype(x, info.default_dtypes(device=device)["real floating"])

Reproducer

import numpy as np, array_api_compat
from ccdproc import _nanfuncs
c = xp.asarray(np.array([[1+2j, 3+4j], [5+6j, 7+8j]]))   # xp = jax.numpy, compat dask, or array_api_strict
for f in (_nanfuncs.nansum, _nanfuncs.nanmean, _nanfuncs.nanstd, _nanfuncs.nanmedian, _nanfuncs.nanmad):
    print(f.__name__, f(c, axis=0, xp=xp))

Observed (jax with 64-bit mode, and dask; np.nanmean(c, axis=0) gives [3.+4.j, 5.+6.j]):

function jax / dask array-api-strict
nansum [6., 10.] float64, imaginary part gone TypeError: ... casting complex128 to float64 should not be permitted
nanmean [3., 5.] float64 same TypeError
nanstd [2., 2.] float64 same TypeError
nanmedian [3., 5.] float64 same TypeError
nanmad [2., 2.] float64 same TypeError

dask emits a ComplexWarning inside a worker and jax a DeprecationWarning; neither is visible from the calling code as anything other than a routine warning.

Notes for whoever picks this up

Simply letting complex through _promote_to_real is not sufficient on its own. I tried xp.isdtype(x.dtype, ("real floating", "complex floating")) and re-ran the table: nansum and nanmean then match NumPy, but nanstd returns a complex value (2+2j where np.std of the same column is real 2.83), and nanmedian/nanmad raise in sort on array-api-strict (Only real numeric dtypes are allowed in sort) while jax and dask return NumPy's lexicographic-order median. Each reduction needs its own decision about what complex input means, or an explicit TypeError up front.

Impact

CCD data are essentially never complex, so this is low priority; recording it so it is not lost. Found while reviewing #1009, where block_average and block_replicate hit the same helper and are being fixed locally with a guard in ccdproc/_blocks.py rather than by changing _promote_to_real.

History

_promote_to_real was introduced in 6064b8d (#1000, 2026-08-30) as part of the MAD fallback for sigma_func, where integer and boolean promotion was the concern and complex input was not considered.

Versions

ccdproc main at 9d18599; astropy 8.0.1; jax, dask and array-api-strict from the py312-alldeps-jax, py312-alldeps-dask and strict tox environments as of 2026-09-11.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions