Complex input to the _nanfuncs reductions loses its imaginary part on non-NumPy backends
- Dominant language
- Python
- Stars
- 93
- Forks
- 92
- Avg merge
- 14h 44m
- Merged PRs (30d)
- 30
Description
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:
https://github.com/astropy/ccdproc/blob/9d18599fd2af69115d0a0ac0e373db0606fa6e92/ccdproc/_nanfuncs.py#L73-L100
### Reproducer
```python
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.
Contributor guide
Research direction
Start in ccdproc/_nanfuncs.py at _setup and _promote_to_real, then trace nansum, nanmean, nanstd, nanmedian, nanmad, and median with the supplied reproducer across the listed backends. Decide and document the supported behavior for complex input for each reduction, then add coverage showing the chosen behavior or rejection consistently across backends.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- numpy, python
- Domain
- data
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 45/100