curryer.correction.verification =============================== .. py:module:: curryer.correction.verification .. autoapi-nested-parse:: Verification module for geolocation requirements compliance. Provides :func:`verify`, a standalone entry point that evaluates the current set of SPICE kernels and alignment parameters against mission geolocation requirements — without running the iterative correction loop. Typical use-cases ----------------- Weekly automated check (CLARREO) Pass pre-computed ``image_matching_results`` (the most common path): >>> result = verify(setup, image_matching_results=weekly_datasets, work_dir=work_dir) >>> if not result.passed: ... send_alert(result.summary_table) Post-correction validation After a full GCS run, verify the optimised parameter set: >>> result = verify(setup, image_matching_results=post_correction_datasets, work_dir=work_dir) One-off compliance check with in-memory geolocated data Supply an already-geolocated dataset together with a GCP chip directory and calibration files; verification auto-pairs and image-matches without any additional setup: >>> result = verify( ... setup, ... geolocated_data=raw_dataset, ... gcp_directory="data/gcps/", ... los_file="cal/b_HS.mat", ... psf_file="cal/optical_PSF_675nm.mat", ... ) Models ------ :class:`RequirementsConfig` Verification thresholds (performance limit and pass-rate). :class:`GCPError` Per-measurement/GCP error detail. :class:`VerificationResult` Structured pass/fail result; serialisable via Pydantic JSON methods. Attributes ---------- .. autoapisummary:: curryer.correction.verification.logger Classes ------- .. autoapisummary:: curryer.correction.verification.GCPError curryer.correction.verification.VerificationResult Functions --------- .. autoapisummary:: curryer.correction.verification._aggregate_results curryer.correction.verification._run_error_stats curryer.correction.verification._check_threshold curryer.correction.verification._generate_warnings curryer.correction.verification._build_per_gcp_errors curryer.correction.verification._build_source_mapping curryer.correction.verification._format_summary_table curryer.correction.verification._log_pairing_summary curryer.correction.verification._get_spice_boresight_and_rotation curryer.correction.verification._extract_spacecraft_position_midframe curryer.correction.verification.image_matching curryer.correction.verification._aggregate_image_matching_results curryer.correction.verification.match_geolocated_to_gcp_files curryer.correction.verification._run_image_matching_for_pairs curryer.correction.verification.verify curryer.correction.verification.compare_results Module Contents --------------- .. py:data:: logger .. py:class:: GCPError(/, **data: Any) Bases: :py:obj:`pydantic.BaseModel` Per-measurement/GCP error detail. Each instance corresponds to one row in the aggregated image-matching output — typically one measurement from a single GCP pair. .. attribute:: gcp_index Zero-based measurement index in the aggregated dataset. :type: int .. attribute:: science_key Identifier for the science data segment (dataset label or index). :type: str .. attribute:: gcp_key Identifier for the ground-control-point source. :type: str .. attribute:: lat_error_deg Latitude error in degrees (positive = northward shift). :type: float .. attribute:: lon_error_deg Longitude error in degrees (positive = eastward shift). :type: float .. attribute:: nadir_equiv_error_m Nadir-equivalent total geolocation error in meters, or ``None`` when error-stats processing was not performed. :type: float or None .. attribute:: correlation Image-matching correlation score, or ``None`` when not available. :type: float or None .. attribute:: passed Whether this measurement satisfies the per-measurement threshold. :type: bool .. py:attribute:: model_config Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict]. .. py:attribute:: gcp_index :type: int .. py:attribute:: science_key :type: str .. py:attribute:: gcp_key :type: str .. py:attribute:: lat_error_deg :type: float .. py:attribute:: lon_error_deg :type: float .. py:attribute:: nadir_equiv_error_m :type: float | None :value: None .. py:attribute:: correlation :type: float | None :value: None .. py:attribute:: passed :type: bool .. py:class:: VerificationResult(/, **data: Any) Bases: :py:obj:`pydantic.BaseModel` Structured result from a :func:`verify` call. Most fields are JSON-serialisable via Pydantic's ``model_dump()`` / ``model_dump_json()``. The :attr:`aggregate_stats` field (an ``xr.Dataset``) must be excluded when serialising to JSON — persist it separately (e.g. via ``aggregate_stats.to_netcdf(path)``):: json_str = result.model_dump_json(exclude={"aggregate_stats"}) result.aggregate_stats.to_netcdf("verification_stats.nc") .. attribute:: passed ``True`` when :attr:`percent_within_threshold` ≥ :attr:`requirements.performance_spec_percent`. :type: bool .. attribute:: per_gcp_errors One entry per measurement in the aggregated dataset. :type: list[GCPError] .. attribute:: aggregate_stats Full output from :meth:`~curryer.correction.error_stats.ErrorStatsProcessor.process_geolocation_errors`. :type: xr.Dataset .. attribute:: requirements The thresholds used for this verification run. :type: RequirementsConfig .. attribute:: summary_table Human-readable ASCII table suitable for logging or reports. :type: str .. attribute:: percent_within_threshold Percentage of measurements with nadir-equivalent error below :attr:`requirements.performance_threshold_m`. :type: float .. attribute:: warnings Non-empty when :attr:`passed` is ``False``. :type: list[str] .. attribute:: timestamp UTC wall-clock time when :func:`verify` was called. :type: datetime .. attribute:: files_processed Science/GCP key pairs that were processed, as ``"+"`` strings. Empty when the source mapping is unavailable. :type: list[str] .. attribute:: elapsed_time_s Wall-clock time for the verify call in seconds, or ``None`` when not measured. :type: float or None .. attribute:: config_snapshot Key config fields used for this run (threshold, spec percent, instrument name), for reproducibility records. :type: dict or None .. py:attribute:: model_config Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict]. .. py:attribute:: passed :type: bool .. py:attribute:: per_gcp_errors :type: list[GCPError] .. py:attribute:: aggregate_stats :type: xarray.Dataset .. py:attribute:: requirements :type: curryer.correction.config.RequirementsConfig .. py:attribute:: summary_table :type: str .. py:attribute:: percent_within_threshold :type: float .. py:attribute:: warnings :type: list[str] .. py:attribute:: timestamp :type: datetime.datetime .. py:attribute:: files_processed :type: list[str] :value: None .. py:attribute:: elapsed_time_s :type: float | None :value: None .. py:attribute:: config_snapshot :type: dict | None :value: None .. py:function:: _aggregate_results(image_matching_results: list[xarray.Dataset], setup: curryer.correction.config.GeolocationSetup) -> xarray.Dataset Aggregate a list of per-GCP-pair image-matching datasets into one. For multiple input datasets, this delegates to the same aggregation logic used by the correction pipeline (:func:`~curryer.correction.pipeline._aggregate_image_matching_results`). For a single input dataset, it is returned directly after ensuring that the ``measurement`` coordinate is present and consists of sequential integer indices. :param image_matching_results: One element per GCP pair. Each dataset must have a ``measurement`` dimension and at minimum ``lat_error_deg`` / ``lon_error_deg`` variables. :type image_matching_results: list[xr.Dataset] :param setup: Used for mission-specific variable names (``spacecraft_position_name``, ``boresight_name``, ``transformation_matrix_name``). :type setup: GeolocationSetup :returns: Combined dataset with a single ``measurement`` dimension. :rtype: xr.Dataset .. py:function:: _run_error_stats(aggregated: xarray.Dataset, setup: curryer.correction.config.GeolocationSetup) -> xarray.Dataset Run :class:`~curryer.correction.error_stats.ErrorStatsProcessor` on *aggregated*. :param aggregated: Combined image-matching result with a ``measurement`` dimension. :type aggregated: xr.Dataset :param setup: Used to build :class:`~curryer.correction.error_stats.ErrorStatsConfig`. :type setup: GeolocationSetup :returns: Processed dataset with ``nadir_equiv_total_error_m`` and related intermediate variables. :rtype: xr.Dataset .. py:function:: _check_threshold(aggregate_stats: xarray.Dataset, requirements: curryer.correction.config.RequirementsConfig) -> tuple[bool, float] Evaluate whether performance meets the threshold requirement. Uses ``nadir_equiv_total_error_m`` from the :class:`~curryer.correction.error_stats.ErrorStatsProcessor` output. :param aggregate_stats: Output of ``ErrorStatsProcessor.process_geolocation_errors()``. :type aggregate_stats: xr.Dataset :param requirements: Performance limits to evaluate against. :type requirements: RequirementsConfig :returns: ``(passed, percent_within_threshold)`` :rtype: tuple[bool, float] .. py:function:: _generate_warnings(passed: bool, percent_below: float, requirements: curryer.correction.config.RequirementsConfig) -> list[str] Generate user-facing warning messages when verification fails. :param passed: Overall pass/fail result. :type passed: bool :param percent_below: Percentage of measurements within the threshold. :type percent_below: float :param requirements: Performance limits used for the check. :type requirements: RequirementsConfig :returns: Empty list when *passed* is ``True``; otherwise one warning string. :rtype: list[str] .. py:function:: _build_per_gcp_errors(aggregate_stats: xarray.Dataset, source_mapping: list[tuple[str, str]], requirements: curryer.correction.config.RequirementsConfig) -> list[GCPError] Build a :class:`GCPError` for every measurement in *aggregate_stats*. :param aggregate_stats: Processed dataset from :func:`~curryer.correction.error_stats.ErrorStatsProcessor.process_geolocation_errors`. :type aggregate_stats: xr.Dataset :param source_mapping: ``[(science_key, gcp_key), ...]`` parallel to the measurement dimension. If shorter than the number of measurements the remainder fall back to ``("sci_{i}", "gcp_{i}")``. :type source_mapping: list[tuple[str, str]] :param requirements: Used for per-measurement pass/fail evaluation. :type requirements: RequirementsConfig :rtype: list[GCPError] .. py:function:: _build_source_mapping(image_matching_results: list[xarray.Dataset]) -> list[tuple[str, str]] Map every measurement back to its (science_key, gcp_key) pair. The mapping is derived from dataset attributes (``sci_key`` / ``gcp_key``) when present, otherwise falls back to ``"result_{i}"`` labels. :param image_matching_results: Raw per-GCP-pair datasets before aggregation. :type image_matching_results: list[xr.Dataset] :returns: Parallel to the ``measurement`` dimension of the aggregated dataset. :rtype: list[tuple[str, str]] .. py:function:: _format_summary_table(per_gcp_errors: list[GCPError], requirements: curryer.correction.config.RequirementsConfig, percent_within: float, passed: bool) -> str Generate a human-readable summary table. Example output:: ┌──────────────────────────────────────────────────────┐ │ Verification Summary │ ├──────┬────────────┬────────────┬──────────┬──────────┤ │ GCP │ Lat err(°) │ Lon err(°) │ Nadir(m) │ Status │ ├──────┼────────────┼────────────┼──────────┼──────────┤ │ 0 │ 0.00123 │ -0.00045 │ 145.2 │ ✓ │ │ 1 │ 0.00567 │ 0.00234 │ 312.8 │ ✗ │ ├──────┴────────────┴────────────┴──────────┴──────────┤ │ Result: PASSED — 60.0% within 250.0m (req: 39.0%) │ └──────────────────────────────────────────────────────┘ :param per_gcp_errors: Per-measurement detail. :type per_gcp_errors: list[GCPError] :param requirements: Thresholds used for evaluation. :type requirements: RequirementsConfig :param percent_within: Percentage of measurements within the threshold. :type percent_within: float :param passed: Overall pass/fail result. :type passed: bool :returns: Multi-line formatted table. :rtype: str .. py:function:: _log_pairing_summary(pairs: list[tuple[pathlib.Path, pathlib.Path]], unpaired: list[pathlib.Path] | None = None) -> None Log a human-readable GCP pairing summary. :param pairs: Successfully paired (observation, gcp) paths. :type pairs: list of (Path, Path) :param unpaired: Observation paths for which no matching GCP was found. :type unpaired: list of Path or None, optional .. py:function:: _get_spice_boresight_and_rotation(instrument_name: str, et_midframe: float, ref_frame: str = 'ITRF93') -> tuple[numpy.ndarray, numpy.ndarray] Return the instrument boresight and HS→CTRS rotation matrix from SPICE. :param instrument_name: SPICE instrument name (e.g. ``"CPRS_HYSICS"``). :type instrument_name: str :param et_midframe: Ephemeris time (ET seconds past J2000) at the mid-frame epoch. :type et_midframe: float :param ref_frame: Target reference frame. Default ``"ITRF93"`` (ECEF). :type ref_frame: str, optional :returns: * **boresight** (*np.ndarray, shape (3,)*) -- Unit boresight vector in instrument (HS) frame. * **t_hs2ctrs** (*np.ndarray, shape (3, 3)*) -- Rotation matrix ``v_ctrs = R @ v_hs``. :raises SpiceyError: If required kernels are not loaded or do not cover *et_midframe*. .. py:function:: _extract_spacecraft_position_midframe(telemetry: pandas.DataFrame, setup: curryer.correction.config.GeolocationSetup | None = None) -> numpy.ndarray Extract spacecraft position at mid-frame from telemetry. :param telemetry: Telemetry DataFrame with spacecraft position columns. :type telemetry: pd.DataFrame :param setup: If provided and ``setup.data_config.position_columns`` is set, those column names are used directly. Otherwise falls back to pattern-guessing (with a deprecation warning). :type setup: GeolocationSetup or None, optional :returns: Shape ``(3,)`` — ``[x, y, z]`` position in meters (J2000 frame). :rtype: np.ndarray :raises ValueError: If ``position_columns`` has wrong length, or specified columns are not found, or pattern-guessing fails. .. py:function:: image_matching(geolocated_data: xarray.Dataset, gcp_reference_file: pathlib.Path, telemetry: pandas.DataFrame | None = None, params_info: list | None = None, setup: curryer.correction.config.GeolocationSetup | None = None, los_vectors_cached: numpy.ndarray | None = None, optical_psfs_cached: list | None = None, r_iss_midframe: numpy.ndarray | None = None) -> xarray.Dataset Image matching using :func:`~curryer.correction.image_match.integrated_image_match`. Performs image correlation between geolocated pixels and a Landsat GCP reference image to measure geolocation error. This function is the single implementation used by both the correction loop (:func:`~curryer.correction.pipeline.loop`) and standalone verification (:func:`verify`). ``pipeline.py`` imports it from here. :param geolocated_data: Geolocation output with ``latitude``, ``longitude``, and a ``frame`` coordinate (GPS seconds = ``ugps_times / 1e6``). :type geolocated_data: xr.Dataset :param gcp_reference_file: Path to GCP reference image (``.mat`` or ``.nc``). :type gcp_reference_file: Path :param telemetry: Telemetry DataFrame with spacecraft state. Required when *r_iss_midframe* is not supplied. :type telemetry: pd.DataFrame or None, optional :param params_info: Current parameter values for error tracking. Defaults to ``[]``. :type params_info: list or None, optional :param setup: Setup for coordinate names, calibration paths, and instrument metadata. :type setup: GeolocationSetup or None, optional :param los_vectors_cached: Pre-loaded LOS vectors. :type los_vectors_cached: np.ndarray or None, optional :param optical_psfs_cached: Pre-loaded optical PSF entries. :type optical_psfs_cached: list or None, optional :param r_iss_midframe: Spacecraft ECEF position in meters at mid-frame. When provided, *telemetry* is not consulted for position. :type r_iss_midframe: np.ndarray of shape (3,) or None, optional :returns: Error measurements: ``lat_error_deg``, ``lon_error_deg``, and metadata. :rtype: xr.Dataset :raises ValueError: If neither *telemetry* nor *r_iss_midframe* is supplied, or if calibration files are missing. .. py:function:: _aggregate_image_matching_results(image_matching_results: list[xarray.Dataset], setup: curryer.correction.config.GeolocationSetup) -> xarray.Dataset Aggregate multiple image matching results into one dataset. :param image_matching_results: Per-GCP-pair datasets from :func:`image_matching`. :type image_matching_results: list[xr.Dataset] :param setup: Used for variable name mappings. :type setup: GeolocationSetup :returns: Combined dataset with a single ``measurement`` dimension. :rtype: xr.Dataset .. py:function:: match_geolocated_to_gcp_files(geolocated_data: xarray.Dataset, gcp_files: list[pathlib.Path], setup: curryer.correction.config.GeolocationSetup, los_vectors_cached: numpy.ndarray | None = None, optical_psfs_cached: list | None = None) -> list[xarray.Dataset] Run image matching between already-geolocated data and GCP reference files. This is the reusable *pipeline tail*: both the correction loop (after kernel tweaking and geolocation) and standalone :func:`verify` call this function. Given geolocated data and a list of GCP reference files it performs image matching against each file and returns the per-GCP error datasets ready for aggregation and error-stats processing. :param geolocated_data: Geolocated observation dataset with ``latitude``, ``longitude``, and a ``frame`` coordinate (GPS seconds). :type geolocated_data: xr.Dataset :param gcp_files: GCP reference files to match against. :type gcp_files: list of Path :param setup: Mission setup (calibration paths, variable names, instrument name). :type setup: GeolocationSetup :param los_vectors_cached: Pre-loaded LOS vectors. :type los_vectors_cached: np.ndarray or None, optional :param optical_psfs_cached: Pre-loaded optical PSF entries. :type optical_psfs_cached: list or None, optional :returns: One error dataset per successfully matched GCP file. Failures are logged as warnings and skipped. :rtype: list of xr.Dataset .. py:function:: _run_image_matching_for_pairs(pairs: list[tuple[str | pathlib.Path, str | pathlib.Path]], los_file: str | pathlib.Path, psf_file: str | pathlib.Path, setup: curryer.correction.config.GeolocationSetup, default_altitude_m: float = 400000.0) -> list[xarray.Dataset] Run image matching for a list of (observation, gcp) file-path pairs. Loads each observation and GCP file, infers spacecraft state, runs :func:`~curryer.correction.image_match.integrated_image_match`, and packages the result as an ``xr.Dataset`` compatible with :func:`verify`. :param pairs: ``(observation_path, gcp_path)`` tuples. :type pairs: list of (Path, Path) :param los_file: Instrument line-of-sight vectors (``.mat`` file). :type los_file: Path :param psf_file: Optical PSF ``.mat`` file. :type psf_file: Path :param setup: Used for spacecraft-state variable names. :type setup: GeolocationSetup :param default_altitude_m: Fallback spacecraft altitude in meters when the observation file does not contain position data. Default 400 000 m (ISS nominal orbit). :type default_altitude_m: float, optional :returns: One dataset per successfully matched pair. Failures are logged as warnings and skipped. :rtype: list[xr.Dataset] .. py:function:: verify(setup: curryer.correction.config.GeolocationSetup, gcp_pairs: list[tuple[str | pathlib.Path, str | pathlib.Path]] | None = None, observation_paths: list[str | pathlib.Path] | None = None, gcp_directory: str | pathlib.Path | None = None, los_file: str | pathlib.Path | None = None, psf_file: str | pathlib.Path | None = None, max_distance_m: float = 0.0, gcp_pattern: str = '*_regridded.nc', default_altitude_m: float = 400000.0, image_matching_results: list[xarray.Dataset] | None = None, geolocated_data: xarray.Dataset | None = None, work_dir: pathlib.Path | None = None) -> VerificationResult Evaluate current alignment against mission requirements. No parameter variation, no kernel creation, no iteration loop. This function checks whether a **given** set of alignment parameters meets geolocation requirements. Input priority (first match wins) ---------------------------------- 1. *image_matching_results* — pre-computed outputs from image matching; the most common entry point for weekly automated checks. 2. *geolocated_data* — raw geolocated data. Either set ``setup.image_matching_func`` for a custom matcher, or supply *gcp_directory*, *los_file*, and *psf_file* to run built-in spatial pairing + image matching. 3. *gcp_pairs* — explicit ``(observation_path, gcp_path)`` file-path pairs. Requires *los_file* and *psf_file*. 4. *observation_paths* + *gcp_directory* — auto-paired via spatial overlap. Requires *los_file* and *psf_file*. 5. None of the above provided — raises :class:`ValueError`. :param setup: Mission setup with all geolocation/calibration settings. :type setup: GeolocationSetup :param gcp_pairs: Explicit ``(observation_path, gcp_path)`` pairs. Each path may be a local path or an ``s3://`` URI (requires ``boto3``). :type gcp_pairs: list of (path, path) or None :param observation_paths: Observation file paths for automatic GCP pairing. Requires *gcp_directory*, *los_file*, and *psf_file*. :type observation_paths: list of path or None :param gcp_directory: Directory of GCP reference images for automatic pairing with *observation_paths*. :type gcp_directory: path or None :param los_file: Instrument line-of-sight vectors (``.mat`` file). Required when *gcp_pairs* or *observation_paths* is provided. :type los_file: path or None :param psf_file: Optical PSF ``.mat`` file. Required when *gcp_pairs* or *observation_paths* is provided. :type psf_file: path or None :param max_distance_m: Spatial pairing margin for the auto-pair mode (default ``0.0`` — GCP center must be inside the observation footprint). :type max_distance_m: float, optional :param gcp_pattern: Glob pattern used to discover GCP chips when *gcp_directory* is provided. Defaults to ``"*_regridded.nc"``. :type gcp_pattern: str, optional :param default_altitude_m: Fallback spacecraft altitude (meters) used when observation files do not contain position data. Default ``400_000.0`` (ISS nominal orbit). Override for other platforms (e.g. ``505_000.0`` for CTIM). :type default_altitude_m: float, optional :param image_matching_results: Pre-computed image-matching datasets, one per GCP pair. :type image_matching_results: list[xr.Dataset] or None :param geolocated_data: Already-geolocated data. Matched either via ``setup.image_matching_func`` (custom override) or, when *gcp_directory*, *los_file*, and *psf_file* are supplied, via built-in spatial pairing + image matching. :type geolocated_data: xr.Dataset or None :param work_dir: Working directory for outputs. Created if absent. :type work_dir: Path or None, optional :returns: Structured pass/fail result with per-GCP detail and a human-readable :attr:`~VerificationResult.summary_table`. :rtype: VerificationResult :raises ValueError: When none of the input modes is provided; when *geolocated_data* is supplied without *gcp_directory* / *los_file* / *psf_file* and ``setup.image_matching_func`` is not set; when *los_file* or *psf_file* is ``None`` for a file-path mode (*gcp_pairs* or *observation_paths* + *gcp_directory*); when *observation_paths* and *gcp_directory* are not both supplied; or when image matching produces no results. :raises FileNotFoundError: If any of the supplied file paths do not exist. .. py:function:: compare_results(before: VerificationResult, after: VerificationResult) -> str Generate a side-by-side comparison of two verification results. Useful for evaluating whether a correction run improved geolocation accuracy relative to a baseline. :param before: Baseline verification result (e.g., pre-correction). :type before: VerificationResult :param after: Updated verification result (e.g., post-correction). :type after: VerificationResult :returns: Human-readable side-by-side comparison table. :rtype: str