Skip to content

Latest commit

 

History

History
431 lines (389 loc) · 15 KB

random_thoughts_and_todos.org

File metadata and controls

431 lines (389 loc) · 15 KB

Random thoughts

  • 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 the SiUnit back into a distinct 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⁻¹ to Velocity, same as CentiMeter•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…

Defining units and types more generally

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.