use crate::substance::{FusionProps, Substance, ThermalProps};
use pantometry_units::{
Density, HeatCapacity, LatentHeat, Mass, Pressure, SpecificHeat, Temperature,
ThermalConductivity, ThermalExpansion, Volume,
};
#[derive(Clone, Debug, PartialEq)]
pub struct Mix {
parts: Vec<(Substance, f64)>,
}
impl Mix {
pub fn of(parts: &[(Substance, f64)]) -> Result<Mix, String> {
if parts.is_empty() {
return Err("a mixture needs at least one substance".to_string());
}
let mut total = 0.0;
for (s, f) in parts {
if !(f.is_finite() && *f > 0.0) {
return Err(format!(
"{}: a volume fraction must be finite and positive, is {f}",
s.name
));
}
total += f;
}
if (total - 1.0).abs() > 1e-9 {
return Err(format!(
"volume fractions must sum to 1, sum to {total} — they are not normalised for you, \
because 45% and 50% is a transcription mistake and not a request for 47.4% and 52.6%"
));
}
Ok(Mix {
parts: parts.to_vec(),
})
}
pub fn parts(&self) -> &[(Substance, f64)] {
&self.parts
}
pub fn density(&self) -> Density {
Density::from_si(
self.parts
.iter()
.map(|(s, f)| f * s.density.to_si())
.sum::<f64>(),
)
}
pub fn mass_fraction(&self, i: usize) -> Option<f64> {
let (s, f) = self.parts.get(i)?;
Some(f * s.density.to_si() / self.density().to_si())
}
pub fn specific_heat(&self) -> Option<SpecificHeat> {
let mut volumetric = 0.0;
for (s, f) in &self.parts {
let t = s.thermal?;
volumetric += f * s.density.to_si() * t.specific_heat.to_si();
}
Some(SpecificHeat::from_si(volumetric / self.density().to_si()))
}
pub fn heat_capacity(&self, volume: Volume) -> Option<HeatCapacity> {
Some(HeatCapacity::from_si(
volume.to_si() * self.density().to_si() * self.specific_heat()?.to_si(),
))
}
pub fn mass_of(&self, volume: Volume) -> Mass {
Mass::from_si(volume.to_si() * self.density().to_si())
}
pub fn conductivity_bounds(&self) -> Option<(ThermalConductivity, ThermalConductivity)> {
let mut voigt = 0.0;
let mut reciprocal = 0.0;
for (s, f) in &self.parts {
let k = s.thermal?.conductivity.to_si();
voigt += f * k;
reciprocal += f / k;
}
Some((
ThermalConductivity::from_si(1.0 / reciprocal),
ThermalConductivity::from_si(voigt),
))
}
pub fn hashin_shtrikman(&self) -> Option<(ThermalConductivity, ThermalConductivity)> {
if self.parts.len() != 2 {
return None;
}
let (k0, f0) = (
self.parts[0].0.thermal?.conductivity.to_si(),
self.parts[0].1,
);
let (k1, f1) = (
self.parts[1].0.thermal?.conductivity.to_si(),
self.parts[1].1,
);
if k0 == k1 {
return Some((
ThermalConductivity::from_si(k0),
ThermalConductivity::from_si(k0),
));
}
let bound = |host: f64, host_f: f64, guest: f64, guest_f: f64| {
host + guest_f / (1.0 / (guest - host) + host_f / (3.0 * host))
};
let (hi_host, hi_f, hi_guest, hi_gf) = if k0 > k1 {
(k0, f0, k1, f1)
} else {
(k1, f1, k0, f0)
};
Some((
ThermalConductivity::from_si(bound(hi_guest, hi_gf, hi_host, hi_f)),
ThermalConductivity::from_si(bound(hi_host, hi_f, hi_guest, hi_gf)),
))
}
pub fn shear_bounds(&self) -> Option<(Pressure, Pressure)> {
self.moduli_bounds(|e, nu| e / (2.0 * (1.0 + nu)))
}
pub fn bulk_bounds(&self) -> Option<(Pressure, Pressure)> {
self.moduli_bounds(|e, nu| e / (3.0 * (1.0 - 2.0 * nu)))
}
pub fn shear_hashin_shtrikman(&self) -> Option<(Pressure, Pressure)> {
self.hashin_shtrikman_pair(false)
}
pub fn bulk_hashin_shtrikman(&self) -> Option<(Pressure, Pressure)> {
self.hashin_shtrikman_pair(true)
}
fn hashin_shtrikman_pair(&self, bulk: bool) -> Option<(Pressure, Pressure)> {
if self.parts.len() != 2 {
return None;
}
let of = |i: usize| -> Option<(f64, f64, f64)> {
let m = self.parts[i].0.mechanical?;
let (e, nu) = (m.youngs_modulus.to_si(), m.poisson_ratio);
Some((
e / (3.0 * (1.0 - 2.0 * nu)),
e / (2.0 * (1.0 + nu)),
self.parts[i].1,
))
};
let (a, b) = (of(0)?, of(1)?);
let bound = |r: (f64, f64, f64), o: (f64, f64, f64)| {
let (kr, gr, fr) = r;
let (ko, go, fo) = o;
if bulk {
kr + fo / (1.0 / (ko - kr) + 3.0 * fr / (3.0 * kr + 4.0 * gr))
} else {
gr + fo
/ (1.0 / (go - gr)
+ 6.0 * fr * (kr + 2.0 * gr) / (5.0 * gr * (3.0 * kr + 4.0 * gr)))
}
};
let (ma, mb) = if bulk { (a.0, b.0) } else { (a.1, b.1) };
if ma == mb {
return Some((Pressure::from_si(ma), Pressure::from_si(ma)));
}
let (one, other) = (bound(a, b), bound(b, a));
Some((
Pressure::from_si(one.min(other)),
Pressure::from_si(one.max(other)),
))
}
pub fn p_wave_modulus_bounds(&self) -> Option<(Pressure, Pressure)> {
self.moduli_bounds(|e, nu| e * (1.0 - nu) / ((1.0 + nu) * (1.0 - 2.0 * nu)))
}
fn moduli_bounds(&self, modulus: impl Fn(f64, f64) -> f64) -> Option<(Pressure, Pressure)> {
let mut voigt = 0.0;
let mut reciprocal = 0.0;
for (s, f) in &self.parts {
let m = s.mechanical?;
let value = modulus(m.youngs_modulus.to_si(), m.poisson_ratio);
if !(value.is_finite() && value > 0.0) {
return None;
}
voigt += f * value;
reciprocal += f / value;
}
Some((
Pressure::from_si(1.0 / reciprocal),
Pressure::from_si(voigt),
))
}
#[allow(clippy::type_complexity)]
pub fn fusion(&self) -> Result<Option<(Temperature, LatentHeat)>, String> {
let melting: Vec<usize> = (0..self.parts.len())
.filter(|i| self.parts[*i].0.fusion.is_some())
.collect();
match melting.as_slice() {
[] => Ok(None),
[i] => {
let f = self.parts[*i].0.fusion.expect("filtered on it");
let w = self.mass_fraction(*i).expect("index came from the list");
Ok(Some((
f.melting_point,
LatentHeat::from_si(w * f.latent_heat.to_si()),
)))
}
many => Err(format!(
"{} of the parts melt — {} — and a composite with two plateaux is not describable by \
one melting point. Mix the non-melting parts and handle the phase change as its own \
region, or model the second one as inert",
many.len(),
many.iter()
.map(|i| format!("{:?}", self.parts[*i].0.name))
.collect::<Vec<_>>()
.join(" and ")
)),
}
}
pub fn as_substance(
&self,
name: &str,
conductivity: ThermalConductivity,
emissivity: f64,
) -> Result<Substance, String> {
let (low, high) = self
.conductivity_bounds()
.ok_or_else(|| format!("{name}: a part does not state its conductivity"))?;
let k = conductivity.to_si();
if !(k.is_finite() && k >= low.to_si() * (1.0 - 1e-12) && k <= high.to_si() * (1.0 + 1e-12))
{
return Err(format!(
"{name}: no microstructure of these parts conducts {k} W/m/K — the Voigt and Reuss \
bounds are {} to {}, and they are attained, so this is impossible rather than \
merely unlikely",
low.to_si(),
high.to_si()
));
}
if !(0.0..=1.0).contains(&emissivity) {
return Err(format!(
"{name}: emissivity is a fraction of a blackbody's and must be in 0..=1, is \
{emissivity}"
));
}
let specific_heat = self
.specific_heat()
.ok_or_else(|| format!("{name}: a part does not state its specific heat"))?;
let expansion = ThermalExpansion::from_si(
self.parts
.iter()
.map(|(s, f)| f * s.thermal.map_or(f64::NAN, |t| t.expansion.to_si()))
.sum::<f64>(),
);
let mut out = Substance::bulk(name, self.density()).with_thermal(ThermalProps {
conductivity,
specific_heat,
expansion,
emissivity,
});
if let Some((point, latent)) = self.fusion().map_err(|e| format!("{name}: {e}"))? {
out = out.with_fusion(FusionProps::new(point, latent));
}
Ok(out)
}
}