Unchained
A fully type safe, compile time only units library.
Install / Use
npx skills add SciNim/UnchainedInstalls into whichever agent you are using.
README
- 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: #+begin_src nim import unchained let x = 10.m let y = 10.Meter doAssert x == y #+end_src
- some imperial units
- all SI prefixes #+begin_src nim import unchained let x = 10.Mm # mega meter let y = 5.ng # nano gram let z = 10.aT # atto tesla #+end_src
- arbitrary math with units composing to new units, e.g. (which do not have to be defined previously!), #+begin_src nim import unchained let x = 10.m * 10.m * 10.m * 10.m * 10.m doAssert typeof(x) is Meter⁵ #+end_src without having to predefine a =Meter⁵= type
- automatic conversion between SI prefixes if a mix is used #+begin_src nim import unchained let x = 5.kg + 5.lbs doAssert typeof(x) is kg doAssert x == 7.26796.kg #+end_src
- manual conversion of units to compatible other units via ~to~
(e.g.
#+begin_src nim
import unchained
let x = 5.m•s⁻¹
defUnit(km•h⁻¹) # needs to be defined to be able to convert to
#
tocould 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)
#+end_src
- comparisons between units compare real value taking into account SI
prefixes and even different units of the same quantity:
#+begin_src nim 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!
#+end_src
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 #+begin_src nim 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 #+end_src
- 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. #+begin_src nim 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 Gram•Meter•Second⁻² 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(Meter•Second⁻²)) is Meter•Second⁻² 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
Alternatively, use accented quotes and the following syntax:
block:
let a = 10.m*s^-2
let b = 5.kg*m*s^-2
check typeof(a) is Meter•Second⁻²
check typeof(b) is Newton
check a == 10.m•s⁻²
check b == 5.N
#+end_src
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 = [[https://en.wikipedia.org/wiki/Chain_(unit)][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...
** These units are hard to write!
Kind of. I type on [[https://neo-layout.org]] and so it's easy for me.
However, all you really need are:
- the unicode separator (default is •, actually a bullet point, but I already started using it before I realized that)
- superscript minus, ⁻
- superscript numbers ¹²³⁴⁵⁶⁷⁸⁹, though ¹²³ are surely the most used.
The former can be customized by any other unicode separator, if you compile with ~-d:UnicodeSep=·~ for example.
Alternatively, write your units using accented quotes,
#+begin_src nim
let G_Newton = 6.6743e-11.N*m^2*kg^-2
#+end_src
for example. Note that this does not allow you to write actual unit
names in function arguments or return types.
See the tests in [[tests/tunchained.nim]] from "Quantity concepts" (or the next section) on how to write functions that use concepts based on quantities to simplify the arguments (if you don't need a specific unit as input).
** 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: #+begin_src nim :tangle examples/speed_tool.nim 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 Leadvill
Related Skills
node-connect
385.5kDiagnose OpenClaw Android, iOS, or macOS node pairing, QR/setup code, route, auth, and connection failures.
blender-python-addon
40.5kBlender Python add-on rules for operators, panels, properties, registration, testing, and API-safe scripting
flutter-development-guidelines-cursorrules-prompt-file
40.5kCursor rules for Flutter development with MVVM architecture, Riverpod state management, Material widgets, and Dart style guidelines.
commit-push-pr
140.6kCommit, push, and open a PR
