use std::fmt;
use serde::{Deserialize, Serialize};
use crate::classification::LuminosityClass;
use crate::error::{Result, TaraError};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Star {
pub mass_solar: f64,
pub radius_solar: f64,
pub temperature_k: f64,
pub luminosity_solar: f64,
pub age_years: f64,
pub spectral_class: SpectralClass,
pub spectral_subclass: u8,
pub luminosity_class: LuminosityClass,
pub metallicity: f64,
}
impl Star {
#[must_use = "returns a Result containing the new Star"]
pub fn new(
mass_solar: f64,
radius_solar: f64,
temperature_k: f64,
luminosity_solar: f64,
age_years: f64,
spectral_class: SpectralClass,
) -> Result<Self> {
Self::builder(
mass_solar,
radius_solar,
temperature_k,
luminosity_solar,
age_years,
)
.spectral_class(spectral_class)
.build()
}
#[must_use]
pub fn builder(
mass_solar: f64,
radius_solar: f64,
temperature_k: f64,
luminosity_solar: f64,
age_years: f64,
) -> StarBuilder {
StarBuilder {
mass_solar,
radius_solar,
temperature_k,
luminosity_solar,
age_years,
spectral_class: None,
spectral_subclass: None,
luminosity_class: LuminosityClass::V,
metallicity: 0.0,
}
}
pub fn sun() -> Result<Self> {
Self::builder(1.0, 1.0, crate::constants::T_SUN, 1.0, 4.6e9)
.luminosity_class(LuminosityClass::V)
.build()
}
}
pub struct StarBuilder {
mass_solar: f64,
radius_solar: f64,
temperature_k: f64,
luminosity_solar: f64,
age_years: f64,
spectral_class: Option<SpectralClass>,
spectral_subclass: Option<u8>,
luminosity_class: LuminosityClass,
metallicity: f64,
}
impl StarBuilder {
#[must_use]
pub fn spectral_class(mut self, class: SpectralClass) -> Self {
self.spectral_class = Some(class);
self
}
#[must_use]
pub fn spectral_subclass(mut self, sub: u8) -> Self {
self.spectral_subclass = Some(sub.min(9));
self
}
#[must_use]
pub fn luminosity_class(mut self, lc: LuminosityClass) -> Self {
self.luminosity_class = lc;
self
}
#[must_use]
pub fn metallicity(mut self, feh: f64) -> Self {
self.metallicity = feh;
self
}
pub fn build(self) -> Result<Star> {
if self.mass_solar <= 0.0 {
let err = TaraError::InvalidParameter(format!(
"mass_solar must be positive, got {}",
self.mass_solar
));
tracing::warn!(mass_solar = self.mass_solar, "{err}");
return Err(err);
}
if self.radius_solar <= 0.0 {
let err = TaraError::InvalidParameter(format!(
"radius_solar must be positive, got {}",
self.radius_solar
));
tracing::warn!(radius_solar = self.radius_solar, "{err}");
return Err(err);
}
if self.temperature_k <= 0.0 {
let err = TaraError::InvalidParameter(format!(
"temperature_k must be positive, got {}",
self.temperature_k
));
tracing::warn!(temperature_k = self.temperature_k, "{err}");
return Err(err);
}
if self.luminosity_solar <= 0.0 {
let err = TaraError::InvalidParameter(format!(
"luminosity_solar must be positive, got {}",
self.luminosity_solar
));
tracing::warn!(luminosity_solar = self.luminosity_solar, "{err}");
return Err(err);
}
if self.age_years < 0.0 {
let err = TaraError::InvalidParameter(format!(
"age_years must be non-negative, got {}",
self.age_years
));
tracing::warn!(age_years = self.age_years, "{err}");
return Err(err);
}
let spectral_class = self.spectral_class.unwrap_or_else(|| {
crate::classification::spectral_class_from_temperature(self.temperature_k)
});
let spectral_subclass = self
.spectral_subclass
.unwrap_or_else(|| crate::classification::spectral_subclass(self.temperature_k));
tracing::debug!(
mass = self.mass_solar,
radius = self.radius_solar,
temp = self.temperature_k,
class = %spectral_class,
sub = spectral_subclass,
"star constructed"
);
Ok(Star {
mass_solar: self.mass_solar,
radius_solar: self.radius_solar,
temperature_k: self.temperature_k,
luminosity_solar: self.luminosity_solar,
age_years: self.age_years,
spectral_class,
spectral_subclass,
luminosity_class: self.luminosity_class,
metallicity: self.metallicity,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum SpectralClass {
W,
O,
B,
A,
F,
G,
K,
M,
L,
T,
Y,
}
impl fmt::Display for SpectralClass {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::W => write!(f, "W"),
Self::O => write!(f, "O"),
Self::B => write!(f, "B"),
Self::A => write!(f, "A"),
Self::F => write!(f, "F"),
Self::G => write!(f, "G"),
Self::K => write!(f, "K"),
Self::M => write!(f, "M"),
Self::L => write!(f, "L"),
Self::T => write!(f, "T"),
Self::Y => write!(f, "Y"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn star_serde_roundtrip() {
let star = Star::new(1.0, 1.0, 5772.0, 1.0, 4.6e9, SpectralClass::G).unwrap();
let json = serde_json::to_string(&star).unwrap();
let back: Star = serde_json::from_str(&json).unwrap();
assert!((back.mass_solar - 1.0).abs() < f64::EPSILON);
assert!((back.temperature_k - 5772.0).abs() < f64::EPSILON);
assert_eq!(back.spectral_class, SpectralClass::G);
}
#[test]
fn spectral_class_serde_roundtrip() {
for class in [
SpectralClass::W,
SpectralClass::O,
SpectralClass::B,
SpectralClass::A,
SpectralClass::F,
SpectralClass::G,
SpectralClass::K,
SpectralClass::M,
SpectralClass::L,
SpectralClass::T,
SpectralClass::Y,
] {
let json = serde_json::to_string(&class).unwrap();
let back: SpectralClass = serde_json::from_str(&json).unwrap();
assert_eq!(back, class);
}
}
#[test]
fn star_rejects_negative_mass() {
assert!(Star::new(-1.0, 1.0, 5772.0, 1.0, 0.0, SpectralClass::G).is_err());
}
#[test]
fn star_rejects_negative_age() {
assert!(Star::new(1.0, 1.0, 5772.0, 1.0, -1.0, SpectralClass::G).is_err());
}
#[test]
fn sun_convenience() {
let sun = Star::sun().unwrap();
assert_eq!(sun.spectral_class, SpectralClass::G);
assert_eq!(sun.spectral_subclass, 2);
assert_eq!(sun.luminosity_class, LuminosityClass::V);
assert!((sun.metallicity).abs() < f64::EPSILON);
}
#[test]
fn builder_auto_classifies() {
let star = Star::builder(1.0, 1.0, 5772.0, 1.0, 4.6e9).build().unwrap();
assert_eq!(star.spectral_class, SpectralClass::G);
assert_eq!(star.spectral_subclass, 2);
}
#[test]
fn builder_override_class() {
let star = Star::builder(1.0, 1.0, 5772.0, 1.0, 4.6e9)
.spectral_class(SpectralClass::F)
.spectral_subclass(5)
.luminosity_class(LuminosityClass::IV)
.metallicity(-0.5)
.build()
.unwrap();
assert_eq!(star.spectral_class, SpectralClass::F);
assert_eq!(star.spectral_subclass, 5);
assert_eq!(star.luminosity_class, LuminosityClass::IV);
assert!((star.metallicity - (-0.5)).abs() < f64::EPSILON);
}
}