use crate::exact::rational::Rational;
use crate::exact::symbolic::Expr;
use crate::units::quantity::{codata, Dim, DimError};
fn dimension_matrix(dims: &[Dim]) -> Vec<Vec<Rational>> {
(0..7)
.map(|row| {
dims.iter().map(|d| Rational::from_i64(d.exponents()[row] as i64, 1)).collect()
})
.collect()
}
fn row_reduce(matrix: &mut [Vec<Rational>]) -> Vec<usize> {
let rows = matrix.len();
let cols = if rows == 0 { 0 } else { matrix[0].len() };
let mut pivots = Vec::new();
let mut row = 0;
for col in 0..cols {
let Some(found) = (row..rows).find(|&r| !matrix[r][col].is_zero()) else {
continue;
};
matrix.swap(row, found);
let inverse = matrix[row][col].recip().expect("the pivot is nonzero");
for c in col..cols {
matrix[row][c] = matrix[row][c].mul(&inverse);
}
for r in 0..rows {
if r == row || matrix[r][col].is_zero() {
continue;
}
let factor = matrix[r][col].clone();
for c in col..cols {
let term = factor.mul(&matrix[row][c]);
matrix[r][c] = matrix[r][c].sub(&term);
}
}
pivots.push(col);
row += 1;
if row == rows {
break;
}
}
pivots
}
pub fn buckingham_pi(dims: &[Dim]) -> Result<Vec<Vec<Rational>>, DimError> {
if dims.is_empty() {
return Err(DimError::Malformed("no quantities"));
}
let n = dims.len();
let mut matrix = dimension_matrix(dims);
let pivots = row_reduce(&mut matrix);
let free: Vec<usize> = (0..n).filter(|c| !pivots.contains(c)).collect();
let mut basis = Vec::with_capacity(free.len());
for &f in &free {
let mut vector = vec![Rational::zero(); n];
vector[f] = Rational::one();
for (r, &p) in pivots.iter().enumerate() {
vector[p] = matrix[r][f].neg();
}
basis.push(vector);
}
Ok(basis)
}
pub fn is_dimensionless_group(dims: &[Dim], exponents: &[Rational]) -> Result<bool, DimError> {
if dims.len() != exponents.len() {
return Err(DimError::Malformed("one exponent per quantity is needed"));
}
for row in 0..7 {
let mut total = Rational::zero();
for (d, e) in dims.iter().zip(exponents) {
let contribution = Rational::from_i64(d.exponents()[row] as i64, 1).mul(e);
total = total.add(&contribution);
}
if !total.is_zero() {
return Ok(false);
}
}
Ok(true)
}
pub fn dimensionless_groups_named() -> Vec<(&'static str, &'static str, &'static str)> {
vec![
("Reynolds", "rho v L / mu", "inertia against viscosity"),
("Froude", "v / sqrt(g L)", "inertia against gravity"),
("Weber", "rho v^2 L / sigma", "inertia against surface tension"),
("Mach", "v / c", "speed against the speed of sound"),
("Prandtl", "nu / alpha", "momentum diffusivity against thermal"),
("Rayleigh", "g beta dT L^3 / (nu alpha)", "buoyancy against diffusion"),
("Peclet", "v L / alpha", "advection against diffusion"),
("Nusselt", "h L / k", "total heat transfer against conduction"),
("Biot", "h L / k_solid", "surface against internal resistance"),
("Strouhal", "f L / v", "shedding frequency against flow"),
("Knudsen", "lambda / L", "mean free path against geometry"),
("Schmidt", "nu / D", "momentum diffusivity against mass"),
("Euler", "dp / (rho v^2)", "pressure against inertia"),
("Capillary", "mu v / sigma", "viscosity against surface tension"),
("Stokes", "tau v / L", "particle response against flow"),
]
}
pub fn natural_units_power(dim: Dim) -> Result<i32, DimError> {
if dim.a != 0 || dim.k != 0 || dim.mol != 0 || dim.cd != 0 {
return Err(DimError::Mismatch {
expected: Dim::new(dim.m, dim.kg, dim.s, 0, 0, 0, 0),
found: dim,
});
}
Ok(dim.kg as i32 - dim.m as i32 - dim.s as i32)
}
pub fn natural_units_convert(value: f64, dim: Dim) -> Result<f64, DimError> {
natural_units_power(dim)?;
let c = codata("speed of light").expect("a listed constant");
let e = codata("elementary charge").expect("a listed constant");
let hbar = codata("reduced Planck constant").expect("a listed constant");
let kg_in_ev = c * c / e;
let inverse_metre_in_ev = hbar * c / e;
let inverse_second_in_ev = hbar / e;
Ok(value
* kg_in_ev.powi(dim.kg as i32)
/ inverse_metre_in_ev.powi(dim.m as i32)
/ inverse_second_in_ev.powi(dim.s as i32))
}
pub fn planck_units() -> Vec<(&'static str, f64, &'static str)> {
let hbar = codata("reduced Planck constant").expect("a listed constant");
let c = codata("speed of light").expect("a listed constant");
let g = codata("gravitational constant").expect("a listed constant");
let kb = codata("Boltzmann constant").expect("a listed constant");
let length = (hbar * g / c.powi(3)).sqrt();
let mass = (hbar * c / g).sqrt();
let time = length / c;
let energy = mass * c * c;
vec![
("Planck length", length, "m"),
("Planck mass", mass, "kg"),
("Planck time", time, "s"),
("Planck energy", energy, "J"),
("Planck temperature", energy / kb, "K"),
]
}
pub fn dimensional_check_formula(
expr: &Expr,
var_dims: &[(&str, Dim)],
) -> Result<Dim, DimError> {
fn pure(arg: &Expr, var_dims: &[(&str, Dim)]) -> Result<Dim, DimError> {
let d = dimensional_check_formula(arg, var_dims)?;
if d.is_dimensionless() {
Ok(Dim::NONE)
} else {
Err(DimError::Mismatch { expected: Dim::NONE, found: d })
}
}
match expr {
Expr::Const(_) | Expr::Rat(_) => Ok(Dim::NONE),
Expr::Var(name) => var_dims
.iter()
.find(|(n, _)| n == name)
.map(|(_, d)| *d)
.ok_or_else(|| DimError::UnknownVar(name.clone())),
Expr::Neg(x) | Expr::Abs(x) => dimensional_check_formula(x, var_dims),
Expr::Add(terms) => {
let mut head: Option<Dim> = None;
for t in terms {
let d = dimensional_check_formula(t, var_dims)?;
if is_literal_zero(t) {
continue;
}
match head {
None => head = Some(d),
Some(h) if d != h => {
return Err(DimError::Mismatch { expected: h, found: d })
}
Some(_) => {}
}
}
Ok(head.unwrap_or(Dim::NONE))
}
Expr::Mul(factors) => {
let mut out = Dim::NONE;
for f in factors {
out = out.mul(&dimensional_check_formula(f, var_dims)?)?;
}
Ok(out)
}
Expr::Pow(base, exponent) => {
let db = dimensional_check_formula(base, var_dims)?;
let de = dimensional_check_formula(exponent, var_dims)?;
if !de.is_dimensionless() {
return Err(DimError::Mismatch { expected: Dim::NONE, found: de });
}
if db.is_dimensionless() {
return Ok(Dim::NONE);
}
let r = literal_rational(exponent)
.ok_or(DimError::Malformed("a dimensioned base needs a literal rational exponent"))?;
let (num, den) = (
r.num.to_i64().ok_or(DimError::Overflow)?,
r.den.to_i64().ok_or(DimError::Overflow)?,
);
let mut out = [0i8; 7];
for (slot, e) in out.iter_mut().zip(db.exponents()) {
if (e as i64) % den != 0 {
return Err(DimError::NotAPerfectRoot(db));
}
let scaled = (e as i64 / den).checked_mul(num).ok_or(DimError::Overflow)?;
*slot = i8::try_from(scaled).map_err(|_| DimError::Overflow)?;
}
Ok(Dim::new(out[0], out[1], out[2], out[3], out[4], out[5], out[6]))
}
Expr::Sqrt(x) => dimensional_check_formula(x, var_dims)?.sqrt(),
Expr::Sin(x)
| Expr::Cos(x)
| Expr::Tan(x)
| Expr::Exp(x)
| Expr::Ln(x)
| Expr::Atan(x)
| Expr::Sinh(x)
| Expr::Cosh(x) => pure(x, var_dims),
}
}
fn is_literal_zero(e: &Expr) -> bool {
match e {
Expr::Const(c) => *c == 0.0,
Expr::Rat(r) => r.is_zero(),
Expr::Neg(x) => is_literal_zero(x),
Expr::Mul(fs) => fs.iter().any(is_literal_zero),
Expr::Add(ts) => !ts.is_empty() && ts.iter().all(is_literal_zero),
_ => false,
}
}
fn literal_rational(e: &Expr) -> Option<Rational> {
match e {
Expr::Rat(r) => Some(r.clone()),
Expr::Const(c) => Rational::from_f64_exact(*c),
Expr::Neg(inner) => literal_rational(inner).map(|r| r.neg()),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::units::quantity::Quantity;
pub(super) fn pipe_flow() -> Vec<Dim> {
vec![
Dim::new(-3, 1, 0, 0, 0, 0, 0),
Dim::new(1, 0, -1, 0, 0, 0, 0),
Dim::LENGTH,
Dim::new(-1, 1, -1, 0, 0, 0, 0),
]
}
#[test]
fn buckingham_returns_variables_minus_rank_groups() {
let dims = super::tests::pipe_flow();
let groups = buckingham_pi(&dims).unwrap();
assert_eq!(groups.len(), 1, "pipe flow should have one group");
assert!(is_dimensionless_group(&dims, &groups[0]).unwrap());
let e: Vec<f64> = groups[0].iter().map(|r| r.to_f64()).collect();
let sign = if e[3] > 0.0 { 1.0 } else { -1.0 };
for (got, want) in e.iter().zip([-1.0, -1.0, -1.0, 1.0]) {
assert!((got * sign - want).abs() < 1e-12, "exponents were {e:?}");
}
let mut more = dims.clone();
more.push(Dim::new(1, 0, -2, 0, 0, 0, 0));
assert_eq!(buckingham_pi(&more).unwrap().len(), 2);
let bases = vec![Dim::LENGTH, Dim::MASS, Dim::TIME];
assert!(buckingham_pi(&bases).unwrap().is_empty());
let repeated = vec![Dim::LENGTH, Dim::LENGTH];
let groups = buckingham_pi(&repeated).unwrap();
assert_eq!(groups.len(), 1);
assert!(is_dimensionless_group(&repeated, &groups[0]).unwrap());
let none = vec![Dim::NONE, Dim::NONE, Dim::NONE];
assert_eq!(buckingham_pi(&none).unwrap().len(), 3);
assert!(buckingham_pi(&[]).is_err());
}
#[test]
fn the_groups_cancel_exactly_rather_than_nearly() {
let dims = crate::units::dimensional::tests::pipe_flow();
let groups = buckingham_pi(&dims).unwrap();
for g in &groups {
assert!(is_dimensionless_group(&dims, g).unwrap());
assert!(g.iter().all(|r| r.to_f64().is_finite()));
}
let wrong = vec![Rational::one(), Rational::zero(), Rational::zero(), Rational::zero()];
assert!(!is_dimensionless_group(&dims, &wrong).unwrap());
assert!(is_dimensionless_group(&dims, &wrong[..2]).is_err());
}
#[test]
fn the_named_groups_are_listed_with_what_they_compare() {
let groups = dimensionless_groups_named();
assert!(groups.len() >= 12);
for (name, formula, meaning) in &groups {
assert!(!name.is_empty() && !formula.is_empty() && !meaning.is_empty());
}
let names: Vec<&str> = groups.iter().map(|(n, _, _)| *n).collect();
for wanted in ["Reynolds", "Froude", "Mach", "Prandtl", "Rayleigh", "Weber"] {
assert!(names.contains(&wanted), "{wanted} is missing");
}
let unique: std::collections::BTreeSet<&str> = names.iter().copied().collect();
assert_eq!(unique.len(), names.len());
}
#[test]
fn natural_units_collapse_a_dimension_to_one_power_of_energy() {
assert_eq!(natural_units_power(Dim::MASS).unwrap(), 1);
assert_eq!(natural_units_power(Dim::LENGTH).unwrap(), -1);
assert_eq!(natural_units_power(Dim::TIME).unwrap(), -1);
assert_eq!(natural_units_power(Dim::NONE).unwrap(), 0);
assert_eq!(natural_units_power(Quantity::joules(1.0).dim).unwrap(), 1);
assert_eq!(natural_units_power(Dim::new(1, 0, -1, 0, 0, 0, 0)).unwrap(), 0);
assert!(natural_units_power(Dim::CURRENT).is_err());
assert!(natural_units_power(Dim::TEMPERATURE).is_err());
assert!(natural_units_power(Dim::AMOUNT).is_err());
assert!(natural_units_power(Dim::LUMINOUS).is_err());
let kg = natural_units_convert(1.0, Dim::MASS).unwrap();
assert!((kg / 5.609_588e35 - 1.0).abs() < 1e-5, "a kilogram came to {kg} eV");
let m = natural_units_convert(1.0, Dim::LENGTH).unwrap();
assert!((m / 5.067_731e6 - 1.0).abs() < 1e-5, "a metre came to {m} inverse eV");
let s = natural_units_convert(1.0, Dim::TIME).unwrap();
assert!((s / 1.519_267e15 - 1.0).abs() < 1e-5, "a second came to {s} inverse eV");
let ev = crate::units::quantity::codata("elementary charge").unwrap();
let one = natural_units_convert(ev, Quantity::joules(1.0).dim).unwrap();
assert!((one - 1.0).abs() < 1e-9, "an electron volt came to {one} eV");
let c = crate::units::quantity::codata("speed of light").unwrap();
let unity = natural_units_convert(c, Dim::new(1, 0, -1, 0, 0, 0, 0)).unwrap();
assert!((unity - 1.0).abs() < 1e-9, "c came to {unity}");
}
#[test]
fn the_planck_units_satisfy_their_own_definitions() {
let units = planck_units();
let get = |n: &str| units.iter().find(|(m, _, _)| *m == n).map(|(_, v, _)| *v).unwrap();
let hbar = crate::units::quantity::codata("reduced Planck constant").unwrap();
let c = crate::units::quantity::codata("speed of light").unwrap();
let g = crate::units::quantity::codata("gravitational constant").unwrap();
let (length, mass, time) = (get("Planck length"), get("Planck mass"), get("Planck time"));
assert!((length - c * time).abs() < 1e-12 * length);
assert!((length * mass - hbar / c).abs() < 1e-9 * length * mass);
assert!((get("Planck energy") - mass * c * c).abs() < 1e-12 * get("Planck energy"));
let schwarzschild = 2.0 * g * mass / (c * c);
assert!(
(schwarzschild - 2.0 * length).abs() < 1e-9 * schwarzschild,
"the Schwarzschild radius came to {schwarzschild}"
);
assert!((length / 1.616_255e-35 - 1.0).abs() < 1e-5);
assert!((mass / 2.176_434e-8 - 1.0).abs() < 1e-5);
assert!((time / 5.391_247e-44 - 1.0).abs() < 1e-5);
assert!((get("Planck temperature") / 1.416_784e32 - 1.0).abs() < 1e-5);
}
}
#[cfg(test)]
mod formula_tests {
use super::*;
use crate::units::quantity::Quantity;
const VELOCITY: Dim = Dim::new(1, 0, -1, 0, 0, 0, 0);
const ACCEL: Dim = Dim::new(1, 0, -2, 0, 0, 0, 0);
const FORCE: Dim = Dim::new(1, 1, -2, 0, 0, 0, 0);
const ENERGY: Dim = Dim::new(2, 1, -2, 0, 0, 0, 0);
const FREQUENCY: Dim = Dim::new(0, 0, -1, 0, 0, 0, 0);
fn vars() -> Vec<(&'static str, Dim)> {
vec![
("m", Dim::MASS),
("v", VELOCITY),
("l", Dim::LENGTH),
("x", Dim::LENGTH),
("g", ACCEL),
("t", Dim::TIME),
("tau", Dim::TIME),
("omega", FREQUENCY),
("k_b", Dim::new(2, 1, -2, 0, -1, 0, 0)),
("temp", Dim::TEMPERATURE),
("n", Dim::NONE),
]
}
fn over(a: Expr, b: Expr) -> Expr {
Expr::mul(vec![a, Expr::pow(b, Expr::c(-1.0))])
}
#[test]
fn a_sum_of_unlike_terms_is_refused_and_of_like_terms_is_not() {
let v = vars();
let kinetic = Expr::mul(vec![
Expr::c(0.5),
Expr::var("m"),
Expr::pow(Expr::var("v"), Expr::c(2.0)),
]);
let potential = Expr::mul(vec![Expr::var("m"), Expr::var("g"), Expr::var("l")]);
let total = Expr::add(vec![kinetic.clone(), potential.clone()]);
assert_eq!(dimensional_check_formula(&total, &v).unwrap(), ENERGY);
let slipped = Expr::mul(vec![Expr::c(0.5), Expr::var("m"), Expr::var("v")]);
let bad = Expr::add(vec![slipped, potential]);
match dimensional_check_formula(&bad, &v) {
Err(DimError::Mismatch { expected, found }) => {
assert_eq!(expected, Dim::new(1, 1, -1, 0, 0, 0, 0));
assert_eq!(found, ENERGY);
}
other => panic!("expected a mismatch, got {other:?}"),
}
}
#[test]
fn a_transcendental_argument_must_be_a_pure_number() {
let v = vars();
let dimensioned = [
Expr::Sin(Box::new(Expr::var("t"))),
Expr::Cos(Box::new(Expr::var("t"))),
Expr::Tan(Box::new(Expr::var("t"))),
Expr::Exp(Box::new(Expr::var("t"))),
Expr::Ln(Box::new(Expr::var("t"))),
Expr::Atan(Box::new(Expr::var("t"))),
Expr::Sinh(Box::new(Expr::var("t"))),
Expr::Cosh(Box::new(Expr::var("t"))),
];
for e in &dimensioned {
assert_eq!(
dimensional_check_formula(e, &v),
Err(DimError::Mismatch { expected: Dim::NONE, found: Dim::TIME }),
"a dimensioned argument slipped through {e:?}"
);
}
let phase = Expr::mul(vec![Expr::var("omega"), Expr::var("t")]);
for e in &dimensioned {
let fixed = e.substitute("t", &phase);
assert_eq!(dimensional_check_formula(&fixed, &v).unwrap(), Dim::NONE);
}
let decay = Expr::Exp(Box::new(Expr::Neg(Box::new(over(
Expr::var("t"),
Expr::var("tau"),
)))));
assert_eq!(dimensional_check_formula(&decay, &v).unwrap(), Dim::NONE);
}
#[test]
fn a_power_is_exact_or_it_is_an_error() {
let v = vars();
let l_over_g = over(Expr::var("l"), Expr::var("g")); assert_eq!(dimensional_check_formula(&l_over_g, &v).unwrap(), Dim::new(0, 0, 2, 0, 0, 0, 0));
let half = Expr::pow(l_over_g.clone(), Expr::Rat(Rational::from_i64(1, 2)));
assert_eq!(dimensional_check_formula(&half, &v).unwrap(), Dim::TIME);
let root = Expr::Sqrt(Box::new(l_over_g.clone()));
assert_eq!(dimensional_check_formula(&root, &v).unwrap(), Dim::TIME);
let third = Expr::pow(l_over_g, Expr::Rat(Rational::from_i64(1, 3)));
assert_eq!(
dimensional_check_formula(&third, &v),
Err(DimError::NotAPerfectRoot(Dim::new(0, 0, 2, 0, 0, 0, 0)))
);
let two_to_n = Expr::pow(Expr::c(2.0), Expr::var("n"));
assert_eq!(dimensional_check_formula(&two_to_n, &v).unwrap(), Dim::NONE);
let two_to_t = Expr::pow(Expr::c(2.0), Expr::var("t"));
assert_eq!(
dimensional_check_formula(&two_to_t, &v),
Err(DimError::Mismatch { expected: Dim::NONE, found: Dim::TIME })
);
assert!(matches!(
dimensional_check_formula(&Expr::pow(Expr::var("l"), Expr::var("n")), &v),
Err(DimError::Malformed(_))
));
let sq = Expr::mul(vec![Expr::var("l"), Expr::var("l")]);
assert_eq!(
dimensional_check_formula(&Expr::pow(sq, Expr::c(0.5)), &v).unwrap(),
Dim::LENGTH
);
assert_eq!(
dimensional_check_formula(&Expr::pow(Expr::var("l"), Expr::c(0.5)), &v),
Err(DimError::NotAPerfectRoot(Dim::LENGTH))
);
assert!(matches!(
dimensional_check_formula(&Expr::pow(Expr::var("l"), Expr::c(0.1)), &v),
Err(DimError::NotAPerfectRoot(_))
));
}
#[test]
fn the_checker_agrees_with_quantity_arithmetic() {
let v = vars();
let m = Quantity::new(2.5, Dim::MASS);
let vel = Quantity::new(3.0, VELOCITY);
let len = Quantity::new(1.5, Dim::LENGTH);
let acc = Quantity::new(9.81, ACCEL);
let by_quantity = m
.mul(&vel)
.unwrap()
.mul(&vel)
.unwrap()
.add(&m.mul(&acc).unwrap().mul(&len).unwrap())
.unwrap();
let by_formula = Expr::add(vec![
Expr::mul(vec![
Expr::var("m"),
Expr::pow(Expr::var("v"), Expr::c(2.0)),
]),
Expr::mul(vec![Expr::var("m"), Expr::var("g"), Expr::var("l")]),
]);
assert_eq!(dimensional_check_formula(&by_formula, &v).unwrap(), by_quantity.dim);
assert!(m.add(&vel).is_err());
assert!(dimensional_check_formula(
&Expr::add(vec![Expr::var("m"), Expr::var("v")]),
&v
)
.is_err());
assert!(Quantity::new(4.0, Dim::LENGTH).sqrt().is_err());
assert!(dimensional_check_formula(&Expr::Sqrt(Box::new(Expr::var("l"))), &v).is_err());
}
#[test]
fn differentiating_divides_the_dimension_by_the_variables() {
let v = vars();
let cases: Vec<(Expr, Dim)> = vec![
(
Expr::add(vec![
Expr::mul(vec![Expr::var("v"), Expr::var("t")]),
Expr::mul(vec![
Expr::c(0.5),
Expr::var("g"),
Expr::pow(Expr::var("t"), Expr::c(2.0)),
]),
]),
Dim::LENGTH,
),
(
Expr::mul(vec![
Expr::var("l"),
Expr::Exp(Box::new(Expr::Neg(Box::new(over(
Expr::var("t"),
Expr::var("tau"),
))))),
Expr::Sin(Box::new(Expr::mul(vec![Expr::var("omega"), Expr::var("t")]))),
]),
Dim::LENGTH,
),
(
Expr::Atan(Box::new(Expr::mul(vec![Expr::var("omega"), Expr::var("t")]))),
Dim::NONE,
),
];
for (e, want) in cases {
assert_eq!(dimensional_check_formula(&e, &v).unwrap(), want);
let mut d = e;
let mut expected = want;
for order in 1..=2 {
d = d.diff("t");
expected = expected.div(&Dim::TIME).unwrap();
assert_eq!(
dimensional_check_formula(&d, &v).unwrap(),
expected,
"derivative of order {order} came out wrong"
);
}
}
}
#[test]
fn buckinghams_groups_check_out_as_formulas() {
let dims = crate::units::dimensional::tests::pipe_flow();
let names = ["rho", "u", "d", "mu"];
let var_dims: Vec<(&str, Dim)> =
names.iter().copied().zip(dims.iter().copied()).collect();
let groups = buckingham_pi(&dims).unwrap();
assert_eq!(groups.len(), 1, "pipe flow has exactly one group");
for group in &groups {
let factors: Vec<Expr> = group
.iter()
.zip(names.iter())
.map(|(e, n)| Expr::pow(Expr::var(n), Expr::Rat(e.clone())))
.collect();
let expr = Expr::mul(factors);
assert_eq!(
dimensional_check_formula(&expr, &var_dims).unwrap(),
Dim::NONE,
"a group the theorem calls dimensionless is not"
);
}
}
#[test]
fn textbook_formulas_come_out_with_their_textbook_dimensions() {
let v = vars();
let cases: Vec<(&str, Expr, Dim)> = vec![
("Newton's second law", Expr::mul(vec![Expr::var("m"), Expr::var("g")]), FORCE),
(
"the equipartition energy",
Expr::mul(vec![Expr::c(1.5), Expr::var("k_b"), Expr::var("temp")]),
ENERGY,
),
(
"the pendulum period",
Expr::Sqrt(Box::new(over(Expr::var("l"), Expr::var("g")))),
Dim::TIME,
),
(
"the thermal de Broglie speed",
Expr::Sqrt(Box::new(over(
Expr::mul(vec![Expr::var("k_b"), Expr::var("temp")]),
Expr::var("m"),
))),
VELOCITY,
),
(
"an oscillator's frequency from its period",
over(Expr::c(1.0), Expr::var("tau")),
FREQUENCY,
),
];
for (name, e, want) in cases {
assert_eq!(dimensional_check_formula(&e, &v).unwrap(), want, "{name}");
}
}
#[test]
fn zero_joins_a_sum_of_any_dimension_but_nothing_else_does() {
let v = vars();
let with_zero = Expr::add(vec![Expr::var("l"), Expr::c(0.0)]);
assert_eq!(dimensional_check_formula(&with_zero, &v).unwrap(), Dim::LENGTH);
let zero_product = Expr::add(vec![
Expr::var("l"),
Expr::mul(vec![Expr::c(0.0), Expr::var("t")]),
]);
assert_eq!(dimensional_check_formula(&zero_product, &v).unwrap(), Dim::LENGTH);
let with_one = Expr::add(vec![Expr::var("l"), Expr::c(1.0)]);
assert!(dimensional_check_formula(&with_one, &v).is_err());
let zero_times_nonsense =
Expr::mul(vec![Expr::c(0.0), Expr::Sin(Box::new(Expr::var("t")))]);
assert!(dimensional_check_formula(
&Expr::add(vec![Expr::var("l"), zero_times_nonsense]),
&v
)
.is_err());
}
#[test]
fn a_variable_with_no_dimension_given_is_named_in_the_error() {
let v = vars();
let e = Expr::add(vec![Expr::var("l"), Expr::var("height")]);
assert_eq!(
dimensional_check_formula(&e, &v),
Err(DimError::UnknownVar("height".to_string()))
);
}
}