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 super::map::StarlightMap;
use super::output::StarlightOutputs;
use super::photometry::scale_outputs;
#[cfg(nsb_bundled_production_starlight)]
use super::validated::ValidatedStarlightMap;
#[cfg(nsb_bundled_production_starlight)]
use crate::data::bundled::{
    BUNDLED_PRODUCTION_STARLIGHT_MANIFEST, BUNDLED_PRODUCTION_STARLIGHT_MAP,
};
use crate::error::{NsbError, Result};
use crate::evaluator::Target;
use crate::units::ScaleFactors;
use siderust::coordinates::spherical::direction;
use siderust::coordinates::transform::TransformFrame;
use std::sync::Arc;

#[derive(Debug, Clone)]
/// Directional starlight evaluator backed by one immutable shared map.
pub(crate) struct Starlight {
    map: Arc<StarlightMap>,
    scale: ScaleFactors,
}

impl Starlight {
    /// Load the bundled production Gaia DR3 XP-derived starlight map.
    ///
    /// This succeeds only when a release map and runtime manifest are both
    /// registered in `crates/nsb/data/manifest.toml`, checksum-verified by the
    /// build script, embedded as static bytes, and admitted by
    /// [`ValidatedStarlightMap`](super::ValidatedStarlightMap).
    #[cfg(nsb_bundled_production_starlight)]
    pub(crate) fn bundled_production_model() -> Result<Self> {
        static MAP: std::sync::OnceLock<std::result::Result<Arc<StarlightMap>, String>> =
            std::sync::OnceLock::new();
        let map = MAP.get_or_init(|| {
            ValidatedStarlightMap::from_bytes_and_manifest(
                BUNDLED_PRODUCTION_STARLIGHT_MAP,
                BUNDLED_PRODUCTION_STARLIGHT_MANIFEST,
            )
            .map(|validated| validated.shared_map())
            .map_err(|err| err.to_string())
        });
        match map {
            Ok(map) => Ok(Self::with_shared_map(Arc::clone(map))),
            Err(message) => Err(NsbError::InvalidMap {
                message: message.clone(),
            }),
        }
    }

    /// Report a missing bundled production starlight asset.
    #[cfg(not(nsb_bundled_production_starlight))]
    pub(crate) fn bundled_production_model() -> Result<Self> {
        Err(missing_bundled_production_asset())
    }

    /// Build from shared ownership of a caller-provided map.
    pub(crate) fn with_shared_map(map: Arc<StarlightMap>) -> Self {
        Self {
            map,
            scale: ScaleFactors::new(1.0),
        }
    }

    /// Transform a target to Galactic coordinates and evaluate the map.
    pub(crate) fn compute(&self, target: Target) -> Result<StarlightOutputs> {
        if !self.scale.is_finite() || self.scale < ScaleFactors::new(0.0) {
            return Err(NsbError::OutOfRange(
                "starlight scale must be finite and non-negative".to_string(),
            ));
        }
        let galactic: direction::Galactic = target.to_frame();
        Ok(scale_outputs(
            self.map.lookup(galactic.to_cartesian()),
            self.scale,
        ))
    }

    /// Return the backing map.
    pub(crate) fn map(&self) -> &StarlightMap {
        &self.map
    }
}

#[cfg(not(nsb_bundled_production_starlight))]
fn missing_bundled_production_asset() -> NsbError {
    NsbError::DataMissing {
        file: "data/manifest.toml",
        message: concat!(
            "bundled production starlight asset is not registered; generate and commit ",
            "the Gaia DR3 XP nside=128 compact release map and runtime manifest, then register both ",
            "as runtime_embedded production assets"
        )
        .to_string(),
    }
}

#[cfg(test)]
pub(super) mod test_support {
    use super::{ScaleFactors, Starlight};

    pub(in crate::components::starlight) fn with_scale(
        mut model: Starlight,
        scale: ScaleFactors,
    ) -> Starlight {
        model.scale = scale;
        model
    }
}