Skip to content

Latest commit

 

History

History
258 lines (178 loc) · 10.9 KB

README.md

File metadata and controls

258 lines (178 loc) · 10.9 KB

units

A lightweight, compile-time, header-only, dimensional analysis and unit conversion library built on c++11 with no dependencies. (play with it on ideone)

Tested on

  • gcc-5.3.0

Compiles Under

  • icc-16 + 17
  • gcc-7 .. gcc-4.7
  • clang 4.0 .. clang 3.4

Contents

Description

The library allows to track the dimensions of physical quantities at compile-time which enforces the the rules of dimension algebra with no runtime overhead. Additionally, the library provides some convenient conversions between different units of mesaure.

Motivation

There exist quite a few other implementations for dimensional tracking eg. Boost.Unit, nholthaus/units or martinmoene/PhysUnits-CT-Cpp11. A more comprehensive list can be found at PhysUnit-CT-Cpp11. All these implementations consist of a considerable amount of code (boost >4000 sloc, nholt/unit 3800 sloc, PysUnit 2500 sloc) and that they make strong use of macros. This makes understanding the code and adjusting it for special needs a time intensive task.

This library tries to adress the issue by beeing small (500 sloc*) and simple to allow users understand and customize the code more easily.

*Note that 500 sloc include SI-Unit-Definitions an input/output and exclude physicalConstants.h. The essential code without input/output and predefined units consists of 250 sloc.

Features

The library can be used for

  • dimensional analysis on unrestricted value-types (double, int, complex ...)
  • printing dimensioned values
  • reading dimensioned values
  • convert units of measure

It is designed to be extendable to special needs. Its mostly constexpr, well tested and incurs no runtime overhead (at optimization -O1). All constants from nist are available (see physicalConstants.h).

Limitations

Storing values in non-base-units (e.g. millisecs) cannot be done with this library (see Representation in non-base-units). Therefore printing and reading is done in terms of baseunits, so eg

cout << 3.0_n * milli(newton); // prints "0.003N"

More advanced printing and reading can be added if reqired.

Since the unit of a variable is a compile time property it cannot change during runtime unless a form of std::variant (c++17) or eg variant-lite (c++11) is used.

Getting Started Guide

The library consists of a single file (units.h). To incorporate the library into your project, simply copy the header into a location in your include path.

Add units.h to your project, along with the using directive for literals which allows for a first simple example.

#include <units.h>

int main() {

    using namespace unit;
    using namespace unit::literals;

    t::newton some_force = 2.0_n*meter * 3.0_n*kilogram / square(2.0_n*second);

    std::cout<<some_force<<"\n"; //prints "1.5N"
    
    return 0;
}

The variable some_force is a double value of 1.5 having a unit type newton attached to it. Quantities like some_force can be used like doubles, an they should be used anywhere double values can be used.

To avoid mistakes, the variable some_force cannot be combined with pure doubles:

some_force = some_force + 2.0;            // compile error
some_force = some_force + 2.0_n*newton; // ok

The literal _n makes the number a (unitless) quantity. The variable some_force can only be combined with compatible quantities according to the rules of dimensional analysis

some_force = some_force + second; // compile error due to summing different units 
some_force = some_force * newton; // compile error due to return type mismatch 
                            // return type is `newton^2`, which cannot be assigned to type 'newton'
newton_squared some_force2 = some_force * newton; // ok

Note that newton_squared is currently not defined in the library, but can be easily added if desired.

Due to the dimensional checking, auto can savely be used, errors will be caught at compile-time

auto a = 2.0_n*meter;
auto b = 3.0_n*meter; 
auto c = sqr(a*a + b*b);  // pytagoras intended

auto force1 = c * 3.0_n*kilogram / sqr(2.0_n*second);
auto force2 = 5.0_n*newton;

force1 += force2; // compile error since force1 is of different type m*N
                  // since we forgot a 't' in 'sqr' making c of type meter^2  

The library provides some math functions (from <cmath>) which provide the respective unit transformations. If a math function is missing it can be easily added if desired.

Converting to base units is also possible:

auto m = 2.0_n*pound; //m == 0,907185 meters
auto f = cube( 500.0_n * deci(inch) ); //f == 2.04838 m^3
auto k = celsius(0); // k == 273.15 kevlin
auto s = 5.0_n*milli(second); // s == 0.005 seconds

If a conversion is missing it can be easily added if desired. Note that all units are always converted to their base-units. This default behaviour can be changed.

Sometimes there is the need to combine a unit with a unitless scalar:

double s = sin(45);
auto a = 2.0_n * meter; 
auto c = a * number{s};

For a fallback there is always to the option to extract the double value from the unit:

auto a = 2.0_n*meter; 
double v = a.magnitude();

Defining New Units

Units can be defined anywhere. The existing definitions can be found in namespace u in units.h, just search there for namespace u to find example definitions.

Adding a new Math Function

Math functions can be added anywhere. The existing math functions can be found in units.h searching for sqrt.

Representation in non-base-units

The representation in non-base-units can be achived by using the type ScaledValue as magnitude-type. This type remembers its scale and behaves accordingly under aritmetic operations. The following example illustrates the usage:

#include <units.h>
#include <ScaledValue.h>

using namespace unit;
using namespace sv;

using SvMilli = ScaledValue<std::milli,double>;
using SvKilo = ScaledValue<std::kilo,double>;
using Sv1 = ScaledValue<std::ratio<1>,double>;

constexpr Quantity<u::newton,Sv1> s_newton {1.0};
constexpr Quantity<u::unitless,Sv1> s_unitLess{1.0};

constexpr Quantity<u::unitless,SvKilo> operator"" _kilo ( long double v )  {return Quantity<u::unitless,SvKilo> {static_cast<double>(v)};}
constexpr Quantity<u::unitless,SvMilli> operator"" _milli ( long double v )  {return Quantity<u::unitless,SvMilli> {static_cast<double>(v)};}

int main() {

    //define unit values on different scales
    auto a = 2.0_kilo * s_newton;
    auto b = 500.0_milli * s_newton;

    //combination
    auto c = a + b;
    auto d = a * b;

    //math
    auto e = sqrt(d);

    //conversion
    auto f = Quantity<u::newton,Sv1>{b};

    std::cout << a << std::endl; // prints "2*kiloN"
    std::cout << b << std::endl; // prints "500*milliN"
    std::cout << c << std::endl; // prints "2.0005*kiloN"
    std::cout << d << std::endl; // prints "1*kilom^2kg^2s^-4" which is 1*kilo N^2
    std::cout << e << std::endl; // prints "0.0316228*kiloN"
    std::cout << f << std::endl; // prints "0.5N"
    
    return 0;
}

The definitions above main() serve readability. They are similar to the ones in units.h which should be adjusted in case of using ScaledValues togehter with units.

Note: One should be aware that this form of delayed rescaling might incur runtime overhead since rescaling has to happen each time two values with different scales are combined.

One should also keep in mind that floating point types like double are especially good at keeping track of their scale. So there should rarely be the need for this form of customization.

Adjusting the Library

The library is specially designed to be customizable. It can be done by changing the lower part of the units.h file, which begins at namespace u (which can be searched for). Everything below namespace u be changed to meet special needs.

Under the Hood

The library essentially consists of 2 classes

template< int ... dimensionExponents > 
struct Unit{};

template <typename Unit, typename MagnitudeRepresentation = double>
class Quantity {

    MagnitudeRepresentation m_magnitude;
};

The class Quanitity combines a magnitude-value and a unit-type.

The class Unit is a pure type wich is different for each physical dimension by storing the exponents of all possible base-units in its template parameter. The following example illustrates the idea of class Unit:

using metre    = Unit<1,0,0>;  // represents m^1 * kg^0 * s^0
using kilogram = Unit<0,1,0>;  //            m^0 * kg^1 * s^0
using second   = Unit<0,0,1>;  //            m^0 * kg^0 * s^1

using meters_per_second = Unit<1,0,-1> //  m^1 * kg^0 * s^-1
using acceleration = Unit<1,0,-2>      //  m^1 * kg^0 * s^-2

using newton_force = Unit<1,1,-2>       // m^1 * kg^1 * s^-2

With these definitions a value of 5 newton could therefore be represented as

Quantity< Unit<1,1,-2>, double > q{5};

or

Quantity< newton_force, double > q{5};

Note that the definitions metre, kilogram and second are arbitraritly chosen and can be changed as well as their count.

The remaining library code past those 2 classes deals mainly with supporting them:

  • deriving return unit-types for operators eg Unit<1,0,0> * Unit<0,0,1> -> Unit<1,0,1>
  • deriving return unit-types for math functions eg sqrt< Unit<6,0,0> >() -> Unit<3,0,0>
  • printing units eg print_unit< Unit<0,2,0> >(); -> prints kg^2
  • providing operators and math for class Quantity
  • conversions between units and scales

Further information can be found in the file units.h.