#![deny(missing_docs)]
#![forbid(unsafe_code)]
use core::fmt;
use core::ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub mod vector;
pub use vector::{AccelerationVec, ForceVec, LengthVec, MomentumVec, QVec3, VelocityVec};
#[derive(Clone, Copy, PartialEq, PartialOrd, Default)]
pub struct Qty<
const L: i8,
const M: i8,
const T: i8,
const I: i8,
const K: i8,
const N: i8,
const J: i8,
>(f64);
impl<const L: i8, const M: i8, const T: i8, const I: i8, const K: i8, const N: i8, const J: i8>
Qty<L, M, T, I, K, N, J>
{
pub const ZERO: Self = Qty(0.0);
pub const fn from_si(value: f64) -> Self {
Qty(value)
}
pub const fn to_si(self) -> f64 {
self.0
}
pub const fn dimension() -> [i8; 7] {
[L, M, T, I, K, N, J]
}
pub fn abs(self) -> Self {
Qty(self.0.abs())
}
pub fn min(self, other: Self) -> Self {
Qty(self.0.min(other.0))
}
pub fn max(self, other: Self) -> Self {
Qty(self.0.max(other.0))
}
pub fn is_finite(self) -> bool {
self.0.is_finite()
}
pub fn signum(self) -> f64 {
self.0.signum()
}
pub fn lerp(self, other: Self, t: f64) -> Self {
Qty(self.0 + (other.0 - self.0) * t)
}
}
macro_rules! generic_op {
($trait:ident, $method:ident, $op:tt) => {
impl<
const L: i8,
const M: i8,
const T: i8,
const I: i8,
const K: i8,
const N: i8,
const J: i8,
> $trait for Qty<L, M, T, I, K, N, J>
{
type Output = Self;
fn $method(self, rhs: Self) -> Self {
Qty(self.0 $op rhs.0)
}
}
};
}
generic_op!(Add, add, +);
generic_op!(Sub, sub, -);
impl<const L: i8, const M: i8, const T: i8, const I: i8, const K: i8, const N: i8, const J: i8>
AddAssign for Qty<L, M, T, I, K, N, J>
{
fn add_assign(&mut self, rhs: Self) {
self.0 += rhs.0;
}
}
impl<const L: i8, const M: i8, const T: i8, const I: i8, const K: i8, const N: i8, const J: i8>
SubAssign for Qty<L, M, T, I, K, N, J>
{
fn sub_assign(&mut self, rhs: Self) {
self.0 -= rhs.0;
}
}
impl<const L: i8, const M: i8, const T: i8, const I: i8, const K: i8, const N: i8, const J: i8> Neg
for Qty<L, M, T, I, K, N, J>
{
type Output = Self;
fn neg(self) -> Self {
Qty(-self.0)
}
}
impl<const L: i8, const M: i8, const T: i8, const I: i8, const K: i8, const N: i8, const J: i8>
Mul<f64> for Qty<L, M, T, I, K, N, J>
{
type Output = Self;
fn mul(self, k: f64) -> Self {
Qty(self.0 * k)
}
}
impl<const L: i8, const M: i8, const T: i8, const I: i8, const K: i8, const N: i8, const J: i8>
Div<f64> for Qty<L, M, T, I, K, N, J>
{
type Output = Self;
fn div(self, k: f64) -> Self {
Qty(self.0 / k)
}
}
impl<const L: i8, const M: i8, const T: i8, const I: i8, const K: i8, const N: i8, const J: i8>
Mul<Qty<L, M, T, I, K, N, J>> for f64
{
type Output = Qty<L, M, T, I, K, N, J>;
fn mul(self, q: Qty<L, M, T, I, K, N, J>) -> Qty<L, M, T, I, K, N, J> {
Qty(self * q.0)
}
}
impl<const L: i8, const M: i8, const T: i8, const I: i8, const K: i8, const N: i8, const J: i8> Div
for Qty<L, M, T, I, K, N, J>
{
type Output = f64;
fn div(self, rhs: Self) -> f64 {
self.0 / rhs.0
}
}
impl<const L: i8, const M: i8, const T: i8, const I: i8, const K: i8, const N: i8, const J: i8>
fmt::Debug for Qty<L, M, T, I, K, N, J>
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)?;
for (symbol, exponent) in [
("m", L),
("kg", M),
("s", T),
("A", I),
("K", K),
("mol", N),
("cd", J),
] {
match exponent {
0 => {}
1 => write!(f, "·{symbol}")?,
e => write!(f, "·{symbol}^{e}")?,
}
}
Ok(())
}
}
impl<const L: i8, const M: i8, const T: i8, const I: i8, const K: i8, const N: i8, const J: i8>
fmt::Display for Qty<L, M, T, I, K, N, J>
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(self, f)
}
}
impl<const L: i8, const M: i8, const T: i8, const I: i8, const K: i8, const N: i8, const J: i8>
Serialize for Qty<L, M, T, I, K, N, J>
{
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
self.0.serialize(s)
}
}
impl<
'de,
const L: i8,
const M: i8,
const T: i8,
const I: i8,
const K: i8,
const N: i8,
const J: i8,
> Deserialize<'de> for Qty<L, M, T, I, K, N, J>
{
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
f64::deserialize(d).map(Qty)
}
}
pub type Dimensionless = Qty<0, 0, 0, 0, 0, 0, 0>;
pub type Length = Qty<1, 0, 0, 0, 0, 0, 0>;
pub type Mass = Qty<0, 1, 0, 0, 0, 0, 0>;
pub type Time = Qty<0, 0, 1, 0, 0, 0, 0>;
pub type Current = Qty<0, 0, 0, 1, 0, 0, 0>;
pub type Temperature = Qty<0, 0, 0, 0, 1, 0, 0>;
pub type Amount = Qty<0, 0, 0, 0, 0, 1, 0>;
pub type LuminousIntensity = Qty<0, 0, 0, 0, 0, 0, 1>;
pub type Area = Qty<2, 0, 0, 0, 0, 0, 0>;
pub type Volume = Qty<3, 0, 0, 0, 0, 0, 0>;
pub type Velocity = Qty<1, 0, -1, 0, 0, 0, 0>;
pub type Acceleration = Qty<1, 0, -2, 0, 0, 0, 0>;
pub type Momentum = Qty<1, 1, -1, 0, 0, 0, 0>;
pub type Force = Qty<1, 1, -2, 0, 0, 0, 0>;
pub type Pressure = Qty<-1, 1, -2, 0, 0, 0, 0>;
pub type Energy = Qty<2, 1, -2, 0, 0, 0, 0>;
pub type Power = Qty<2, 1, -3, 0, 0, 0, 0>;
pub type Density = Qty<-3, 1, 0, 0, 0, 0, 0>;
pub type Frequency = Qty<0, 0, -1, 0, 0, 0, 0>;
pub type Irradiance = Qty<0, 1, -3, 0, 0, 0, 0>;
pub type ThermalConductivity = Qty<1, 1, -3, 0, -1, 0, 0>;
pub type Conductance = Qty<2, 1, -3, 0, -1, 0, 0>;
pub type SpecificHeat = Qty<2, 0, -2, 0, -1, 0, 0>;
pub type Diffusivity = Qty<2, 0, -1, 0, 0, 0, 0>;
pub type ThermalExpansion = Qty<0, 0, 0, 0, -1, 0, 0>;
pub type MomentOfInertia = Qty<2, 1, 0, 0, 0, 0, 0>;
pub type AngularMomentum = Qty<2, 1, -1, 0, 0, 0, 0>;
pub type Stiffness = Qty<0, 1, -2, 0, 0, 0, 0>;
pub type Damping = Qty<0, 1, -1, 0, 0, 0, 0>;
pub type Charge = Qty<0, 0, 1, 1, 0, 0, 0>;
pub type Voltage = Qty<2, 1, -3, -1, 0, 0, 0>;
pub type Resistance = Qty<2, 1, -3, -2, 0, 0, 0>;
pub type HeatCapacity = Qty<2, 1, -2, 0, -1, 0, 0>;
macro_rules! product {
($a:ty, $b:ty => $c:ty) => {
impl Mul<$b> for $a {
type Output = $c;
fn mul(self, rhs: $b) -> $c {
Qty(self.0 * rhs.0)
}
}
impl Mul<$a> for $b {
type Output = $c;
fn mul(self, rhs: $a) -> $c {
Qty(self.0 * rhs.0)
}
}
impl Div<$b> for $c {
type Output = $a;
fn div(self, rhs: $b) -> $a {
Qty(self.0 / rhs.0)
}
}
impl Div<$a> for $c {
type Output = $b;
fn div(self, rhs: $a) -> $b {
Qty(self.0 / rhs.0)
}
}
};
}
macro_rules! square {
($a:ty => $c:ty) => {
impl Mul<$a> for $a {
type Output = $c;
fn mul(self, rhs: $a) -> $c {
Qty(self.0 * rhs.0)
}
}
impl Div<$a> for $c {
type Output = $a;
fn div(self, rhs: $a) -> $a {
Qty(self.0 / rhs.0)
}
}
};
}
square!(Length => Area);
product!(Area, Length => Volume);
product!(Velocity, Time => Length);
product!(Acceleration, Time => Velocity);
product!(Mass, Acceleration => Force);
product!(Mass, Velocity => Momentum);
product!(Force, Length => Energy);
product!(Force, Time => Momentum);
product!(Pressure, Area => Force);
product!(Power, Time => Energy);
product!(Irradiance, Area => Power);
product!(Density, Volume => Mass);
product!(Current, Time => Charge);
product!(Voltage, Current => Power);
product!(Resistance, Current => Voltage);
product!(Mass, Area => MomentOfInertia);
product!(MomentOfInertia, Frequency => AngularMomentum);
product!(Stiffness, Length => Force);
product!(Damping, Velocity => Force);
product!(Mass, SpecificHeat => HeatCapacity);
product!(Conductance, Temperature => Power);
product!(Conductance, Time => HeatCapacity);
product!(HeatCapacity, Temperature => Energy);
product!(Frequency, Time => Dimensionless);
impl Volume {
pub fn m3(v: f64) -> Volume {
Qty(v)
}
pub fn cm3(v: f64) -> Volume {
Qty(v * 1e-6)
}
pub fn mm3(v: f64) -> Volume {
Qty(v * 1e-9)
}
pub fn litres(v: f64) -> Volume {
Qty(v * 1e-3)
}
}
impl Area {
pub fn m2(v: f64) -> Area {
Qty(v)
}
pub fn cm2(v: f64) -> Area {
Qty(v * 1e-4)
}
pub fn mm2(v: f64) -> Area {
Qty(v * 1e-6)
}
}
impl Area {
pub fn sqrt(self) -> Length {
Qty(self.0.sqrt())
}
}
impl Resistance {
pub fn ohm(v: f64) -> Resistance {
Qty(v)
}
pub fn milliohm(v: f64) -> Resistance {
Qty(v * 1e-3)
}
}
impl Current {
pub fn a(v: f64) -> Current {
Qty(v)
}
pub fn ma(v: f64) -> Current {
Qty(v * 1e-3)
}
}
impl Voltage {
pub fn v(v: f64) -> Voltage {
Qty(v)
}
pub fn mv(v: f64) -> Voltage {
Qty(v * 1e-3)
}
}
impl Length {
pub fn m(v: f64) -> Length {
Qty(v)
}
pub fn mm(v: f64) -> Length {
Qty(v * 1e-3)
}
pub fn um(v: f64) -> Length {
Qty(v * 1e-6)
}
pub fn nm(v: f64) -> Length {
Qty(v * 1e-9)
}
pub fn in_mm(self) -> f64 {
self.0 * 1e3
}
pub fn in_um(self) -> f64 {
self.0 * 1e6
}
pub fn in_nm(self) -> f64 {
self.0 * 1e9
}
}
impl Time {
pub fn s(v: f64) -> Time {
Qty(v)
}
pub fn ms(v: f64) -> Time {
Qty(v * 1e-3)
}
pub fn us(v: f64) -> Time {
Qty(v * 1e-6)
}
pub fn ns(v: f64) -> Time {
Qty(v * 1e-9)
}
pub fn in_ms(self) -> f64 {
self.0 * 1e3
}
pub fn in_us(self) -> f64 {
self.0 * 1e6
}
}
impl Temperature {
pub fn kelvin(v: f64) -> Temperature {
Qty(v)
}
pub fn celsius(v: f64) -> Temperature {
Qty(v + 273.15)
}
pub fn in_celsius(self) -> f64 {
self.0 - 273.15
}
}
impl Mass {
pub fn kg(v: f64) -> Mass {
Qty(v)
}
pub fn g(v: f64) -> Mass {
Qty(v * 1e-3)
}
}
impl Density {
pub fn g_per_cm3(v: f64) -> Density {
Qty(v * 1e3)
}
pub fn kg_per_m3(v: f64) -> Density {
Qty(v)
}
}
impl Power {
pub fn w(v: f64) -> Power {
Qty(v)
}
pub fn mw(v: f64) -> Power {
Qty(v * 1e-3)
}
pub fn uw(v: f64) -> Power {
Qty(v * 1e-6)
}
pub fn in_mw(self) -> f64 {
self.0 * 1e3
}
}
impl Energy {
pub fn j(v: f64) -> Energy {
Qty(v)
}
pub fn mj(v: f64) -> Energy {
Qty(v * 1e-3)
}
}
impl Frequency {
pub fn hz(v: f64) -> Frequency {
Qty(v)
}
pub fn khz(v: f64) -> Frequency {
Qty(v * 1e3)
}
pub fn mhz(v: f64) -> Frequency {
Qty(v * 1e6)
}
pub fn period(self) -> Time {
Qty(1.0 / self.0)
}
}
impl Velocity {
pub fn m_per_s(v: f64) -> Velocity {
Qty(v)
}
pub fn mm_per_s(v: f64) -> Velocity {
Qty(v * 1e-3)
}
}
impl Irradiance {
pub fn w_per_m2(v: f64) -> Irradiance {
Qty(v)
}
pub fn mw_per_cm2(v: f64) -> Irradiance {
Qty(v * 10.0)
}
}
impl ThermalConductivity {
pub fn w_per_m_k(v: f64) -> ThermalConductivity {
Qty(v)
}
}
impl SpecificHeat {
pub fn j_per_kg_k(v: f64) -> SpecificHeat {
Qty(v)
}
}
impl Conductance {
pub fn w_per_k(v: f64) -> Conductance {
Qty(v)
}
}
impl HeatCapacity {
pub fn j_per_k(v: f64) -> HeatCapacity {
Qty(v)
}
}
impl ThermalExpansion {
pub fn ppm_per_k(v: f64) -> ThermalExpansion {
Qty(v * 1e-6)
}
}
impl Dimensionless {
pub fn ratio(v: f64) -> Dimensionless {
Qty(v)
}
}
pub const C: Velocity = Qty(299_792_458.0);
pub const PLANCK: Qty<2, 1, -1, 0, 0, 0, 0> = Qty(6.626_070_15e-34);
pub const BOLTZMANN: HeatCapacity = Qty(1.380_649e-23);
pub const STEFAN_BOLTZMANN: Qty<0, 1, -3, 0, -4, 0, 0> = Qty(5.670_374_419e-8);
pub const G0: Acceleration = Qty(9.806_65);
pub fn photon_energy(wavelength: Length) -> Energy {
Qty(PLANCK.0 * C.0 / wavelength.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn products_land_on_the_named_dimension() {
let m = Mass::kg(2.0);
let a = Acceleration::from_si(3.0);
let f: Force = m * a;
assert!((f.to_si() - 6.0).abs() < 1e-12);
let by_work: Energy = f * Length::m(4.0);
let by_power: Energy = Power::w(24.0) * Time::s(1.0);
assert!((by_work - by_power).abs().to_si() < 1e-12);
let swapped: Force = a * m;
assert_eq!(f, swapped);
}
#[test]
fn unit_prefixes_are_only_a_doorway() {
let lens = Length::mm(25.4);
let green = Length::nm(550.0);
assert!(lens > green);
assert!((lens.to_si() - 0.0254).abs() < 1e-15);
assert!((green.in_nm() - 550.0).abs() < 1e-9);
let waves = lens / green;
assert!((waves - 46_181.8).abs() < 0.1, "got {waves}");
}
#[test]
fn like_over_like_is_a_bare_number() {
let reflected = Power::mw(0.42);
let incident = Power::mw(10.0);
let r: f64 = reflected / incident;
assert!((r - 0.042).abs() < 1e-12);
}
#[test]
fn celsius_is_an_offset_not_a_factor() {
assert!((Temperature::celsius(20.0).to_si() - 293.15).abs() < 1e-12);
assert!((Temperature::kelvin(293.15).in_celsius() - 20.0).abs() < 1e-12);
let rise = Temperature::kelvin(313.15) - Temperature::kelvin(293.15);
assert!((rise.to_si() - 20.0).abs() < 1e-12);
}
#[test]
fn absorbed_light_becomes_a_temperature_rise() {
let irradiance = Irradiance::mw_per_cm2(50.0); let area: Area = Length::mm(10.0) * Length::mm(10.0); let absorptance = 0.02; let absorbed: Power = irradiance * area * absorptance;
assert!((absorbed.to_si() - 0.001).abs() < 1e-12, "{absorbed:?}");
let glass = Mass::g(2.0);
let c_p = SpecificHeat::j_per_kg_k(858.0); let capacity: HeatCapacity = glass * c_p;
let heat: Energy = absorbed * Time::s(1.0);
let rise: Temperature = heat / capacity;
assert!(
(rise.to_si() - 0.000_582_7).abs() < 1e-7,
"expected about 0.58 mK, got {rise:?}"
);
}
#[test]
fn photon_energy_matches_the_textbook_figure() {
let e = photon_energy(Length::nm(550.0));
assert!((e.to_si() - 3.612e-19).abs() < 1e-21, "{e:?}");
let per_joule = Energy::j(1.0) / e;
assert!((per_joule - 2.768e18).abs() < 1e15, "got {per_joule:e}");
}
#[test]
fn debug_shows_the_dimension() {
assert_eq!(format!("{:?}", Force::from_si(6.0)), "6·m·kg·s^-2");
assert_eq!(format!("{:?}", Dimensionless::ratio(0.5)), "0.5");
assert_eq!(Force::dimension(), [1, 1, -2, 0, 0, 0, 0]);
}
#[test]
fn serialises_as_a_bare_si_number() {
let json = serde_json::to_string(&Length::mm(25.4)).unwrap();
assert_eq!(json, "0.0254");
let back: Length = serde_json::from_str(&json).unwrap();
assert_eq!(back, Length::mm(25.4));
}
}