Numeric.Dimensional.CGS -- CGS system of units
Bjorn Buckwalter, bjorn.buckwalter@gmail.com
License: BSD3

!!! EXPERIMENTAL !!!


= Introduction =

This module was prompted by an email from Chuck Blake[1]. He asked if
the Dimensional library could support other systems of units than
SI, in particular systems such as the centimeter-gram-second (CGS)
system where fractional exponents of dimensions occur. He also
wondered whether it was possible to convert quantities between
different systems while statically ensuring that a given conversion
was valid.

In this module we show that we can in a straight forward manner
support systems with rational exponents, provided that the rationals
that may be encountered are known a priori. As an example we provide
a rudimentary implementation of the CGS system.

We also show that we can indeed statically prohibit invalid conversions
between different systems.


= Caveats =

I'm ignorantly assuming that when working with the CGS (or MKS)
system you will only (meaningfully?) encounter half-exponents and
only of the length and mass dimensions. Of course, in other systems
other rational exponents may be encountered.

I am also assuming that the CGS system would not be employed when
working with temperature, amount or luminosity. This is evident in
the below type signatures where I have assumed zero extent in the
temperature, amount and luminosity dimensions. If this is incorrect
I would appreciate pointers to the CGS representation of these
dimensions.

Please correct and inform me if my assumptions are wrong!


= Preliminaries =

> {-# LANGUAGE UndecidableInstances
>            , ScopedTypeVariables
>            , EmptyDataDecls
>            , MultiParamTypeClasses
>            , FlexibleInstances
>            , FlexibleContexts
> #-}
> {-# OPTIONS_GHC -fcontext-stack=30 #-}
> {- |
>    Copyright  : Copyright (C) 2006-2013 Bjorn Buckwalter
>    License    : BSD3
>
>    Maintainer : bjorn.buckwalter@gmail.com
>    Stability  : Experimental
>    Portability: GHC only?
>
> Please refer to the literate Haskell code for documentation of both API
> and implementation.
> -}

> module Numeric.Units.Dimensional.CGS where

> import Prelude
>   ( undefined, Num, Fractional, Floating, Show, recip, Double, unwords, (.) )
> import qualified Prelude
> import Numeric.Units.Dimensional hiding ( DLength, DMass, DTime, DElectricCurrent )
> import Numeric.Units.Dimensional.Quantities as SIQ
> import qualified Numeric.Units.Dimensional.SIUnits as SI
> import qualified Numeric.NumType as N
> import Numeric.NumType ( Neg2, Neg1, Zero, Pos1, Pos2, Pos3, NumType )
> import Numeric.NumType ( neg2, pos2, pos3 )
> import Data.Maybe (catMaybes)


= Dimensions =

Analogously with the SI we collect the base dimensions of the CGS
system in the data type 'CGSDim'.

> data CGSDim lh mh t

In the above 'lh' and 'mh' represent the number of half-exponents
of length and mass respectively while 't' represents the number of
whole-exponents. The base dimensions illustrate this.

> type DLength = CGSDim Pos2 Zero Zero
> type DMass   = CGSDim Zero Pos2 Zero
> type DTime   = CGSDim Zero Zero Pos1

We add a few non-base dimensions for the sake of example. Charge
is particularly interesting as it illustrates the need for
half-exponents as described in [2].

> type DElectricCurrent = CGSDim Pos3 Pos1 Neg2
> type DCharge = CGSDim Pos3 Pos1 Neg1


= 'Mul', 'Div', 'Pow' and 'Root' instances =

The 'Mul', 'Div', 'Pow' and 'Root' instances are strictly analogous
with the SI.

> instance ( N.Sum lh lh' lh''
>          , N.Sum mh mh' mh''
>          , N.Sum t  t'  t'' ) => Mul (CGSDim lh   mh   t)
>                                      (CGSDim lh'  mh'  t')
>                                      (CGSDim lh'' mh'' t'')

> instance ( N.Sum lh lh' lh''
>          , N.Sum mh mh' mh''
>          , N.Sum t  t'  t'' ) => Div (CGSDim lh'' mh'' t'')
>                                      (CGSDim lh'  mh'  t')
>                                      (CGSDim lh   mh   t)

> instance ( N.Mul lh x lh'
>          , N.Mul mh x mh'
>          , N.Mul t  x t' ) => Pow (CGSDim lh  mh  t) x
>                                   (CGSDim lh' mh' t')

> instance ( N.Div lh x lh'
>          , N.Div mh x mh'
>          , N.Div t  x t' ) => Root (CGSDim lh  mh  t) x
>                                    (CGSDim lh' mh' t')


= Units =

We define the base units of the system. By defining 'meter' with a
"scale" of 100 we get a scale of one for 'centi meter'.

> meter  :: Num a => Unit DLength a
> meter  = Dimensional 100
> gram   :: Num a => Unit DMass a
> gram   = Dimensional 1
> second :: Num a => Unit DTime a
> second = Dimensional 1

We continue by defining the CGS equivalents of the other base SI
units. Actually we limit ourselves to 'ampere' since I am not sure
if or how the SI base dimensions other than current are expressed
in CGS.

> ampere :: Floating a => Unit DElectricCurrent a
> ampere = prefix (recip 3.33564e-10) ((SI.centi meter ^ pos3) ^/ pos2 * gram ^/ pos2 * second ^ neg2)

We also define the preferred CGS unit for charge.

> franklin :: Floating a => Unit DCharge a -- Also known as "esu".
> franklin = gram ^/ pos2 * (SI.centi meter ^ pos3) ^/ pos2 / second


= Conversion from SI =

At some point we may wish to convert an SI quantity to a CGS quantity
or vice versa.

In order to convert a 'Quantity' from the SI system to the CGS
system we use the strategy of dividing the quantity by the SI base
unit and multiplying the resulting number (sans dimension) by the
equivalent CGS unit. To realize this strategy we must be able to
obtain the SI base unit and the equivalent CGS unit for a given
quantity. We start with the SI unit since it is trivial.

> unit_SI :: Num a => Quantity (Dim l m t i th n j) a -> Unit (Dim l m t i th n j) a
> unit_SI _ = Dimensional 1

(Perhaps the above function would be better defined in another
module.)

Obtaining the CGS unit corresponding to the SI base unit of a
Quantity isn't quite as trivial. The function body itself is
straight-forward enough, the hairy part is the type signature.

> unit_CGS :: forall a l m t i l2 m2 il it l' m' t'.
>          ( Floating a
>          , N.Mul Zero l Zero, N.Mul Pos2 l l2
>          , N.Mul Zero m Zero, N.Mul Pos2 m m2
>          , N.Mul Zero t Zero, N.Mul Pos1 t t
>          , N.Sum l2 Zero l2
>          , N.Sum Zero m2 m2,  N.Sum m2 Zero m2
>          , N.Sum Zero t  t
>          , N.Mul Pos3 i  il
>          , N.Mul Pos1 i  i
>          , N.Mul Neg2 i  it
>          , N.Sum l2 il l'
>          , N.Sum m2 i  m'
>          , N.Sum t  it t'
>          ) => Quantity (Dim l m t i Zero Zero Zero) a -> Unit (CGSDim l' m' t') a
> unit_CGS _ = meter        ^ (undefined :: l)
>            * SI.kilo gram ^ (undefined :: m)
>            * second       ^ (undefined :: t)
>            * ampere       ^ (undefined :: i)

Note that since the base dimensions of the CGS are a subset of those
of the SI the mapping of types from SI to CGS is unambiguous.

Also note that complex as the type signature may be producing it is a
mostly mechanical process.

With the above two functions we can define the function that converts
a unit from the SI. We omit the type signature since it is hairy
but can be readily inferred.

> fromSI x = x /~ unit_SI  x *~ unit_CGS x


= Conversion to SI =

We use the same strategy to convert from CGS to SI. However, when
converting from CGS to SI there may be several valid SI dimensionalities
for any given CGS dimensionality. We will handle this ambiguity by
requiring the user to specify the desired type (except when it is
inferable) of the resulting quantity.  For example:

] toSI (3.2 *~ centi meter) :: Length Double

In order to do this we must employ lexically scoped type variables
and provide the hairy type signature for the 'toSI' function.

> toSI :: forall a l m t i l2 m2 il it l' m' t'.
>          ( Floating a
>          , N.Mul Zero l Zero, N.Mul Pos2 l l2
>          , N.Mul Zero m Zero, N.Mul Pos2 m m2
>          , N.Mul Zero t Zero, N.Mul Pos1 t t
>          , N.Sum l2 Zero l2
>          , N.Sum Zero m2 m2,  N.Sum m2 Zero m2
>          , N.Sum Zero t  t
>          , N.Mul Pos3 i  il
>          , N.Mul Pos1 i  i
>          , N.Mul Neg2 i  it
>          , N.Sum l2 il l'
>          , N.Sum m2 i  m'
>          , N.Sum t  it t'
>          ) => Quantity (CGSDim l' m' t') a -> Quantity (Dim l m t i Zero Zero Zero) a
> toSI x = x /~ unit_CGS (undefined :: Quantity (Dim l m t i Zero Zero Zero) a)
>            *~ unit_SI  (undefined :: Quantity (Dim l m t i Zero Zero Zero) a)

Again, the type signature is complex but deriving it is a mechanical
process.


= 'Show' instance =

We round off by writing 'Show' instance for 'CGSDim' analogous to
that of 'Dim'.

Out of laziness we use the notation "sqrt(cm)" to represent halves
of integral dimensions. Nothing is technically keeping us from doing
a better job here.

> instance forall lh mh t.
>     ( NumType lh
>     , NumType mh
>     , NumType t
>     ) => Show (CGSDim lh mh t) where
>     show _ = (unwords . catMaybes)
>              [ dimUnit "sqrt(cm)" (undefined :: lh)
>              , dimUnit "sqrt(g)"  (undefined :: mh)
>              , dimUnit "s"        (undefined :: t)
>              ]


= Examples =

Let us try the Coulomb attraction example from [2]. We start by
performing the calculation in the SI.

> q_si  = 1.6021773e-19 *~ SI.coulomb -- Elementary charge in SI.
> r_si  = 0.1 *~ SI.nano SI.meter     -- Distance in SI
> f_si  = q_si ^ pos2 / (_4 * pi * e0 * r_si ^ pos2)
>   where
>       e0 = 8.8541878e-12 *~ (SI.ampere * SI.second / (SI.volt * SI.meter))

The same calculation in the CGS system.

> q_cgs = fromSI q_si -- Elementary charge in CGS.
> r_cgs = fromSI r_si -- Distance in CGS
> f_cgs = q_cgs ^ pos2 / r_cgs ^ pos2

Inspecting the values in GHCi shows us that the results are consistent
(within reasonable accuracy) with [2].

  *Numeric.Dimensional.CGS> f_si
  2.3070794737101255e-8 m kg s^-2
  *Numeric.Dimensional.CGS> f_cgs
  2.30708078598602e-3 sqrt(cm)^2 sqrt(g)^2 s^-2

To convert from CGS to SI we must specify the type of the SI 'Quantity'.

> f_si' = toSI f_cgs :: SIQ.Force Double

  *Numeric.Dimensional.CGS> f_si'
  2.3070807859860202e-8 m kg s^-2

We follow up with another conversion example demonstrating the
ambiguity in the conversion from CGS to SI.

> c     = 1 *~ SI.farad -- A SI capacitance.
> c_cgs = fromSI c      -- Capacitance has dimensionality L in CGS.
> c'    = toSI c_cgs :: SIQ.Capacitance Double
> c''   = toSI c_cgs :: Length Double

  *Numeric.Dimensional.CGS> c
  1.0 m^-2 kg^-1 s^4 A^2
  *Numeric.Dimensional.CGS> c_cgs
  8.98755691740885e11 sqrt(cm)^2
  *Numeric.Dimensional.CGS> c'
  1.0 m^-2 kg^-1 s^4 A^2
  *Numeric.Dimensional.CGS> c''
  8.98755691740885e9 m


= Future work =

This is a very rudimentary implementation. To make it more practical
a significant number of quantities and units, in particularly those
commonly used with the CGS, would need to be added. In the mean
time all units defined for the SI can be used with the CGS by
applying 'fromSI' to quantities defined from the SI units.

If anyone is willing to add quantities/units (or other enhancements)
I will happily to accept patches. Personally I do not expect to use
this module and therefore do not intend to invest much more time
in it. If the module has other users I might reconsider.

And of course, another direction of future work is to define
additional systems (e.g. natural, relativistic) using this module
as a template. I imagine this should be fairly straight forward.


= References =

[1] http://code.google.com/p/dimensional/wiki/ChuckBlake20070611
[2] http://www.tf.uni-kiel.de/matwis/amat/mw1_ge/kap_2/basics/b2_1_14.html
