Skip to content

DHI.Physics — Dimensions, Units & Quantities

This package gives you a small, focused, dimensionally aware units system you can use across any domain module (TimeSeries, Jobs, Models, …). It's designed to be simple to embed in your entities and services, but expressive enough to surface dimension mismatches—like accidentally treating kilometers as seconds—as explicit runtime errors at the point of conversion, rather than silent wrong results.

What you get: * Dimension — vector of base-dimension exponents with arithmetic * Unit / IUnit — linear units with factor/offset and dimensional compatibility checks * Quantity — domain concept = "what is being measured" (id/description/unit) * UnitConverter — convenience wrapper around Unit.Convert(…) * Units — a large catalog of predefined SI + US/UK units and many derived combinations This library is published on nuget.org. Add a reference to your module and go:

dotnet add package DHI.Physics


1) Design model

  • A Dimension is an 8-slot exponent vector over: Length, Mass, Time, Temperature, ElectricCurrent, AmountOfSubstance, LuminousIntensity, NonDimensional.
    • Multiply/divide dimensions → add/subtract exponents.
    • Raise to a power → multiply exponents.
    • Equality uses a small tolerance (1e-16).
  • A Unit is a linear transform to some canonical base:
    value_in_target = (value * from.Factor + from.Offset - to.Offset) / to.Factor
    
    • Factor rescales (e.g., km = 1000 * m).
    • Offset shifts (e.g., Fahrenheit vs Celsius).
    • A Unit also carries a Dimension; only compatible dimensions can be converted.
  • A Quantity bundles what we measure (id/description) with how we measure (unit).

2) Quick start (copy-paste)

using DHI.Physics;
using static DHI.Physics.Units;
// 1) Convert 3.5 m/s -> km/h
double ms = 3.5;
double kmh = Unit.Convert(ms, meterPerSec, kilometerPerHour); // 12.6
// 2) Fahrenheit -> Celsius
double f = 68;
double c = Unit.Convert(f, degreeFahrenheit, degreeCelsius);  // ≈ 20
// 3) Check dimensional compatibility
bool ok = meterPerSec.IsCompatibleWith(kilometerPerHour);     // true
bool nope = meterPerSec.IsCompatibleWith(kilogram);           // false
// 4) Define your own derived unit (m^3/s/km^2)
Unit runoff = m3 / sec / km2;
// 5) Use quantities to describe variables
var discharge = new Quantity("discharge", "Volumetric flow", m3PerSec);

3) Dimension

[Serializable]
public class Dimension : IEquatable<Dimension>
{
    // Static bases:
    public static readonly Dimension Length, Mass, Time, Temperature,
                                     ElectricCurrent, AmountOfSubstance,
                                     LuminousIntensity, NonDimensional;
    // Access exponent for a base:
    public double this[DimensionBase b] { get; set; }
    // Operators:
    public static Dimension operator *(Dimension a, Dimension b);
    public static Dimension operator /(Dimension a, Dimension b);
    public Dimension Power(double p);
}
How to think about it * Dimension.Length is [1,0,0,0,0,0,0,0] * Dimension.Length * Dimension.Time becomes [1,0,1,0,0,0,0,0] * (Length / Time).Power(2) yields L^2 T^-2 (think m²/s²) Notes * Equality is tolerant (|Δ| <= 1e-16 per slot). * ToString() prints the non-zero bases: e.g., Length^1 * Time^-1.


4) Unit / IUnit

public interface IUnit : IEquatable<IUnit>
{
    string Id { get; }
    string Description { get; }
    string Abbreviation { get; }
    Dimension Dimension { get; }
    double Factor { get; }
    double Offset { get; }
    bool IsCompatibleWith(IUnit other);
    Unit Power(double power);
    double Convert(double value, IUnit other); // instance convenience
}
[Serializable]
public class Unit : IUnit
{
    // Core constructors
    public Unit(string id, string description, string abbreviation, Dimension dimension);
    public Unit(string id, string description, string abbreviation, IUnit unit);
    public Unit(string id, string description, string abbreviation, double factor, Dimension dim, double offset = 0.0);
    // Static converters (overloads for double/float/int):
    public static double Convert(double value, Unit from, Unit to, bool allowNonCompatibility = false);
    // Composition operators:
    public static Unit operator *(Unit a, Unit b);
    public static Unit operator /(Unit a, Unit b);
    public static Unit operator ^(Unit u, int power);
    public static Unit operator *(double k, Unit u); // scale labels too
    public Unit Power(double p);
}
Compatibility * IsCompatibleWith checks dimension equality (with tolerance). Example: m/s and km/h are compatible; m/s and kg are not. Offsets * Offsets make temperature work: * degreeFahrenheit is defined with factor 5/9 and offset -(32*5)/9 relative to Celsius. * Convert uses the formula at the top of this doc. Conversion guard * Unit.AllowConversion(from, to, allowNonCompatibility) returns: * true if conversion is compatible and necessary, * false if same unit or incompatible but allowed (allowNonCompatibility=true), * otherwise it throws with a detailed incompatibility message.

Tip: If you just want the number, the one-liner is Unit.Convert(value, from, to). Use allowNonCompatibility=true to avoid exceptions and get the unchanged value back when incompatible.


5) Quantity

[Serializable]
public class Quantity : IEquatable<Quantity>
{
    public Quantity();                                      // "Undefined quantity"
    public Quantity(string id);
    public Quantity(string id, string description);
    public Quantity(string id, string description, Unit unit);
    public string Id { get; }
    public string Description { get; }
    public Unit Unit { get; }
}
Use Quantity in your domain entities to make intent obvious and to centralize the canonical unit:
public sealed class GaugeReading
{
    public DateTime Timestamp { get; init; }
    public double Value { get; init; }
    public Quantity Quantity { get; init; } // e.g. "water_level", Units.meter
}
Equality logic: * Equals compares Id + Description + Unit (case sensitive for Id/Description). * operator== compares Id case-insensitively + Unit equality.

Recommendation: treat Id as a stable identifier (snake/kebab case), and prefer Equals when you need strict matching.


6) UnitConverter (optional helper)

public sealed class UnitConverter
{
    public UnitConverter(Unit source, Unit target, bool allowNonCompatibility = false);
    public double GetValue(double value);
    public float  GetValue(float value);
    public double GetValue(int value);
}
Intended for hot-paths where you will convert many numbers between the same units. Important usage pattern Only construct a UnitConverter when conversion is actually needed and compatible:
if (Unit.AllowConversion(meterPerSec, kilometerPerHour))
{
    var conv = new UnitConverter(meterPerSec, kilometerPerHour);
    // reuse conv.GetValue(...) in a loop
}
else
{
    // either identical units (no-op) or incompatible (handle accordingly)
}

If units are identical or conversion is incompatible (and you passed allowNonCompatibility=true), prefer calling Unit.Convert(...) directly for those one-off cases.


7) The Units catalog

DHI.Physics.Units exposes a large static catalog of predefined units, built from the primitives and operators: * Length: meter, kilometer, centimeter, inch, feet, yard, mile, nauticalmile, … * Mass: kilogram, gram, ton, Pound, … * Time: sec, minute, hour, day, year, … * Area & Volume: m2, ft2, acre, ha, m3, liter, ft3, acft, … * Flow & Velocity: m3PerSec, ft3PerSec, literPerSec, meterPerSec, kilometerPerHour, knot, … * Density & Concentration: kiloGramPerM3, milliGramPerL, … * Energy & Power: Joule, kiloJoule, watt, kwatt, KiloWattHour, … * Pressure: Pascal, kiloPascal, Bar, psi, … * Temperature: degreeCelsius, degreeFahrenheit, degreeKelvin (+ deltas and reciprocals) * Electrical: ampere, volt, siemens, siemensPerMeter, … * Angles: radian, degree, … * Rates & frequencies: perSec, hertz, perDay, percentPerHour, … * Lots of domain-specific composites frequently used in hydro, metocean, water utilities, etc. All of these are defined using the same rules you'll use:

public static Unit kilometer    => new Unit("kilometer", "kilometer", "km", 1000 * meter);
public static Unit m3PerSec     => new Unit("m3PerSec", "meter^3/sec", "m^3/s", m3 / sec);
public static Unit degreeKelvin => new Unit("degreeKelvin", "degreeKelvin", "degK", degreeCelsius - 273.15);

The catalog is broad; you don't have to remember names—IntelliSense on Units. is the fastest way to find what you need.


8) Recipes

A) Normalize data to canonical units

double NormalizeToSI(double value, Unit source)
{
    if (source.IsCompatibleWith(Units.meter)) return Unit.Convert(value, source, Units.meter);
    if (source.IsCompatibleWith(Units.kilogram)) return Unit.Convert(value, source, Units.kilogram);
    if (source.IsCompatibleWith(Units.sec)) return Unit.Convert(value, source, Units.sec);
    // … add what you care about
    throw new NotSupportedException($"No canonical mapping for {source.Id}");
}

B) Build a new domain Quantity

// Rainfall intensity: mm/h
var rainfall = new Quantity("rainfall", "Rainfall intensity", Units.millimeterPerHour);

C) Derived unit on the fly

// Diffusivity: m^2/s
Unit diffusivity = Units.m2 / Units.sec;
// Manning-like: m^(1/3)/s
Unit chezy = Units.meter ^ (1.0/3.0) / Units.sec;

9) Equality & hashing (be precise)

  • Dimension.Equals is tolerant; GetHashCode() uses the underlying array's hash (reference based). Do not use Dimension as a dictionary key unless you control instance identity.
  • Unit.Equals(IUnit) compares Dimension + Factor + Offset only. Id/Description/Abbreviation are not part of equality (they're labels).
  • Quantity.Equals vs operator== differ in Id comparison (case-sensitive vs case-insensitive). Recommendation: use Equals for strict comparisons; normalize Id casing in your own code.

10) Integration in domain services

  • Treat Unit/Quantity as plain model types inside your DHI.Services entities.
  • Services can enforce invariants like "all inputs must be compatible with m3/s":
    void Ingest(double value, Unit unit)
    {
        if (!unit.IsCompatibleWith(Units.m3PerSec))
            throw new ArgumentException("Value must be volumetric flow.");
        double si = Unit.Convert(value, unit, Units.m3PerSec);
        // persist si …
    }
    
  • When serializing entities, persist either the Unit.Id (and map it back using your own registry) or persist the full Unit triple (factor/offset/dimension exponents) depending on your storage strategy. (Dimension is [Serializable].)

11) Behaviour

  • UnitConverter construction Only construct when Unit.AllowConversion(source, target) returns true. For identity or non-compatible cases, call Unit.Convert(...) directly.
  • String labels during composition Operator-built units generate composite Id/Description/Abbreviation strings. These are meant for debugging and display; if you need strict branding, build your own new Unit(id, description, abbreviation, IUnit baseUnit) with the labels you want.
  • NonDimensional Things like radian, percent, and counts use Dimension.NonDimensional. You can still multiply/divide them into composites (e.g., percentPerHour).
  • Precision Dimensional equality uses a hardcoded 1e-16—good enough for exact operator compositions. Avoid creating dimensions with arbitrary doubles unless you mean to.

12) Reference cheatsheet

Dimension

  • Static bases: Length, Mass, Time, Temperature, ElectricCurrent, AmountOfSubstance, LuminousIntensity, NonDimensional
  • Indexer: double this[DimensionBase b] { get; set; }
  • Ops: *, /, Power(double)
  • Equality: tolerant; ToString() prints non-zero exponents

Unit / IUnit

  • Data: Id, Description, Abbreviation, Dimension, Factor, Offset
  • Compatibility: IsCompatibleWith(IUnit)
  • Convert: Unit.Convert(double|float|int, Unit from, Unit to, bool allowNonCompatibility=false)
  • Compose: *, /, ^, scalar * and /
  • Create: by dimension (new Unit(id, desc, abbr, dim)), by re-labeling another IUnit, by raw factor/dimension/offset

Quantity

  • Data: Id, Description, Unit
  • Ctors: Quantity(), Quantity(id), Quantity(id, desc), Quantity(id, desc, unit)

UnitConverter

  • Ctor: UnitConverter(Unit source, Unit target, bool allowNonCompatibility=false)
  • Methods: GetValue(double|float|int)

Units

* Large static catalog under DHI.Physics.Units (use IntelliSense). Examples: meter, m2, m3PerSec, degreeCelsius, psi, KiloWattHour, siemensPerMeter, percentPerHour, …

public static class MyUnits
{
    // Define a branded display with the right dimension
    public static readonly Unit AcreFeetPerDayPerAcre =
        new Unit("AFD/ac", "acre-feet per day per acre", "ac-ft/d/ac", Units.acft / Units.day / Units.acre);
    // Or wrap an existing unit but change labels
    public static readonly Unit Discharge =
        new Unit("Q", "Discharge", "Q", Units.m3PerSec);
}