Computing with orientations

Suffice it to say, this is quite hard to do correctly.

Orientation to Transform

For each orientation, we associate a transform, by computing the transform from a reference orientation to the orientation we wish to transform to. This is not exposed to the user.

Representing a transform

The transform consists of a permutation, parity flags, and a destination extent. The permutation is expressed as an array of indices, Rep. The parity flags are represented as an array of values that may be +1 or -1, Parity. The destination extent is an intPoint, that represents the dimension of the space of the image of the transform.

Applying a transform

To apply a transform f to a point p, we first apply the permutation:

q[0] = p[f.Rep[0]];
q[1] = p[f.Rep[1]];
q[2] = p[f.Rep[2]];
q[3] = p[f.Rep[3]];

Then we need to apply parity. This involves reversing the sign, and adding one less than the appropriate spatial dimension. For example, if the spatial width was 64, 0 needs to be mapped to 63 = (64 - 0) - 1, and 63 is mapped to 0 = (64-63) -1. Clearly, 0 cannot be mapped to 64, since 64 is outside the space. We illustrate the algorithm:

if(Parity[0]==-1) q[0] = extent[0] - q[0] -1;
if(Parity[1]==-1) q[1] = extent[1] - q[1] -1;
if(Parity[2]==-1) q[2] = extent[2] - q[2] -1;
if(Parity[3]==-1) q[3] = extent[3] - q[3] -1;

A simple example: suppose f.Rep = ( 2,1,0,3 ). We compute f(p) where p=(0,1,2,3). using the permutation rule above, we get

q = (p[f.Rep[0]], p[f.Rep[1]], p[f.Rep[2]], p[f.Rep[3]] ) = ( p[2], p[1], p[0], p[3] ) = (2,1,0,3)

Note that in general, the array Rep is always the same as the image under the permutation of the point (0,1,2,3).

Inverting a Transform

To invert a transform, first we need to invert the permutation. This is simple enough. We define an array arr by the rule:

arr[Rep[i]] = i
That the permutations are inverse follows by definition, since their composition is the identity permutation. To compute parity and extent, we need:

par[Rep[i]] = f.Parity[i];

and

e[i] = f.extent[Rep[i]];

We will discuss the formula for the parity computation further down.

Composing a Transform

First, we need a way to represent transforms. Each transform operates on the set of ordered 4-tuples in {A,B,C,D} x {1,-1} containing each of the symbols A,B,C,D. The permutations permute the elements of the tuple, and the parity transforms are products of generators, g0 ... g3, where gi changes the sign of the ith symbol. Note that we can view parity transforms as functions g: {0,1,2,3} -> {-1,1} , where g(i) is sign change of ith symbol under g.

The problem of composing a transform is as follows: we have created a normal form for a transform, T = r p , where r is a parity transformation, and p is a permutation. The problem is that when we compose these transforms, we get

r1p1 r2p2

and this is not in the desired normal form. To proceed further, we need to use properties of these transforms, to find a way of rewriting p1 r2 as r'p', for some reflection r' and permutation p'. Note that the expression is easily rewritten as

r1 (p1r2p1-1) p1 p2

So if we could convince ourselves that (p1r2p1-1) was a parity transform, and compute it, we would be in good shape. Fortunately, it is, and we can. We demonstrate this: start with the following ordered 4-tuple in {A,B,C,D} x {1,-1}

Ax1 Bx1 Cx1 Dx1

Take the transposition p that transposes the first two symbols, and take the parity transform g0. Applying a transposition p to the tuple, we get:

Bx1 Ax1 Cx1 Dx1
and applying a parity transform r, we get:
Bx(-1) Ax1 Cx1 Dx1

Applying the inverse of the transposition, we get:

Ax1 Bx(-1) Cx1 Dx1

Note that the result is equivalent to the parity transform p * r where p * r is defined as the transform r(i) = r (p(i)). This simple example generalises quite easily: in the case where we use g1 instead of g0, the outcome is entirely analogous. If we use g2 or g3 then the parity and permutation operations commute, and the result still holds. By symmetry, we could choose any parity transform, and any transposition, and get the same result. Since transpositions and simple parity flips generate all transformations, the result follows.

Moving back to the computer notation, this implies that the resulting values of Parity and Rep are:

Parity[i] = f.Parity[ g.Rep[i] ] * g.Parity[i]
Rep[i] = f.Rep [ g.Rep [i] ]

The extent is simple to compute, if both extents are given. In general, the extents should satisfy the invariant that

if f.Rep[i] is equal to g.Rep[j], then f.extent[i] is equal to g.extent[j]

Pseudo code to enforce this:


for (int i = 0; i < 4; ++i )
for (int j = 0; j < 4; ++j )
if (f.Rep[i]==g.Rep[j])
f.extent[i]=g.extent[j];

This turns out to be particularly useful in the case where we want to allow client code to leave unspecified the dimensions in a transform. This is often likely to be the case, and it doesn't make much sense to require dimensions for both transforms since one can be inferred from the other.

Revisiting Inversion

Using the conjugacy law, we can see how the parity computation in inversion works:


(rp)-1 = 
p-1 r-1 =
p-1 r =
(p-1 r  p ) p-1 

and we already know how to compute r' = p-1 r p , recall that it is given by r'(i) = r(p-1(i) ). An equivalent way to express this is: r'(p(i)) = r(i) .