Giter VIP home page Giter VIP logo

unchained's Introduction

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⁻¹

unchained's People

Contributors

metagn avatar vindaar avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

unchained's Issues

toUnit() not converting values

Noticed this when writing some code to convert between Foot and Meter. When using to(Meter), the code works properly, but the alternate form toMeter() only converts the unit, not the value.

import unchained

let 
  val1 = 100.Foot
  val2 = val1.to(Meter)     # Works properly
  val3 = val1.toMeter()     # Only converts unit, not value

doAssert val1 == 100.Foot
doAssert val2 == 30.48.Meter
doAssert val2 == val1
echo val3                   # prints "100 m"
doAssert val3 == val2       # Fails

`const` variables lose their type information

I've stumbled on this before, but ignored it, because I didn't have the time to isolate it and provide a Nim issue about it. But now I got caught by this bug again, so it's time to at least file an issue here.

If one defines a variable as const that has an associated type, the type information will be lost by the time this variable is used in some computation. If the type contains SI prefixes this is especially dangerous, because one misses conversion factors. Raw type information is a (comparatively) minor issue, because it would at least show up as a CT error due to missing types (which still might happen of course, but in cases where the types match regardless (i.e. just a computation and manual conversion to float) the numbers are plain wrong.

const g_aγ = 1e-10.GeV⁻¹
let x = g_aγ * 1.0.eV²

will result in x of type eV² and the conversion factor of 1e-9 missing from the result.

Add remaining derived SI units

A decent summary on Wikipedia: https://en.wikipedia.org/wiki/SI_derived_unit

Definitely have to add all remaining derived SI units (things like Tesla, Radian etc. are still missing).

Maybe a good idea to also add some of the quantities listed in the examples section. While we shouldn't enforce distinctness there maybe (e.g. then m⁻¹ is of an unclear quantity all of a sudden), it would be handy to be able to refer to Wavenumber and have that match anything that is of inverse length.

Declarative unit definitions and unit systems

The current way to add new units is

  1. annoying
  2. verbose
  3. littered all over

Instead we need to unify the unit construction. Instead of having:

  • units defined as full name
  • units defined as short name (alias)
  • unit as element in UnitKind
  • string parsing helper based on each UnitKind
  • conversion factor based on UnitKind
  • mapping to base unit
  • mapping to quantitý
  • mapping to base type

We need to auto generate all this based on some macro. Essentially thinking about something like this:

declareUnit:
  name: Meter
  short: m
  quant: Length
declareUnit:
  name: Inch
  short: inch # cannot use `in`
  quant: Length
  conv: Inch -> 2.54.cm
declareUnit:
  name: Newton
  short: N
  quant: Force
  comp: KiloGramMeterSecond⁻²

where the fact that Meter does not define a conversion conv means it's a base unit. Newton is neither a base unit as it provides its description in form of a compound of base units.

This allows to put another layer on top, namely one of full unit systems. So that we can have one set of SI units, another for CGS, imperial, .... With some helpers we can then even autogenerate conversions between unit systems.

Add more imperial units

For the units tutorial I'm planning to write someday (hopefully soon) I think it would be nice to have lots of examples mixing SI and Imperial units (because it's a mess you don't want to deal with manually). So I thought I'd create a PR for it. The only thing I really need is which imperial units do we want to support? There are so many obscure ones out there. I know you want chain to be included but do you have any other preferences @Vindaar?

Tests fail on non Nim devel

It seems like we're making use of some functionality that only works on Nim devel?

Investigate and find out if it's a bug in old Nim or if we're making use of a regression in the Nim compiler?

Possibly it's just that . dot operators don't exist in older Nim?

Unit math with certain compounds

A note on certain compounds like Liter, Acre (the only two compounds that have no UnitKind that is its base type).

Currently our implementation is confused by compound units that are not defined as their own actual unit. So Meter³, which is the base of Liter is not actually considered for auto conversion (only for type checking).

That means math like the following has a weird quirk:

block:
  let x = 1.Liter
  let y = 1.m³
  doAssert type(x + y) is Liter
  doAssert type(y + x) is Meter³

It wouldn't be too bad. The problem is the scale conversions are applied in both cases. So in the first case the actual number does just become 1.001. But it thinks it's liters!

UFCS breaks type checking of proc arguments

The following code should CT error:

proc E_to_γ(E: GeV): UnitLess =
  result = E.to(Joule) / (m_μ * c * c) + 1
echo 5.eV.E_to_γ()

Instead of throwing an error, it compiles and runs just fine. The argument instead of 5.eV is instead 1.GeV! To make matters worse, the result of the procedure called (which is computed correctly given a wrong value of 1.GeV) is not returned. Instead 5 is returned as a value of the expression (without any kind of unit).

It works perfectly fine using regular call syntax E_to_γ(5.eV), throws a CT error.

Is this an upstream bug or a bizarre side effect + bug of our macros?

Division of (some?) units is broken

Take the following code:

proc xrayEnergyToFreq(E: keV): s⁻¹ =
  ## converts the input energy in keV to a correct frequency
  result = E.to(Joule) / hp
echo 1.keV.xrayEnergyToFreq

it gives a CT error, because the resulting unit it computes is KiloGram²•Meter⁴•Second⁻³ instead of Second⁻¹.

The issue is that we are inverting both the power of the units as well as its base units for compounds. But the base units can never be inverted! That implies that the meaning of the compound unit was just changed.

The fix is easy. Just remove the inversion of the base powers in invert.

Can't use two generic parameters of the same quantity

import unchained

type Foo[DA: Length, DB: Length] = object
  a: DA
  b: DB

proc initFoo[DA: Length, DB: Length](a: DA, b: DB): Foo[DA, DB] =
  Foo[DA, DB](a: a, b: b)

echo initFoo(1.m, 2.m) # compiles correctly
echo initFoo(1.m, 2.cm) # got: <typedesc[Meter], typedesc[CentiMeter]> but expected: <DA: Length, DB: Length>

I've tried a couple of other quantities, and I got same error, but can't say if it happens for all quantities.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.