Source code for nav.nav_model.nav_model_stars

import copy
import itertools
import math
from collections.abc import Sequence
from typing import Any, cast

import numpy as np
import polymath
from oops import Event, Meshgrid, Observation
from oops.backplane import Backplane
from starcat import (
    SCLASS_TO_B_MINUS_V,
    SCLASS_TO_SURFACE_TEMP,
    SpiceStarCatalog,
    UCAC4StarCatalog,
    YBSCStarCatalog,
)

from nav.annotation import (
    TEXTINFO_BOTTOM,
    TEXTINFO_BOTTOM_LEFT,
    TEXTINFO_BOTTOM_RIGHT,
    TEXTINFO_LEFT,
    TEXTINFO_RIGHT,
    TEXTINFO_TOP,
    TEXTINFO_TOP_LEFT,
    TEXTINFO_TOP_RIGHT,
    Annotation,
    Annotations,
    AnnotationTextInfo,
    TextLocInfo,
)
from nav.config import Config
from nav.support.flux import clean_sclass
from nav.support.image import draw_rect
from nav.support.time import now_dt
from nav.support.types import MutableStar

from .nav_model import NavModel
from .nav_model_result import NavModelResult

_DEBUG_STARS_MODEL_IMGDISP = False


def _ring_occlusion_annulus_pair(pair: object, planet_key: str) -> tuple[float, float]:
    """Parse one ``ring_occlusion_radii_km`` annulus into finite inner/outer radii in km.

    Parameters:
        pair: Sequence of two numeric values (not strings or bytes).
        planet_key: Planet name from YAML (for error messages).

    Returns:
        ``(inner_km, outer_km)`` as finite floats.

    Raises:
        ValueError: If ``pair`` is not a length-2 sequence of finite real numbers.
    """
    if isinstance(pair, (str, bytes)) or not isinstance(pair, Sequence):
        raise ValueError(
            f'ring_occlusion_radii_km: annulus for {planet_key!r} must be a length-2 '
            f'sequence of numbers, got {type(pair).__name__}: {pair!r}'
        )
    if len(pair) != 2:
        raise ValueError(
            f'ring_occlusion_radii_km: annulus for {planet_key!r} must have exactly 2 '
            f'elements, got {len(pair)}: {pair!r}'
        )
    radii_km: list[float] = []
    for label, raw in (('inner', pair[0]), ('outer', pair[1])):
        if isinstance(raw, bool):
            raise ValueError(
                f'ring_occlusion_radii_km: {label} radius for {planet_key!r} must be '
                f'numeric, got bool: {raw!r}'
            )
        if not isinstance(raw, (int, float, np.integer, np.floating)):
            raise ValueError(
                f'ring_occlusion_radii_km: {label} radius for {planet_key!r} must be '
                f'numeric, got {type(raw).__name__}: {raw!r}'
            )
        val = float(raw)
        if not math.isfinite(val):
            raise ValueError(
                f'ring_occlusion_radii_km: {label} radius for {planet_key!r} must be '
                f'finite, got {raw!r}'
            )
        radii_km.append(val)
    return radii_km[0], radii_km[1]


_STAR_CATALOG_UCAC4: UCAC4StarCatalog | None = None
_STAR_CATALOG_TYCHO2: SpiceStarCatalog | None = None
_STAR_CATALOG_YBSC: YBSCStarCatalog | None = None


def _get_star_catalog_ucac4() -> UCAC4StarCatalog:
    """Get UCAC4 star catalog, creating it lazily."""
    global _STAR_CATALOG_UCAC4
    if _STAR_CATALOG_UCAC4 is None:
        _STAR_CATALOG_UCAC4 = UCAC4StarCatalog()
    return _STAR_CATALOG_UCAC4


def _get_star_catalog_tycho2() -> SpiceStarCatalog:
    """Get Tycho-2 star catalog, creating it lazily."""
    global _STAR_CATALOG_TYCHO2
    if _STAR_CATALOG_TYCHO2 is None:
        _STAR_CATALOG_TYCHO2 = SpiceStarCatalog('tycho2')
    return _STAR_CATALOG_TYCHO2


def _get_star_catalog_ybsc() -> YBSCStarCatalog:
    """Get YBSC star catalog, creating it lazily."""
    global _STAR_CATALOG_YBSC
    if _STAR_CATALOG_YBSC is None:
        _STAR_CATALOG_YBSC = YBSCStarCatalog()
    return _STAR_CATALOG_YBSC