vle-units 0.3.2

Dimensional analysis and unit conversion for vapor-liquid equilibrium and thermodynamic calculations
Documentation

vle-units

Dimensional analysis and unit conversion for vapor-liquid equilibrium (VLE) and thermodynamic calculations.

Crates.io Docs.rs License: MIT

Works standalone. Designed as the unit layer for vle-thermo but useful in any Rust project that needs thermodynamic-flavored units (gauge pressure, °C / °F, kJ/kmol, barg, psig, mmHg, etc.).

Features

  • Compile-time typed quantities via uom — zero runtime cost, full dimension safety. Use uom::si::f64::* directly (ThermodynamicTemperature, Pressure, MolarEnergy, TemperatureInterval, …); the crate's canonical units (K, kPa, kJ/kmol, K) match VLE conventions.
  • Runtime unit registry — parse user-supplied strings like "25 degC", "3.5 barg", "1 atm" at the FFI boundary.
  • Gauge ↔ absolute conversion with a runtime-configurable atmospheric pressure (never hardcoded). Default: 101.325 kPa.
  • Extensible — define custom units and derived dimensions at runtime (TOML or direct API) without recompiling.

Install

[dependencies]
vle-units = "0.1"

Example

use vle_units::UnitRegistry;

let mut reg = UnitRegistry::with_vle_defaults();

// Parse user input in arbitrary units.
let t = reg.parse("25 degC")?;
let p = reg.parse("3.5 barg")?;

assert!((t.value_kelvin() - 298.15).abs() < 1e-9);
assert!((p.value_kpa()    - 451.325).abs() < 1e-9); // gauge + P_atm

// Configure atmospheric pressure for a different location / altitude.
reg.set_atmospheric_pressure(95.0)?;
# Ok::<(), vle_units::RegistryError>(())

Full API docs: https://docs.rs/vle-units.

How it works

vle-units is two thin layers stacked on top of uom:

                                ┌────────────────────────────────────────┐
   compile-time                 │  uom::si::f64::*                       │
   (zero runtime cost)          │  Pressure, ThermodynamicTemperature,   │
                                │  MolarEnergy, TemperatureInterval, …   │
                                │  — dimension mismatches caught by rustc│
                                └────────────────────────────────────────┘
                                                ▲
                                                │  values enter in canonical
                                                │  units (K, kPa, kJ/kmol, …)
                                                │
                                ┌────────────────────────────────────────┐
   runtime                      │  vle_units::UnitRegistry               │
   (string-driven, extensible)  │  • parses "3.5 barg", "25 degC", …     │
                                │  • runtime-configurable P_atm for      │
                                │    gauge ↔ absolute pressure           │
                                │  • define() new units / load TOML      │
                                │  • exposed to Python via PyO3 so       │
                                │    `pint` shares the same source       │
                                └────────────────────────────────────────┘

Why uom underneath. uom is the most mature Rust units library: ~50 quantity types, ~500 unit definitions, dimension checking via the type system (typenum-encoded SI exponent vectors), and zero runtime overhead — a Pressure<f64> compiles to the same code as a bare f64. We get all of that "for free" and use uom's types directly in engine code, without wrapping them in our own aliases.

Why a layer on top. uom is purely compile-time. It can't:

  • Parse a string like "3.5 barg" that arrived from a Jupyter notebook, a TOML config, or a CLI flag.
  • Carry a runtime-configurable offset — gauge pressure depends on atmospheric pressure, which the operator must be able to override (84.5 kPa at 1500 m elevation, etc.).
  • Let downstream users register a brand-new unit without recompiling the library.

The UnitRegistry provides exactly those three things, and nothing more. Once a value crosses into the engine it's a plain f64 in canonical units, ready to be wrapped in a uom quantity for type-safe algebra.

Defining custom dimensions and units

The defaults cover the VLE quantities (temperature, pressure, molar energy/entropy/volume, amount). To work with any other dimension — length, mass, time, current, luminous intensity, or anything derived — define it on the registry at runtime. Length is a good walkthrough because it isn't in the defaults:

use vle_units::{DimensionVector, UnitRegistry};

let mut reg = UnitRegistry::with_vle_defaults();

// SI exponent vector is (L, M, T, I, Θ, N, J). Length is L^1.
reg.define_dimension("length", DimensionVector::new([1, 0, 0, 0, 0, 0, 0]))?;

// `scale` is meters per 1 of this unit. `offset = 0.0` for everything that
// isn't affine (°C/°F have a non-zero offset, length units don't).
for (name, meters_per_unit) in [
    ("m",     1.0),
    ("mm",    1.0e-3),
    ("cm",    1.0e-2),
    ("km",    1.0e3),
    ("in",    0.0254),     // exact, by 1959 international agreement
    ("ft",    0.3048),     // 12 in
    ("yd",    0.9144),     // 3 ft
    ("mi",    1609.344),   // 5280 ft
    ("nmi",   1852.0),     // international nautical mile
] {
    reg.define_with_dimension(name, "length", meters_per_unit, 0.0)?;
}

// Round-trips are now possible across every registered unit.
let one_mile  = reg.parse("1 mi")?;
let in_meters = reg.from_canonical(one_mile.canonical, "m")?;
let in_yards  = reg.from_canonical(one_mile.canonical, "yd")?;
assert!((in_meters - 1609.344).abs() < 1e-9);
assert!((in_yards  - 1760.0   ).abs() < 1e-9);
# Ok::<(), vle_units::RegistryError>(())

A few things worth noting:

  • The DimensionVector is the SI 7-tuple — you can build any derived dimension by combining exponents (e.g. velocity = [1, 0, -1, 0, 0, 0, 0], force = [1, 1, -2, 0, 0, 0, 0]).
  • Aliasing a unit is just registering it twice under different names with the same scale (e.g. "mi" and "mile", "in" and "inch").
  • The same data can be loaded from a TOML file via load_from_toml if you'd rather keep the catalog out of source code — see src/data/defaults.toml for the format.

Mini CLIs

Two ready-to-run demos ship in examples/. Both follow the same ~30-line shape — build a UnitRegistry, register the dimension and units, then call reg.parse(&argv[1])? and reg.from_canonical(q.canonical, &argv[2])? — so reading both side-by-side is the fastest way to see how the pattern generalizes.

length_convert — a base-dimension example

examples/length_convert.rs covers the simplest case: a single SI base dimension (length, L¹).

$ cargo run -p vle-units --example length_convert -- "1 mile" m
1 mile = 1609.344 m

$ cargo run -p vle-units --example length_convert -- "100 yd" ft
100 yd = 300 ft

$ cargo run -p vle-units --example length_convert -- "2.5 km" mi
2.5 km = 1.5534279805933349 mi

$ cargo run -p vle-units --example length_convert -- "12 in" cm
12 in = 30.479999999999997 cm

mass_flow_convert — a derived-dimension example

examples/mass_flow_convert.rs shows the same flow for a derived dimension — mass ÷ time (M¹·T⁻¹) — with a realistic catalog covering SI metric (kg/s, kg/h, g/s, t/h, …), US customary (lb/s, lb/h, lb/day, oz/s, ston/h, …), and the engineering shorthand you see on heat-balance sheets and refinery PFDs (klb/h, MMlb/day).

$ cargo run -p vle-units --example mass_flow_convert -- "10 kg/s" lb/h
10 kg/s = 79366.41438655593 lb/h

$ cargo run -p vle-units --example mass_flow_convert -- "1 MMlb/day" t/h
1 MMlb/day = 18.89968208333333 t/h

$ cargo run -p vle-units --example mass_flow_convert -- "500 klb/h" kg/s
500 klb/h = 62.99894027777778 kg/s

$ cargo run -p vle-units --example mass_flow_convert -- "60 kg/min" kg/h
60 kg/min = 3600 kg/h

The two examples together demonstrate the full pattern — base or derived dimension, SI or imperial, simple scale-only units or affine. The same shape applies to anything else you'd add (volumetric flow, heat flux, viscosity, …): pick an SI exponent vector, register it, then register your units.

Why a separate crate?

Thermodynamics code needs a few things on top of a general units library:

  1. Gauge pressure (barg, psig, kPag) where absolute = gauge + P_atm, and P_atm is a runtime parameter the operator can set.
  2. Temperature differences vs. absolute temperatures (ΔT in K vs. T in °Cuom models these as distinct dimensions to prevent misuse).
  3. Molar units (kJ/kmol, cm³/mol) that map to the canonical internal units used by cubic-EOS codebases going back to the 1980s.

See docs/en/units/dimensional-analysis.md for the full design rationale.

License

MIT. See LICENSE.