Skip to content

Combiner: no array-API fallback for NaN-aware mean/sum/std (only median has one) #986

Description

@mwcraig

Follow-up to #906 / PR #978, which gave Combiner.median_combine a spec-only NaN-aware median (ccdproc/_nanmedian.py) for namespaces that do not provide nanmedian. Its three siblings did not get the same treatment.

_default_average, _default_sum and _default_std (ccdproc/combiner.py:44-84) all still end in

    try:
        return xp.nanmean      # or xp.nansum / xp.nanstd
    except AttributeError as e:
        raise RuntimeError(
            "No NaN-aware mean function available. Please install bottleneck."
        ) from e

nanmean/nansum/nanstd are not in the array API standard, so on any conforming-but-minimal namespace the average and sum paths raise instead of computing. The error message is also misleading in that situation: installing bottleneck does not help, because _default_* only reaches for bottleneck when the namespace is numpy.

Impact

On main (d85524a) the array-api-strict job reports 99 failures. 23 of them are this bug — the single largest remaining cluster, all in ccdproc/tests/test_combiner.py:

RuntimeError: No NaN-aware mean function available (14)

  • test_combiner_average, test_combiner_mask_average, test_combiner_dtype, test_combiner_3d
  • test_1Dweights, test_combiner_weighted_average_with_mask_by_pixel, test_combiner_weighted_average_with_clipping, test_combiner_weighted_average_preserves_custom_scale_func
  • test_average_combine_uncertainty, test_combiner_uncertainty_average
  • test_combine_result_uncertainty_and_mask[average_combine-True/False]
  • test_writeable_after_combine[average_combine], test_user_supplied_combine_func_that_relies_on_masks[average_combine]

RuntimeError: No NaN-aware sum function available (9)

  • test_combiner_sum, test_combiner_mask_sum, test_combiner_sum_weighted, test_combiner_sum_weighted_by_pixel, test_combiner_sum_weighted_with_mask
  • test_sum_combine_uncertainty, test_combine_result_uncertainty_and_mask[sum_combine-True/False], test_writeable_after_combine[sum_combine]

No failure is currently attributed to _default_std, but that is only because average_combine calls _default_average (line 687) and sum_combine calls _default_sum (line 771) before either reaches _default_std (lines 693 / 774). Fixing mean and sum without std would just move the same RuntimeError a few lines down, so all three need to land together.

What is needed

Three fallbacks in the spirit of _nanmedian.nanmedian, but much simpler — no sort is required, only masking and counting:

  • nansum(x, axis=0)xp.sum(xp.where(xp.isnan(x), 0, x), axis=axis)
  • nanmean(x, axis=0) — the above divided by the count of non-NaN entries
  • nanstd(x, axis=0) — the usual two-pass or sum-of-squares form over the same count, ddof=0

Points to get right, mirroring what nanmedian already had to handle:

  • All-NaN slices. numpy.nansum returns 0.0, while numpy.nanmean and numpy.nanstd return NaN (verified locally; both emit a RuntimeWarning, which the fallbacks should not do since pytest turns warnings into errors here). Confirm bottleneck agrees before relying on it.
  • Dtype promotion. Integer/boolean input should be promoted via xp.__array_namespace_info__().default_dtypes(device=...)["real floating"] rather than a hardcoded float64 — jax without JAX_ENABLE_X64 has no float64 and warns when asked for one.
  • Device. Every scalar built inside the fallback needs device=array_api_compat.device(x); the device-mismatch failures in array-api-strict job: device-mismatch noise from inline array creation in test bodies #946 are exactly what happens when that is missed.
  • Axis. All call sites use axis=0 only (combiner.py:687-705, 771-790), so matching nanmedian's "single integer axis, NotImplementedError otherwise" signature is sufficient.

The error message in the remaining raise paths, if any survive, should stop recommending bottleneck for non-numpy namespaces.

Tests

ccdproc/tests/test_nanmedian.py is the model: exercise the fallbacks directly against the numpy reference across dtypes, all-NaN slices, partially-masked slices and non-default devices, independently of whether the active backend happens to provide native versions.

Notes

Once this lands, the strict job should drop from 99 to roughly 76, and the Combiner failures that remain are the .any()/.nbytes method calls at combiner.py:517 and combiner.py:844-859 plus the astropy.stats densification tracked in #929.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions