Skip to main content

symplex/units/
constants.rs

1//! Physical constants as dimension-typed symbolic expressions.
2//!
3//! Each constant displays as its standard symbol (e.g., "c" for speed of light)
4//! and evaluates to its exact value when numerical evaluation is requested.
5//! All values are exact — defined by the 2019 SI redefinition or by international agreement.
6//!
7//! # Examples
8//!
9//! ```
10//! use symplex::prelude::*;
11//! use symplex::units::*;
12//! use symplex::units::constants;
13//!
14//! let ctx = Context::new();
15//! let c = constants::speed_of_light(&ctx);
16//! let m = Mass::symbol(&ctx, "m");
17//! let energy = Energy::from_ex(m.inner() * c.inner() * c.inner());  // E = mc²
18//! // Displays symbolically, not as "89875517873681764*m"
19//! assert_eq!(format!("{}", energy.inner()), "m*c^2");
20//! ```
21
22use super::dim::*;
23use super::qty::Qty;
24use super::si::*;
25use typenum::{N1, N2, P1, P2, P3, Z0};
26
27/// Speed of light in vacuum: c = 299,792,458 m/s (exact since 2019 SI redefinition).
28pub fn speed_of_light(ctx: &crate::api::context::Context) -> Velocity {
29    Velocity::from_ex(ctx.physical_constant("c", ctx.int(299_792_458)))
30}
31
32/// Standard acceleration of gravity: g₀ = 9.80665 m/s² (exact by definition, 1901).
33pub fn standard_gravity(ctx: &crate::api::context::Context) -> Acceleration {
34    Acceleration::from_ex(ctx.physical_constant("g_0", ctx.rational(980665, 100_000)))
35}
36
37/// Elementary charge: e = 1.602176634 × 10⁻¹⁹ C (exact since 2019 SI redefinition).
38pub fn elementary_charge(ctx: &crate::api::context::Context) -> Charge {
39    Charge::from_ex(ctx.physical_constant("e_0", &ctx.int(1_602_176_634) / &ctx.int(10).powi(28)))
40}
41
42/// Planck constant: h = 6.62607015 × 10⁻³⁴ J·s (exact since 2019 SI redefinition).
43///
44/// Returns as `AngularMomentum` (dimension M·L²·T⁻¹, same as Action = Energy × Time).
45pub fn planck_constant(ctx: &crate::api::context::Context) -> AngularMomentum {
46    AngularMomentum::from_ex(
47        ctx.physical_constant("h", &ctx.int(662_607_015) / &ctx.int(10).powi(42)),
48    )
49}
50
51/// Reduced Planck constant: ℏ = h/(2π) (exact).
52///
53/// Note: This involves π, so the value is symbolic: h/(2π).
54/// For numerical evaluation, both h and π resolve to exact values.
55pub fn reduced_planck_constant(ctx: &crate::api::context::Context) -> AngularMomentum {
56    let h = ctx.physical_constant("h", &ctx.int(662_607_015) / &ctx.int(10).powi(42));
57    let two_pi = &(ctx.int(2) * &ctx.pi());
58    AngularMomentum::from_ex(ctx.physical_constant("hbar", &h / two_pi))
59}
60
61/// Boltzmann constant: k_B = 1.380649 × 10⁻²³ J/K (exact since 2019 SI redefinition).
62///
63/// Dimension: M·L²·T⁻²·Θ⁻¹ (Energy per Temperature).
64/// Returns as `Qty` since there's no named type for this dimension.
65pub fn boltzmann_constant(
66    ctx: &crate::api::context::Context,
67) -> Qty<Dim<P2, P1, N2, Z0, N1, Z0, Z0>> {
68    Qty::from_ex(ctx.physical_constant("k_B", &ctx.int(1_380_649) / &ctx.int(10).powi(29)))
69}
70
71/// Avogadro constant: N_A = 6.02214076 × 10²³ mol⁻¹ (exact since 2019 SI redefinition).
72///
73/// Dimension: N⁻¹ (inverse amount of substance).
74/// Returns as `Qty` since there's no named type for this dimension.
75pub fn avogadro_constant(
76    ctx: &crate::api::context::Context,
77) -> Qty<Dim<Z0, Z0, Z0, Z0, Z0, N1, Z0>> {
78    // N_A = 602214076 × 10^15
79    let val = &ctx.int(602_214_076) * &ctx.int(10).powi(15);
80    Qty::from_ex(ctx.physical_constant("N_A", val))
81}
82
83/// Newtonian gravitational constant: G ≈ 6.67430 × 10⁻¹¹ m³/(kg·s²).
84///
85/// NOTE: Unlike the other constants here, G is NOT exact — it is measured experimentally.
86/// The value 6.67430e-11 is the 2018 CODATA recommended value.
87/// Dimension: L³·M⁻¹·T⁻²
88pub fn gravitational_constant(
89    ctx: &crate::api::context::Context,
90) -> Qty<Dim<P3, N1, N2, Z0, Z0, Z0, Z0>> {
91    Qty::from_ex(ctx.physical_constant("G", &ctx.int(667_430) / &ctx.int(10).powi(16)))
92}
93
94/// Pre-built dimension map containing all physical constants.
95///
96/// Use with `infer_dimension` for runtime dimension checking of expressions
97/// containing physical constants.
98pub fn physical_constants_dimmap() -> super::inference::DimMap {
99    use super::inference::DimMap;
100    DimMap::new()
101        .with("c", ConstDim::VELOCITY)
102        .with("g_0", ConstDim::ACCELERATION)
103        .with("e_0", ConstDim::CHARGE)
104        .with("h", ConstDim::ANGULAR_MOMENTUM)
105        .with("hbar", ConstDim::ANGULAR_MOMENTUM)
106        .with("k_B", ConstDim::new(2, 1, -2, 0, -1, 0, 0))
107        .with("N_A", ConstDim::new(0, 0, 0, 0, 0, -1, 0))
108        .with("G", ConstDim::new(3, -1, -2, 0, 0, 0, 0))
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn speed_of_light_is_velocity() {
117        let ctx = crate::api::context::Context::new();
118        let c = speed_of_light(&ctx);
119        assert!(
120            format!("{}", c.inner()).contains("c"),
121            "should display as c"
122        );
123    }
124
125    #[test]
126    fn speed_of_light_eval_f64() {
127        let ctx = crate::api::context::Context::new();
128        let c = speed_of_light(&ctx);
129        let val = c.eval_f64().unwrap();
130        assert!((val - 299_792_458.0).abs() < 1.0, "c = {val}");
131    }
132
133    #[test]
134    fn elementary_charge_eval() {
135        let ctx = crate::api::context::Context::new();
136        let e = elementary_charge(&ctx);
137        let val = e.eval_f64().unwrap();
138        assert!(
139            (val - 1.602176634e-19).abs() / 1.602176634e-19 < 1e-10,
140            "e = {val}"
141        );
142    }
143
144    #[test]
145    fn planck_constant_eval() {
146        let ctx = crate::api::context::Context::new();
147        let h = planck_constant(&ctx);
148        let val = h.eval_f64().unwrap();
149        assert!(
150            (val - 6.62607015e-34).abs() / 6.62607015e-34 < 1e-10,
151            "h = {val}"
152        );
153    }
154
155    #[test]
156    fn boltzmann_eval() {
157        let ctx = crate::api::context::Context::new();
158        let kb = boltzmann_constant(&ctx);
159        let val = kb.eval_f64().unwrap();
160        assert!(
161            (val - 1.380649e-23).abs() / 1.380649e-23 < 1e-10,
162            "k_B = {val}"
163        );
164    }
165
166    #[test]
167    fn standard_gravity_eval() {
168        let ctx = crate::api::context::Context::new();
169        let g = standard_gravity(&ctx);
170        let val = g.eval_f64().unwrap();
171        assert!((val - 9.80665).abs() < 1e-10, "g = {val}");
172    }
173
174    #[test]
175    fn e_equals_mc_squared() {
176        let ctx = crate::api::context::Context::new();
177        crate::syms!(ctx; m);
178        let c = speed_of_light(&ctx);
179        let mass = Mass::symbol(&ctx, "m");
180        // Use raw expression arithmetic to avoid missing named-mul impls
181        let energy = Energy::from_ex(mass.inner() * c.inner() * c.inner());
182        // Display should contain "c", not the numeric value
183        let display = format!("{}", energy.inner());
184        assert!(
185            display.contains("c"),
186            "E=mc² should display symbolically: {display}"
187        );
188        // Evaluate with m=1 kg
189        let val = energy.subs(&m, &ctx.int(1)).eval_f64().unwrap();
190        let expected = 299_792_458.0_f64 * 299_792_458.0;
191        assert!(
192            (val - expected).abs() / expected < 1e-10,
193            "E(m=1) = {val}, expected {expected}"
194        );
195    }
196
197    #[test]
198    fn constant_diff_is_zero() {
199        let ctx = crate::api::context::Context::new();
200        crate::syms!(ctx; x);
201        let c = speed_of_light(&ctx);
202        let dc_dx = c.inner().diff(&x);
203        assert!(
204            dc_dx.is_zero().unwrap_or(false) || format!("{}", dc_dx) == "0",
205            "d/dx(c) should be 0, got {dc_dx}"
206        );
207    }
208
209    #[test]
210    fn constant_in_product_diff() {
211        let ctx = crate::api::context::Context::new();
212        crate::syms!(ctx; x);
213        let c = speed_of_light(&ctx);
214        let cx = c.inner() * &x;
215        let d = cx.diff(&x);
216        // d/dx(c*x) = c
217        let display = format!("{}", d);
218        assert!(
219            display.contains("c"),
220            "d/dx(c*x) should contain c: {display}"
221        );
222    }
223
224    #[test]
225    fn constant_survives_simplify() {
226        let ctx = crate::api::context::Context::new();
227        crate::syms!(ctx; x, y);
228        let c = speed_of_light(&ctx);
229        let expr = c.inner() * &x + c.inner() * &y;
230        let simplified = expr.simplify();
231        let display = format!("{}", simplified);
232        assert!(
233            display.contains("c"),
234            "simplify should preserve c: {display}"
235        );
236    }
237
238    #[test]
239    fn physical_constants_dimmap_works() {
240        let ctx = crate::api::context::Context::new();
241        crate::syms!(ctx; m);
242        let dims = physical_constants_dimmap().with("m", ConstDim::MASS);
243        let c = speed_of_light(&ctx);
244        let mc2 = &(m.clone() * c.inner()) * c.inner();
245        let dim = crate::units::inference::infer_dimension(&mc2, &dims);
246        assert!(dim.is_ok(), "should infer dimension of mc²: {:?}", dim);
247        assert!(
248            dim.unwrap().eq(ConstDim::ENERGY),
249            "mc² should have Energy dimension"
250        );
251    }
252}