use serde::{Deserialize, Serialize};
use crate::constants;
use crate::star::SpectralClass;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct HrPosition {
pub temperature_k: f64,
pub absolute_magnitude: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum LuminosityClass {
Ia,
Ib,
II,
III,
IV,
V,
VI,
VII,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum HrRegion {
MainSequenceHot,
MainSequenceMid,
MainSequenceCool,
RedGiantBranch,
HorizontalBranch,
AsymptoticGiantBranch,
WhiteDwarfRegion,
Supergiant,
}
#[must_use]
pub fn spectral_class_from_temperature(temperature_k: f64) -> SpectralClass {
if temperature_k >= constants::T_W_MIN {
SpectralClass::W
} else if temperature_k >= constants::T_O_MIN {
SpectralClass::O
} else if temperature_k >= constants::T_B_MIN {
SpectralClass::B
} else if temperature_k >= constants::T_A_MIN {
SpectralClass::A
} else if temperature_k >= constants::T_F_MIN {
SpectralClass::F
} else if temperature_k >= constants::T_G_MIN {
SpectralClass::G
} else if temperature_k >= constants::T_K_MIN {
SpectralClass::K
} else if temperature_k >= constants::T_M_MIN {
SpectralClass::M
} else if temperature_k >= constants::T_L_MIN {
SpectralClass::L
} else if temperature_k >= constants::T_T_MIN {
SpectralClass::T
} else {
SpectralClass::Y
}
}
#[must_use]
pub fn spectral_subclass(temperature_k: f64) -> u8 {
let (t_max, t_min) = class_temp_range(spectral_class_from_temperature(temperature_k));
if t_max <= t_min {
return 0;
}
let frac = (t_max - temperature_k) / (t_max - t_min);
let sub = (frac * 10.0).clamp(0.0, 9.0) as u8;
sub.min(9)
}
#[must_use]
fn class_temp_range(class: SpectralClass) -> (f64, f64) {
match class {
SpectralClass::W => (100_000.0, constants::T_W_MIN),
SpectralClass::O => (constants::T_W_MIN, constants::T_O_MIN),
SpectralClass::B => (constants::T_O_MIN, constants::T_B_MIN),
SpectralClass::A => (constants::T_B_MIN, constants::T_A_MIN),
SpectralClass::F => (constants::T_A_MIN, constants::T_F_MIN),
SpectralClass::G => (constants::T_F_MIN, constants::T_G_MIN),
SpectralClass::K => (constants::T_G_MIN, constants::T_K_MIN),
SpectralClass::M => (constants::T_K_MIN, constants::T_M_MIN),
SpectralClass::L => (constants::T_M_MIN, constants::T_L_MIN),
SpectralClass::T => (constants::T_L_MIN, constants::T_T_MIN),
SpectralClass::Y => (constants::T_T_MIN, 250.0),
}
}
#[must_use]
pub fn format_classification(temperature_k: f64, luminosity_class: LuminosityClass) -> String {
let class = spectral_class_from_temperature(temperature_k);
let sub = spectral_subclass(temperature_k);
let lc = match luminosity_class {
LuminosityClass::Ia => "Ia",
LuminosityClass::Ib => "Ib",
LuminosityClass::II => "II",
LuminosityClass::III => "III",
LuminosityClass::IV => "IV",
LuminosityClass::V => "V",
LuminosityClass::VI => "VI",
LuminosityClass::VII => "VII",
};
format!("{class}{sub}{lc}")
}
#[must_use]
pub fn luminosity_class_from_log_g(log_g: f64) -> LuminosityClass {
if log_g < 1.0 {
LuminosityClass::Ia
} else if log_g < 2.0 {
LuminosityClass::II
} else if log_g < 3.5 {
LuminosityClass::III
} else if log_g < 4.0 {
LuminosityClass::IV
} else if log_g < 5.5 {
LuminosityClass::V
} else if log_g < 7.0 {
LuminosityClass::VI
} else {
LuminosityClass::VII
}
}
#[must_use]
pub fn hr_region(temperature_k: f64, luminosity_solar: f64) -> HrRegion {
let abs_mag = crate::luminosity::absolute_bolometric_magnitude(luminosity_solar);
if abs_mag > 10.0 {
return HrRegion::WhiteDwarfRegion;
}
if abs_mag < -4.0 {
return HrRegion::Supergiant;
}
if temperature_k < 5500.0 && abs_mag < 2.0 {
if abs_mag < 0.0 {
return HrRegion::AsymptoticGiantBranch;
}
return HrRegion::RedGiantBranch;
}
if abs_mag > -1.0 && abs_mag < 2.0 && temperature_k > 5500.0 && temperature_k < 8000.0 {
return HrRegion::HorizontalBranch;
}
if temperature_k >= 10_000.0 {
HrRegion::MainSequenceHot
} else if temperature_k >= 3700.0 {
HrRegion::MainSequenceMid
} else {
HrRegion::MainSequenceCool
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hr_position_serde_roundtrip() {
let pos = HrPosition {
temperature_k: 5772.0,
absolute_magnitude: 4.83,
};
let json = serde_json::to_string(&pos).unwrap();
let back: HrPosition = serde_json::from_str(&json).unwrap();
assert!((back.temperature_k - 5772.0).abs() < f64::EPSILON);
assert!((back.absolute_magnitude - 4.83).abs() < f64::EPSILON);
}
#[test]
fn sun_spectral_class() {
let class = spectral_class_from_temperature(constants::T_SUN);
assert_eq!(class, SpectralClass::G);
}
#[test]
fn hot_star_class() {
assert_eq!(spectral_class_from_temperature(35_000.0), SpectralClass::O);
assert_eq!(spectral_class_from_temperature(15_000.0), SpectralClass::B);
}
#[test]
fn cool_star_class() {
assert_eq!(spectral_class_from_temperature(3000.0), SpectralClass::M);
assert_eq!(spectral_class_from_temperature(4000.0), SpectralClass::K);
}
#[test]
fn wolf_rayet_class() {
assert_eq!(spectral_class_from_temperature(60_000.0), SpectralClass::W);
assert_eq!(spectral_class_from_temperature(50_000.0), SpectralClass::W);
}
#[test]
fn brown_dwarf_classes() {
assert_eq!(spectral_class_from_temperature(1_800.0), SpectralClass::L);
assert_eq!(spectral_class_from_temperature(1_000.0), SpectralClass::T);
assert_eq!(spectral_class_from_temperature(400.0), SpectralClass::Y);
}
#[test]
fn solar_subclass() {
let sub = spectral_subclass(constants::T_SUN);
assert_eq!(sub, 2, "Solar subclass: G{sub}, expected G2");
}
#[test]
fn solar_classification_string() {
let s = format_classification(constants::T_SUN, LuminosityClass::V);
assert_eq!(s, "G2V");
}
#[test]
fn solar_luminosity_class() {
let lc = luminosity_class_from_log_g(4.44);
assert_eq!(lc, LuminosityClass::V);
}
#[test]
fn giant_luminosity_class() {
let lc = luminosity_class_from_log_g(2.5);
assert_eq!(lc, LuminosityClass::III);
}
#[test]
fn white_dwarf_luminosity_class() {
let lc = luminosity_class_from_log_g(8.0);
assert_eq!(lc, LuminosityClass::VII);
}
#[test]
fn sun_hr_region() {
let region = hr_region(constants::T_SUN, 1.0);
assert_eq!(region, HrRegion::MainSequenceMid);
}
#[test]
fn luminosity_class_serde_roundtrip() {
for lc in [
LuminosityClass::Ia,
LuminosityClass::Ib,
LuminosityClass::II,
LuminosityClass::III,
LuminosityClass::IV,
LuminosityClass::V,
LuminosityClass::VI,
LuminosityClass::VII,
] {
let json = serde_json::to_string(&lc).unwrap();
let back: LuminosityClass = serde_json::from_str(&json).unwrap();
assert_eq!(back, lc);
}
}
#[test]
fn hr_region_serde_roundtrip() {
for r in [
HrRegion::MainSequenceHot,
HrRegion::MainSequenceMid,
HrRegion::MainSequenceCool,
HrRegion::RedGiantBranch,
HrRegion::WhiteDwarfRegion,
HrRegion::Supergiant,
] {
let json = serde_json::to_string(&r).unwrap();
let back: HrRegion = serde_json::from_str(&json).unwrap();
assert_eq!(back, r);
}
}
}