Indexing

Indexing is a critical operation in linear algebra: it would be difficult to imagine defining operations on vectors, matrices, and arrays without some way of determining the coefficient at a particular location in the object. Similarly, we often want to extract coefficients from multivectors to perform operations.

However, the philosophy of this package – treating multivectors a number system on the same footing as that of complex numbers or quaternions – means that the AbstractCliffordNumber type foregoes array semantics. All AbstractCliffordNumber instances broadcast as scalars: for two instances x and y, x * y is identical to x .* y, both of which calculate the geometric product.

On top of this, the grade representation of multivectors makes it difficult to relate the indices of the backing Tuple for each type to the blades represented by the multivector for anything other than the dense CliffordNumber. To solve this issue, this package provides types specifically intended for indexing AbstractCliffordNumber subtypes: BladeIndex{Q} and BladeIndices{Q,C}.

BladeIndex{Q}

The BladeIndex{Q} type is the index type for AbstractCliffordNumber{Q}. This types wraps a UInt: the first dimension(Q) bits represent the presence or absence of the basis vectors associated with each dimension that are used to construct the indexed blade. The most significant bit is a sign bit: this is needed to represent the parity associated with the order of wedge products used to construct the blade.

Regardless of what position in the underlying Tuple a coefficient may be, if x == y, then the same BladeIndex b will index x and y identically:

julia> k = KVector{1,VGA(3)}(4,2,0)
3-element KVector{1, VGA(3), Int64}:
4e₁ + 2e₂

julia> b = BladeIndex(k, 1)
BladeIndex(Val(VGA(3)), 1)

julia> k == OddCliffordNumber(k)
true

julia> k[b] == OddCliffordNumber(k)[b]
true

It should also be noted that indexing an AbstractCliffordNumber at an index which is not explicitly represented by the type returns zero:

julia> k[BladeIndex(k, 1, 2)]
0

The only way of throwing an error with a BladeIndex object is by having a mismatch of algebra type parameters between the Clifford number and the index.

Construction

The internal constructor BladeIndex{Q}(signbit::Bool, blade::Unsigned) converts blade to a UInt and changes the most significant bit to match signbit. However, in many cases, constructing a BladeIndex{Q} directly from the sign bit and representation of the blade as an unsigned integer is inconvenient.

For this reason, we define two constructors, the first being BladeIndex(::Val{Q}, i::Integer...), which takes the algebra Q wrapped in Val (for reasons of type stability) and the integers i corresponding to basis blade indices defined in Q. The second constructor, BladeIndex(x, i::Integer...) calls BladeIndex(Val(signature(x))), i...), automatically determining Q.

In both cases, the parity of the permutation of indices is determined automatically from the integer arguments, so this bit is automatically assigned. The presence of a parity bit allows for correct indexing with non-lexicographic conventions: in some literature, $e_3 e_1$ is used instead of $e_1 e_3$ for one of the bivector components of APS and STA.

julia> l = KVector{2,VGA(3)}(0,6,9)
3-element KVector{2, VGA(3), Int64}:
6e₁e₃ + 9e₂e₃

julia> l[BladeIndex(l, 1, 3)]
6

julia> l[BladeIndex(l, 3, 1)]
-6

Tuples of BladeIndex{Q}

Julia AbstractArray instances can be indexed not just with integers, but with arrays of integers or special objects representing iterable ranges, such as :. Perhaps surprisingly, tuples containing integers (or any other valid index object) are not valid indices of AbstractArray, even though vectors of integers are:

julia> (1:10)[[2,3,4]]
3-element Vector{Int64}:
 2
 3
 4

julia> (1:10)[(2, 3, 4)]
ERROR: ArgumentError: invalid index: (2, 3, 4) of type Tuple{Int64, Int64, Int64}

In the case of AbstractCliffordNumber, we have a compelling reason to use tuples of BladeIndex{Q} objects for indexing: since the length of a Tuple is known statically, we can use that information to construct a new Tuple of coefficients with statically known length, which may be useful if we want to leverage indexing to convert types. Therefore, indexing an AbstractCliffordNumber{Q,T} with an NTuple{N,BladeIndex{Q}} returns an NTuple{N,T}, which can be fed into the constructor for a different type, and this is what the package uses internally to perform conversion.

Operations

BladeIndex{Q} supports a variety of unary and binary operations, many of which are used internally for tasks like calculating geometric products. Many of these operations are also supported for AbstractCliffordNumber{Q} instances, such as negation (-) and the geometric product (*).

Considering that the indices of an AbstractCliffordNumber provided by this package are known from the type, it makes sense to define a type which represents all indices of a subtype or instance of AbstractCliffordNumber. This package defines the singleton type BladeIndices{Q,C}, a subtype of AbstractArray{BladeIndex{Q}}, which compactly represents the unique, explicitly present indices of a Clifford number of type C.

BladeIndices objects can be constructed using BladeIndices, eachindex, or keys:

julia> k = KVector{1,VGA(3)}(4, 2, 0)
3-element KVector{1, VGA(3), Int64}:
4e₁ + 2e₂

julia> BladeIndices(k)
3-element BladeIndices{VGA(3), KVector{1, VGA(3)}}:
 BladeIndex(Val(VGA(3)), 1)
 BladeIndex(Val(VGA(3)), 2)
 BladeIndex(Val(VGA(3)), 3)

julia> eachindex(k)
3-element BladeIndices{VGA(3), KVector{1, VGA(3)}}:
 BladeIndex(Val(VGA(3)), 1)
 BladeIndex(Val(VGA(3)), 2)
 BladeIndex(Val(VGA(3)), 3)

julia> keys(k)
3-element BladeIndices{VGA(3), KVector{1, VGA(3)}}:
 BladeIndex(Val(VGA(3)), 1)
 BladeIndex(Val(VGA(3)), 2)
 BladeIndex(Val(VGA(3)), 3)

Notice how the output type lacks the scalar type parameter or length parameter of the type of the AbstractCliffordNumber. Stripping the scalar type prevents excessive proliferation of indexing types (which is useful for generated functions), and ensures that the indexing types of two Clifford numbers with different scalar types are equal in the === sense:

julia> kk = float(k)
3-element KVector{1, VGA(3), Float64}:
4.0e₁ + 2.0e₂

julia> BladeIndices(kk) === BladeIndices(k)
true

For this reason (and others), we recommend using BladeIndices(k), eachindex(k), or keys(k) as appropriate in generic code instead of BladeIndices{signature(k),typeof(k)}().

Conversion

One of the greatest uses for BladeIndices objects is that they can be used to perform tasks such as conversion or grade projection:

julia> inds = BladeIndices(OddCliffordNumber{VGA(3)})
4-element BladeIndices{VGA(3), OddCliffordNumber{VGA(3)}}:
 BladeIndex(Val(VGA(3)), 1)
 BladeIndex(Val(VGA(3)), 2)
 BladeIndex(Val(VGA(3)), 3)
 BladeIndex(Val(VGA(3)), 1, 2, 3)

julia> k[inds]
4-element OddCliffordNumber{VGA(3), Int64}:
4e₁ + 2e₂

This reindexing operation converted k from a KVector{1,VGA(3)} to an OddCliffordNumber{VGA(3)} automatically. All construction and conversion operations between Clifford numbers of the same algebra have a simple implementation in terms of indexing.

Applying involutions

BladeIndices{Q,C} is actually not an independent type: it is an alias of an internal type, CliffordNumbers.CGNBladeIndices{Q,C,N,I,R}. The final three type parameters lazily represent three common operations: negation (N), grade involution (I), and reversion (R). Their composition is efficiently represented in these type parameters.

Rather than deal directly with the possible types and their instances, custom broadcasting implementations are used (though the return type is visible):

julia> grade_involution.(inds)
4-element CliffordNumbers.CGNBladeIndices{VGA(3), OddCliffordNumber{VGA(3)}, false, true, false}:
 -BladeIndex(Val(VGA(3)), 1)
 -BladeIndex(Val(VGA(3)), 2)
 -BladeIndex(Val(VGA(3)), 3)
 -BladeIndex(Val(VGA(3)), 1, 2, 3)

julia> reverse.(inds) # equivalent to adjoint.(inds)
4-element CliffordNumbers.CGNBladeIndices{VGA(3), OddCliffordNumber{VGA(3)}, false, false, true}:
 BladeIndex(Val(VGA(3)), 1)
 BladeIndex(Val(VGA(3)), 2)
 BladeIndex(Val(VGA(3)), 3)
 -BladeIndex(Val(VGA(3)), 1, 2, 3)

The Clifford conjugate is equivalent to combining grade involution with reversion:

julia> conj.(inds)
4-element CliffordNumbers.CGNBladeIndices{VGA(3), OddCliffordNumber{VGA(3)}, false, true, true}:
 -BladeIndex(Val(VGA(3)), 1)
 -BladeIndex(Val(VGA(3)), 2)
 -BladeIndex(Val(VGA(3)), 3)
 BladeIndex(Val(VGA(3)), 1, 2, 3)

And as with all involutions, applying them twice undoes them, leaving behind an ordinary BladeIndices object:

julia> conj.(conj.(inds))
4-element BladeIndices{VGA(3), OddCliffordNumber{VGA(3)}}:
 BladeIndex(Val(VGA(3)), 1)
 BladeIndex(Val(VGA(3)), 2)
 BladeIndex(Val(VGA(3)), 3)
 BladeIndex(Val(VGA(3)), 1, 2, 3)
Warn

reverse(::BladeIndices) actually reverses the elements of a BladeIndices object, just as it would with any other AbstractVector! The dot syntax for broadcasting is necessary:

julia> reverse(inds)
4-element Vector{BladeIndex{VGA(3)}}:
 BladeIndex(Val(VGA(3)), 1, 2, 3)
 BladeIndex(Val(VGA(3)), 3)
 BladeIndex(Val(VGA(3)), 2)
 BladeIndex(Val(VGA(3)), 1)

adjoint(::BladeIndices) does broadcast the reverse operation (it is equivalent to '), but also changes the array type to a LinearAlgebra.Adjoint wrapper:

julia> adjoint(inds)
1×4 adjoint(::BladeIndices{VGA(3), OddCliffordNumber{VGA(3)}}) with eltype BladeIndex{VGA(3)}:
 BladeIndex(Val(VGA(3)), 1)  BladeIndex(Val(VGA(3)), 2)  BladeIndex(Val(VGA(3)), 3)  -BladeIndex(Val(VGA(3)), 1, 2, 3)

As with conversion and construction, efficient implementations of the common involutions are all done in terms of indexing with CGNBladeIndex objects.