Skip to main content

use_thermal_expansion/
lib.rs

1#![forbid(unsafe_code)]
2//! Primitive thermal-expansion helpers.
3//!
4//! Initial calculations assume SI units unless otherwise documented. For
5//! temperature differences, `ΔK` and `Δ°C` are treated equivalently.
6//!
7//! # Examples
8//!
9//! ```rust
10//! use use_thermal_expansion::{
11//!     LinearExpansionCoefficient, area_expansion_coefficient, coefficient_from_lengths,
12//!     final_length, linear_expansion, volume_expansion_coefficient,
13//! };
14//!
15//! let coefficient = LinearExpansionCoefficient::new(12.0e-6).unwrap();
16//! let expansion = linear_expansion(2.0, 12.0e-6, 50.0).unwrap();
17//! let expanded_length = final_length(2.0, 12.0e-6, 50.0).unwrap();
18//! let inferred = coefficient_from_lengths(2.0, 2.0012, 50.0).unwrap();
19//!
20//! assert_eq!(coefficient.per_kelvin(), 12.0e-6);
21//! assert!((expansion - 0.0012).abs() < 1.0e-12);
22//! assert!((expanded_length - 2.0012).abs() < 1.0e-12);
23//! assert!((inferred - 12.0e-6).abs() < 1.0e-12);
24//! assert_eq!(area_expansion_coefficient(12.0e-6).unwrap(), 24.0e-6);
25//! assert_eq!(volume_expansion_coefficient(12.0e-6).unwrap(), 36.0e-6);
26//! ```
27
28#[derive(Debug, Clone, Copy, PartialEq)]
29pub struct LinearExpansionCoefficient {
30    per_kelvin: f64,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum ThermalExpansionError {
35    InvalidCoefficient,
36    InvalidInitialLength,
37    InvalidFinalLength,
38    InvalidTemperatureChange,
39}
40
41fn validate_coefficient(value: f64) -> Result<f64, ThermalExpansionError> {
42    if !value.is_finite() {
43        Err(ThermalExpansionError::InvalidCoefficient)
44    } else {
45        Ok(value)
46    }
47}
48
49fn validate_initial_length(value: f64) -> Result<f64, ThermalExpansionError> {
50    if !value.is_finite() || value <= 0.0 {
51        Err(ThermalExpansionError::InvalidInitialLength)
52    } else {
53        Ok(value)
54    }
55}
56
57fn validate_temperature_change(value: f64, allow_zero: bool) -> Result<f64, ThermalExpansionError> {
58    if !value.is_finite() || (!allow_zero && value == 0.0) {
59        Err(ThermalExpansionError::InvalidTemperatureChange)
60    } else {
61        Ok(value)
62    }
63}
64
65impl LinearExpansionCoefficient {
66    pub fn new(per_kelvin: f64) -> Result<Self, ThermalExpansionError> {
67        Ok(Self {
68            per_kelvin: validate_coefficient(per_kelvin)?,
69        })
70    }
71
72    #[must_use]
73    pub fn per_kelvin(&self) -> f64 {
74        self.per_kelvin
75    }
76}
77
78pub fn linear_expansion(
79    initial_length_m: f64,
80    coefficient_per_k: f64,
81    delta_temp_k: f64,
82) -> Result<f64, ThermalExpansionError> {
83    Ok(validate_initial_length(initial_length_m)?
84        * validate_coefficient(coefficient_per_k)?
85        * validate_temperature_change(delta_temp_k, true)?)
86}
87
88pub fn final_length(
89    initial_length_m: f64,
90    coefficient_per_k: f64,
91    delta_temp_k: f64,
92) -> Result<f64, ThermalExpansionError> {
93    Ok(validate_initial_length(initial_length_m)?
94        + linear_expansion(initial_length_m, coefficient_per_k, delta_temp_k)?)
95}
96
97pub fn coefficient_from_lengths(
98    initial_length_m: f64,
99    final_length_m: f64,
100    delta_temp_k: f64,
101) -> Result<f64, ThermalExpansionError> {
102    let initial_length_m = validate_initial_length(initial_length_m)?;
103
104    if !final_length_m.is_finite() {
105        return Err(ThermalExpansionError::InvalidFinalLength);
106    }
107
108    Ok((final_length_m - initial_length_m)
109        / (initial_length_m * validate_temperature_change(delta_temp_k, false)?))
110}
111
112pub fn area_expansion_coefficient(
113    linear_coefficient_per_k: f64,
114) -> Result<f64, ThermalExpansionError> {
115    Ok(2.0 * validate_coefficient(linear_coefficient_per_k)?)
116}
117
118pub fn volume_expansion_coefficient(
119    linear_coefficient_per_k: f64,
120) -> Result<f64, ThermalExpansionError> {
121    Ok(3.0 * validate_coefficient(linear_coefficient_per_k)?)
122}
123
124#[cfg(test)]
125mod tests {
126    use super::{
127        LinearExpansionCoefficient, ThermalExpansionError, area_expansion_coefficient,
128        coefficient_from_lengths, final_length, linear_expansion, volume_expansion_coefficient,
129    };
130
131    #[test]
132    fn computes_thermal_expansion_values() {
133        let coefficient = LinearExpansionCoefficient::new(12.0e-6).unwrap();
134        let expansion = linear_expansion(2.0, 12.0e-6, 50.0).unwrap();
135        let expanded_length = final_length(2.0, 12.0e-6, 50.0).unwrap();
136        let inferred = coefficient_from_lengths(2.0, 2.0012, 50.0).unwrap();
137
138        assert_eq!(coefficient.per_kelvin(), 12.0e-6);
139        assert!((expansion - 0.0012).abs() < 1.0e-12);
140        assert!((expanded_length - 2.0012).abs() < 1.0e-12);
141        assert!((inferred - 12.0e-6).abs() < 1.0e-12);
142        assert_eq!(area_expansion_coefficient(12.0e-6).unwrap(), 24.0e-6);
143        assert_eq!(volume_expansion_coefficient(12.0e-6).unwrap(), 36.0e-6);
144    }
145
146    #[test]
147    fn allows_negative_coefficients_and_temperature_differences() {
148        assert!((linear_expansion(1.0, -1.0e-6, 10.0).unwrap() + 0.00001).abs() < 1.0e-12);
149        assert!((linear_expansion(1.0, 1.0e-6, -10.0).unwrap() + 0.00001).abs() < 1.0e-12);
150    }
151
152    #[test]
153    fn rejects_invalid_thermal_expansion_inputs() {
154        assert_eq!(
155            LinearExpansionCoefficient::new(f64::NAN),
156            Err(ThermalExpansionError::InvalidCoefficient)
157        );
158        assert_eq!(
159            linear_expansion(0.0, 12.0e-6, 50.0),
160            Err(ThermalExpansionError::InvalidInitialLength)
161        );
162        assert_eq!(
163            coefficient_from_lengths(2.0, 2.0012, 0.0),
164            Err(ThermalExpansionError::InvalidTemperatureChange)
165        );
166        assert_eq!(
167            coefficient_from_lengths(2.0, f64::INFINITY, 50.0),
168            Err(ThermalExpansionError::InvalidFinalLength)
169        );
170    }
171}