Source code for photutils.psf.flags

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

from typing import ClassVar

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

__all__ = ['PSF_FLAGS', 'decode_psf_flags']


class _PSFFlags(FlagRegistry):
    """
    Centralized definition of PSF photometry flags.

    This class provides a single source of truth for all PSF flag
    definitions, including bit values, names, and descriptions. It
    enables consistent flag handling across the PSF photometry codebase
    and supports dynamic docstring generation.

    Examples
    --------
    >>> from photutils.psf.flags import _PSFFlags
    >>> flags = _PSFFlags()
    >>> flags.N_PIXELS_FIT_PARTIAL
    1
    >>> flags.get_name(1)
    'n_pixels_fit_partial'
    >>> flags.get_description(8)
    'possible non-convergence'
    """

    # Define all PSF flags with their properties
    FLAG_DEFINITIONS: ClassVar = [
        FlagDefinition(
            bit_value=1,
            name='n_pixels_fit_partial',
            description=("``'n_pixels_fit'`` smaller than full "
                         "``'fit_shape'`` region"),
            detailed_description=('The number of fitted pixels '
                                  '(n_pixels_fit) is smaller than the '
                                  'full fit_shape region, indicating '
                                  'partial PSF fitting.'),
        ),
        FlagDefinition(
            bit_value=2,
            name='outside_bounds',
            description='fitted position outside input image bounds',
            detailed_description=('The fitted source position is outside the '
                                  'bounds of the input image.'),
        ),
        FlagDefinition(
            bit_value=4,
            name='negative_flux',
            description='non-positive flux',
            detailed_description=('The fitted flux value is negative or zero, '
                                  'which is non-physical.'),
        ),
        FlagDefinition(
            bit_value=8,
            name='no_convergence',
            description='possible non-convergence',
            detailed_description=('The PSF fitting algorithm may not have '
                                  'converged to a stable solution.'),
        ),
        FlagDefinition(
            bit_value=16,
            name='no_covariance',
            description='missing parameter covariance',
            detailed_description=('Parameter covariance matrix is not '
                                  'available, preventing error estimation.'),
        ),
        FlagDefinition(
            bit_value=32,
            name='near_bound',
            description='fitted parameter near a bound',
            detailed_description=('One or more fitted parameters are very '
                                  'close to their imposed bounds.'),
        ),
        FlagDefinition(
            bit_value=64,
            name='no_overlap',
            description='no overlap with data',
            detailed_description=('The source PSF fitting region has no '
                                  'overlap with valid data pixels.'),
        ),
        FlagDefinition(
            bit_value=128,
            name='fully_masked',
            description='fully masked source',
            detailed_description=('All pixels in the source fitting region '
                                  'are masked.'),
        ),
        FlagDefinition(
            bit_value=256,
            name='too_few_pixels',
            description='too few pixels for fitting',
            detailed_description=('Insufficient unmasked pixels available '
                                  'for reliable PSF fitting.'),
        ),
        FlagDefinition(
            bit_value=512,
            name='non_finite_position',
            description='non-finite fitted position',
            detailed_description=('The fitted x or y position is NaN or inf, '
                                  'indicating an invalid or failed fit.'),
        ),
        FlagDefinition(
            bit_value=1024,
            name='non_finite_flux',
            description='non-finite fitted flux',
            detailed_description=('The fitted flux value is NaN or inf, '
                                  'indicating an invalid or failed fit.'),
        ),
        FlagDefinition(
            bit_value=2048,
            name='non_finite_localbkg',
            description='non-finite local background',
            detailed_description=('The local background value is NaN or '
                                  'inf, so it was not subtracted before '
                                  'fitting.'),
        ),
    ]

    domain: ClassVar = 'psf'

    # Remove in 4.0
    _DEPRECATED_FLAG_NAMES: ClassVar = {
        'npixfit_partial': 'n_pixels_fit_partial',
    }

    # Remove in 4.0
    _DEPRECATED_CONSTANT_NAMES: ClassVar = {
        'NPIXFIT_PARTIAL': 'N_PIXELS_FIT_PARTIAL',
    }

    _DEPRECATED_SINCE: ClassVar = '3.0'
    _DEPRECATED_UNTIL: ClassVar = '4.0'


# Create a singleton instance for global use
PSF_FLAGS = _PSFFlags()


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

    The ``<flag_descriptions>`` placeholder in the function docstring
    is replaced with a bullet list generated from ``PSF_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, PSF_FLAGS, indent=4)


[docs] @_update_decode_docstring @deprecated_positional_kwargs(since='3.0', until='4.0') def decode_psf_flags(flags, return_bit_values=False): # numpydoc ignore: RT05 """ Decode PSF photometry bit flags into individual components. This function takes integer flag values from PSF photometry results and returns a list of human-readable descriptions of the issues that occurred during fitting. 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 occurred during PSF fitting. return_bit_values : bool, optional If `True`, return the decoded bit flags (integers) instead of the flag descriptions (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. Each string (or integer) represents a specific condition that was detected during PSF fitting. If no flags are set, an empty list is returned. Possible flags are: <flag_descriptions> Examples -------- Decode a single flag value: >>> from photutils.psf import decode_psf_flags >>> issues = decode_psf_flags(5) # bits 1 and 4 set >>> print(issues) ['n_pixels_fit_partial', 'negative_flux'] >>> 'n_pixels_fit_partial' in issues True >>> 'no_convergence' in issues False Decode multiple flag values: >>> flags = [0, 8, 136] # 0, bit 8, bits 8+128 >>> decoded_list = decode_psf_flags(flags) >>> len(decoded_list) 3 >>> decoded_list[0] # No issues [] >>> decoded_list[1] # Convergence issue ['no_convergence'] >>> decoded_list[2] # Multiple issues ['no_convergence', 'fully_masked'] Check for specific issues: >>> issues = decode_psf_flags(136) >>> if 'no_convergence' in issues: ... print("Fit may not have converged") Fit may not have converged >>> if issues: # Any issues present ... print(f"Found {len(issues)} issues: {', '.join(issues)}") Found 2 issues: no_convergence, fully_masked Working with PSF photometry results: >>> import numpy as np >>> from astropy.modeling import models >>> from astropy.table import Table >>> from photutils.psf import (CircularGaussianPRF, PSFPhotometry, ... decode_psf_flags) >>> # Create minimal test data >>> yy, xx = np.mgrid[:21, :21] >>> m1 = CircularGaussianPRF(flux=-10, x_0=10, y_0=10, fwhm=2) >>> m2 = CircularGaussianPRF(flux=10, x_0=3, y_0=3, fwhm=2) >>> m3 = CircularGaussianPRF(flux=10, x_0=21, y_0=21, fwhm=2) >>> data = m1(xx, yy) + m2(xx, yy) + m3(xx, yy) >>> psf_model = CircularGaussianPRF(flux=1, x_0=10, y_0=10, fwhm=2) >>> init_params = Table({'x': (10, 3, 21), 'y': (10, 3, 21), ... 'flux': (1, 10, 10)}) >>> photometry = PSFPhotometry(psf_model, (3, 3)) >>> results = photometry(data, init_params=init_params) >>> issues_list = decode_psf_flags(results['flags']) >>> for i, issues in enumerate(issues_list): ... if issues: ... print(f"Source {i+1}: {', '.join(issues)}") Source 1: negative_flux Source 3: n_pixels_fit_partial, no_covariance, too_few_pixels, \ non_finite_position, non_finite_flux """ return decode_flags(flags, PSF_FLAGS, return_bit_values=return_bit_values)