• Stars
    star
    101
  • Rank 327,643 (Top 7 %)
  • Language
    Nim
  • Created over 3 years ago
  • Updated 3 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A fully type safe, compile time only units library.

Unchained - Compile time only units checking

https://github.com/SciNim/unchained/workflows/unchained%20CI/badge.svg

Unchained is a fully type safe, compile time only units library. There is absolutely no performance loss over pure float based code (aside from insertion of possible conversion factors, but those would have to be written by hand otherwise of course).

It supports:

  • all base SI units and (most) compound SI units
  • units as short and long name:
    import unchained
    let x = 10.m
    let y = 10.Meter
    doAssert x == y
        
  • some imperial units
  • all SI prefixes
    import unchained
    let x = 10.Mm # mega meter
    let y = 5.ng # nano gram
    let z = 10.aT # atto tesla 
        
  • arbitrary math with units composing to new units, e.g. (which do not have to be defined previously!),
    import unchained
    let x = 10.m * 10.m * 10.m * 10.m * 10.m
    doAssert typeof(x) is Meter

    without having to predefine a Meter⁵ type

  • automatic conversion between SI prefixes if a mix is used
    import unchained
    let x = 5.kg + 5.lbs
    doAssert typeof(x) is kg
    doAssert x == 7.26796.kg
        
  • manual conversion of units to compatible other units via to (e.g.
    import unchained
    let x = 5.m•s⁻¹
    defUnit(km•h⁻¹) # needs to be defined to be able to convert to
                    # `to` could be a macro that defines it for us 
    doAssert x.to(km•h⁻¹) == 18.km•h⁻¹
    # the `toDef` macro can be used to both define and convert a unit,
    # but under certain use cases it can break (see its documentation)
        
  • comparisons between units compare real value taking into account SI prefixes and even different units of the same quantity:
import unchained
let x = 10.Mm # mega meter
doAssert x == 10_000_000.m
let y = 5.ng # nano gram
doAssert y == 5e-9.g
let z = 10.aT # atto tesla
doAssert z == 10e-18.T
# and even different units of same quantity
let a = 5000.inch•s⁻¹
let b = a.toDef(km•h⁻¹) # defines the unit and convers `a` to it
doAssert b == 457.2.km•h⁻¹
doAssert typeof(a) is inch•s⁻¹ # SI units have higher precedence than non SI
doAssert typeof(b) is km•h⁻¹
doAssert a == b # comparison is true, as the effective value is the same!

Note: comparison between units is performed using an almostEqual implementation. By default it uses ε = 1e-8. The power can be changed at CT by using the -d:UnitCompareEpsilon=<integer> where the given integer is the negative power used.

  • all quantities (e.g. Length, Mass, …) defined as a concept to allow matching different units of same quantity in function argument
    import unchained
    proc force[M: Mass, A: Acceleration](m: M, a: A): Force = m * a
    let m = 80.kg
    let g = 9.81.m•s⁻²
    let f = force(m, g)
    doAssert typeof(f) is Newton
    doAssert f == 784.8.N
        
  • define your own custom unit systems, see examples/custom_unit_system.nim

A longer snippet showing different features below. See also examples/bethe_bloch.nim for a more complicated use case.

import unchained
block:
  # defining simple units
  let mass = 5.kg
  let a = 9.81.m•s⁻²
block:
  # addition and subtraction of same units
  let a = 5.kg
  let b = 10.kg
  doAssert typeof(a + b) is KiloGram
  doAssert a + b == 15.kg
  doAssert typeof(a - b) is KiloGram
  doAssert a - b == -5.kg
block:
  # addition and subtraction of units of the same ``quantity`` but different scale
  let a = 5.kg
  let b = 500.g
  doAssert typeof(a + b) is KiloGram
  doAssert a + b == 5.5.kg
  # if units do not match, the SI unit is used!
block:
  # product of prefixed SI unit keeps same prefix unless multiple units of same quantity involved
  let a = 1.m•s⁻²
  let b = 500.g
  doAssert typeof(a * b) is GramMeterSecond⁻²
  doAssert typeof((a * b).to(MilliNewton)) is MilliNewton
  doAssert a * b == 500.g•m•s⁻²
block:
  let mass = 5.kg
  let a = 9.81.m•s⁻²
  # unit multiplication has to be commutative
  let F: Newton = mass * a
  let F2: Newton = a * mass
  # unit division works as expected
  doAssert typeof(F / mass) is N•kg⁻¹
  doAssert typeof((F / mass).to(MeterSecond⁻²)) is MeterSecond⁻²
  doAssert F / mass == a
block:
  # automatic deduction of compound units for simple cases
  let force = 1.kg * 1.m * 1.s⁻²
  echo force # 1 Newton
  doAssert typeof(force) is Newton
block:
  # conversion between units of the same quantity
  let f = 10.N
  doAssert typeof(f.to(kN)) is KiloNewton
  doAssert f.to(kN) == 0.01.kN
block:
  # pre-defined physical constants
  let E_e⁻_rest: Joule = m_e * c*c # math operations `*cannot*` use superscripts!
  # m_e = electron mass in kg
  # c = speed of light in vacuum in m/s
from std/math import sin  
block:
  # automatic CT error if argument of e.g. sin, ln are not unit less
  let x = 5.kg
  let y = 10.kg
  discard sin(x / y) ## compiles gives correct result (~0.48)
  let x2 = 10.m
  # sin(x2 / y) ## errors at CT due to non unit less argument
block:
  # imperial units
  let mass = 100.lbs
  let distance = 100.inch
block:
  # mixing of non SI and SI units (via conversion to SI units)
  let m1 = 100.lbs
  let m2 = 10.kg
  doAssert typeof(m1 + m2) is KiloGram
  doAssert m1 + m2 == 55.359237.KiloGram
block:
  # natural unit conversions
  let speed = (0.1 * c).toNaturalUnit() # fraction of c, defined in `constants`
  let m_e = 9.1093837015e-31.kg.toNaturalUnit()
  # math between natural units remains natural
  let p = speed * m_e # result will be in `eV`
  doAssert p.to(keV) == 51.099874.keV

## If there is demand the following kind of syntax may be implemented in the future
when false:
  # units using english language (using accented quotes)
  let a = 10.`meter per second squared`
  let b = 5.`kilogram meter per second squared`
  check typeof(a) is MeterSecond⁻²
  check typeof(b) is Newton
  check a == 10.m•s⁻²
  check b == 5.N

Things to note:

  • real units use capital letters and are verbose
  • shorthands defined for all typical units using their common abbreviation (upper or lower case depending on the unit, e.g. s (second) and N (Newton)
  • conversion of numbers to units done using `.` call and using shorthand names
  • `•` symbol is product of units to allow unambiguous parsing of units -> specific unicode symbol may become user customizable in the future
  • no division of units, but negative exponents
  • exponents are in superscript
  • usage of `•` and superscript is to circumvent Nim’s identifier rules!
  • SI units are the base. If ambiguous operation that can be solved by unit conversion, SI units are used (in the default SI unit system predefined when simply importing unchained)
  • math operations cannot use superscripts!
  • some physical constants are defined, more likely in the future
  • conversion from prefixed SI unit to non prefixed SI unit only happens if multiple prefixed units of same quantity involved
  • UnitLess is a distinct float unit that has a converter to float (such that UnitLess magically works with math functions expecting floats).

Why “Unchained”?

Un = Unit Chain = A unit

You shall be unchained from the shackles of dealing with painful errors due to unit mismatches by using this lib! Tada!

Hint: The unit Chain does not exist in this library…

Units and cligen

cligen is arguably the most powerful and at the same time convenient to use command line argument parser in Nim land (and likely across languages…; plus a lot of other things!).

For that reason it is a common desire to combine Unchained units as an command line argument to a program that uses cligen to parse the arguments. Thanks to cligen's extensive options to expand its features, we now provide a simple submodule you can import in order to support Unchained units in your program. Here’s a short example useful for the runners among you, a simple script to convert a given speed (in mph, km/h or m/s) to a time per minute / per mile / 5K / 10K / … distance or vice versa:

import unchained, math, strutils
defUnit(mi•h⁻¹)
defUnit(km•h⁻¹)
defUnit(m•s⁻¹)
proc timeStr[T: Time](t: T): string =
  let (h, mr) = splitDecimal(t.to(Hour).float)
  let (m, s)  = splitDecimal(mr.Hour.to(Minute).float)
  result =
    align(pretty(h.Hour, 0, true, ffDecimal), 6, ' ') &
    " " & align(pretty(m.Minute, 0, true, ffDecimal), 8, ' ') &
    " " & align(pretty(s.Minute.to(Second), 0, true, ffDecimal), 6, ' ')
template print(d, x) = echo "$#: $#" % [alignLeft(d, 9), align(x, 10)]
proc echoTimes[V: Velocity](v: V) =
  print("1K",       timeStr 1.0 / (v / 1.km))
  print("1 mile",   timeStr 1.0 / (v / 1.Mile))
  print("5K",       timeStr 1.0 / (v / 5.km))
  print("10K",      timeStr 1.0 / (v / 10.km))
  print("Half",     timeStr 1.0 / (v / (42.195.km / 2.0)))
  print("Marathon", timeStr 1.0 / (v / 42.195.km))
  print("50K",      timeStr 1.0 / (v / 50.km))
  print("100K",     timeStr 1.0 / (v / 100.km))   # maybe a bit aspirational at the same pace, huh?
  print("100 mile", timeStr 1.0 / (v / 100.Mile)) # let's hope it's not Leadville
proc mph(v: mi•h⁻¹) = echoTimes(v)
proc kmh(v: km•h⁻¹) = echoTimes(v)
proc mps(v:  m•s⁻¹) = echoTimes(v)
proc speed(d: km, hour = 0.0.h, min = 0.0.min, sec = 0.0.s) =
  let t = hour + min + sec
  print("km/h", pretty((d / t).to(km•h⁻¹), 2, true))
  print("mph",  pretty((d / t).to(mi•h⁻¹), 2, true))
  print("m/s",  pretty((d / t).to( m•s⁻¹), 2, true))
when isMainModule:
  import unchained / cligenParseUnits # just import this and then you can use `unchained` units as parameters!
  import cligen
  dispatchMulti([mph], [kmh], [mps], [speed])
nim c examples/speed_tool
examples/speed_tool mph -v 7.0 # without unit, assumed is m•h⁻¹
echo "----------------------------------------"
examples/speed_tool kmh -v 12.5.km•h⁻¹ # with explicit unit
echo "----------------------------------------"
examples/speed_tool speed -d 11.24.km --min 58 --sec 4

which outputs:

1K       :  0 h  5 min 20 s
1 mile   :  0 h  8 min 34 s
5K       :  0 h 26 min 38 s
10K      :  0 h 53 min 16 s
Half     :  1 h 52 min 22 s
Marathon :  3 h 44 min 44 s
50K      :  4 h 26 min 18 s
100K     :  8 h 52 min 36 s
100 mile : 14 h 17 min  9 s
----------------------------------------
1K       :  0 h  4 min 48 s
1 mile   :  0 h  7 min 43 s
5K       :  0 h 24 min  0 s
10K      :  0 h 48 min  0 s
Half     :  1 h 41 min 16 s
Marathon :  3 h 22 min 32 s
50K      :  4 h  0 min  0 s
100K     :  8 h  0 min  0 s
100 mile : 12 h 52 min 29 s
----------------------------------------
km/h     : 12 km•h⁻¹
mph      : 7.2 mi•h⁻¹
m/s      : 3.2 m•s⁻¹

More Repositories

1

nim-plotly

plotly wrapper for nim-lang
Nim
168
star
2

Datamancer

A dataframe library with a dplyr like API
Nim
120
star
3

numericalnim

A collection of numerical methods written in Nim
Nim
84
star
4

flambeau

Nim bindings to libtorch
Nim
81
star
5

getting-started

Getting started with Nim for Scientific Computing
Nim
58
star
6

nimjl

A bridge between Nim-lang and Julia !
Nim
46
star
7

nimcuda

Nim bindings for CUDA
HTML
37
star
8

nimblas

BLAS for Nim
Nim
37
star
9

scinim

The core types and functions of the SciNim ecosystem
Nim
35
star
10

theo

Theo is an optimized bigint and number theory library for Nim
Nim
26
star
11

rnim

A bridge between R and Nim
Nim
24
star
12

freccia

Apache Arrow implementation in Nim
Nim
22
star
13

Measuremancer

A library to handle measurement uncertainties & error propagation
Nim
20
star
14

nimfftw3

Nim bindings for FFTW3.
Nim
18
star
15

nimcl

High level wrapper over OpenCL
Nim
17
star
16

nimlapack

LAPACK bindings for Nim
Nim
16
star
17

astGrad

Symbolic differentiation based on the Nim AST
Nim
15
star
18

impulse

Impulse will be a collection of primitives for signal processing (FFT, Convolutions, ...)
C++
14
star
19

vectorize

SIMD vectorization backend
Nim
14
star
20

nim-constants

Mathematical numerical named static constants useful for different disciplines
Nim
13
star
21

polynumeric

A nim module to handle polynomials
Nim
12
star
22

xrayAttenuation

A library to compute transmission / absorption of X-rays through matter
Nim
10
star
23

bio

A library to work with biological sequences.
Nim
7
star
24

fmu.nim

create your own multiplatform FMU (Functional Mock-up Interface) models with Nim
Nim
6
star
25

horizonsAPI

API for the JPL Horizons tool to retrieve solar system parameters
Nim
5
star
26

symengine.nim

A Nim wrapper of SymEngine
Nim
5
star
27

runtimeGrad

A playground for different runtime based automatic differentiation implementations
Nim
4
star
28

Arrayfire-Nim

Fork of https://github.com/bitstormFA/ArrayFire-Nim
Nim
4
star
29

mcpl

A wrapper for the Monte Carlo Particle Lists (MCPL) library
Nim
2
star
30

netcdf

NetCDF bindings for Nim
Nim
2
star
31

mclimit

A Nim port of the mclimit confidence limit computation library
C++
1
star