use dualis_units::{
Density, Diffusivity, HeatCapacity, Length, Mass, Pressure, SpecificHeat, Temperature,
ThermalConductivity, ThermalExpansion, Velocity, Volume,
};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Substance {
pub name: String,
pub density: Density,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thermal: Option<ThermalProps>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mechanical: Option<MechanicalProps>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub acoustic: Option<AcousticProps>,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct ThermalProps {
pub conductivity: ThermalConductivity,
pub specific_heat: SpecificHeat,
pub expansion: ThermalExpansion,
pub emissivity: f64,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct MechanicalProps {
pub youngs_modulus: Pressure,
pub poisson_ratio: f64,
pub yield_strength: Pressure,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct AcousticProps {
pub sound_speed: Velocity,
}
impl Substance {
pub fn diffusivity(&self) -> Option<Diffusivity> {
let t = self.thermal?;
Some(Diffusivity::from_si(
t.conductivity.to_si() / (self.density.to_si() * t.specific_heat.to_si()),
))
}
pub fn heat_capacity(&self, volume: Volume) -> Option<HeatCapacity> {
let t = self.thermal?;
Some(self.mass_of(volume) * t.specific_heat)
}
pub fn mass_of(&self, volume: Volume) -> Mass {
self.density * volume
}
pub fn expansion_of(&self, length: Length, rise: Temperature) -> Option<Length> {
let t = self.thermal?;
Some(Length::from_si(
length.to_si() * t.expansion.to_si() * rise.to_si(),
))
}
pub fn constrained_stress(&self, rise: Temperature) -> Option<Pressure> {
let t = self.thermal?;
let m = self.mechanical?;
Some(Pressure::from_si(
m.youngs_modulus.to_si() * t.expansion.to_si() * rise.to_si(),
))
}
pub fn survives(&self, rise: Temperature) -> Option<bool> {
let stress = self.constrained_stress(rise)?;
let limit = self.mechanical?.yield_strength;
Some(stress < limit)
}
pub fn borosilicate_crown() -> Substance {
Substance {
name: "N-BK7".to_string(),
density: Density::g_per_cm3(2.51),
thermal: Some(ThermalProps {
conductivity: ThermalConductivity::w_per_m_k(1.114),
specific_heat: SpecificHeat::j_per_kg_k(858.0),
expansion: ThermalExpansion::ppm_per_k(7.1),
emissivity: 0.90,
}),
mechanical: Some(MechanicalProps {
youngs_modulus: Pressure::from_si(82.0e9),
poisson_ratio: 0.206,
yield_strength: Pressure::from_si(60.0e6),
}),
acoustic: Some(AcousticProps {
sound_speed: Velocity::m_per_s(5_680.0),
}),
}
}
pub fn aluminium_6061() -> Substance {
Substance {
name: "Al 6061".to_string(),
density: Density::g_per_cm3(2.70),
thermal: Some(ThermalProps {
conductivity: ThermalConductivity::w_per_m_k(167.0),
specific_heat: SpecificHeat::j_per_kg_k(896.0),
expansion: ThermalExpansion::ppm_per_k(23.6),
emissivity: 0.09,
}),
mechanical: Some(MechanicalProps {
youngs_modulus: Pressure::from_si(68.9e9),
poisson_ratio: 0.33,
yield_strength: Pressure::from_si(276.0e6),
}),
acoustic: Some(AcousticProps {
sound_speed: Velocity::m_per_s(6_320.0),
}),
}
}
pub fn water() -> Substance {
Substance {
name: "water".to_string(),
density: Density::g_per_cm3(0.998),
thermal: Some(ThermalProps {
conductivity: ThermalConductivity::w_per_m_k(0.598),
specific_heat: SpecificHeat::j_per_kg_k(4_182.0),
expansion: ThermalExpansion::ppm_per_k(69.0),
emissivity: 0.96,
}),
mechanical: None,
acoustic: Some(AcousticProps {
sound_speed: Velocity::m_per_s(1_482.0),
}),
}
}
pub fn bulk(name: &str, density: Density) -> Substance {
Substance {
name: name.to_string(),
density,
thermal: None,
mechanical: None,
acoustic: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use dualis_units::Length;
#[test]
fn diffusivity_matches_the_published_figures() {
let glass = Substance::borosilicate_crown().diffusivity().unwrap();
let metal = Substance::aluminium_6061().diffusivity().unwrap();
assert!(
(glass.to_si() - 5.17e-7).abs() < 1e-8,
"N-BK7 diffusivity {glass:?}"
);
assert!(
(metal.to_si() - 6.9e-5).abs() < 1e-6,
"aluminium diffusivity {metal:?}"
);
assert!(metal.to_si() / glass.to_si() > 100.0);
}
#[test]
fn stability_limits_differ_by_two_orders_of_magnitude() {
let cell = Length::mm(1.0);
let limit = |s: &Substance| {
let a = s.diffusivity().unwrap().to_si();
cell.to_si() * cell.to_si() / (2.0 * a)
};
let glass = limit(&Substance::borosilicate_crown());
let metal = limit(&Substance::aluminium_6061());
assert!((glass - 0.97).abs() < 0.1, "glass limit {glass} s");
assert!((metal - 0.0072).abs() < 0.001, "metal limit {metal} s");
assert!(glass / metal > 100.0);
}
#[test]
fn a_lens_sized_piece_holds_a_few_joules_per_kelvin() {
let glass = Substance::borosilicate_crown();
let volume = Volume::from_si(std::f64::consts::PI * (0.0125f64).powi(2) * 0.005);
let mass = glass.mass_of(volume);
assert!((mass.to_si() * 1e3 - 6.16).abs() < 0.05, "{mass:?}");
let capacity = glass.heat_capacity(volume).unwrap();
assert!((capacity.to_si() - 5.28).abs() < 0.05, "{capacity:?}");
}
#[test]
fn constrained_expansion_breaks_glass_before_metal() {
let glass = Substance::borosilicate_crown();
let metal = Substance::aluminium_6061();
let growth = glass
.expansion_of(Length::mm(100.0), Temperature::from_si(20.0))
.unwrap();
assert!((growth.in_um() - 14.2).abs() < 0.1, "{growth:?}");
let stress = glass
.constrained_stress(Temperature::from_si(20.0))
.unwrap();
assert!((stress.to_si() / 1e6 - 11.6).abs() < 0.2, "{stress:?}");
assert_eq!(glass.survives(Temperature::from_si(20.0)), Some(true));
assert_eq!(glass.survives(Temperature::from_si(120.0)), Some(false));
assert_eq!(metal.survives(Temperature::from_si(120.0)), Some(true));
}
#[test]
fn unknown_properties_are_absent_not_guessed() {
let unknown = Substance::bulk("unobtainium", Density::g_per_cm3(19.0));
assert_eq!(unknown.diffusivity(), None);
assert_eq!(unknown.heat_capacity(Volume::from_si(1e-6)), None);
assert_eq!(unknown.survives(Temperature::from_si(50.0)), None);
assert!((unknown.mass_of(Volume::from_si(1e-6)).to_si() - 0.019).abs() < 1e-9);
assert_eq!(
Substance::water().constrained_stress(Temperature::from_si(10.0)),
None
);
assert!(Substance::water().diffusivity().is_some());
}
#[test]
fn substances_round_trip_through_json() {
let glass = Substance::borosilicate_crown();
let json = serde_json::to_string(&glass).unwrap();
assert_eq!(serde_json::from_str::<Substance>(&json).unwrap(), glass);
let plain = Substance::bulk("x", Density::kg_per_m3(1.0));
let json = serde_json::to_string(&plain).unwrap();
assert!(!json.contains("thermal"), "{json}");
}
}