A macro for pretty geometric algebra equations?

The following slide is from Steven De Keninck’s presentation at SIBGAPI, an international conference annually promoted by the Special Interest Group on Computer Graphics and Image Processing (CEGRAPI) of the Brazilian Computer Society (SBC).

The slide shows the differences between the “standard” math syntax and the “standard” programming syntax in geometric algebra equations. The “standard” math syntax is less cluttered and is preferred by mathematicians. In contrast, the “standard” programming syntax is preferred by programmers because the operators are ASCII characters and those operators are relatively easy to overload in most programming languages.

Although I have not yet written a Julia macro, I’m hoping to write one that will offer the best of both syntaxes by translating the unicode operators in the math syntax to ASCII operators in the programming syntax, allowing the geometric algebra equations in a Julia program to be both uncluttered (the input to the macro) and portable (the output of the macro). For example, Julia correctly parses the following equation including the \wedge unicode operator:

julia> ex1 = Meta.parse("a ∧ b") # \wedge
:(a ∧ b)

julia> dump(ex1)
Expr
  head: Symbol call
  args: Array{Any}((3,))
    1: Symbol ∧
    2: Symbol a
    3: Symbol b

julia> sizeof.(ex1.args)
3-element Vector{Int64}:
 3
 1
 1

For the geometric product, I was hoping that the \thinspace unicode character would similarly be parsed as an operator, but it appears that the default behavior of the Julia parser is to interpret \thinspace as a space:

julia> ex2 = Meta.parse("a  b") # \thinspace
ERROR: Base.Meta.ParseError("extra token \"b\" after end of expression")
Stacktrace:
 [1] #parse#3
   @ .\meta.jl:237 [inlined]
 [2] parse(str::String; raise::Bool, depwarn::Bool)
   @ Base.Meta .\meta.jl:268
 [3] parse(str::String)
   @ Base.Meta .\meta.jl:267
 [4] top-level scope
   @ REPL[122]:1

julia> s2 = Symbol(" ") # \thinspace
Symbol(" ")

julia> sizeof(s2)
3

Is there some way to get Julia to parse the \thinspace unicode character as an operator instead of a space?

1 Like

Macros don’t change how Julia syntax is parsed. One option is to write a string macro, which allows you to create your own parser. This is how regex works in Julia for example. Given some of syntax your slide implies you want, this may be your only option.

Thanks. Do you have a suggestion for a good document for learning about Julia’s string macros? (I had never before even heard of them, although I am familiar with regular expressions from other programming languages.)

The stacked operators in the slide’s math syntax column are probably a bit much: the tilde over a short character like ‘a’ looks ok but over a tall character like ‘b’ is not an improvement … at least not in the fonts I’ve seen it. I think a \tilde preceding the variable would be close enough.

I’d start with the Julia specific documentation here: Metaprogramming · The Julia Language

1 Like

I wrote this string macro that, for now, only handles the simple case of a single character substitution:

macro ga_str(s)
	C = collect(s)
	n = length(C)
	for i = 1:n
		if C[i] == ' '		# \thinspace
			C[i] = '*'
		elseif C[i] == '∧'	# \wedge
			C[i] = '^'
		elseif C[i] == '∨'	# \vee
			C[i] = '&'
		elseif C[i] == '·'	# \cdotp
			C[i] = '|'
		end
	end
	return Meta.parse(String(C))
end

My initial test tries to calculate these two geometric algebra equations in math syntax:

axis_z = ga"e1 ∧ e2"
origin = ga"axis_z ∧ e3"

From the REPL, the results look correct:

julia> axis_z = ga"e1 ∧ e2"
16-element Vector{Float32}:
 0.0
 0.0
 0.0
 0.0
 0.0
 0.0
 0.0
 ⋮
 0.0
 0.0
 0.0
 0.0
 0.0
 0.0
 0.0

julia> origin = ga"axis_z ∧ e3"
16-element Vector{Float32}:
 0.0
 0.0
 0.0
 0.0
 0.0
 0.0
 0.0
 ⋮
 0.0
 0.0
 0.0
 0.0
 0.0
 1.0
 0.0

However, with those same two geometric algebra equations in a program, the result of the first equation is not available for the second:

julia> include("pga.jl")
utest (generic function with 3 methods)

julia> utest(false)
ERROR: UndefVarError: axis_z not defined
Stacktrace:
 [1] utest(flgOutput::Bool, nD::Int64)      
   @ Main C:\dev\olarth\PGA\pga3d\pga.jl:533
 [2] utest(flgOutput::Bool)
   @ Main C:\dev\olarth\PGA\pga3d\pga.jl:532
 [3] top-level scope
   @ REPL[2]:1

I’ll keep investigating and reading Julia’s Metaprogramming doc to try to understand this unexpected behavior of macro return values in a program. If anyone knows the reason off hand, let me know. Also, I’m interested in opinions about the aesthetics of these geometric algebra equations in math syntax:

axis_z = ga"e1 ∧ e2"
origin = ga"axis_z ∧ e3"

Does the ga"…" add more clutter than it removes?

I don’t see anything wrong with gprod(a, b), oprod(a, b), rprod(a, b), iprod(a, b), dual(a), and sprod(a,b). Why invent, in my opinion unintuitive, operator notation?

When there are just two operands, overloaded operators don’t help much (e.g., 1+2 = add(1,2)). I think the overloaded operators help more when there is a long string of operations (e.g., 1+2+3+4+5 = add(5,add(4,add(3,add(1,2))))).

However, this is all experimental at this point. Based upon the collection of sample projective geometric algebra applications at https://enkimute.github.io/ganja.js/examples/coffeeshop.html#pga2d_points_and_lines, I think a common use case will be projective geometric algebra applications that have a fairly small block of projective geometric algebra equations. I think a good goal is to somehow make that small block of equations as easy as possible to read.

I’m new to Julia macros and I’m still scratching my head about why a macro would behave differently inside a function. I’ve made the following minimum working example:

# file: gammwe (geometric algebra macro minimum working example)

# convert GA math syntax to GA programming syntax
macro ga_str(s)
	C = collect(s)
	n = length(C)
	for i = 1:n
		if C[i] == ' '		# \thinspace for geometric product
			C[i] = '*'
		elseif C[i] == '∧'	# \wedge for outer product
			C[i] = '^'
		elseif C[i] == '∨'	# \vee for regressive product
			C[i] = '&'
		elseif C[i] == '·'	# \cdotp for inner product
			C[i] = '|'
		end
	end
	return Meta.parse(String(C))
end

i = ga"2 ∧ 3" # ga macro will translate to i = 2 ^ 3
println("i: $i")
j = ga"i ∧ 2" # ga macro will translate to j = i ^ 2
println("j: $j")

# geometric algebra macro test
# For some reason, within a function, the result i2
# is not available for the calculation of j2.
function gamtest()
	i2 = ga"2 ∧ 3"
	println("i2: $i2")
	j2 = ga"i2 ∧ 2"
	println("j2: $j2")
end

The output:

julia> include("gammwe.jl")
i: 8
j: 64
gamtest (generic function with 1 method)

julia> gamtest()
i2: 8
ERROR: UndefVarError: i2 not defined
Stacktrace:
 [1] gamtest()
   @ Main C:\dev\olarth\PGA\pga3d\gammwe.jl:32
 [2] top-level scope
   @ REPL[2]:1

After using a lot of @macroexpand and watching Tom Kwong’s video about Julia macro hygiene, it appears that an esc() in the last line of the macro fixes the problem:

# convert GA math syntax to GA programming syntax
macro ga_str(s)
	C = collect(s)
	n = length(C)
	for i = 1:n
		if C[i] == ' '		# \thinspace for geometric product
			C[i] = '*'
		elseif C[i] == '∧'	# \wedge for outer product
			C[i] = '^'
		elseif C[i] == '∨'	# \vee for regressive product
			C[i] = '&'
		elseif C[i] == '·'	# \cdotp for inner product
			C[i] = '|'
		end
	end
	return esc(Meta.parse(String(C)))
end

You probably want to use the tool MacroTools.postwalk from the MacroTools.jl package.

This macro probably does what you want

julia> using MacroTools

julia> macro geom(ex)
           MacroTools.postwalk(ex) do x
               if x == :* 
                   :geom_mul
               elseif x == :∧
                   :outer_wedge
               elseif x == :&
                   :regressive_product
               elseif x == :|
                   :inner_product
               elseif x == :!
                   :dual
               elseif x == :>>>
                   :sandwich_product
               else
                   x
               end
           end |> esc
       end
@geom (macro with 1 method)

julia> MacroTools.@macroexpand(
       @geom begin 
           x * y
           a ∧ b
           c & s
       end
       ) |> MacroTools.prettify
quote
    geom_mul(x, y)
    outer_wedge(a, b)
    regressive_product(c, s)
end
2 Likes

Thanks for the tip about MacroTools. (I was unaware of that package.)

The following macro to give the option of writing geometric algebra equations using “standard” math syntax operators instead of “standard” programming syntax operators is good enough for me for now:

# convert GA math syntax to GA programming syntax
macro ga_str(str)
	C = collect(str)
	n = length(C)
	for i = 1:n
		if C[i] == ' '		# \thinspace for geometric product
			C[i] = '*'
		elseif C[i] == '∧'	# \wedge for outer product
			C[i] = '^'
		elseif C[i] == '∨'	# \vee for regressive product
			C[i] = '&'
		elseif C[i] == '·'	# \cdotp for inner product
			C[i] = '|'
		elseif C[i] == '\u20f0'	# \asteraccent for dual
			j = i-1
			while j > 0 # shift operator from postfix to prefix
				if isletter(C[j]) || isnumeric(C[j])
					C[j+1] = C[j]
					j -= 1
				else
					break
				end
			end
			C[j+1] = '!' # prefix '!'
		end
	end
	return esc(Meta.parse(String(C)))
end

The macro’s translation from math syntax to programming syntax slowed my unit test down by just 2.5% (4.69 us versus 4.58 us, according to @btime utest(false)).

I’m not yet sure if I will implement the translation for the sandwich operator because I am content with the look and speed of the geometric product operator and the tilde (i.e., reverse) operator implementing the sandwich operation.

Next week’s task: integrating the Julia reference implementation of projective geometric algebra with the interactive graphics of Makie.

Thanks for the help and the feedback.

Is macro really needed here? Operators like · can be directly defined in Julia. The main issue is with \thinspace, but maybe using some kind of multiplication symbol instead of a space would still be fine?

1 Like

The macro isn’t needed: programming syntax can be used if that is the preference. I agree that \thinspace is the main issue. A narrow strip of white space instead of the asterisk significantly unclutters the geometric algebra equations, and I’m hoping that will make them a little easier to read. The use of any symbol for multiplication other than white space would necessarily be more cluttered, although possibly less cluttered than using the asterisk.

The advantage of using programming syntax in the implementation is portability. I think a common use case for jprojective geometric algebra applications will be a tiny block of geometric algebra equations. A goal is to be able just copy that block of projective geometric algebra equations (e.g., from a ganja.js application) into some Julia code and see it work immediately … without any manual translation of operators.

What exactly do you mean by “programming syntax”?
Some entries from the table in the first post are already valid Julia code without any macros:

a ∧ b
a ∨ b
a ⋅ b

Some can look pretty close, but not equal, to the “math” notation:

# dual:
!a
# or
~a

Only the geometric and sandwich products remain. Maybe, choosing two multiplication-like symbols from this list would be cleaner than introducing a string macro?

And anyway, even if a macro is desired, · don’t need any conversion. Just define these operators directly on your GA vector type.

I’m using the phrase “programming syntax” as defined in the right column of the initially referenced slide by Steven De Keninck.