Source code for photutils.aperture.flags

# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
Tools for working with aperture photometry and statistics flags,
including centralized flag definitions and decoding utilities.
"""

from typing import ClassVar

import numpy as np

from photutils.utils._flags import (FlagDefinition, FlagRegistry, decode_flags,
                                    update_flag_docstring)

__all__ = ['APERTURE_FLAGS', 'decode_aperture_flags']


class _ApertureFlags(FlagRegistry):
    """
    Centralized definition of aperture photometry and statistics
    flags.

    This class provides a single source of truth for all
    aperture flag definitions, including bit values, names,
    and descriptions. The same flag definitions are used
    by `~photutils.aperture.PixelAperture._photometry`,
    `~photutils.aperture.AperturePhotometry`, and
    `~photutils.aperture.ApertureStats`, so a given bit always has the
    same meaning.

    Examples
    --------
    >>> from photutils.aperture.flags import _ApertureFlags
    >>> flags = _ApertureFlags()
    >>> flags.NO_OVERLAP
    1
    >>> flags.get_name(1)
    'no_overlap'
    >>> flags.get_description(8)
    'masked pixels within the aperture'
    """

    # Define all aperture flags with their properties
    FLAG_DEFINITIONS: ClassVar = [
        FlagDefinition(
            bit_value=1,
            name='no_overlap',
            description='aperture fully outside the data',
            detailed_description=('The aperture is fully outside the '
                                  'data array: no pixel with nonzero '
                                  'aperture weight falls inside the '
                                  'data.'),
        ),
        FlagDefinition(
            bit_value=2,
            name='partial_overlap',
            description='aperture partially outside the data',
            detailed_description=('The aperture is partially outside '
                                  'the data array: one or more pixels '
                                  'with nonzero aperture weight fall '
                                  'outside the data.'),
        ),
        FlagDefinition(
            bit_value=4,
            name='no_pixels',
            description='no aperture pixels within the data',
            detailed_description=('The aperture contains zero pixels '
                                  'with nonzero weight inside the data, '
                                  'e.g., a fully off-image aperture or '
                                  'a tiny aperture that contains no '
                                  'pixel (or subpixel) centers with '
                                  'the "center" or "subpixel" methods.'),
        ),
        FlagDefinition(
            bit_value=8,
            name='masked_pixels',
            description='masked pixels within the aperture',
            detailed_description=('One or more input-masked pixels '
                                  '(``mask`` keyword) have nonzero '
                                  'aperture weight.'),
        ),
        FlagDefinition(
            bit_value=16,
            name='all_masked',
            description='no valid pixels within the aperture',
            detailed_description=('The aperture contains pixels, but '
                                  'none are valid: every nonzero-weight '
                                  'pixel inside the data is masked, '
                                  'non-finite, or excluded by '
                                  'segmentation masking.'),
        ),
        FlagDefinition(
            bit_value=32,
            name='non_finite_data',
            description='non-finite data values within the aperture',
            detailed_description=('One or more unmasked data values '
                                  '(NaN or inf) with nonzero aperture '
                                  'weight are non-finite.'),
        ),
        FlagDefinition(
            bit_value=64,
            name='non_finite_error',
            description='non-finite error values within the aperture',
            detailed_description=('One or more unmasked error values '
                                  '(NaN or inf) with nonzero aperture '
                                  'weight are non-finite.'),
        ),
        FlagDefinition(
            bit_value=128,
            name='neighbor_pixels',
            description='segmentation-masked pixels within the aperture',
            detailed_description=('One or more pixels within the '
                                  'aperture were excluded, restricted, '
                                  'or corrected due to neighboring '
                                  'sources in the segmentation image.'),
        ),
        FlagDefinition(
            bit_value=256,
            name='uncorrected_pixels',
            description='uncorrectable neighbor pixels within the aperture',
            detailed_description=('With ``mask_method="correct"``, one '
                                  'or more neighbor-source pixels could '
                                  'not be corrected (the mirror pixel '
                                  'was unavailable) and were excluded '
                                  'instead.'),
        ),
        FlagDefinition(
            bit_value=512,
            name='sigma_clipped',
            description='sigma-clipped pixels within the aperture',
            detailed_description=('One or more pixels within the '
                                  'aperture were rejected by sigma '
                                  'clipping.'),
        ),
        FlagDefinition(
            bit_value=1024,
            name='all_clipped',
            description='all pixels within the aperture were sigma clipped',
            detailed_description=('All valid pixels within the aperture '
                                  'were rejected by sigma clipping.'),
        ),
        FlagDefinition(
            bit_value=2048,
            name='too_few_pixels',
            description='too few pixels to compute a statistic',
            detailed_description=('There are too few valid pixels '
                                  'within the aperture to compute a '
                                  'requested statistic (e.g., the '
                                  'variance and standard deviation are '
                                  'undefined when the number of valid '
                                  'pixels is not larger than ``ddof``).'),
        ),
        FlagDefinition(
            bit_value=4096,
            name='singular_covariance',
            description='singular or nearly singular source covariance',
            detailed_description=('The source covariance matrix is '
                                  'singular or nearly singular (the '
                                  'minor-axis variance is below '
                                  '``1/12``, the variance of a uniform '
                                  'distribution across a single pixel), '
                                  'so covariance-derived shape '
                                  'properties (e.g., '
                                  '``semimajor_axis``, ``orientation``, '
                                  '``eccentricity``) are ill-defined '
                                  'and have been regularized or set to '
                                  'NaN. This is a stats-only flag that '
                                  'is set only when a covariance-derived '
                                  'property has been computed.'),
        ),
    ]

    domain: ClassVar = 'aperture'


# Create a singleton instance for global use
APERTURE_FLAGS = _ApertureFlags()


def _update_decode_docstring(func):
    """
    Decorator to update a function docstring with the aperture flag
    documentation.

    The ``<flag_descriptions>`` placeholder in the function docstring is
    replaced with a bullet list generated from ``APERTURE_FLAGS`` (see
    `photutils.utils._flags.update_flag_docstring`).

    Parameters
    ----------
    func : function
        The function to decorate.

    Returns
    -------
    func : function
        The decorated function with updated docstring.
    """
    return update_flag_docstring(func, APERTURE_FLAGS, indent=4)


[docs] @_update_decode_docstring def decode_aperture_flags(flags, *, return_bit_values=False): # numpydoc ignore: RT05 """ Decode aperture bit flags into individual components. This function takes integer flag values from aperture photometry or aperture statistics results and returns a list of human-readable names of the conditions that were detected. This is useful for understanding what problems were encountered without needing to manually perform bitwise operations. Parameters ---------- flags : int or array-like of int Integer flag value(s) to decode. Each bit in the flag represents a specific condition that was detected when measuring the aperture. return_bit_values : bool, optional If `True`, return the decoded bit flags (integers) instead of the flag names (strings). Default is `False`. Returns ------- decoded : list of str, list of int, or nested list List of active flag names (or bit values) for a scalar input. For an array input, a nested list with the same shape as the input is returned, where each innermost element is the list of active flag names (or bit values) for the corresponding flag. If no flags are set, an empty list is returned. Possible flags are: <flag_descriptions> Examples -------- Decode a single flag value: >>> from photutils.aperture import decode_aperture_flags >>> issues = decode_aperture_flags(10) # bits 2 and 8 set >>> print(issues) ['partial_overlap', 'masked_pixels'] >>> 'partial_overlap' in issues True >>> 'no_overlap' in issues False Decode multiple flag values: >>> flags = [0, 8, 24] # 0, bit 8, bits 8+16 >>> decoded_list = decode_aperture_flags(flags) >>> decoded_list[0] # No issues [] >>> decoded_list[1] ['masked_pixels'] >>> decoded_list[2] ['masked_pixels', 'all_masked'] Return the bit values instead of the names: >>> decode_aperture_flags(10, return_bit_values=True) [2, 8] """ return decode_flags(flags, APERTURE_FLAGS, return_bit_values=return_bit_values)
def _counts_to_flag_bits(flag_counts, overlap, w_out): """ Map per-source pixel counts to aperture flag bit values. Parameters ---------- flag_counts : 2D `~numpy.ndarray` (int) The per-source pixel counts, with one row per source and the columns given by the ``FLAG_COL_*`` constants in `photutils.aperture._batch_photometry`. Rows must be all zero for sources whose aperture bounding box does not overlap the data. overlap : 1D `~numpy.ndarray` (bool) Whether the aperture bounding box overlaps the data. w_out : 1D `~numpy.ndarray` (bool) Whether the aperture has one or more pixels with nonzero aperture weight outside the data. Returns ------- flags : 1D `~numpy.ndarray` (int) The bitwise quality flags for each source. """ # Import here to avoid loading the compiled extension when only the # registry or decoder is needed from photutils.aperture._batch_photometry import (FLAG_COL_MASKED, FLAG_COL_NONFINITE_DATA, FLAG_COL_NONFINITE_ERROR, FLAG_COL_NPIX, FLAG_COL_SEG, FLAG_COL_UNCORRECTED, FLAG_COL_VALID) flag_counts = np.atleast_2d(flag_counts) overlap = np.atleast_1d(overlap) w_out = np.atleast_1d(w_out) w_in = flag_counts[:, FLAG_COL_NPIX] flags = np.zeros(overlap.shape, dtype=int) no_overlap = ~overlap | ((w_in == 0) & w_out) flags[no_overlap] |= APERTURE_FLAGS.NO_OVERLAP flags[(w_in > 0) & w_out] |= APERTURE_FLAGS.PARTIAL_OVERLAP flags[w_in == 0] |= APERTURE_FLAGS.NO_PIXELS # For sources without overlap, the counts are all zero, so the # pixel-membership bits below are never set for them. masked = flag_counts[:, FLAG_COL_MASKED] > 0 flags[masked] |= APERTURE_FLAGS.MASKED_PIXELS all_masked = (w_in > 0) & (flag_counts[:, FLAG_COL_VALID] == 0) flags[all_masked] |= APERTURE_FLAGS.ALL_MASKED nonfinite = flag_counts[:, FLAG_COL_NONFINITE_DATA] > 0 flags[nonfinite] |= APERTURE_FLAGS.NON_FINITE_DATA nonfinite_err = flag_counts[:, FLAG_COL_NONFINITE_ERROR] > 0 flags[nonfinite_err] |= APERTURE_FLAGS.NON_FINITE_ERROR neighbor = flag_counts[:, FLAG_COL_SEG] > 0 flags[neighbor] |= APERTURE_FLAGS.NEIGHBOR_PIXELS uncorrected = flag_counts[:, FLAG_COL_UNCORRECTED] > 0 flags[uncorrected] |= APERTURE_FLAGS.UNCORRECTED_PIXELS return flags