- when multiplying or dividing units resulting units need to use same
order always, e.g.
let m: Meter let s: Second doAssert typeof(m * s) == typeof(s * m) == m•s
We can do this by having a specific order of all units, like an importance ranking. Then when combining reflect that. Once we have more complex units it’s probably a good idea to bring back the idea of the
SiUnit
compile time object, which should abstract away many parts and result in simpler handling, because we don’t have to work with strings. Essentially should be able to define a comparison operator of units that solves this for us and a serialization procedure, which turns theSiUnit
back into adistinct
unit. - add remaining SI prefixes and automate their units, i.e. write a macro that generates all units with all prefixes
- add combinatorical units <- not required I think
- add remaining standard units
- add natural units (how to best do it? Something like “NaturalLength” for full name?)
- add imperial units
- add physical constants
- add support for affine unit conversions (Celsius -> Fahrenheit)
- add parsing of english language units (“meterPerSecondSquared”); low priority
- generate all common units as english language (low priority). But generation is much easier than parsing?
- add option to change the `•` and `⁻` syntax? Easy for parsing, but what about pre defined types?
- add minute, hour, day, degree, arcminute, arcsecond -> need a good way to have conversions between non base types. Base conversion works well using SI prefix. On top of that need a higher level conversion between individual units! lbs -> kg, … For these probably a good idea to define conversion from and to (which are the inverse for non affine, linear units), because that allows to have non linear transformations as well (is that ever needed? in the exp. sense of non linear?)
- have converters from “specific unit X” to “base quantity
Y”. E.g. allow conversion of
Meter•Second⁻¹
toVelocity
, same asCentiMeter•Second⁻¹
. That allows to write:proc s(v: Velocity, t: Time): Length = result = v * t s(5.m•s⁻¹, 60.s) # result is units of `m` s(10.km•h⁻¹, 5.h) # result units of `km` s(8000.m•s⁻¹, 1.h) # result is units of `m`
Note: to resolve types like `Newton` or `N` manually, need to recurse the get type tree by:
let xT = x.getTypeInst
while xT.getType.kind != nnkDistinct and xT.getType.strVal.isNoBaseUnit():
if xT.len == 3:
xT = xT.getImpl[2]
else:
???
# break if xT is just an ident
Have a clear unit hierarchy:
Unit (distinct float) -> UnitLess (distinct Unit) (allows auto conversion to float) -> Quantity (distinct Unit) -> CompoundQuantity (distinct Quantity) (product of different quantities, e.g. Length * Mass) -> Base quantities: Time, Mass, Length, ... (distinct Quantity) -> Derived compound quantities: Momentum, Velocity, ... (distinct Quantity) ## from here is where actual units we work with start -> Base SI units: Kilogram, Second, ... (distinct Time, distinct Mass, ...) -> Derived SI units: specific form of CompoundQuantity containing non bare SI units: Newton, Joule, ... Essentially just named CompoundQuantities -> Other derived units / clarification of Compound (kg•m•s⁻¹: Momentum, ...) -> Aliased names (not distinct)
Can we avoid having to define all types with SI prefixes? Should work:
Define all relevant types as is w/o SI prefix: Then use approach similar to ggplotnim new formula macro. Emit code to add encountered type to a CT table, call another macro. Extract symbols from CT table.
Note: only need to generate the SI prefixed units for all individual (and pre-defined compound units that have a name, e.g. Newton / N) units. From there we can simply construct the compound units in the macro. This is because we only care about being able to match the individual parts of the type we get.
Possibly we will need a CT table for lookup of named compound types to be able to convert names to actual units easier. In theory this could already be implemented as pre defined CTCompoundUnit objects!
To convert known units not in base units, do as follows:
- read possible SI prefix and exponent from type string
- ask for distinct base of given unit
- convert distinct base and multiply possible exponents / factors
This should work, because all SI prefixed units are distinct
<BaseUnit>
. So by accessing the distinct base we get the correct
description in base units.
That means we should define all units in base units first. UPDATE
not true. We can always resolve aliases, see below.
Accessing types:
of nnkSym: getTypeInst
of nnkBracketExpr: [1].getImpl # from typedesc
of nnkDistinctTy: [0].getImpl
Resolving aliases: Since we know that every unit is always some
distinct type and aliases are not distinct, we can always get the
first unit of interest by recursing on getType*/Impl
until we get
the first distinct type!
We know all base units with their SI prefixes. That means we can build a CT table of those to map them to their types from strings. That allows to work with untyped macros! While generating all SI prefixes, simply add to CT table. Then in untyped, split into pieces (• and powers) and feed each element into that table. Result is a known base unit (Newton, Farad, …). For these have another table to map them to SI base units.
- “Newton” -> “KiloGram•Meter•Second⁻²” etc …
Allows resolving from untyped context to something we can parse to CT unit.
Only remaining thing then is to keep track of powers (multiply each
base unit by power of alias) and SI prefix. For SI prefix maybe add a
prefix field (or a scale) to CTCompoundUnit
.
Need ways to deal with typed and untyped contexts. Typed is easy, because compiler does checking for us.
Natural units:
- for natural units we need to have a notion of the quantity. The
distinct base gives us that, but in
CTCompoundUnit
we may need the quantity kind field so that we are aware of what is what.
Maybe in general better to have an enum of all available units (in
their base form). By storing that in addition in the CTUnit
it makes
it much easier to reason. Conversion from unit A to B can then be done
by dispatching on the unit type.
In that vain we can do the following: have conversion procs for
QuantityKind and UnitKind. Conversion between UnitKind of same
QuantityKind possible, else error.
In this context:
How do we deal with CompoundUnits? They cannot be represented by a
BaseUnitKind
. In principle CTUnit
would already be an object like
CTCompoundUnit
, i.e. store multiple base units.
Maybe we can combine CTUnit
and CTCompoundUnit
into one variant object.
Sounds like a lot…
import std / [macros, sets, sequtils]
proc genTypeClass*(e: var seq[NimNode]): NimNode =
## Helper to generate a "type class" (using `|`) of multiple
## types, because for _reasons_ the Nim AST for that is nested infix
## calls apparently.
if e.len == 2:
result = nnkInfix.newTree(ident"|", e[0], e[1])
else:
let el = e.pop
result = nnkInfix.newTree(ident"|",
genTypeClass(e),
el)
type
QuantityType = enum
qtBase, qtDerived
CTBaseQuantity = object
name: string
QuantityPower = object
quant: CTBaseQuantity
power: int
## A quantity can either be a base quantity or a compound consisting of multiple
## CTBaseQuantities of different powers.
CTQuantity = object
case kind: QuantityType
of qtBase: b: CTBaseQuantity
of qtDerived:
name: string # name of the derived quantity (e.g. Force)
baseSeq: seq[QuantityPower]
proc `==`(q1, q2: CTQuantity): bool =
if q1.kind == q2.kind:
case q1.kind
of qtBase: result = q1.b == q2.b
of qtDerived: result = q1.name == q2.name and
q1.baseSeq == q2.baseSeq
else:
result = false
proc contains(s: HashSet[CTBaseQuantity], key: string): bool =
result = CTBaseQuantity(name: key) in s
proc getName(q: CTQuantity): string =
case q.kind
of qtBase: result = q.b.name
of qtDerived: result = q.name
proc parseBaseQuantities(quants: NimNode): seq[CTQuantity] =
## Parses the given quantities
##
## Given:
##
## Base:
## Time
## Length
## ...
##
## As Nim AST:
## Call
## Ident "Base"
## StmtList
## Ident "Time"
## Ident "Length"
## ...
##
## into corresponding `CTQuantity` objects.
doAssert quants.len == 2
doAssert quants[0].kind == nnkIdent and quants[0].strVal == "Base"
doAssert quants[1].kind == nnkStmtList
for quant in quants[1]:
case quant.kind
of nnkIdent:
# simple base quantity
result.add CTQuantity(kind: qtBase, b: CTBaseQuantity(name: quant.strVal))
else:
error("Invalid node kind " & $quant.kind & " in `Base:` for description of base quantities.")
proc parseDerivedQuantities(quants: NimNode, baseQuantities: HashSet[CTBaseQuantity]): seq[CTQuantity] =
## Parses the given derived quantities
##
## Given:
##
## Derived:
## Frequency = (Time, -1)
## ...
##
## As Nim AST:
## Call
## Ident "Derived"
## StmtList
## Call
## Ident "Acceleration"
## StmtList
## Bracket
## TupleConstr
## Ident "Mass"
## IntLit 1
## TupleConstr
## Ident "Speed"
## IntLit -2
## ...
##
##
## into corresponding `CTQuantity` objects.
doAssert quants.len == 2
doAssert quants[0].kind == nnkIdent and quants[0].strVal == "Derived"
doAssert quants[1].kind == nnkStmtList
for quant in quants[1]:
case quant.kind
of nnkCall:
doAssert quant.len == 2
doAssert quant[0].kind == nnkIdent
doAssert quant[1].kind == nnkStmtList
doAssert quant[1][0].kind == nnkBracket
var qt = CTQuantity(kind: qtDerived, name: quant[0].strVal)
for tup in quant[1][0]:
case tup.kind
of nnkTupleConstr:
doAssert tup[0].kind == nnkIdent and tup[1].kind == nnkIntLit
let base = tup[0].strVal
let power = tup[1].intVal.int
if base notin baseQuantities:
error("Given base quantitiy `" & $base & "` is unknown! Make sure to define " &
"it in the `Base:` block.")
qt.baseSeq.add QuantityPower(quant: CTBaseQuantity(name: base), power: power)
result.add qt
else:
error("Invalid node kind " & $tup.kind & " in dimensional argument to derived quantity " &
$quant.repr & ".")
else:
error("Invalid node kind " & $quant.kind & " in `Derived:` for description of derived quantities.")
proc genQuantityTypes(quants: seq[CTQuantity]): NimNode =
## Generates the base quantities based on the given list of quantities
##
## type
## Time* = distinct Quantity
## Length* = distinct Quantity
## ...
##
## BaseQuantity* = Time | Length | ...
var quantList = newSeq[NimNode]()
for quant in quants:
let q = case quant.kind
of qtBase: ident"Quantity"
of qtDerived: ident"CompoundQuantity"
let qName = ident(quant.getName())
result.add nnkTypeDef.newTree(
qName,
newEmptyNode(),
nnkDistinctTy.newTree(q)
)
quantList.add qName
#let qtc = case quant.kind
# of qtBase: ident"BaseQuantity"
# of qtDerived: ident"DerivedQuantity"
#result.add nnkTypeDef.newTree(qtc, newEmptyNode(), genTypeClass(quantList))
macro declareQuantities(typs: untyped): untyped =
var baseQuant = nnkTypeDef.newTree(ident"BaseQuantity")
result = nnkTypeSection.newTree()
var baseQuantities: seq[CTQuantity]
var derivedQuants: seq[CTQuantity]
for typ in typs:
if typ.kind != nnkCall:
error("Invalid node kind " & $typ.kind & " in declaration of quantities.")
if typ[0].strVal == "Base":
# defines the base quantities
baseQuantities = parseBaseQuantities(typ)
elif typ[0].strVal == "Derived":
# defines derived quantities
if baseQuantities.len == 0:
error("`Base:` block to define base quantities must come before `Derived:` block.")
derivedQuants = parseDerivedQuantities(typ, baseQuantities.mapIt(it.b).toHashSet())
else:
error("Invalid type of quantities: " & $typ.repr)
echo baseQuantities
echo derivedQuants
declareQuantities:
Base:
Time
Length
Mass
Current
Temperature
AmountOfSubstance
Luminosity
Derived:
Frequency: [(Time, -1)]
Velocity: [(Length, 1), (Time, -1)]
Acceleration: [(Length, 1), (Time, -2)]
Area: [(Length, 2)]
Momentum: [(Mass, 1), (Length, 1), (Time, -1)]
Force: [(Length, 1), (Mass, 1), (Time, -2)]
Energy: [(Mass, 1), (Length, 2), (Time, -2)]
ElectricPotential: [(Mass, 1), (Length, 2), (Time, -3), (Current, -1)]
Charge: [(Time, 1), (Current, 1)]
Power: [(Length, 2), (Mass, 1), (Time, -3)]
ElectricResistance: [(Mass, 1), (Length, 2), (Time, -3), (Current, -2)]
Inductance: [(Mass, 1), (Length, 2), (Time, -2), (Current, -2)]
Capacitance: [(Mass, -1), (Length, -2), (Time, 4), (Current, 2)]
Pressure: [(Mass, 1), (Length, -1), (Time, -2)]
Density: [(Mass, 1), (Length, -3)]
Angle: [(Length, 1), (Length, -1)]
SolidAngle: [(Length, 2), (Length, -2)]
MagneticFieldStrength: [(Mass, 1), (Time, -2), (Current, -1)]
Activity: [(Time, -1)]
# generates
# type
# Time* = distinct Quantity
# Length* = distinct Quantity
# Mass* = distinct Quantity
# Current* = distinct Quantity
# Temperature* = distinct Quantity
# AmountOfSubstance* = distinct Quantity
# Luminosity* = distinct Quantity
#
# BaseQuantity* = Time | Length | Mass | Current | Temperature | AmountOfSubstance | Luminosity
# possibly a `declareCompoundQuantity`?
#declareUnits:
# # SI
# Meter:
# short: m
# quantity: Length
# isBaseUnit: true
# isCompound: false # (unnecessary as `isBaseUnit` is `true`)
# # other non compound units
# Pound:
# short: lbs
# quantity: Mass
# isBaseUnit: false
# toBaseUnit: 0.45359237.kg
# isCompound: false
# # compound SI
# Newton:
# short: N
# quantity: Force
# isBaseUnit: false
# isCompound: true
# toBaseUnit: 1.kg•m•s⁻²
# generates the following things:
# - enum UnitKind
# - proc isBaseUnit
# - proc parseUnitKind
# - proc getConversionFactor
# - proc toCTBaseUnitSeq
# - proc toBaseUnit
# - proc toQuantity
# - proc isCompound
# - proc toShortName
How then do we get to different unit systems? By defining different things as base units and defining different conversions for each unit.