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.
On every non-NumPy backend, the reductions in
ccdproc._nanfuncscast 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 bynansum,nanmean,nanstd,nanmedian,nanmadandmedian) calls_promote_to_real, which passes only"real floating"dtypes through unchanged and casts everything else, including complex, to the default real floating dtype:ccdproc/ccdproc/_nanfuncs.py
Lines 73 to 100 in 9d18599
Reproducer
Observed (jax with 64-bit mode, and dask;
np.nanmean(c, axis=0)gives[3.+4.j, 5.+6.j]):nansum[6., 10.]float64, imaginary part goneTypeError: ... casting complex128 to float64 should not be permittednanmean[3., 5.]float64TypeErrornanstd[2., 2.]float64TypeErrornanmedian[3., 5.]float64TypeErrornanmad[2., 2.]float64TypeErrordask emits a
ComplexWarninginside a worker and jax aDeprecationWarning; 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_realis not sufficient on its own. I triedxp.isdtype(x.dtype, ("real floating", "complex floating"))and re-ran the table:nansumandnanmeanthen match NumPy, butnanstdreturns a complex value (2+2jwherenp.stdof the same column is real2.83), andnanmedian/nanmadraise insorton 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 explicitTypeErrorup 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_averageandblock_replicatehit the same helper and are being fixed locally with a guard inccdproc/_blocks.pyrather than by changing_promote_to_real.History
_promote_to_realwas introduced in 6064b8d (#1000, 2026-08-30) as part of the MAD fallback forsigma_func, where integer and boolean promotion was the concern and complex input was not considered.Versions
ccdproc
mainat 9d18599; astropy 8.0.1; jax, dask and array-api-strict from thepy312-alldeps-jax,py312-alldeps-daskandstricttox environments as of 2026-09-11.