nsb 0.1.0

Night-sky background evaluation and observation planning for ground-based astronomy.
Documentation
// SPDX-License-Identifier: AGPL-3.0-only
// Copyright (C) 2026 Vallés Puig, Ramon
use crate::units::radiometry::{
    PhotonsPerSquareCentimeterNanosecondSteradian as BandPhotonRadiance, S10s,
};

#[derive(Debug, Clone, Copy, PartialEq)]
/// Integrated starlight radiance and diagnostic B/V values.
pub(crate) struct StarlightOutputs {
    /// Photon radiance integrated over 300–650 nm.
    pub(crate) integrated: BandPhotonRadiance,
    /// B-reference S10 diagnostic.
    pub(crate) b_flux_s10: S10s,
    /// V-reference S10 diagnostic.
    pub(crate) v_flux_s10: S10s,
    /// Statistical one-sigma uncertainty of the integrated photon radiance.
    pub(crate) statistical_uncertainty: Option<BandPhotonRadiance>,
    /// Systematic one-sigma uncertainty of the integrated photon radiance.
    pub(crate) systematic_uncertainty: Option<BandPhotonRadiance>,
    /// Total one-sigma uncertainty of the integrated photon radiance.
    pub(crate) total_uncertainty: Option<BandPhotonRadiance>,
    /// Whether B/V S10 columns were supplied by the map (false for packed candidate maps).
    pub(crate) s10_diagnostics_provided: bool,
}

impl StarlightOutputs {
    /// Construct starlight outputs.
    pub(crate) fn new(integrated: BandPhotonRadiance, b_flux_s10: S10s, v_flux_s10: S10s) -> Self {
        Self {
            integrated,
            b_flux_s10,
            v_flux_s10,
            statistical_uncertainty: None,
            systematic_uncertainty: None,
            total_uncertainty: None,
            s10_diagnostics_provided: true,
        }
    }

    /// Attach a complete absolute-uncertainty triplet.
    ///
    /// Map validation rejects non-finite, negative, partial, or inconsistent
    /// triplets before an output can be returned by a [`super::StarlightMap`].
    pub(crate) fn with_uncertainties(
        mut self,
        statistical: BandPhotonRadiance,
        systematic: BandPhotonRadiance,
        total: BandPhotonRadiance,
    ) -> Self {
        self.statistical_uncertainty = Some(statistical);
        self.systematic_uncertainty = Some(systematic);
        self.total_uncertainty = Some(total);
        self
    }

    /// Relative total one-sigma uncertainty when it is mathematically defined.
    pub(crate) fn relative_uncertainty(self) -> Option<f64> {
        let total = self.total_uncertainty?.value();
        let integrated = self.integrated.value();
        if integrated > 0.0 {
            Some(total / integrated)
        } else if integrated == 0.0 && total == 0.0 {
            Some(0.0)
        } else {
            None
        }
    }

    pub(crate) fn is_finite_non_negative(self) -> bool {
        let values_are_valid = self.integrated.is_finite()
            && self.b_flux_s10.is_finite()
            && self.v_flux_s10.is_finite()
            && self.integrated.value() >= 0.0
            && self.b_flux_s10.value() >= 0.0
            && self.v_flux_s10.value() >= 0.0;
        values_are_valid && self.has_valid_uncertainties()
    }

    pub(crate) fn has_uncertainties(self) -> bool {
        self.statistical_uncertainty.is_some()
    }

    fn has_valid_uncertainties(self) -> bool {
        match (
            self.statistical_uncertainty,
            self.systematic_uncertainty,
            self.total_uncertainty,
        ) {
            (None, None, None) => true,
            (Some(statistical), Some(systematic), Some(total)) => {
                statistical.is_finite()
                    && systematic.is_finite()
                    && total.is_finite()
                    && statistical.value() >= 0.0
                    && systematic.value() >= 0.0
                    && total >= statistical
                    && total >= systematic
            }
            _ => false,
        }
    }
}