Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Markdown

Taking Your Brain to Another Dimension - A C# library for Physical Units

4.98/5 (77 votes)
12 Mar 2023CPOL16 min read 92.5K   1.5K  
A C# library for use in physics and engineering calculations
In this article, you will learn how to keep your spacecraft safe from coding errors.

Introduction

The initial inspiration for this project is the loss of NASA's Mars Climate Orbiter in 1999. This failed to enter Mars orbit due to a mix up between metric (SI) and United States Customary Units. One sub-system was supplying measurements in pound-force seconds to another sub-system expecting them in Newton Seconds. As the probe braked to enter orbit, it travelled too close to the planet's atmosphere and either burned up or ricocheted off into solar orbit.

So I have tried to build a code library in which this kind of error should be ruled out by design. It has the following features:

  • It can be used to perform many standard calculations from physics and engineering.
  • It is based on dimensional analysis, so all quantities have a corresponding physical dimension, such as Length or Mass.
  • It is strongly typed, so quantities of different dimension can only be combined in scientifically valid ways.
  • Internally, all values are stored in S.I. (metric) units.
  • Values are only converted to a particular system of units at its external interfaces, for example, when converting to and from strings.

It is written using C# version 9 and utilizes the .NET 5.0 framework.

Here is an example of the library in use:

C#
// Tsiolkovsky rocket equation
Mass EmptyMass = 120000.Kilograms();
Mass PropellantMass = 1200000.Kilograms();
Mass WetMass = EmptyMass + PropellantMass;
Velocity ExhaustVelocity = 3700.MetresPerSecond();
Velocity DeltaV = ExhaustVelocity * Math.Log(WetMass / EmptyMass);
// DeltaV = 8872.21251 m/s

Throughout this article, and in the sample code and unit tests, I have used examples from my old grammar school physics textbook - Nelkon and Parker Advanced Level Physics. This was the standard sixth form physics book in Britain throughout the sixties and seventies.

Background

The library is based on the concepts of dimensions and units.

Dimensions

The Dimension of a physical quantity determines how it is related to a set of fundamental quantities such as mass, length and time. These are usually abbreviated to M, L, T, etc. New dimensions can be derived by combining these fundamental ones using multiplication and division. So:

  • Area = Length x Length = L²
  • Volume = Length x Length x Length = L³
  • Density = Mass / Volume = M/L³ = ML⁻³
  • Velocity = Length / Time = L/T = LT⁻¹
  • Acceleration = velocity / Time = LT⁻²
  • Force = Mass x Acceleration = MLT⁻²

And so on.

The dimension of any particular quantity can be represented as a sequence of powers of the fundamental dimensions (e.g., Force = MLT⁻² above). It is invalid to try to add or subtract quantities if their dimensions do not match. So it is invalid to add a mass to a volume for instance.

The International System of Units (S.I.) uses the following basic dimensions:

Dimension Symbol Unit Unit Symbol
Mass M kilogramme kg
Length L metre m
Time T second s
Electric Current I ampere A
Thermodynamic Temperature Θ kelvin K
Amount of Substance N mole mol
Luminous Intensity J candela cd

The library defines these basic dimensions, and many derived ones.

Units

A unit system can define different basic units to correspond to the various dimensions. So whereas the S.I. system has a unit of kilogrammes for mass, the American and British systems use the pound. Similarly, we have the foot in place of the metre as the unit of length. There are also differences between the American and British systems when it comes to measurement of volume. Thankfully, the units for the other basic dimensions are the same in all three systems.

Although the library has definitions for both the S.I., American and British systems, it is possible to create and use new ones. For example, you could create a system using the Japanese shakkanho system, with the shaku (尺) as the unit of length and the kan (貫) as the unit of mass.

Using the Code

The supplied code in the attached ZIP consists of a Visual Studio solution with two projects: the library itself and a command line programme which tests and demonstrates the library features. To use the library in your own project, add the library project file in "\KMB.Library.Units\KMB.Library.Units.csproj", then add the following using statements to your code:

C#
using KMB.Library.Units;
using KMB.Library.Units.Metric;
using KMB.Library.Units.TimeUnits;      // for hours, minutes etc.
using KMB.Library.Units.British;        // For feet and pounds. Or use USA if you prefer

Contents of the Library

The Units library defines various classes and interfaces. The primary ones are discussed here:

class Dimensions

This class is used to represent a physical dimension or combination of them. It has a read-only field for the power of each dimension:

C#
public readonly short M; // Mass
public readonly short L; // Length
public readonly short T; // Time
public readonly short I; // Current
public readonly short Θ; // Temperature
public readonly short N; // Amount of Substance
public readonly short J; // Luminous Intensity
public readonly short A; // Angle.

Note the value for angle. Strictly angles are dimensionless, but it is convenient to treat them as having a distinct dimension. This way, we can distinguish angles from dimensionless quantities, when converting to a string, for example.

The class has various constructors, and also defines operators for multiplication and division:

C#
public static Dimensions operator *(Dimensions d1, Dimensions d2)...
public static Dimensions operator /(Dimensions d1, Dimensions d2)...

Using this class, we can define the basic dimensions:

C#
public static readonly Dimensions Dimensionless = new Dimensions(0, 0, 0);
public static readonly Dimensions Mass = new Dimensions(1, 0, 0);
public static readonly Dimensions Length = new Dimensions(0, 1, 0);
public static readonly Dimensions Time = new Dimensions(0, 0, 1);
        :

And define any derived dimensions:

C#
public static readonly Dimensions Area = Length * Length;
public static readonly Dimensions Volume = Area * Length;
public static readonly Dimensions Density = Mass / Volume;
public static readonly Dimensions Velocity = Length / Time;
public static readonly Dimensions AngularVelocity = Angle / Time;
        :

The overloaded ToString() method of Dimensions outputs the powers of each dimension:

C#
Dimensions.Pressure.ToString()  // returns "M L⁻¹ T⁻²"
Dimensions.Resistivity.ToString()  // returns "M L³ T⁻³ I⁻²"

Interface IPhysicalQuantity

This interface is the basis for all physical quantities in the system. It has two properties:

C#
double Value { get; }
Dimensions Dimensions { get; }

For each defined value of Dimensions, there will be a corresponding structure which implements the IPhysicalQuantity interface. For example, Length, Area, Mass and so on.

Example Physical Quantity - Length

The Length structure implements the IPhysicalQuantity interface:

C#
public readonly partial struct Length: IPhysicalQuantity

It has a read-only Value property:

C#
public readonly double Value { get; init; }

And a Dimensions property:

C#
public readonly Dimensions Dimensions { get { return Dimensions.Length; } }

Notice how the Dimensions property returns the corresponding statically defined Dimensions value.

So given this structure, we can now define a variable to represent a particular length:

C#
Length l0 = new Length(3.4);        // 3.4 metres

The struct defines lots of operators. For example, you can add a length to another one:

C#
public static Length operator+(Length v1, Length v2)
{
    return new Length(v1.Value + v2.Value);
}

Or compare two lengths:

C#
public static bool operator >(Length v1, Length v2)
 {
     return Compare(v1, v2) > 0;
 }

Or you can create an Area by multiplying two lengths together:

C#
public static Area operator*(Length v1, Length v2)
{
    return new Area(v1.Value * v2.Value);
}

Or a Velocity by dividing a length by a time:

C#
public static Velocity operator/(Length v1, Time v2)
{
    return new Velocity(v1.Value / v2.Value);
}

Here's this divide operator in use:

C#
Length l = 100.Metres();
Time t = 9.58.Seconds();
Velocity v = l / t;         // v = 10.43 m/s

There are also various ToString() and Parse() methods:

C#
public override string ToString();
public string ToString(UnitsSystem.FormatOption option);
public string ToString(UnitsSystem system, UnitsSystem.FormatOption option);
public string ToString(params Unit[] units);
public static Length Parse(string s);
public static Length Parse(string s, UnitsSystem system);

The formatting and parsing of strings is actually delegated to the current unit system. See below.

Here are some examples to demonstrate the various options for ToString() and Parse():

C#
Length l = 1234.567.Metres();
string s = l.ToString();    // s = "1.234567 km" (same as BestFit)
// Formatting options:
s = l.ToString(UnitsSystem.FormatOption.Standard); // s = "1234.567 m" 
                                                   // (standard unit for length is metres)
s = l.ToString(UnitsSystem.FormatOption.BestFit);  // s = "1.234567 km" 
                                                   // (kilometres is the best fit unit 
                                                   // for the value)
s = l.ToString(UnitsSystem.FormatOption.Multiple); // s = "1 km 234 m 56 cm 7 mm" 
                                                   // (use multiple units in decreasing value)
// Specify the units:
s = l.ToString(MetricUnits.Metres, MetricUnits.Centimetres); // s = "1234 m 56.7 cm" 
// British units:
s = l.ToString(BritishUnits.System, UnitsSystem.FormatOption.Standard); // s = "4050.41667 ft"
s = l.ToString(BritishUnits.System, UnitsSystem.FormatOption.BestFit);  // s = "1350.13889 yd"
s = l.ToString(BritishUnits.System, UnitsSystem.FormatOption.Multiple); // s = "1350 yd 5 in"
// Specified British units:
s = l.ToString(BritishUnits.Miles, 
               BritishUnits.Feet, BritishUnits.Inches); // s = "4050 ft 5 in"

// Parsing
l = Length.Parse("42 m");    // l = 42 m
l = Length.Parse("42 m 76 cm"); // l = 42.76 m
l = Length.Parse("5 ft 4 in", BritishUnits.System); // l = 1.6256 m
// This will throw an exception
l = Length.Parse("42 m 76 kg");

Because there are so many classes, operators and methods required for the quantities, these classes are generated using the T4 Template processor. See the Code Generation section.

Temperatures

The library contains two classes for dealing with temperatures - AbsoluteTemperature and TemperatureChange. The first is used for absolute temperatures, as you would read from a thermometer:

C#
AbsoluteTemperature t3 = 600.65.Kelvin();       // melting point of lead
AbsoluteTemperature c2 = 60.Celsius();          // c2 = 333.15 K

The second is used in many formulae where it is the temperature change that is important:

C#
TemperatureChange deltaT = 100.Celsius() - 20.Celsius();
ThermalCapacity tcKettle = 100.CaloriesPerDegreeKelvin();
SpecificHeat shWater = 4184.JoulesPerKilogramPerDegreeKelvin();
Mass mWater = 1.Kilograms();
ThermalCapacity tcWater = mWater * shWater;
ThermalCapacity tcTotal = tcKettle + tcWater;
Energy e = tcTotal * deltaT;    // e = 368208 J

struct PhysicalQuantity

This is the get out of jail card for cases when the strongly typed quantities won't do. It is weakly typed so has its own property to represent the dimensions:

C#
public readonly partial struct PhysicalQuantity: IPhysicalQuantity
{
    public double Value { get; init; }
    public Dimensions Dimensions { get; init; }

Like the strongly typed quantities, it has operators for addition, etc., but these are checked at run time instead of preventing compilation. So it is possible to do this:

C#
PhysicalQuantity l1 = new PhysicalQuantity(2.632, Dimensions.Length);
PhysicalQuantity l2 = new PhysicalQuantity(2.632, Dimensions.Length);
PhysicalQuantity sum = l1 + l2;

But this will throw an exception:

C#
PhysicalQuantity m = new PhysicalQuantity(65, Dimensions.Mass);
sum = l1 + m;

But multiplication and division will correctly calculate the correct dimensions:

C#
PhysicalQuantity product = l1 * m;
string s = product.ToString(); // s = "171.08 kg⋅m"

Shapes

The library defines some utility classes for doing calculations relating to 2-D and 3-D shapes. For example to get the area of a circle with radius 3 cm:

C#
Circle circle = Circle.OfRadius(3.Centimetres());
Area area = circle.Area;    // = 28.27 cm²

And here is an example using a solid 3-D shape:

C#
SolidCylinder cylinder = new SolidCylinder()
                              {
                                  Mass = 20.Pounds(),
                                  Radius = 2.Inches(),
                                  Height = 6.Inches()
                              };
  area = cylinder.SurfaceArea;            // = 101.2 in²
  Volume volume = cylinder.Volume;            // = 75.23 in³
  Density density = cylinder.Density          // = 0.2653 lb/in³
  Length radiusOfGyration = cylinder.RadiusOfGyration;// = 1.414 in
  MomentOfInertia I = cylinder.MomentOfInertia;   // = 0.2778 lb⋅ft²

class VectorOf

The library defines a VectorOf class which can be used for directed quantities such as displacement, velocity or force. I have called it VectorOf to avoid a name clash with the System.Numerics.Vector class.

C#
public class VectorOf<T> where T: IPhysicalQuantity, new()

It has constructors that take either 3 scalar values:

C#
public VectorOf(T x, T y, T z)

Or a magnitude and direction (for 2-D values):

C#
public VectorOf(T magnitude, Angle direction)

Or a magnitude and two angles (inclination and azimuth) for 3-D vectors:

C#
public VectorOf(T magnitude, Angle inclination, Angle azimuth)

For example:

C#
// Suppose a ship is travelling due east at 30 mph and a boy runs across the deck
// in a north west direction at 6 mph.  What is the speed and direction of the boy
// relative to the sea?
var v2 = new VectorOf<velocity>(30.MilesPerHour(), 90.Degrees());
var v3 = new VectorOf<velocity>(6.MilesPerHour(), 315.Degrees());
var v4 = v2 + v3;
Velocity m2 = v4.Magnitude; // 26 mph
Angle a3 = v4.Direction;    // 81 degrees

Currently the VectorOf class uses the PhysicalQuantity type internally. This is because 'generic maths' is not supported in .Net 5. When I get around to a .Net 6 or 7 version I will define static methods in the IPhysicalQuantity interface that support maths operators and then the vector maths can be re-implemented.

class UnitsSystem

The library defines an abstract base class for unit systems:

C#
public abstract class UnitsSystem

Subclasses of UnitsSystem are responsible for converting quantities to and from strings. So there are various virtual methods for string conversion. There is also a static reference to the current units system, which defaults to Metric.

C#
public static UnitsSystem Current = Metric;

By default, the ToString() and Parse() methods will use the current unit system.

C#
internal static string ToString(IPhysicalQuantity qty)
{
    return Current.DoToString(qty);
}
C#
internal static PhysicalQuantity Parse(string s)
{
    return Current.DoParse(s);
}

Or you can specify which system to use:

C#
internal static string ToString(IPhysicalQuantity qty, UnitsSystem system)
{
    return system.DoToString(qty);
}
C#
public static PhysicalQuantity Parse(string s, UnitsSystem system)
{
    return system.DoParse(s);
}

By default, the unit system will perform the string conversion using a lookup table of unit definitions. The unit definition uses this class:

C#
public class Unit
{
    public readonly UnitsSystem System;
    public readonly string Name;
    public readonly string ShortName;
    public readonly string CommonCode;
    public readonly Dimensions Dimensions;
    public readonly double ConversionFactor; //to convert from ISO units
                    :

So, for example, here are some of the definitions for the metric system:

C#
public static Unit Metres =
  new Unit(System, "metres", "m", "MTR", Dimensions.Length, 1.0, Unit.DisplayOption.Standard);
public static Unit SquareMetres =
  new Unit(System, "squaremetres", "m²", "MTK", Dimensions.Area, 1.0, Unit.DisplayOption.Standard);
public static Unit CubicMetres =
  new Unit(System, "cubicmetres", "m³", "MTQ", Dimensions.Volume, 1.0, Unit.DisplayOption.Standard);
public static Unit Kilograms =
  new Unit(System, "kilograms", "kg", "KGM", Dimensions.Mass, 1.0, Unit.DisplayOption.Standard);
public static Unit Seconds =
  new Unit(System, "seconds", "s", "SEC", Dimensions.Time, 1.0, Unit.DisplayOption.Standard);

Or similar ones for the British units:

C#
public static Unit Feet = new Unit
  (System, "feet", "ft", "FOT", Dimensions.Length, feetToMetres, Unit.DisplayOption.Standard);
public static Unit Inches = new Unit
  (System, "inches", "in", "INH", Dimensions.Length, (feetToMetres/12.0), Unit.DisplayOption.Multi);
public static Unit Fortnight = new Unit
  (System, "fortnight", "fn", "fn", Dimensions.Time, 3600.0*24.0*14.0, Unit.DisplayOption.Explicit);
public static Unit Pounds = new Unit
  (System, "pounds", "lb", "LBR", Dimensions.Mass, poundsToKilogrammes, Unit.DisplayOption.Standard);

UN/CEFACT Common Codes

Each unit has a unique UN/CEFACT code. So for example British gallons and US gallons have the codes GLI and GLL respectively. These codes can be used when exchanging quantity information electronically, for example in Bills of Material or Purchase Orders. In the few cases where there is no common code the short name is used instead.

For example:

C#
string code = BritishUnits.CubicFeet.CommonCode;  // code == "FTQ"

Extension Methods

The unit system also defines a set of extension methods like this:

C#
public static Length Metres(this double v)
{
        return new Length(v);
}

That allows easy creation of a quantity from a floating point or integer value:

C#
Length l1 = 4.2.Metres();
Mass m1 = 12.Kilograms();

Code Generation

As mentioned previously, because the library has a lot of repetitive code, we use the T4 macro processor available in Visual Studio. This tool allows us to automate the creation of source code by creating a template file which contains a mix of C# code and the required output text. In general, we start with an XML file of definitions which we read, then use the template to generate the required C# classes and data.

For example, here is a line from the XML file defining the metric unit system:

XML
<unit name="Volts" shortname="volt" dimension="ElectricPotential" display="Standard" CommonCode="VLT" />

This template snippet will then create the static unit definitions:

C#
<#+ foreach(var ui in unitInfoList)
    {
#>
		public static Unit <# =ui.longName #>= new Unit(System, "<#= ui.longName.ToLower() #>", 
                           "<#= ui.shortName #>", "<#= ui.CommonCode #>",
                        Dimensions.<#= ui.dimension #>, <#= ui.factor #>, 
                        Unit.DisplayOption.<#= ui.displayOption #>);
<#+ }	// end foreach ui
#>

Resulting in a line like this in the final code:

C#
public static Unit Volts = new Unit
  (System, "Volts", "volt", "VLT", Dimensions.ElectricPotential, 1.0, Unit.DisplayOption.Standard);

This technique allows us to generate the large number of operator definitions we require for each quantity class. For example, given this definition in the Dimensions.xml file:

XML
<dimension name="Density" equals="Mass / Volume" />

We can generate the Density class and all of the following operators:

C#
public static Density operator/(Mass v1, Volume v2)
public static Volume operator/(Mass v1, Density v2)
public static Mass operator*(Volume v1, Density v2)

The following XML definition files are supplied:

File Description
Dimensions.xml This defines the dimensions and the relations between them
MetricUnits.xml Unit definitions for the metric system
BritishUnits.xml British units like foot and pound
USAUnits.xml American Units. These overlap with the British units somewhat.
TimeUnits.xml Units of time apart from the second, such as hours and days

Summary Table

This table summarises the classes, dimensions, formulae and units supported by the library:

Name Formula Dimensions Units
AbsoluteTemperature   Θ K (Kelvin)
°C (Celsius)
°F (Fahrenheit)
Acceleration Velocity / Time
VelocitySquared / Length
Length / TimeSquared
Length * AngularVelocitySquared
L T⁻² m/s² (MetresPerSecondSquared)
g0 (AccelerationOfGravity)
AmountOfSubstance   N mol (Mole)
nmol (NanoMoles)
AmountOfSubstanceByArea AmountOfSubstance / Area L⁻² N m⁻²⋅mol
AmountOfSubstanceByTime AmountOfSubstance / Time T⁻¹ N mol⋅s⁻¹
Angle   A rad (Radians)
° (Degrees)
AngularMomentum MomentOfInertia * AngularVelocity M L² T⁻¹ A kg⋅m²/s (KilogramMetreSquaredPerSecond)
AngularVelocity Angle / Time
TangentialVelocity / Length
T⁻¹ A rad⋅s⁻¹
AngularVelocitySquared AngularVelocity * AngularVelocity T⁻² A² rad²⋅s⁻²
Area Length * Length m² (SquareMetres)
cm² (SquareCentimetres)
ha (Hectares)
ByArea Dimensionless / Area
Length / Volume
L⁻² m⁻²
ByLength Dimensionless / Length
Area / Volume
L⁻¹ m⁻¹
CoefficientOfThermalExpansion Dimensionless / TemperatureChange Θ⁻¹ K⁻¹ (PerDegreeKelvin)
CoefficientOfViscosity Force / KinematicViscosity
Pressure / VelocityGradient
Momentum / Area
MassByArea * Velocity
MassByAreaByTimeSquared / VelocityByDensity
M L⁻¹ T⁻¹ Pa⋅s (PascalSeconds)
P (Poises)
Current   I amp (Ampere)
Density Mass / Volume M L⁻³ kg/m³ (KilogramsPerCubicMetre)
gm/cc (GramsPerCC)
gm/Ltr (GramsPerLitre)
mg/cc (MilligramsPerCC)
DiffusionFlux AmountOfSubstanceByArea / Time
KinematicViscosity * MolarConcentrationGradient
AmountOfSubstanceByTime / Area
L⁻² T⁻¹ N m⁻²⋅mol⋅s⁻¹
Dimensionless     1 (Units)
% (Percent)
doz (Dozen)
hundred (Hundreds)
thousand (Thousands)
million (Millions)
billion (Billions)
trillion (Trillions)
ElectricCharge Current * Time T I amp⋅s
ElectricPotential Energy / ElectricCharge M L² T⁻³ I⁻¹ volt (Volts)
ElectricPotentialSquared ElectricPotential * ElectricPotential M² L⁴ T⁻⁶ I⁻² kg²⋅m⁴⋅amp⁻²⋅s⁻⁶
Energy Force * Length
Mass * VelocitySquared
AngularMomentum * AngularVelocitySquared
SurfaceTension * Area
M L² T⁻² J (Joules)
cal (Colories)
eV (ElectronVolts)
kWh (KilowattHours)
toe (TonnesOfOilEquivalent)
erg (Ergs)
EnergyFlux Power / Area M T⁻³ kg⋅s⁻³
Force Mass * Acceleration
Momentum / Time
MassFlowRate * Velocity
M L T⁻² N (Newtons)
dyn (Dynes)
gm⋅wt (GramWeight)
kg⋅wt (KilogramWeight)
FourDimensionalVolume Volume * Length
Area * Area
L⁴ m⁴
Frequency Dimensionless / Time
AngularVelocity / Angle
T⁻¹ Hz (Hertz)
Illuminance LuminousFlux / Area L⁻² J A² lux (Lux)
KinematicViscosity Area / Time
Area * VelocityGradient
L² T⁻¹ m²/s (SquareMetresPerSecond)
cm²/s (SquareCentimetresPerSecond)
Length   L m (Metres)
km (Kilometres)
cm (Centimetres)
mm (Millimetres)
μ (Micrometres)
nm (Nanometres)
Å (Angstroms)
au (AstronomicalUnits)
LuminousFlux LuminousIntensity * SolidAngle J A² lm (Lumen)
LuminousIntensity   J cd (Candela)
Mass   M kg (Kilograms)
g (Grams)
mg (MilliGrams)
μg (MicroGrams)
ng (NanoGrams)
t (Tonnes)
Da (Daltons)
MassByArea Mass / Area
Length * Density
M L⁻² kg⋅m⁻²
MassByAreaByTimeSquared MassByArea / TimeSquared
Acceleration * Area
M L⁻² T⁻² kg⋅m⁻²⋅s⁻²
MassByLength Mass / Length M L⁻¹ kg⋅m⁻¹
MassFlowRate Mass / Time
CoefficientOfViscosity * Length
M T⁻¹ kg/s (KilogramsPerSecond)
MolarConcentration AmountOfSubstance / Volume
Density / MolarMass
L⁻³ N mol/m3 (MolesPerCubicMetre)
mol/L (MolesPerLitre)
MolarConcentrationGradient MolarConcentration / Length L⁻⁴ N m⁻⁴⋅mol
MolarConcentrationTimesAbsoluteTemperature MolarConcentration * AbsoluteTemperature L⁻³ Θ N m⁻³⋅K⋅mol
MolarMass Mass / AmountOfSubstance M N⁻¹ kg/mol (KilogramsPerMole)
gm/mol (GramsPerMole)
MolarSpecificHeat ThermalCapacity / AmountOfSubstance
Pressure / MolarConcentrationTimesAbsoluteTemperature
M L² T⁻² Θ⁻¹ N⁻¹ J⋅K⁻¹⋅mol⁻¹ (JoulesPerDegreeKelvinPerMole)
MomentOfInertia Mass * Area M L² kg⋅m² (KilogramMetreSquared)
Momentum Mass * Velocity M L T⁻¹ kg⋅m/s (KilogramMetresPerSecond)
Power Energy / Time
ElectricPotential * Current
ElectricPotentialSquared / Resistance
M L² T⁻³ W (Watts)
kW (Kilowatts)
PowerGradient Power / Length M L T⁻³ kg⋅m⋅s⁻³
Pressure Force / Area
MassByArea * Acceleration
M L⁻¹ T⁻² Pa (Pascals)
mmHg (MillimetresOfMercury)
dyn/cm² (DynesPerSquareCentimetre)
Resistance ElectricPotential / Current M L² T⁻³ I⁻² Ω (Ohms)
ResistanceTimesArea Resistance * Area M L⁴ T⁻³ I⁻² kg⋅m⁴⋅amp⁻²⋅s⁻³
ResistanceToFlow MassFlowRate / FourDimensionalVolume M L⁻⁴ T⁻¹ kg⋅m⁻⁴⋅s⁻¹
Resistivity Resistance * Length
ResistanceTimesArea / Length
M L³ T⁻³ I⁻² Ω⋅m (OhmMetres)
SolidAngle Angle * Angle sr (Steradians)
°² (SquareDegrees)
SpecificHeat ThermalCapacity / Mass L² T⁻² Θ⁻¹ J⋅kg⁻¹⋅K⁻¹ (JoulesPerKilogramPerDegreeKelvin)
SurfaceTension Force / Length
Length * Pressure
M T⁻² N/m (NewtonsPerMetre)
dyne/cm (DynesPerCentimetre)
TangentialVelocity Velocity L T⁻¹ m/s (MetresPerSecond)
cm/s (CentimetersPerSecond)
kph (KilometresPerHour)
TemperatureChange   Θ K (Kelvin)
°C (Celsius)
°F (Fahrenheit)
TemperatureGradient TemperatureChange / Length L⁻¹ Θ m⁻¹⋅K
ThermalCapacity Energy / TemperatureChange M L² T⁻² Θ⁻¹ J/K (JoulesPerDegreeKelvin)
cal/K (CaloriesPerDegreeKelvin)
ThermalCapacityByVolume ThermalCapacity / Volume
MolarConcentration * MolarSpecificHeat
Pressure / AbsoluteTemperature
M L⁻¹ T⁻² Θ⁻¹ kg⋅m⁻¹⋅K⁻¹⋅s⁻²
ThermalConductivity EnergyFlux / TemperatureGradient
PowerGradient / TemperatureChange
M L T⁻³ Θ⁻¹ W⋅m⁻¹⋅K⁻¹ (WattsPerMetrePerDegree)
Time   T s (Seconds)
ms (MilliSeconds)
min (Minutes)
hr (Hours)
day (Days)
week (Weeks)
month (Months)
yr (JulianYears)
aₛ (SiderialYears)
TimeSquared Time * Time
Velocity Length / Time L T⁻¹ m/s (MetresPerSecond)
cm/s (CentimetersPerSecond)
kph (KilometresPerHour)
VelocityByDensity Velocity / Density M⁻¹ L⁴ T⁻¹ kg⁻¹⋅m⁴⋅s⁻¹
VelocityGradient Velocity / Length T⁻¹ Hz (Hertz)
VelocitySquared Velocity * Velocity L² T⁻² m²⋅s⁻²
Volume Area * Length m³ (CubicMetres)
cc (CubicCentimetres)
L (Litres)
VolumeFlowRate Volume / Time
Pressure / ResistanceToFlow
L³ T⁻¹ m³/s (CubicMetresPerSecond)
cc/s (CubicCentimetresPerSecond)

More Examples

Here are some more examples using the library, based on questions from Nelkon and Parker.

The reckless jumper:

C#
// A person of mass 50 kg who is jumping from a height of 5 metres
// will land on the ground
// with a velocity = √2gh = √ 2 x 9.8 x 5 = 9.9 m/s , assuming g = 9.8 m/s.
Mass m = 50.Kilograms();
Length h = 5.Metres();
Acceleration g = 9.80665.MetresPerSecondSquared();
Velocity v = Functions.Sqrt(2 * g * h); // v = 9.90285312 m/s
// If he does not flex his knees on landing,
// he will be brought to rest very quickly, say in
// 1/10th second.  The force F acting is then given
// by momentum change/time = 50 * 9.9 / 0.1 = 4951 N
Momentum mm = m * v;
Time t = 0.1.Seconds();
Force f = mm / t; // f = 4951.42656 N

And the flying cricket ball:

C#
// Suppose a cricket ball was thrown straight up with an initial velocity,
// u, of 30 m/s.
// The time taken to reach the top of its motion can be obtained from the equation
// v = u + at.
// The velocity, v, at the top is zero; and since u = 30 m and
// a = —9.8 or 10 m/s²(approx),
// we have 30 - 10t = 0.
// Therefore t = 30 / 10 = 3s
// The highest distance reached is thus given by
// d = ut + 1 / 2 at ^ 2 = 30x3 - 5x3 ^ 2 = 45 m.
var u = 30.MetresPerSecond();
var g = 9.80665.MetresPerSecondSquared();
var t = u / g;  // t = 3.05914864 s
var d = u * t + -g * t * t / 2.0;   // d = 45.8872296 m

Surface Tension:

C#
// Calculate the work done against surface tension in blowing a bubble of 1 cm in diamter
// if surface tension of a soap solution = 25 dynes/cm.
Length r = 1.Centimetres() / 2;
SurfaceTension surfaceTensionOfSoapSolution = 25.DynesPerCentimetre();
// The initial surface area is zero
// The final surface area = 2 x 4π x 0.5² = 2π sq cm.
Area a = new Sphere(r).Area * 2;
// Therefor work done = T x increase in surface area = 25 x 2π = 157 ergs.
Energy e = surfaceTensionOfSoapSolution * a; // 157.1 erg

Young's Modulus:

C#
// If a 2kg weight is attached to the end of a wire of length 200cm and diameter 0.64mm
// and the extension is 0.6mm then what is the Young's Modulus E of the wire?
Force f = 2.KilogramWeight();
Area a = Circle.OfDiameter(0.64.Millimetres()).Area;
var stress = f / a;
Length l = 200.Centimetres();
Length e = 0.6.Millimetres();
var strain = e / l;
// E = (2000 x 980 / π x 0.032²) / (0.06/200) = 2e12 dynes/cm²
var E = stress / strain;    // 2.032E+12 dyn/cm²

Fick's First Law:

C#
// In a region of an unsaturated solution of sucrose the molar concentration gradient is -0.1 mol/L/cm.
// What quantity of sucrose molecules pass through an area of 1cm² in 10 minutes?
MolarConcentration c = 0.1.Mole() / 1.Litres();
MolarConcentrationGradient cg = -c / 1.Centimetres();
Area a = 1.SquareCentimetres();
Time t = 10.Minutes();
AreaFlowRate d = 0.522e-9.SquareMetresPerSecond();    // diffusion coefficient
DiffusionFlux j = d * cg;
AmountOfSubstance n = j * a * t;    // -313.2 nmol

Points of Interest

Unit Tests

The sample program also tests the library, but does not use a unit testing framework. Instead, it uses a simple static class Check which allows us to write code like this:

C#
Check.Equal(42.0, d5, "wrong value for d5");

This will throw an exception if the first two arguments are not equal.

Performance

I had hoped that by creating immutable data types and making copious use of the aggressive inlining and aggressive optimization hints that the performance of the quantity classes would be comparable to the performance of 'raw' doubles. But this has turned out not to be the case. To test this, I implemented the same rocket simulation twice, once using plain doubles and again using the quantity classes. In a release build, the version using doubles is around 6 times faster. The reason can be seen by examining the code generated for some typical arithmetic. For example, this code:

C#
double d1 = 4.2;
double d2 = 5.3;
double d3 = 6.4;
double d4 = d1 + d2 + d3;

Generates code for the addition like this:

00007FFCCC4B6A46  vmovsd      xmm3,qword ptr [rbp-8]  
00007FFCCC4B6A4B  vaddsd      xmm3,xmm3,mmword ptr 
                              [UnitTests.Program.TestDouble()+0B0h (07FFCCC4B6AC0h)]  
00007FFCCC4B6A53  vaddsd      xmm3,xmm3,mmword ptr [rbp-10h]  
00007FFCCC4B6A58  vmovsd      qword ptr [rbp-18h],xmm3  

Whereas the same formula using the class library:

C#
Dimensionless d1 = 4.2;
Dimensionless d2 = 5.3;
Dimensionless d3 = 6.4;
Dimensionless d4 = d1 + d2 + d3;

Generates much longer code:

00007FFCD5726B59  mov         rcx,qword ptr [rsp+70h]  
00007FFCD5726B5E  mov         qword ptr [rsp+58h],rcx  
00007FFCD5726B63  mov         rcx,qword ptr [rsp+68h]  
00007FFCD5726B68  mov         qword ptr [rsp+50h],rcx  
00007FFCD5726B6D  vmovsd      xmm0,qword ptr [rsp+58h]  
00007FFCD5726B73  vaddsd      xmm0,xmm0,mmword ptr [rsp+50h]  
00007FFCD5726B79  vmovsd      qword ptr [rsp+48h],xmm0  
00007FFCD5726B7F  mov         rcx,qword ptr [rsp+48h]  
00007FFCD5726B84  mov         qword ptr [rsp+40h],rcx  
00007FFCD5726B89  mov         rcx,qword ptr [rsp+60h]  
00007FFCD5726B8E  mov         qword ptr [rsp+38h],rcx  
00007FFCD5726B93  vmovsd      xmm0,qword ptr [rsp+40h]  
00007FFCD5726B99  vaddsd      xmm0,xmm0,mmword ptr [rsp+38h]  
00007FFCD5726B9F  vmovsd      qword ptr [rsp+30h],xmm0  
00007FFCD5726BA5  mov         rcx,qword ptr [rsp+30h]  
00007FFCD5726BAA  mov         qword ptr [rsp+78h],rcx  

There are lots of superfluous move instructions. Perhaps someone with a deeper understanding of the JIT compiler can shed some light on this.

Comparison with F#

The F# language has built in support for units of measure, which also has the aim of preventing programming errors. So it is possible to write statements like this:

F#
let l1 = 12.0<m>      // define a length in metres
let l2 = 7.0<m>           // define another length
let l3 = l1 + l2        // add lengths together
let a = l1 * l2         // define an area (a has type float<m^2>)
let v = l1 * l2 * l3    // define a volume (v has type float<m^3>)
let m1 = 5.0<kg>      // define a mass in kilogrammes
let d = m1 / v;         // define a density (d has type float<kg/m^3>)

And given the above, this statement will not compile:

F#
let x = m1 + l1; // !! The unit of measure 'm' does not match the unit of measure 'kg'

The standard library of units defines the basic S.I. unit like metre, but does not define derived units like centimetres. You can define your own units like this:

F#
[<Measure>] type cm     // centimetres

And you can use it in the same way:

F#
let l4 = 42.0<cm>

But there is no way to indicate that centimetres and metres are the same dimension. So whereas l1 above has type float<m>, l4 has type float<cm>, and attempting to add them will not compile:

F#
let l5 = l1 + l4;    // !! The unit of measure 'cm' does not match the unit of measure 'm'

You can only get around this by defining a conversion function:

F#
let convertcm2m (x : float<cm>) = x / 1000.0<cm/m>

Then using it in the expression:

F#
let l5 = l1 + convertcm2m(l4);

You also have to be careful to always use the same numeric type when using units of measure. This is because in this definition:

F#
let l6 = 5<m>

The type of l6 is int<m>, and this cannot be added to a value of type float<m>. So this line will not compile either:

F#
let l7 = l1 + l6;           // !! The type float<m> does not match the type int<m>

Finally, although the units of measure are checked at compile time, the types do not carry through to the compiled code. The values are just defined as floating point numbers. Consequently, you cannot discover at run time what the unit of measure of a value actually is. So you can only print these types of values as floating point, like this:

F#
printfn "l5 = %e" l5        // outputs "l5 = 1.204200e+001"

Even if you use the format specifier %O:

F#
printfn "l5 = %O" l5        // outputs "l5 = 12.042"

So although the F# system has the same goal of preventing invalid mathematical operations, it is more restrictive due to its basis on units rather than dimensions.

History

  • 6th July, 2021: Initial version
  • 6th August, 2021: Added AbsoluteTemperature to the library. Added a table to the article summarising the contents of the library.
  • 8th August, 2021: Corrected format of summary table.
  • 7th December 2021: Added examples of equations from calorimetry, thermal expansion, thermal conductivity and ideal gases.
  • 9th May 2022: Added shapes, vectors, statics, hydrostatics, surface tension, elasticity, and friction.
  • 1st Sep 2022: Added viscosity, osmosis and diffusion. Also updated the summary table.
  • 12th Mar 2023: Added common codes, some light and optics equations, and corrected errors.

I've been working on this for over two years in my spare time. Currently, the library has the basics in place, and can be used for equations in dynamics, statics, heat, light and some electrics. I am continuing to add more derived dimensions and quantity classes to support more equations as I gradually work my way through Nelkon and Parker.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)