Source code for spindoctor.nav_model.nav_model_rings_simulated

"""Simulated-rings NavModel.

Predicts ring features for a simulated observation from the idealized
``ring_system`` view the information boundary exposes
(``obs.nav_params['ring_system']``): the shared projection geometry and the
navigable features' catalog orbits, shapes, and declared orbit
uncertainties.  Non-navigable features never reach this model (the boundary
filter drops them), and the planted per-feature ``orbit_error`` is a truth
key this model cannot see -- the navigator predicts catalog positions and
must absorb the planted misplacement honestly.

One model instance per navigable feature.  Each banded feature (ringlet)
emits a ``RING_ANNULUS`` template for the correlation path; every feature
emits one ``RING_EDGE`` polyline per predicted catalog boundary for the
distance-transform fit.  Rendering is delegated to
:mod:`spindoctor.nav_model.sim_ring`, whose projection and orbit math are
shared with the image-side renderer by design.
"""

from __future__ import annotations

import math
from typing import TYPE_CHECKING, Any

import numpy as np
import oops

from spindoctor.annotation import Annotations
from spindoctor.config import Config
from spindoctor.feature.feature import NavFeature, NavReliabilityBreakdown
from spindoctor.feature.feature_type import NavFeatureType
from spindoctor.feature.flags import RingAnnulusFlags, RingEdgeFlags
from spindoctor.feature.geometry import RingAnnulusGeometry, RingEdgePolyline
from spindoctor.nav_model.nav_model import NavModel
from spindoctor.nav_model.nav_model_rings_base import NavModelRingsBase
from spindoctor.nav_model.sim_ring import PredictedRingFeature, predict_ring_feature
from spindoctor.support.filters import NavFilterKind, NavFilterSpec
from spindoctor.support.time import now_dt
from spindoctor.support.types import NDArrayFloatType

if TYPE_CHECKING:  # pragma: no cover - typing-only import
    from spindoctor.nav_orchestrator.nav_context import NavContext

__all__ = ['NavModelRingsSimulated']

# Base per-vertex ring-edge uncertainty for a simulated ring: the rendered
# edge is sharp and noise-free, so the floor reflects the one-pixel polyline
# sampling resolution.  A feature's declared_orbit_sigma raises the radial
# sigma above this floor (the navigator is entitled to its catalog error
# bars, never to the drawn orbit-error values).
_RING_EDGE_SIGMA_RADIAL_PX: float = 1.0
_RING_EDGE_SIGMA_ALONG_PX: float = 0.5
# A polyline whose max deviation from its best-fit line is below this is treated
# as straight (rank-1 constraint); a curved ring arc exceeds it and constrains
# the offset in both axes.
_RING_EDGE_FLAT_CURVATURE_PX: float = 1.0


def _ring_edge_is_straight(vertices_vu: NDArrayFloatType) -> bool:
    """Return True when the polyline's deviation from a line is below threshold.

    Computed by SVD of the centred vertices: the smaller singular direction's
    spread is the max perpendicular deviation from the best-fit line.
    """
    if vertices_vu.shape[0] < 3:
        return True
    centred = vertices_vu - vertices_vu.mean(axis=0, keepdims=True)
    _u, _s, vt = np.linalg.svd(centred, full_matrices=False)
    deviations = centred @ vt[1]
    return bool(float(np.max(np.abs(deviations))) <= _RING_EDGE_FLAT_CURVATURE_PX)