Skip to main content

oxiphysics_core/
unit_conversion.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Unit conversion and fundamental physical constants.
5//!
6//! Provides enumerations for common measurement units and conversion functions
7//! between them, as well as a struct containing widely-used physical constants
8//! in SI units.
9
10// ──────────────────────────────────────────────────────────────────────────────
11// PhysicalConstants
12// ──────────────────────────────────────────────────────────────────────────────
13
14/// Fundamental physical constants in SI units (CODATA 2018 values).
15#[allow(dead_code)]
16#[derive(Debug, Clone, PartialEq)]
17pub struct PhysicalConstants {
18    /// Speed of light in vacuum (m/s).
19    pub c: f64,
20    /// Planck constant (J·s).
21    pub h: f64,
22    /// Boltzmann constant (J/K).
23    pub k_b: f64,
24    /// Avogadro constant (mol⁻¹).
25    pub n_a: f64,
26    /// Molar gas constant (J/(mol·K)).
27    pub r: f64,
28    /// Newtonian gravitational constant (m³/(kg·s²)).
29    pub g: f64,
30    /// Electric constant / permittivity of free space (F/m).
31    pub epsilon_0: f64,
32    /// Magnetic constant / permeability of free space (H/m).
33    pub mu_0: f64,
34}
35
36impl Default for PhysicalConstants {
37    fn default() -> Self {
38        Self {
39            c: 2.997_924_58e8,
40            h: 6.626_070_15e-34,
41            k_b: 1.380_649e-23,
42            n_a: 6.022_140_76e23,
43            r: 8.314_462_618,
44            g: 6.674_30e-11,
45            epsilon_0: 8.854_187_812_8e-12,
46            mu_0: 1.256_637_062_12e-6,
47        }
48    }
49}
50
51impl PhysicalConstants {
52    /// Construct constants with the default (CODATA 2018) values.
53    pub fn new() -> Self {
54        Self::default()
55    }
56}
57
58// ──────────────────────────────────────────────────────────────────────────────
59// Length
60// ──────────────────────────────────────────────────────────────────────────────
61
62/// Units of length.
63#[allow(dead_code)]
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum LengthUnit {
66    /// Meter (SI base unit).
67    Meter,
68    /// Centimeter (1e-2 m).
69    Centimeter,
70    /// Millimeter (1e-3 m).
71    Millimeter,
72    /// Micrometre / micron (1e-6 m).
73    Micrometer,
74    /// Nanometer (1e-9 m).
75    Nanometer,
76    /// Ångström (1e-10 m).
77    Angstrom,
78    /// Foot (0.3048 m).
79    Foot,
80    /// Inch (0.0254 m).
81    Inch,
82    /// Light-year (9.460_730_472_580_8e15 m).
83    LightYear,
84    /// Astronomical Unit (1.495_978_707e11 m).
85    AstronomicalUnit,
86    /// Parsec (3.085_677_581_49e16 m).
87    Parsec,
88}
89
90impl LengthUnit {
91    /// Return the conversion factor to metres.
92    fn to_meters(self) -> f64 {
93        match self {
94            LengthUnit::Meter => 1.0,
95            LengthUnit::Centimeter => 1e-2,
96            LengthUnit::Millimeter => 1e-3,
97            LengthUnit::Micrometer => 1e-6,
98            LengthUnit::Nanometer => 1e-9,
99            LengthUnit::Angstrom => 1e-10,
100            LengthUnit::Foot => 0.3048,
101            LengthUnit::Inch => 0.0254,
102            LengthUnit::LightYear => 9.460_730_472_580_8e15,
103            LengthUnit::AstronomicalUnit => 1.495_978_707e11,
104            LengthUnit::Parsec => 3.085_677_581_49e16,
105        }
106    }
107}
108
109/// Convert a length value from `from` units to `to` units.
110///
111/// # Examples
112/// ```no_run
113/// use oxiphysics_core::unit_conversion::{LengthUnit, convert_length};
114/// let inches = convert_length(1.0, LengthUnit::Foot, LengthUnit::Inch);
115/// assert!((inches - 12.0).abs() < 1e-10);
116/// ```
117#[allow(dead_code)]
118pub fn convert_length(value: f64, from: LengthUnit, to: LengthUnit) -> f64 {
119    value * from.to_meters() / to.to_meters()
120}
121
122// ──────────────────────────────────────────────────────────────────────────────
123// Mass
124// ──────────────────────────────────────────────────────────────────────────────
125
126/// Units of mass.
127#[allow(dead_code)]
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub enum MassUnit {
130    /// Kilogram (SI base unit).
131    Kilogram,
132    /// Gram (1e-3 kg).
133    Gram,
134    /// Milligram (1e-6 kg).
135    Milligram,
136    /// Microgram (1e-9 kg).
137    Microgram,
138    /// Pound-mass (0.453_592_37 kg).
139    Pound,
140    /// Ounce (0.028_349_523_125 kg).
141    Ounce,
142    /// Atomic mass unit / dalton (1.660_539_066_60e-27 kg).
143    AtomicMassUnit,
144    /// Dalton — identical to `AtomicMassUnit`.
145    Dalton,
146}
147
148impl MassUnit {
149    /// Return the conversion factor to kilograms.
150    fn to_kg(self) -> f64 {
151        match self {
152            MassUnit::Kilogram => 1.0,
153            MassUnit::Gram => 1e-3,
154            MassUnit::Milligram => 1e-6,
155            MassUnit::Microgram => 1e-9,
156            MassUnit::Pound => 0.453_592_37,
157            MassUnit::Ounce => 0.028_349_523_125,
158            MassUnit::AtomicMassUnit | MassUnit::Dalton => 1.660_539_066_60e-27,
159        }
160    }
161}
162
163/// Convert a mass value from `from` units to `to` units.
164///
165/// # Examples
166/// ```no_run
167/// use oxiphysics_core::unit_conversion::{MassUnit, convert_mass};
168/// let grams = convert_mass(1.0, MassUnit::Kilogram, MassUnit::Gram);
169/// assert!((grams - 1000.0).abs() < 1e-10);
170/// ```
171#[allow(dead_code)]
172pub fn convert_mass(value: f64, from: MassUnit, to: MassUnit) -> f64 {
173    value * from.to_kg() / to.to_kg()
174}
175
176// ──────────────────────────────────────────────────────────────────────────────
177// Energy
178// ──────────────────────────────────────────────────────────────────────────────
179
180/// Units of energy.
181#[allow(dead_code)]
182#[derive(Debug, Clone, Copy, PartialEq, Eq)]
183pub enum EnergyUnit {
184    /// Joule (SI).
185    Joule,
186    /// Kilojoule (1e3 J).
187    Kilojoule,
188    /// Electronvolt (1.602_176_634e-19 J).
189    Electronvolt,
190    /// Kilocalorie (4184 J, thermochemical).
191    Kilocalorie,
192    /// Calorie (4.184 J, thermochemical).
193    Calorie,
194    /// Erg (1e-7 J, CGS).
195    Erg,
196    /// British thermal unit (1055.05585262 J).
197    Btu,
198    /// Hartree energy (4.359_744_650_98e-18 J).
199    Hartree,
200}
201
202impl EnergyUnit {
203    /// Return the conversion factor to joules.
204    fn to_joules(self) -> f64 {
205        match self {
206            EnergyUnit::Joule => 1.0,
207            EnergyUnit::Kilojoule => 1e3,
208            EnergyUnit::Electronvolt => 1.602_176_634e-19,
209            EnergyUnit::Kilocalorie => 4184.0,
210            EnergyUnit::Calorie => 4.184,
211            EnergyUnit::Erg => 1e-7,
212            EnergyUnit::Btu => 1055.05585262,
213            EnergyUnit::Hartree => 4.359_744_650_98e-18,
214        }
215    }
216}
217
218/// Convert an energy value from `from` units to `to` units.
219///
220/// # Examples
221/// ```no_run
222/// use oxiphysics_core::unit_conversion::{EnergyUnit, convert_energy};
223/// let kj = convert_energy(1000.0, EnergyUnit::Joule, EnergyUnit::Kilojoule);
224/// assert!((kj - 1.0).abs() < 1e-10);
225/// ```
226#[allow(dead_code)]
227pub fn convert_energy(value: f64, from: EnergyUnit, to: EnergyUnit) -> f64 {
228    value * from.to_joules() / to.to_joules()
229}
230
231// ──────────────────────────────────────────────────────────────────────────────
232// Temperature
233// ──────────────────────────────────────────────────────────────────────────────
234
235/// Units of temperature.
236#[allow(dead_code)]
237#[derive(Debug, Clone, Copy, PartialEq, Eq)]
238pub enum TemperatureUnit {
239    /// Kelvin (absolute thermodynamic temperature).
240    Kelvin,
241    /// Degrees Celsius.
242    Celsius,
243    /// Degrees Fahrenheit.
244    Fahrenheit,
245    /// Rankine (absolute Fahrenheit scale).
246    Rankine,
247}
248
249/// Convert a temperature value from `from` units to `to` units.
250///
251/// Temperature conversion is not a simple multiplicative factor, so this
252/// function applies the appropriate affine transformation.
253///
254/// # Examples
255/// ```no_run
256/// use oxiphysics_core::unit_conversion::{TemperatureUnit, convert_temperature};
257/// let f = convert_temperature(100.0, TemperatureUnit::Celsius, TemperatureUnit::Fahrenheit);
258/// assert!((f - 212.0).abs() < 1e-8);
259/// ```
260#[allow(dead_code)]
261pub fn convert_temperature(value: f64, from: TemperatureUnit, to: TemperatureUnit) -> f64 {
262    // First convert to Kelvin.
263    let kelvin = match from {
264        TemperatureUnit::Kelvin => value,
265        TemperatureUnit::Celsius => value + 273.15,
266        TemperatureUnit::Fahrenheit => (value - 32.0) * 5.0 / 9.0 + 273.15,
267        TemperatureUnit::Rankine => value * 5.0 / 9.0,
268    };
269    // Then convert from Kelvin to target.
270    match to {
271        TemperatureUnit::Kelvin => kelvin,
272        TemperatureUnit::Celsius => kelvin - 273.15,
273        TemperatureUnit::Fahrenheit => (kelvin - 273.15) * 9.0 / 5.0 + 32.0,
274        TemperatureUnit::Rankine => kelvin * 9.0 / 5.0,
275    }
276}
277
278// ──────────────────────────────────────────────────────────────────────────────
279// Pressure
280// ──────────────────────────────────────────────────────────────────────────────
281
282/// Units of pressure.
283#[allow(dead_code)]
284#[derive(Debug, Clone, Copy, PartialEq, Eq)]
285pub enum PressureUnit {
286    /// Pascal (SI, N/m²).
287    Pascal,
288    /// Kilopascal (1e3 Pa).
289    Kilopascal,
290    /// Megapascal (1e6 Pa).
291    Megapascal,
292    /// Gigapascal (1e9 Pa).
293    Gigapascal,
294    /// Bar (1e5 Pa).
295    Bar,
296    /// Standard atmosphere (101_325 Pa).
297    Atmosphere,
298    /// Pound-force per square inch (6894.757_293_168_36 Pa).
299    Psi,
300    /// Torr / mmHg (101_325/760 Pa).
301    Torr,
302}
303
304impl PressureUnit {
305    /// Return the conversion factor to pascals.
306    fn to_pascals(self) -> f64 {
307        match self {
308            PressureUnit::Pascal => 1.0,
309            PressureUnit::Kilopascal => 1e3,
310            PressureUnit::Megapascal => 1e6,
311            PressureUnit::Gigapascal => 1e9,
312            PressureUnit::Bar => 1e5,
313            PressureUnit::Atmosphere => 101_325.0,
314            PressureUnit::Psi => 6_894.757_293_168_361,
315            PressureUnit::Torr => 101_325.0 / 760.0,
316        }
317    }
318}
319
320/// Convert a pressure value from `from` units to `to` units.
321///
322/// # Examples
323/// ```no_run
324/// use oxiphysics_core::unit_conversion::{PressureUnit, convert_pressure};
325/// let pa = convert_pressure(1.0, PressureUnit::Atmosphere, PressureUnit::Pascal);
326/// assert!((pa - 101_325.0).abs() < 1e-4);
327/// ```
328#[allow(dead_code)]
329pub fn convert_pressure(value: f64, from: PressureUnit, to: PressureUnit) -> f64 {
330    value * from.to_pascals() / to.to_pascals()
331}
332
333// ──────────────────────────────────────────────────────────────────────────────
334// Tests
335// ──────────────────────────────────────────────────────────────────────────────
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340
341    // ── PhysicalConstants ────────────────────────────────────────────────────
342
343    #[test]
344    fn test_speed_of_light_positive() {
345        let c = PhysicalConstants::new();
346        assert!(c.c > 0.0);
347    }
348
349    #[test]
350    fn test_planck_constant_order_of_magnitude() {
351        let pc = PhysicalConstants::new();
352        assert!(pc.h > 1e-35 && pc.h < 1e-33);
353    }
354
355    #[test]
356    fn test_boltzmann_constant_order() {
357        let pc = PhysicalConstants::new();
358        assert!(pc.k_b > 1e-24 && pc.k_b < 1e-22);
359    }
360
361    #[test]
362    fn test_avogadro_order() {
363        let pc = PhysicalConstants::new();
364        assert!(pc.n_a > 6.0e23 && pc.n_a < 6.1e23);
365    }
366
367    #[test]
368    fn test_gas_constant_relation() {
369        // R = N_A * k_B
370        let pc = PhysicalConstants::new();
371        let computed = pc.n_a * pc.k_b;
372        assert!((computed - pc.r).abs() / pc.r < 1e-6);
373    }
374
375    #[test]
376    fn test_gravitational_constant_order() {
377        let pc = PhysicalConstants::new();
378        assert!(pc.g > 6.6e-11 && pc.g < 6.7e-11);
379    }
380
381    // ── Length ───────────────────────────────────────────────────────────────
382
383    #[test]
384    fn test_length_identity() {
385        assert!((convert_length(5.0, LengthUnit::Meter, LengthUnit::Meter) - 5.0).abs() < 1e-12);
386    }
387
388    #[test]
389    fn test_foot_to_inches() {
390        let inches = convert_length(1.0, LengthUnit::Foot, LengthUnit::Inch);
391        assert!((inches - 12.0).abs() < 1e-10);
392    }
393
394    #[test]
395    fn test_meter_to_centimeter() {
396        let cm = convert_length(1.0, LengthUnit::Meter, LengthUnit::Centimeter);
397        assert!((cm - 100.0).abs() < 1e-10);
398    }
399
400    #[test]
401    fn test_meter_to_millimeter() {
402        let mm = convert_length(1.0, LengthUnit::Meter, LengthUnit::Millimeter);
403        assert!((mm - 1000.0).abs() < 1e-10);
404    }
405
406    #[test]
407    fn test_nanometer_to_angstrom() {
408        let ang = convert_length(1.0, LengthUnit::Nanometer, LengthUnit::Angstrom);
409        assert!((ang - 10.0).abs() < 1e-6);
410    }
411
412    #[test]
413    fn test_length_roundtrip() {
414        let val = 42.0;
415        let cm = convert_length(val, LengthUnit::Meter, LengthUnit::Centimeter);
416        let back = convert_length(cm, LengthUnit::Centimeter, LengthUnit::Meter);
417        assert!((back - val).abs() < 1e-10);
418    }
419
420    #[test]
421    fn test_au_larger_than_ly() {
422        // 1 AU < 1 ly, so converting 1 ly → AU yields a large number.
423        let aus = convert_length(1.0, LengthUnit::LightYear, LengthUnit::AstronomicalUnit);
424        assert!(aus > 60_000.0);
425    }
426
427    #[test]
428    fn test_inch_to_meter() {
429        let m = convert_length(1.0, LengthUnit::Inch, LengthUnit::Meter);
430        assert!((m - 0.0254).abs() < 1e-10);
431    }
432
433    // ── Mass ─────────────────────────────────────────────────────────────────
434
435    #[test]
436    fn test_mass_identity() {
437        assert!((convert_mass(3.0, MassUnit::Kilogram, MassUnit::Kilogram) - 3.0).abs() < 1e-12);
438    }
439
440    #[test]
441    fn test_kg_to_gram() {
442        let g = convert_mass(1.0, MassUnit::Kilogram, MassUnit::Gram);
443        assert!((g - 1000.0).abs() < 1e-10);
444    }
445
446    #[test]
447    fn test_pound_to_kg() {
448        let kg = convert_mass(1.0, MassUnit::Pound, MassUnit::Kilogram);
449        assert!((kg - 0.453_592_37).abs() < 1e-8);
450    }
451
452    #[test]
453    fn test_ounce_to_gram() {
454        // 1 lb = 16 oz, 1 lb = 453.59237 g → 1 oz ≈ 28.3495 g
455        let g = convert_mass(1.0, MassUnit::Ounce, MassUnit::Gram);
456        assert!((g - 28.349_523_125).abs() < 1e-6);
457    }
458
459    #[test]
460    fn test_amu_dalton_equal() {
461        let a = convert_mass(1.0, MassUnit::AtomicMassUnit, MassUnit::Kilogram);
462        let b = convert_mass(1.0, MassUnit::Dalton, MassUnit::Kilogram);
463        assert!((a - b).abs() < 1e-40);
464    }
465
466    #[test]
467    fn test_mass_roundtrip() {
468        let val = 7.5;
469        let lb = convert_mass(val, MassUnit::Kilogram, MassUnit::Pound);
470        let back = convert_mass(lb, MassUnit::Pound, MassUnit::Kilogram);
471        assert!((back - val).abs() < 1e-10);
472    }
473
474    // ── Energy ───────────────────────────────────────────────────────────────
475
476    #[test]
477    fn test_energy_identity() {
478        assert!((convert_energy(2.0, EnergyUnit::Joule, EnergyUnit::Joule) - 2.0).abs() < 1e-12);
479    }
480
481    #[test]
482    fn test_kj_to_j() {
483        let j = convert_energy(1.0, EnergyUnit::Kilojoule, EnergyUnit::Joule);
484        assert!((j - 1000.0).abs() < 1e-10);
485    }
486
487    #[test]
488    fn test_calorie_to_joule() {
489        let j = convert_energy(1.0, EnergyUnit::Calorie, EnergyUnit::Joule);
490        assert!((j - 4.184).abs() < 1e-10);
491    }
492
493    #[test]
494    fn test_kcal_to_cal() {
495        let cal = convert_energy(1.0, EnergyUnit::Kilocalorie, EnergyUnit::Calorie);
496        assert!((cal - 1000.0).abs() < 1e-10);
497    }
498
499    #[test]
500    fn test_erg_to_joule() {
501        let j = convert_energy(1.0, EnergyUnit::Erg, EnergyUnit::Joule);
502        assert!((j - 1e-7).abs() < 1e-18);
503    }
504
505    #[test]
506    fn test_energy_roundtrip() {
507        let val = 100.0;
508        let ev = convert_energy(val, EnergyUnit::Joule, EnergyUnit::Electronvolt);
509        let back = convert_energy(ev, EnergyUnit::Electronvolt, EnergyUnit::Joule);
510        assert!((back - val).abs() / val < 1e-10);
511    }
512
513    // ── Temperature ──────────────────────────────────────────────────────────
514
515    #[test]
516    fn test_celsius_to_kelvin_freezing() {
517        let k = convert_temperature(0.0, TemperatureUnit::Celsius, TemperatureUnit::Kelvin);
518        assert!((k - 273.15).abs() < 1e-8);
519    }
520
521    #[test]
522    fn test_celsius_to_fahrenheit_boiling() {
523        let f = convert_temperature(100.0, TemperatureUnit::Celsius, TemperatureUnit::Fahrenheit);
524        assert!((f - 212.0).abs() < 1e-8);
525    }
526
527    #[test]
528    fn test_fahrenheit_freezing_to_celsius() {
529        let c = convert_temperature(32.0, TemperatureUnit::Fahrenheit, TemperatureUnit::Celsius);
530        assert!(c.abs() < 1e-8);
531    }
532
533    #[test]
534    fn test_rankine_to_kelvin() {
535        // 0 R = 0 K
536        let k = convert_temperature(0.0, TemperatureUnit::Rankine, TemperatureUnit::Kelvin);
537        assert!(k.abs() < 1e-10);
538    }
539
540    #[test]
541    fn test_rankine_459_67_is_zero_fahrenheit() {
542        // 459.67 R = 0 °F
543        let f = convert_temperature(
544            459.67,
545            TemperatureUnit::Rankine,
546            TemperatureUnit::Fahrenheit,
547        );
548        assert!(f.abs() < 1e-6);
549    }
550
551    #[test]
552    fn test_temperature_roundtrip_k_c() {
553        let val = 500.0_f64;
554        let c = convert_temperature(val, TemperatureUnit::Kelvin, TemperatureUnit::Celsius);
555        let back = convert_temperature(c, TemperatureUnit::Celsius, TemperatureUnit::Kelvin);
556        assert!((back - val).abs() < 1e-8);
557    }
558
559    #[test]
560    fn test_temperature_identity_kelvin() {
561        let val = 300.0_f64;
562        let k = convert_temperature(val, TemperatureUnit::Kelvin, TemperatureUnit::Kelvin);
563        assert!((k - val).abs() < 1e-10);
564    }
565
566    // ── Pressure ─────────────────────────────────────────────────────────────
567
568    #[test]
569    fn test_pressure_identity() {
570        assert!(
571            (convert_pressure(5.0, PressureUnit::Pascal, PressureUnit::Pascal) - 5.0).abs() < 1e-12
572        );
573    }
574
575    #[test]
576    fn test_atm_to_pascal() {
577        let pa = convert_pressure(1.0, PressureUnit::Atmosphere, PressureUnit::Pascal);
578        assert!((pa - 101_325.0).abs() < 1e-4);
579    }
580
581    #[test]
582    fn test_bar_to_kpa() {
583        let kpa = convert_pressure(1.0, PressureUnit::Bar, PressureUnit::Kilopascal);
584        assert!((kpa - 100.0).abs() < 1e-8);
585    }
586
587    #[test]
588    fn test_torr_to_pa() {
589        let pa = convert_pressure(760.0, PressureUnit::Torr, PressureUnit::Pascal);
590        assert!((pa - 101_325.0).abs() < 1e-4);
591    }
592
593    #[test]
594    fn test_psi_to_pa() {
595        let pa = convert_pressure(1.0, PressureUnit::Psi, PressureUnit::Pascal);
596        assert!((pa - 6_894.757_293_168_361).abs() < 1e-4);
597    }
598
599    #[test]
600    fn test_pressure_roundtrip() {
601        let val = 3.5;
602        let atm = convert_pressure(val, PressureUnit::Megapascal, PressureUnit::Atmosphere);
603        let back = convert_pressure(atm, PressureUnit::Atmosphere, PressureUnit::Megapascal);
604        assert!((back - val).abs() / val < 1e-10);
605    }
606
607    #[test]
608    fn test_gpa_to_mpa() {
609        let mpa = convert_pressure(1.0, PressureUnit::Gigapascal, PressureUnit::Megapascal);
610        assert!((mpa - 1000.0).abs() < 1e-8);
611    }
612}