A c++17 library for all your unit needs
Are you tired of writing the following code and still not even sure if your dimensional analysis is correct?
float airDensity_kgpm3(float baseBaro_Pa, float baseTemp_K, float basePos_m,
float h_m) {
float g_mps2 = 9.80864;
// gas constant for dry air [J/(kg·K)]
float R_JpkgK = 287.05f;
// temperature lapse rate [K/m]
float L_Kpm = 0.0065f;
// Altitude difference
float dz_m = (h_m - basePos_m);
// Temperature at new altitude
float T_K = baseTemp_K - L_Kpm * dz_m;
// Pressure at new altitude
float exponent = g_mps2 / (R_JpkgK * L_Kpm);
float p_Pa = baseBaro_Pa * std::pow(T_K / baseTemp_K, exponent);
// Density at new altitude
float ret_kgpm3 = p_Pa / (R_JpkgK * T_K);
return ret;
}units.h is a library that checks all of that icky analysis for you at compile time. It allows you to express your unit types in English. Furthermore, it is as fast as using regular floating point values when compiling at higher optimization levels. Just #include "units.h" and start converting your variables to units, like this:
Density airDensity(Pressure basePressure, Temperature baseTemp, Length basePos,
Length h) {
Acceleration g = 1.0_g;
typedef Quantity<Div<Joule, Mult<Kilogram, Kelvin>>> RType;
// gas constant for dry air [J/(kg·K)]
constexpr RType R{287.05f};
typedef Quantity<Div<Kelvin, Meter>> LapseRate;
constexpr LapseRate L{0.0065f}; // temperature lapse rate [K/m]
// Altitude difference
Length dz = (h - basePos);
// Temperature at new altitude
Temperature T = baseTemp - L * dz;
// Pressure at new altitude
Constant exponent = g / (R * L);
Pressure p = basePressure * std::pow((T / baseTemp).value, exponent.value);
// Density at new altitude
return p / (R * T);
// if you forget parentheses, you will get a type error
return p / R * T; // errors out!
}units.h enables dimensional analysis to happen safely and be checked:
Length l = 1.0_m;
Time t = 1.0_s;
// it understands operations between different unit types
Velocity v = l / t;
Length l2 = v * t;
// this will throw a compile time error
Velocity v2 = l * t;units.h distinguishes the following things: tag types and Quantity types. A tag type has no properties and is useless on its own, Quantity types are where the actual interesting stuff goes on and they are defined as a Quantity of a tag type (eg Quantity<TagType>)
The following base tag types are available:
One: Defines a unit less value (such as a ratio)MeterKilogramAmpereMoleCandelaSecondKelvinRadian(Equivalent toOne)Ratio(Equivalent toOne)Newton(Equivalent to kg*m/s^2)Pascal(Equivalent to N/m^2)Tesla(Equivalent to N/(A*m))Hertz(Equivalent to 1/s)
units.h also defines the following tags that are convertible to one of the above base tags:
FootGauss(Convertible toTesla)InchMercury(Convertible toPascal)InchGravity(equal to 9.80665 m/s^2)MinuteHourCelsiusFahrenheitDegreeRevolutionPercent(convertible toRatio)
It also defines the following public meta-tags (tags that compose other tags together):
Centi<>(egCenti<Meter>)Deci<>Nano<>Micro<>Milli<>Tera<>Giga<>Mega<>Kilo<>AGL<>(for "Above Ground Level", more on this later)Mult<...>(more on this later)Div<N, D>(more on this later)
Quantity<> takes a tag type and defines a Quantity on it. Quantity<> is the class that provides all of the operations to use. Quantity<> also provides a full set of operator overloads so that you can use it as if it were a floating point value. For most programs, you should just use the following pre-defined Quantity types for representing various things
Length:Quantity<Meter>Mass:Quantity<Kilogram>Time:Quantity<Second>Velocity:Quantity<m/s>Acceleration:Quantity<m/s^2>Area:Quantity<m*m>Volume:Quantity<m*m*m>Force:Quantity<Newton>Pressure:Quantity<Pascal>Temperature:Quantity<Kelvin>Density:Quantity<Mass/Volume>Work:Quantity<Joule>Constant:Quantity<One>Current:Quantity<Ampere>Substance:Quantity<Mole>LuminousIntensity:Quantity<Candela>Voltage:Quantity<Volt>MagneticField:Quantity<Tesla>Angle:Quantity<Radian>AngularVelocity:Quantity<Radian/Second>AngularAcceleration:Quantity<Radian/Second^2>Level:Quantity<Ratio>(Describes something like "amounts of something". eg a servo that is 50% deployed means it isLevel(0.5) == Level::from<Percent>(50))
For the times when the basic units arent sufficient, you can compose other units together with the given meta-tags.
The most basic unit tags are Mult<> and Div<>. These allow you to compose multiple different units together multiplicatively. For example, acceleration (m/s^2) is defined as Div<Meter, Mult<Second, Second>>. You can also mix and match multiplying and dividing tags and quantities, units.h transparently handles all of that: Div<Kelvin, Length> (for Lapse Rate (K/m)).
Most of the other tags are for metric prefixes (eg centi-). For this, you just pass in a tag type (eg Centi<Meter>). The use of this will be more apparent later with from() and as()
A good exmple of composing these units is the definition for mdps and mg in units_short.h
There are a few options for creating a value of a Quantity<> type.
- Use the constructor (not recommended)
- Use
from<T>() - Use literals
Quantity<> provides a Quantity(float) constructor that creates a Quantity from the given value, however, you should avoid using it as it assumes the internal tag type for the Quantity<>. For example, Length(1) would mean something very different if Length was defined as Quantity<Centi<Meter>>. This constructor should only be used in situations where the tag type is very obvious (eg when you are making your own tag type, see airDensity in demo/main.cpp).
Length v = Length(1.0);
// can also do
Length v2{1.0};from<T>() creates a quantity from the passed tag type and then converts it into the base tag type. See main in demo/main.cpp for an example.
Length v = Length::from<Centi<Meter>>(100.0);
std::cout << v << std::endl; // prints "1 m"units.h defines a lot of literals for use with numbers. (See user-defined literals for help).
Length v = 1.0_m;
Length v2 = 100.0_cm;
std::cout << v << ", " << v2 << std::endl; // prints "1 m, 1 m"The following literals are defined, along with their associate return type
_m:Length_cm:Length_in:Length_ft:Length_mps:Velocity_mps2:Acceleration_mg:Acceleration_g:Acceleration_rad:Angle_radps:AngularVelocity_mdps:AngularVelocity_ms:Time_s:Time_kg:Mass_Pa:Pressure_K:Temperature_C:Temperature_pct:Level
The most basic thing to do with quantities is operations between them. The following operations are supported:
- scalar multiplication and division (does not change the quantity type)
- addition and subtraction between two quantities of the same type (does not change the quantity type)
- multiplication and division between any quantity types (returns a new quantity type)
- comparison between quantities (
<,>,<=,==, etc)
Length v{1};
// no change
Length v2 = v * 2;
// no change
Length v3 = v + v2;
// the type changes
Area a = v * v2;Another use of the quantity is directly accessing the value stored inside using the value field. This is NOT recommended as it can break the assumptions that Quantity<> makes (it's like using unsafe in Rust).
The as<T>() function allows converting to a non-base unit. This is usually used only for printing and logging as it is best to keep everything in the base unit for performance
Length l = 1.0_m;
auto l2 = l.as<Centi<Meter>>();
std::cout << l << ", " << l2 << std::endl; // prints "1 m, 100 cm"The distinction between whether a vertical value is "above ground level" or "above sea level" is very important in rocketry. If you're deploying airbrakes, basing your deployment level off of an ASL value will cause your rocket to massively undershoot.
units.h makes the assumption that all vertical values are ASL unless otherwise specified. Let me reiterate, units.h FORCES all of your vertical values to be ASL
If you want to use an AGL value, cast your Length value to a Quantity<AGL<Meter>> with the helper functions agl_from_asl and asl_from_agl. These functions both take in a ground level ASL altitude as well:
Length groundLevel = 100_m;
Length heightASL = 300_m;
Quantity<AGL<Meter>> heightAGL = agl_from_asl(heightASL, groundLevel);
std::cout << heightAGL; // prints "200 m agl"
Length heightASL2 = asl_from_agl(heightAGL, groundLevel);AGL<Meter> values can also be transparently converted to other AGL<> tag types.
// 200 m agl -> 656.168 ft agl
Quantity<AGL<Foot>> heightFtAGL = heightAGL.as<AGL<Foot>>();The biggest annoyance when dealing with AGL values is that you can not transparently combine them with other values. This annoyance is intentional: your values should not be stored as AGL. This can be an annoyance when taking a delta between two AGL values
Quantity<AGL<Meter>> l1{1};
Quantity<AGL<Meter>> l2{2};
// logic says that a delta between two values should just be a Length, not an AGL length because it no longer represents a vertical value
Quantity<AGL<Meter>> delta = l2 - l1;
// to get past this, use .reinterpret()
Length actualDelta = delta.reinterpret<Meter>();units.h currently provides the following math functions:
abs: Takes the absolute value of any quantitymax: Returns the maximum of two quantitiesmin: Returns the minimum of two quantitiessin: Takes the sin of anAngleand returns aLevelcos: Takes the cos of anAngleand returns aLevel
units.h current provides the following helper function:
unit_value_from<T2, T1>(v1): Converts a floating point value in the unit ofT1into a floating point value of the unit ofT2(egunit_value_from<Foot, Meter>(1) ~= 3.3)