use super::calibration::load_builtin_standard;
use super::domain::AirglowNightPhase;
use super::extinction::{effective_airglow_airmass, noll_scattering_factors};
use super::units::DEFAULT_SOLAR_RADIO_FLUX;
use super::*;
use crate::site::{
AtmosphericConditions, GenericClearSky, SiteProfile, SiteProfileConfig, SiteProfileTag,
};
use crate::units::radiometry::PhotonsPerSquareCentimeterNanosecondSteradian as BandPhotonRadiance;
use crate::units::SolarFluxUnits;
use chrono::{DateTime, Utc};
use siderust::catalogs::observatories;
use siderust::coordinates::centers::Geodetic;
use siderust::coordinates::frames::{EquatorialMeanJ2000, ECEF};
use siderust::coordinates::spherical::Direction as SphericalDirection;
use siderust::qtty::{Degrees, Hectopascals, Kilometers, Meters, Nanometers};
use siderust::time::{ModifiedJulianDate, TT};
use tempoch::{Time, UTC};
fn t(input: &str) -> Time<UTC> {
Time::<UTC>::from_chrono(
DateTime::parse_from_rfc3339(input)
.unwrap()
.with_timezone(&Utc),
)
}
fn target(ra: f64, dec: f64) -> SphericalDirection<EquatorialMeanJ2000> {
SphericalDirection::<EquatorialMeanJ2000>::new(Degrees::new(ra), Degrees::new(dec))
}
fn paranal() -> Geodetic<ECEF> {
observatories::EL_PARANAL.geodetic()
}
fn cta_s() -> Geodetic<ECEF> {
Geodetic::new_raw(
Degrees::new(-70.31634444444444),
Degrees::new(-24.683427777777776),
Meters::new(2184.6),
)
}
fn cta_n() -> Geodetic<ECEF> {
Geodetic::new_raw(
Degrees::new(-17.892005),
Degrees::new(28.762164),
Meters::new(2240.2),
)
}
struct CtaNorth;
impl SiteProfileTag for CtaNorth {
const NAME: &'static str = "ctao-north-planning";
}
struct CtaSouth;
impl SiteProfileTag for CtaSouth {
const NAME: &'static str = "ctao-south-planning";
}
fn ctao_north_planning() -> SiteProfileConfig {
SiteProfile::<CtaNorth>::planning(
Kilometers::new(2.2),
AtmosphericConditions::clear_sky_with_pressure(Hectopascals::new(770.0)).unwrap(),
"CTAO-North planning preset (test helper)",
)
.unwrap()
.erase()
}
fn ctao_south_planning() -> SiteProfileConfig {
SiteProfile::<CtaSouth>::planning(
Kilometers::new(2.1),
AtmosphericConditions::paranal_average(),
"CTAO-South planning preset (test helper)",
)
.unwrap()
.erase()
}
fn high_arctic(latitude_deg: f64) -> Geodetic<ECEF> {
Geodetic::new_raw(
Degrees::new(0.0),
Degrees::new(latitude_deg),
Meters::new(0.0),
)
}
fn synthetic_vertical_profile() -> VerticalEmissionProfile {
VerticalEmissionProfile::new(VerticalEmissionProfileDefinition {
schema_version: VERTICAL_EMISSION_PROFILE_SCHEMA_VERSION,
profile_id: "synthetic-airglow-integration-test".into(),
altitude_km: vec![
Kilometers::new(75.0),
Kilometers::new(85.0),
Kilometers::new(95.0),
Kilometers::new(110.0),
],
relative_emissivity: vec![0.0, 0.7, 1.0, 0.0],
normalization: VerticalProfileNormalization::UnitVerticalIntegral,
wavelength: AirglowWavelengthApplicability {
min: Nanometers::new(300.0),
max: Nanometers::new(650.0),
band: "synthetic-300-650-nm".into(),
},
assumptions: "synthetic broad optical layer for software integration tests".into(),
provenance: "deterministic NSB test fixture; not observational data".into(),
license: "CC0-1.0 synthetic fixture".into(),
validated_zenith: ValidatedZenithDomain {
min: Degrees::new(0.0),
max: Degrees::new(90.0),
},
})
.unwrap()
}
fn tt_mjd_to_utc(time: ModifiedJulianDate) -> Time<UTC> {
Time::<TT>::from(time).to::<UTC>()
}
fn night_phase_time(seed: Time<UTC>, location: Geodetic<ECEF>, phase: f64) -> Time<UTC> {
let night = super::temporal::test_support::astronomical_night(seed, location)
.expect("complete astronomical night");
let start = night.start.raw().value();
let end = night.end.raw().value();
tt_mjd_to_utc(ModifiedJulianDate::new(start + (end - start) * phase))
}
fn phase_at(seed: Time<UTC>, location: Geodetic<ECEF>, phase: f64) -> Option<AirglowNightPhase> {
super::temporal::test_support::night_phase(night_phase_time(seed, location, phase), location)
}
#[test]
fn solar_radio_flux_is_typed_and_changes_result() {
let location = paranal();
let time = t("2023-09-04T01:48:00Z");
let target = target(266.41683, -29.00781);
let low = super::model::test_support::standard_clear_sky(location)
.unwrap()
.with_solar_radio_flux(SolarFluxUnits::new(50.0))
.compute(time, target)
.unwrap();
let high = super::model::test_support::standard_clear_sky(location)
.unwrap()
.with_solar_radio_flux(SolarFluxUnits::new(250.0))
.compute(time, target)
.unwrap();
assert_ne!(low.integrated.value(), high.integrated.value());
}
#[test]
fn scale_changes_result() {
let location = paranal();
let time = t("2023-09-04T01:48:00Z");
let target = target(266.41683, -29.00781);
let base = super::model::test_support::standard_clear_sky(location)
.unwrap()
.compute(time, target)
.unwrap();
let scaled = super::model::test_support::standard_clear_sky(location)
.unwrap()
.with_scale(crate::units::ScaleFactors::new(2.0))
.compute(time, target)
.unwrap();
let ratio = scaled.integrated.value() / base.integrated.value();
assert!((ratio - 2.0).abs() < 1e-12);
}
#[test]
fn palace_month_changes_with_longitude_near_month_boundary() {
let time = t("2023-03-31T18:00:00Z");
let east = Geodetic::new_raw(Degrees::new(179.0), Degrees::new(0.0), Meters::new(0.0));
let west = Geodetic::new_raw(Degrees::new(-179.0), Degrees::new(0.0), Meters::new(0.0));
let east_season = super::temporal::palace_climatology_coordinates(time, east)
.unwrap()
.0;
let west_season = super::temporal::palace_climatology_coordinates(time, west)
.unwrap()
.0;
assert_ne!(east_season, west_season);
assert_eq!(west_season, 3);
assert_eq!(east_season, 4);
}
#[test]
fn site_profile_airglow_constructor_matches_profile_scale() {
let location = cta_n();
let time = t("2023-09-04T02:00:00Z");
let target = target(266.41683, -29.00781);
let profile = ctao_north_planning().resolve(location);
let from_profile =
super::model::test_support::for_site_profile(location, &ctao_north_planning())
.unwrap()
.compute(time, target)
.unwrap();
let explicit =
super::model::test_support::with_continuum(location, load_builtin_standard().unwrap())
.with_atmosphere(profile.atmosphere)
.with_scale(profile.airglow.scale)
.compute(time, target)
.unwrap();
assert_eq!(profile.airglow.scale, crate::units::ScaleFactors::new(1.0));
assert_eq!(from_profile.integrated.value(), explicit.integrated.value());
}
#[test]
fn cta_site_profile_airglow_results_are_site_sensitive() {
let target = target(266.41683, -29.00781);
let north = super::model::test_support::for_site_profile(cta_n(), &ctao_north_planning())
.unwrap()
.compute(t("2023-09-04T02:00:00Z"), target)
.unwrap();
let south = super::model::test_support::for_site_profile(cta_s(), &ctao_south_planning())
.unwrap()
.compute(t("2023-09-04T04:00:00Z"), target)
.unwrap();
assert!(north.integrated > BandPhotonRadiance::zero());
assert!(south.integrated > BandPhotonRadiance::zero());
assert_ne!(north.integrated.value(), south.integrated.value());
}
#[test]
fn geometry_selection_changes_only_the_geometry_multiplier() {
let location = Geodetic::new_raw(
Degrees::new(44.0),
Degrees::new(-11.0),
Meters::new(1_234.0),
);
let altitude = Degrees::new(15.0);
let zenith = Degrees::new(75.0);
let continuum = load_builtin_standard().unwrap();
let atmosphere = AtmosphericConditions::generic_clear_sky(location);
let van = AirglowGeometryModel::default();
let vertical = AirglowGeometryModel::VerticalProfile(synthetic_vertical_profile());
let van_factor = van.geometry_factor(location, zenith).unwrap().value();
let vertical_factor = vertical.geometry_factor(location, zenith).unwrap().value();
let evaluate = |geometry: AirglowGeometryModel| {
super::continuum::test_support::evaluate_continuum_with_night_phase(
&continuum,
t("2023-09-04T01:48:00Z"),
altitude,
super::continuum::AirglowEvaluationContext {
location,
atmosphere,
geometry: geometry.clone(),
solar_radio_flux: DEFAULT_SOLAR_RADIO_FLUX,
user_scale: crate::units::ScaleFactors::new(1.0),
},
AirglowNightPhase::FirstThird,
)
.unwrap()
};
let van_output = evaluate(van);
let vertical_output = evaluate(vertical);
let expected_ratio = vertical_factor / van_factor;
for ratio in [
vertical_output.integrated.value() / van_output.integrated.value(),
vertical_output.b_flux_s10.value() / van_output.b_flux_s10.value(),
vertical_output.v_flux_s10.value() / van_output.v_flux_s10.value(),
] {
assert!((ratio - expected_ratio).abs() < 1.0e-12);
}
let van_uncertainty = van_output.relative_uncertainty.unwrap();
let vertical_uncertainty = vertical_output.relative_uncertainty.unwrap();
assert!(
(van_uncertainty - vertical_uncertainty).abs() < 1.0e-15,
"multiplicative geometry must propagate through signal and absolute uncertainty consistently"
);
}
#[test]
fn vertical_profile_runs_through_normal_airglow_api_at_arbitrary_location() {
let location = Geodetic::new_raw(Degrees::new(18.4), Degrees::new(-33.9), Meters::new(120.0));
let model = super::model::test_support::standard_clear_sky(location)
.unwrap()
.with_geometry(AirglowGeometryModel::VerticalProfile(
synthetic_vertical_profile(),
));
assert_eq!(
super::model::test_support::geometry(&model).model_id(),
"vertical_profile"
);
let out = model
.compute(t("2023-06-21T22:00:00Z"), target(200.0, -45.0))
.unwrap();
assert!(out.integrated.value().is_finite() && out.integrated.value() >= 0.0);
}
#[test]
fn below_horizon_contract_clamps_to_horizon_for_both_geometry_models() {
let location = cta_s();
let continuum = load_builtin_standard().unwrap();
let atmosphere = AtmosphericConditions::generic_clear_sky(location);
for geometry in [
AirglowGeometryModel::default(),
AirglowGeometryModel::VerticalProfile(synthetic_vertical_profile()),
] {
let evaluate = |altitude| {
super::continuum::test_support::evaluate_continuum_with_night_phase(
&continuum,
t("2023-09-04T04:00:00Z"),
Degrees::new(altitude),
super::continuum::AirglowEvaluationContext {
location,
atmosphere,
geometry: geometry.clone(),
solar_radio_flux: DEFAULT_SOLAR_RADIO_FLUX,
user_scale: crate::units::ScaleFactors::new(1.0),
},
AirglowNightPhase::FirstThird,
)
.unwrap()
};
let horizon = evaluate(0.0);
let below = evaluate(-20.0);
assert_eq!(
horizon.integrated.value().to_bits(),
below.integrated.value().to_bits()
);
}
}
#[test]
fn night_phases_follow_cta_s_astronomical_night_phase() {
let location = cta_s();
let seed = t("2023-09-04T04:00:00Z");
assert_eq!(
phase_at(seed, location, 1.0 / 6.0),
Some(AirglowNightPhase::FirstThird)
);
assert_eq!(
phase_at(seed, location, 0.5),
Some(AirglowNightPhase::MiddleThird)
);
assert_eq!(
phase_at(seed, location, 5.0 / 6.0),
Some(AirglowNightPhase::LastThird)
);
}
#[test]
fn night_phases_follow_cta_n_astronomical_night_phase() {
let location = cta_n();
let seed = t("2023-09-04T02:00:00Z");
assert_eq!(
phase_at(seed, location, 1.0 / 6.0),
Some(AirglowNightPhase::FirstThird)
);
assert_eq!(
phase_at(seed, location, 0.5),
Some(AirglowNightPhase::MiddleThird)
);
assert_eq!(
phase_at(seed, location, 5.0 / 6.0),
Some(AirglowNightPhase::LastThird)
);
}
#[test]
fn twilight_edges_are_outside_airglow_calibration_domain() {
let location = cta_s();
let seed = t("2023-09-04T04:00:00Z");
let night = super::temporal::test_support::astronomical_night(seed, location)
.expect("complete astronomical night");
let one_minute_days = 1.0 / 1440.0;
let before_dusk = tt_mjd_to_utc(ModifiedJulianDate::new(
night.start.raw().value() - one_minute_days,
));
let after_dusk = tt_mjd_to_utc(ModifiedJulianDate::new(
night.start.raw().value() + one_minute_days,
));
let before_dawn = tt_mjd_to_utc(ModifiedJulianDate::new(
night.end.raw().value() - one_minute_days,
));
let after_dawn = tt_mjd_to_utc(ModifiedJulianDate::new(
night.end.raw().value() + one_minute_days,
));
assert_eq!(
super::temporal::test_support::night_phase(before_dusk, location),
None
);
assert_eq!(
super::temporal::test_support::night_phase(after_dusk, location),
Some(AirglowNightPhase::FirstThird)
);
assert_eq!(
super::temporal::test_support::night_phase(before_dawn, location),
Some(AirglowNightPhase::LastThird)
);
assert_eq!(
super::temporal::test_support::night_phase(after_dawn, location),
None
);
}
#[test]
fn polar_summer_without_astronomical_night_has_no_phase() {
assert_eq!(
super::temporal::test_support::night_phase(t("2023-06-21T12:00:00Z"), high_arctic(78.0)),
None
);
}
fn airglow_ctx(
location: Geodetic<ECEF>,
atmosphere: AtmosphericConditions,
) -> super::continuum::AirglowEvaluationContext {
airglow_ctx_with(
location,
atmosphere,
DEFAULT_SOLAR_RADIO_FLUX,
crate::units::ScaleFactors::new(1.0),
)
}
fn airglow_ctx_with(
location: Geodetic<ECEF>,
atmosphere: AtmosphericConditions,
solar_radio_flux: SolarFluxUnits,
user_scale: crate::units::ScaleFactors,
) -> super::continuum::AirglowEvaluationContext {
super::continuum::AirglowEvaluationContext {
location,
atmosphere,
geometry: AirglowGeometryModel::default(),
solar_radio_flux,
user_scale,
}
}
#[test]
fn polar_winter_astronomical_night_preserves_airglow() {
let location = high_arctic(89.0);
let time = t("2023-12-21T12:00:00Z");
assert!(super::temporal::test_support::night_phase(time, location).is_some());
let continuum = load_builtin_standard().unwrap();
let out = super::continuum::evaluate_continuum(
&continuum,
time,
Degrees::new(60.0),
airglow_ctx(
location,
AtmosphericConditions::generic_clear_sky(paranal()),
),
)
.unwrap();
assert!(out.integrated > BandPhotonRadiance::zero());
}
#[test]
fn valid_daytime_airglow_is_physical_zero_outside_astronomical_night() {
let continuum = load_builtin_standard().unwrap();
let out = super::continuum::evaluate_continuum(
&continuum,
t("2023-09-04T16:00:00Z"),
Degrees::new(60.0),
airglow_ctx(cta_s(), AtmosphericConditions::generic_clear_sky(cta_s())),
)
.unwrap();
assert_eq!(out.integrated, BandPhotonRadiance::zero());
assert_eq!(out.physical_outcome, AirglowPhysicalOutcome::PhysicalZero);
assert_eq!(
out.physical_zero_reason,
Some(AirglowPhysicalZeroReason::OutsideAstronomicalNight)
);
}
fn assert_out_of_range(err: crate::error::NsbError) {
assert!(
matches!(err, crate::error::NsbError::OutOfRange(_)),
"expected OutOfRange, got {err}"
);
}
#[test]
fn altitude_at_or_below_minus_ninety_is_out_of_range() {
let continuum = load_builtin_standard().unwrap();
let err = super::continuum::evaluate_continuum(
&continuum,
t("2023-09-04T01:48:00Z"),
Degrees::new(-90.0),
airglow_ctx(
paranal(),
AtmosphericConditions::generic_clear_sky(paranal()),
),
)
.expect_err("altitude <= -90 must fail");
assert_out_of_range(err);
}
#[test]
fn airglow_selection_helpers_expose_stable_identifiers() {
assert_eq!(AirglowSelection::Automatic.as_str(), "automatic");
assert_eq!(AirglowSelection::Automatic.requested_model(), None);
let explicit = AirglowSelection::Explicit(AirglowModel::ParanalPalaceV1);
assert_eq!(explicit.as_str(), "explicit");
assert_eq!(
explicit.requested_model(),
Some(AirglowModel::ParanalPalaceV1)
);
}
#[test]
fn invalid_altitude_errors_at_night_and_outside_astronomical_night() {
let continuum = load_builtin_standard().unwrap();
let ctx = airglow_ctx(
paranal(),
AtmosphericConditions::generic_clear_sky(paranal()),
);
for time in [t("2023-09-04T01:48:00Z"), t("2023-09-04T16:00:00Z")] {
let err = super::continuum::evaluate_continuum(
&continuum,
time,
Degrees::new(f64::NAN),
ctx.clone(),
)
.expect_err("invalid altitude must not silently become zero");
assert_out_of_range(err);
}
}
#[test]
fn invalid_f107_errors_at_night_and_outside_astronomical_night() {
let continuum = load_builtin_standard().unwrap();
let ctx = airglow_ctx_with(
paranal(),
AtmosphericConditions::generic_clear_sky(paranal()),
SolarFluxUnits::new(f64::NAN),
crate::units::ScaleFactors::new(1.0),
);
for time in [t("2023-09-04T01:48:00Z"), t("2023-09-04T16:00:00Z")] {
let err =
super::continuum::evaluate_continuum(&continuum, time, Degrees::new(60.0), ctx.clone())
.expect_err("invalid F10.7 must not silently become zero");
assert_out_of_range(err);
}
}
#[test]
fn invalid_scale_errors_at_night_and_outside_astronomical_night() {
let continuum = load_builtin_standard().unwrap();
let ctx = airglow_ctx_with(
paranal(),
AtmosphericConditions::generic_clear_sky(paranal()),
DEFAULT_SOLAR_RADIO_FLUX,
crate::units::ScaleFactors::new(f64::NEG_INFINITY),
);
for time in [t("2023-09-04T01:48:00Z"), t("2023-09-04T16:00:00Z")] {
let err =
super::continuum::evaluate_continuum(&continuum, time, Degrees::new(60.0), ctx.clone())
.expect_err("invalid scale must not silently become zero");
assert_out_of_range(err);
}
}
#[test]
fn palace_reference_solar_flux_is_100_sfu() {
let continuum = load_builtin_standard().unwrap();
let reference = continuum.sample(250, 9, 2, 100.0).unwrap().0;
let repeated = continuum.sample(250, 9, 2, 100.0).unwrap().0;
assert_eq!(reference.to_bits(), repeated.to_bits());
}
#[test]
fn high_zenith_target_differs_from_zenith_due_to_geometry_and_scattering() {
let location = paranal();
let time = t("2023-09-04T01:48:00Z");
let zenith_target = target(266.41683, -29.00781);
let low_altitude_target = target(80.0, -20.0);
let model = super::model::test_support::standard_clear_sky(location).unwrap();
let zenith = model.compute(time, zenith_target).unwrap();
let low = model.compute(time, low_altitude_target).unwrap();
assert_ne!(
zenith.integrated.value(),
low.integrated.value(),
"geometry and scattering stack must change between targets"
);
}
#[test]
fn site_profile_atmosphere_changes_airglow_at_fixed_geometry() {
let location = paranal();
let time = t("2023-09-04T01:48:00Z");
let target = target(266.41683, -29.00781);
let continuum = load_builtin_standard().unwrap();
let low_pressure = super::model::test_support::with_continuum(location, continuum.clone())
.with_atmosphere(AtmosphericConditions {
surface_pressure: siderust::qtty::Hectopascals::new(600.0),
..AtmosphericConditions::paranal_average()
})
.compute(time, target)
.unwrap();
let high_pressure = super::model::test_support::with_continuum(location, continuum)
.with_atmosphere(AtmosphericConditions {
surface_pressure: siderust::qtty::Hectopascals::new(900.0),
..AtmosphericConditions::paranal_average()
})
.compute(time, target)
.unwrap();
assert_ne!(
low_pressure.integrated.value(),
high_pressure.integrated.value()
);
}
#[test]
fn regression_noll_geometry_reference_values() {
#[allow(clippy::approx_constant)] let references = [
(0.0, 1.0, -0.146, -0.318),
(
30.0,
1.149_349_365_080_909,
-0.045_105_511_442_811,
-0.213_297_031_647_063,
),
(
60.0,
1.920_946_875_988_246,
0.327_187_126_765_308,
0.173_048_594_102_764,
),
(
75.0,
3.277_162_524_289_61,
0.714_366_128_417_269,
0.574_842_501_149_617,
),
];
for (zenith_deg, expected_x, expected_fr, expected_fm) in references {
let zenith = Degrees::new(zenith_deg);
let x = effective_airglow_airmass(zenith);
let (f_r, f_m) = noll_scattering_factors(zenith);
assert!((x - expected_x).abs() < 1e-12, "X_ag at z={zenith_deg}");
assert!((f_r - expected_fr).abs() < 1e-12, "f_R at z={zenith_deg}");
assert!((f_m - expected_fm).abs() < 1e-12, "f_M at z={zenith_deg}");
}
}
#[test]
fn spectral_extinction_differs_from_unextincted_baseline_integral() {
let continuum = load_builtin_standard().unwrap();
let location = paranal();
let atmosphere = AtmosphericConditions::paranal_average();
let spectral = super::continuum::integrate_attenuated_continuum(
&continuum,
t("2023-09-04T01:48:00Z"),
Degrees::new(30.0),
&airglow_ctx_with(
location,
atmosphere,
DEFAULT_SOLAR_RADIO_FLUX,
crate::units::ScaleFactors::new(1.0),
),
)
.unwrap();
assert!(spectral.integrated.value().is_finite());
assert!(spectral.integrated > BandPhotonRadiance::zero());
}
#[test]
fn regression_paranal_integrated_values_at_representative_zeniths() {
let location = paranal();
let time = t("2023-09-04T01:48:00Z");
let query_target = target(266.41683, -29.00781);
let model = super::model::test_support::for_site_profile(location, &ctao_south_planning())
.unwrap()
.compute(time, query_target)
.unwrap();
assert!(
(model.integrated.value() - 0.110_457_872_401_343_15).abs() < 1e-10,
"zenith reference changed: {}",
model.integrated.value()
);
let low = super::model::test_support::for_site_profile(location, &ctao_south_planning())
.unwrap()
.compute(time, target(80.0, -20.0))
.unwrap();
assert!(
(low.integrated.value() - 0.194_011_832_826_166_6).abs() < 1e-10,
"30° zenith reference changed: {}",
low.integrated.value()
);
}
#[test]
fn integrated_only_matches_full_path_across_profiles_seasons_phases_and_fluxes() {
let continuum = load_builtin_standard().unwrap();
for (location, profile_config) in [
(
cta_s(),
SiteProfile::<GenericClearSky>::generic_clear_sky().erase(),
),
(cta_s(), ctao_south_planning()),
(cta_n(), ctao_north_planning()),
] {
let profile_id = profile_config.name();
let profile = profile_config.resolve(location);
for time in [t("2023-01-15T05:00:00Z"), t("2023-07-15T05:00:00Z")] {
for phase in [
AirglowNightPhase::FirstThird,
AirglowNightPhase::MiddleThird,
AirglowNightPhase::LastThird,
] {
for altitude in [0.0, 30.0, 60.0, 90.0] {
for flux in [50.0, 130.0, 250.0] {
let context = || super::continuum::AirglowEvaluationContext {
location,
atmosphere: profile.atmosphere,
geometry: AirglowGeometryModel::default(),
solar_radio_flux: SolarFluxUnits::new(flux),
user_scale: profile.airglow.scale,
};
let full =
super::continuum::test_support::evaluate_continuum_with_night_phase(
&continuum,
time,
Degrees::new(altitude),
context(),
phase,
)
.unwrap()
.integrated;
let integrated =
super::continuum::evaluate_integrated_continuum_with_night_phase(
&continuum,
time,
Degrees::new(altitude),
context(),
phase,
)
.unwrap();
assert!(
(full.value() - integrated.value()).abs() <= 1.0e-12,
"integrated mismatch: profile={profile_id}, time={time:?}, phase={phase:?}, altitude={altitude}, flux={flux}, full={full:?}, integrated={integrated:?}"
);
}
}
}
}
}
}