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.
- Factor rescales (e.g.,
- 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);
¶
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);
}
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);
}
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). UseallowNonCompatibility=trueto 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; }
}
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
}
Equals compares Id + Description + Unit (case sensitive for Id/Description).
* operator== compares Id case-insensitively + Unit equality.
Recommendation: treat
Idas a stable identifier (snake/kebab case), and preferEqualswhen 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);
}
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 callingUnit.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;
¶
// 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.Equalsis tolerant;GetHashCode()uses the underlying array's hash (reference based). Do not useDimensionas a dictionary key unless you control instance identity.Unit.Equals(IUnit)compares Dimension + Factor + Offset only.Id/Description/Abbreviationare not part of equality (they're labels).Quantity.Equalsvsoperator==differ in Id comparison (case-sensitive vs case-insensitive). Recommendation: useEqualsfor strict comparisons; normalize Id casing in your own code.
10) Integration in domain services¶
- Treat
Unit/Quantityas plain model types inside yourDHI.Servicesentities. - 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. (Dimensionis[Serializable].)
11) Behaviour¶
- UnitConverter construction
Only construct when
Unit.AllowConversion(source, target)returns true. For identity or non-compatible cases, callUnit.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 anotherIUnit, by rawfactor/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, …¶
13) Extending with your own units (recommended pattern)¶
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);
}