Ivory: Geometric Units

In this page we will leave the ordinary number line, and begin to introduce the geometric level of the Ivory tower. Our starting point is the equation:

a2 + 1 = 0

Or, as an s-expression (where the sqr function multiplies a number by itself):

(= (+ (sqr a) 1)
0)

This looks simple enough, but let’s take a moment to consider its broader meaning. In particular, the sum includes one quantity that’s squared (sqr a) and one quantity that isn’t squared 1. This feels “off”, for a couple of reasons:

• As a physicist: the variable a must have different units than the constants 1 and 0 in order to be dimensionally consistent. For example, if a were a length then the constants must be areas.
• As a computer scientist: this feels like an ill-typed expression, like we’re mixing up encodings of semantically-distinct quantities.

Sure, those fears might be unfounded; but we can put ourselves at ease by re-stating the equation entirely with squared terms. This requires introducing a couple of extra variables, which we’ll call b and c:

(= (+ (sqr a) (sqr b))
(sqr c))

Now let’s solve this equation:

• We know that (= (sqr b) 1) and (= (sqr c) 0), by definition.
• We can rearrange the equation to find that (= (sqr a) (- (sqr b))) and hence (= (sqr a) -1)

You may be tempted to “square root” these and say that (= b 1), (= c 0) and (if you’re familiar with complex numbers) (= a i); however, those are just some of the possible solutions to these equations. Not only are there negative solutions too, but Geometric Algebra provides even more by extending our arithmetic to include extra numbers! These come in three flavours, one for each of our variables (apologies for the intimidating names, which actually pre-date Geometric Algebra!):

• We’ll call solutions to (= (sqr b) 1) (other than 1 and -1) hyperbolic units and write them as h₀, h₁, h₂, etc.
• We’ll call solutions to (= (sqr c) 0) (other than 0) dual units and write them as d₀, d₁, d₂, etc.
• We’ll call solutions to (= (sqr a) -1) imaginary units and write them as i₀, i₁, i₂, etc.

Practical applications of GA will only use a few of these units, but I want my code to support arbitrarily-many. Each of these units is a perfectly legitimate number, but they are not part of rational; hence they must occur at a higher level of our numerical tower. We’ll define a new level called geometric to contain all of them. I’ll be referring to them as GA/geometric/non-rational units”. Note that we cannot call them “irrational”, since that already means something else!

These non-rational numbers do not appear on the familiar number line. We’ll give their geometric interpretation later. For now we’ll just treat them as symbolic constants, the same way we treat τ, 𝑒, ϕ, etc.

Representing Geometric Units In Scheme

This is pretty simple, since each unit contains two pieces of information: the flavour and the index. We’ll represent the flavour using a symbol: either h or d or i. The index will just be a number (we’ll be sticking to natural indexes, but won’t enforce it). We’ll combine these into a pair, by either giving them as inputs to the cons operation, like (cons 'd 0) for d₀; or with a quotation, like '(i . 2) for i₂ (where the . makes this a pair, rather than a list).

It looks like we’re calling functions named d, h and i with a number as input; but for something to be a name, there must be some underlying definition that it’s referring to. In this case we have no definitions (or, if you prefer, symbols are merely names for themselves). These are “uninterpreted functions”, meaning Racket will just pass around these expressions as-is.

It may feel like cheating to claim these values are “incorporated deeply” into the language, compared to “usual” numbers. Admittedly the natural type is a special case (due to its place-value notation), but it turns out that all of Scheme’s standard numerical tower relies on this “uninterpreted function” trick!

Consider integer: this includes both natural numbers and their negatives. The latter are represented by prefixing the former with a - symbol, representing negation, which is left uninterpreted. The higher levels, rational and complex, use uninterpreted functions with two inputs (numerator & denominator, for rational; “real” & “imaginary” for complex).

In any case, here are some Scheme functions for manipulating these GA units:

;; Predicates for spotting if a value is a GA unit (i.e. an appropriate pair)

(define/match (unit-d? n)
[((cons 'd index)) (number? index)]
[(_) #f])

(define/match (unit-h? n)
[((cons 'h index)) (number? index)]
[(_) #f])

(define/match (unit-i? n)
[((cons 'i index)) (number? index)]
[(_) #f])

(define unit-ga? (disjoin unit-h? unit-d? unit-i?))

;; Functions to access the flavour and index of a GA unit. The 'car'/'cdr'
;; functions return the first/second element of a pair.
(define unit-flavour car)
(define unit-index cdr)

;; Helper for sorting units alphabetically
(define (unit<? x y)
(let ([fx (unit-flavour x)]
[fy (unit-flavour y)]
[ix (unit-index  x)]
[iy (unit-index  y)])
(or (symbol<? fx fy)
(and (equal? fx fy) (< iy ix)))))

Parsing Geometric Units

Now we have a representation for geometric units, as a pair of flavour and index, we can define a function to read this subscript syntax:

(define (subscripts-to-digits s)
(for/fold ([digits s])
([i    "0123456789"]
[char "₀₁₂₃₄₅₆₇₈₉"])
(string-replace digits (string char) (string i))))

(define (parse-unit in)
;; Look for flavour and index, using regexp capture groups
(match (regexp-match
;; Match flavour then index; disallow leading zeros
#px"^([dhi])((₀(?![₀₁₂₃₄₅₆₇₈₉]))|([₁₂₃₄₅₆₇₈₉][₀₁₂₃₄₅₆₇₈₉]*))"
in)
;; No match
[#f #f]

;; Found a match: s is entire match, groups are captured substrings
[(cons s (cons flavour-group (cons index-group _)))
(cons
(string->symbol (bytes->string/utf-8 flavour-group))
(string->number
(subscripts-to-digits (bytes->string/utf-8 index-group))))]))