What Are You Pointing At?

| 0 Comments

One of the nice properties of complex numbers is that all of the arithmetic operations that we are familiar with more or less behave as expected when applied to them, as we demonstrated with our ak.complex class[1].
We have also seen that whilst vectors are simply quantities having both direction and length, or magnitude, the easiest way to manipulate them is to define them as arrays of coordinates, or elements, and their arithmetic operations in terms of arithmetic operations upon those elements.
So what's to stop us using complex numbers for their elements?

Nothing but a complete lack of imagination!

Elementary Complexity

By way of an example, consider vector addition which, under this representation, results in a vector whose elements are simply the sums of the corresponding element of the original vectors
\[ \begin{pmatrix} x_0\\ x_1\\ x_2 \end{pmatrix} + \begin{pmatrix} y_0\\ y_1\\ y_2 \end{pmatrix} = \begin{pmatrix} x_0+y_0\\ x_1+y_1\\ x_2+y_2 \end{pmatrix} \]
Following the same rule with complex numbers, we trivially have
\[ \begin{pmatrix} a_0 + ib_0\\ a_1 + ib_1\\ a_2 + ib_2 \end{pmatrix} + \begin{pmatrix} c_0 + id_0\\ c_1 + id_1\\ c_2 + id_2 \end{pmatrix} = \begin{pmatrix} \left(a_0+c_0\right) + i\left(b_0+d_0\right)\\ \left(a_1+c_1\right) + i\left(b_1+d_1\right)\\ \left(a_2+c_2\right) + i\left(b_2+d_2\right) \end{pmatrix} \]
where \(i\) is the square root of minus one.

Given that I have previously justified the validity of \(i\) by claiming that if positive multiples of plus and minus one lie ahead and behind zero then positive multiples of plus and minus \(i\) lie to its left and its right, you might be asking yourself just what on Earth this actually means; how can these vectors have directions if each of their coordinates are, in actual fact, pairs of coordinates?
I'm afraid that I can't give a satisfactory answer to that question, but in my defence I don't believe that it actually needs one. The notion that mathematical entities must have some meaningful interpretation above and beyond being symbols that can be manipulated by some given set of rules is seen by many, if not almost all, mathematicians as a rather old fashioned one. That they can be used to solve real world problems is generally seen as something between an incredibly happy coincidence and an incredibly profound insight into the very nature of reality, or at least into our perception of it.

Philosophical musing aside, the behaviour of the arithmetic operators of complex vectors pretty much follow that of addition; replace the arithmetic operations on the elements with their complex equivalents. All but the magnitude, that is.
Recall that the magnitude of a vector is the square root of the sum of the squares of its elements. To see why this is inappropriate for complex vectors, consider
\[ \begin{pmatrix} 1\\ i \end{pmatrix} \]
The sum of the squares of its elements is
\[ 1^2 + i^2 = 1 + -1 = 0 \]
and so it would be a non-zero vector with zero magnitude, which is something we should really want to avoid. Thankfully we can do so by slightly changing the definition of the magnitude of a vector to the sum of the squares of the magnitudes of its elements. This has no effect upon the magnitude of a normal vector, but corrects this problem for complex vectors, as we can see by taking the sum of the squares of the magnitudes of the above vector's elements, both of which are units
\[ |1|^2 + |i|^2 = 1^2 + 1^2 = 2 \]
where the vertical bars represent the magnitude of the value between them.

The product of two vectors is equal to the sum of the products of their corresponding elements
\[ \begin{pmatrix} x_0\\ x_1\\ x_2 \end{pmatrix} \times \begin{pmatrix} y_0\\ y_1\\ y_2 \end{pmatrix} = x_0 \times y_0 + x_1 \times y_1 + x_2 \times y_2 \]
and so, for a vector \(\mathbf{x}\), we trivially have
\[ \mathbf{x} \times \mathbf{x} = |\mathbf{x}|^2 \]
Now the magnitude of any given complex number is also equal to the square root of the sum of the squares of its elements, being its real and imaginary parts, and if we multiply a complex number by its conjugate, in which the imaginary part is negated, we recover its square
\[ (x + iy) \times (x - iy) = x^2 - x \times iy + iy \times x - i^2y^2 = x^2 + y^2 = |x + iy|^2 \]
This means that we have a similar relation between the products and the magnitudes of complex vectors in
\[ \mathbf{z} \times \mathbf{z}^\ast = \mathbf{z}^\ast \times \mathbf{z} = |\mathbf{z}|^2 \]
where \(\mathbf{z}^\ast\) is the conjugate of \(\mathbf{z}\), having elements that are the conjugates of its.

A Complex Vector Class

As you might very well expect, the implementation of complex vectors is extremely similar to our ak.vector class[2], albeit replacing JavaScript's native arithmetic operators with our own overloaded arithmetic operators so as to support complex numbers.
Before we can get to them, however, we'll need a class to represent complex vectors, as provided by ak.complexVector given in listing 1.

Listing 1: ak.complexVector
ak.COMPLEX_VECTOR_T = 'ak.complexVector';

function ComplexVector(){}
ComplexVector.prototype = {
 TYPE: ak.COMPLEX_VECTOR_T, valueOf: function(){return ak.NaN;}
};

ak.complexVector = function() {
 var v     = new ComplexVector();
 var state = [];
 var arg0  = arguments[0];

 constructors[ak.nativeType(arg0)](state, arg0, arguments);

 v.dims = function()  {return state.length;};
 v.at   = function(i) {return state[Number(i)];};

 v.re = function() {return ak.vector(state.map(function(x){return x.re();}));};
 v.im = function() {return ak.vector(state.map(function(x){return x.im();}));};

 v.toArray  = function() {return state.slice(0);};
 v.toString = function() {return '['+state.toString()+']';};

 v.toExponential = function(d) {
  return '['+state.map(function(x){return x.toExponential(d);})+']';
 };

 v.toFixed = function(d) {
  return '['+state.map(function(x){return x.toFixed(d);})+']';
 };

 v.toPrecision = function(d) {
  return '['+state.map(function(x){return x.toPrecision(d);})+']';
 };

 return Object.freeze(v);
};

var constructors = {};

We follow our usual scheme of dispatching to a constructors object to initialise the state and provide property access and type conversion methods. Of the former, the dims method returns the number of dimensions of the vector and the at method provides access to its elements. Of the latter, the toArray method converts the vector into a JavaScript array and the toString, toExponential, toFixed and toPrecision methods convert it into strings of the various formats supported by JavaScript's numbers. All of this is pretty much exactly the same as we did for ak.vector.
Finally, given that this is a complex vector, we also provide methods to access its real and imaginary parts with re and im, just as we did for ak.complex, albeit returning ak.vector objects rather than numbers.

ak.complexVector Constructors

Just as was the case for ak.vector, the most natural way to construct ak.complexVector objects is with JavaScript arrays. In this case, however, we might want initialise them with arrays of ak.complex objects, or at least of values from which they may in turn be constructed, or with pairs of arrays representing their real and imaginary parts, as shown by listing 2.

Listing 2: ak.complexVector Array Constructors
constructors[ak.ARRAY_T] = function(state, arr, args) {
 var arg1 = args[1];

 state.length = arr.length;
 constructors[ak.ARRAY_T][ak.nativeType(arg1)](state, arr, arg1);
};

constructors[ak.ARRAY_T][ak.ARRAY_T] = function(state, re, im) {
 var n = state.length;
 var i;

 if(im.length!==n) {
  throw new Error('real and imaginary size mismatch in ak.complexVector');
 }

 for(i=0;i<n;++i) state[i] = ak.complex(Number(re[i]), Number(im[i]));
};

constructors[ak.ARRAY_T][ak.UNDEFINED_T] = function(state, arr) {
 var n = state.length;
 var i;

 for(i=0;i<n;++i) state[i] = ak.complex(arr[i]);
};

Here we simply dispatch to the ak.ARRAY_T constructor using the second argument to switch between the complex array and the real and imaginary arrays constructors, exploiting the fact that in the former case the second argument is undefined.

We take a similar approach when initialising an ak.complexVector with a number of dimensions and a complex number, or the real and imaginary parts of a complex number, to set its elements to, as illustrated by listing 3.

Listing 3: ak.complexVector Size Constructors
constructors[ak.NUMBER_T] = function(state, n, args) {
 var arg1 = args[1];

 state.length = n;
 constructors[ak.NUMBER_T][ak.nativeType(arg1)](state, arg1, args);
};

constructors[ak.NUMBER_T][ak.NUMBER_T] = function(state, arg1, args) {
 var arg2 = args[2];

 constructors[ak.NUMBER_T][ak.NUMBER_T][ak.nativeType(arg2)](state, arg1, arg2);
};

constructors[ak.NUMBER_T][ak.NUMBER_T][ak.NUMBER_T] = function(state, re, im) {
 var n = state.length;
 var c = ak.complex(re, im);
 var i;

 for(i=0;i<n;++i) state[i] = c;
};

constructors[ak.NUMBER_T][ak.NUMBER_T][ak.UNDEFINED_T] = function(state, val) {
 var n = state.length;
 var i;

 val = ak.complex(val);
 for(i=0;i<n;++i) state[i] = val;
};

constructors[ak.NUMBER_T][ak.OBJECT_T] = function(state, obj) {
 var n = state.length;
 var i;

 obj = ak.complex(obj);
 for(i=0;i<n;++i) state[i] = obj;
};

constructors[ak.NUMBER_T][ak.UNDEFINED_T] = function(state) {
 var n = state.length;
 var c = ak.complex(0);
 var i;

 for(i=0;i<n;++i) state[i] = c;
};

Note that in the last constructor, where we only specify the number of dimensions, the vector's elements are set to zero.

We also allow the elements to be set by a function of their position, as shown by listing 4.

Listing 4: ak.complexVector Position Constructor
constructors[ak.NUMBER_T][ak.FUNCTION_T] = function(state, func) {
 var n = state.length;
 var i;

 for(i=0;i<n;++i) state[i] = ak.complex(func(i));
};

Finally we allow construction with ak.complexVector like objects or with real and imaginary ak.vector like objects, as illustrated by listing 5.

Listing 5: ak.complexVector Vector Constructors
constructors[ak.OBJECT_T] = function(state, obj, args) {
 var arg1 = args[1];
 var n = obj.dims;
 var i;

 n = (ak.nativeType(n)===ak.FUNCTION_T) ? Number(n()) : Number(n);

 state.length = n;
 constructors[ak.OBJECT_T][ak.nativeType(arg1)](state, obj, arg1);
};

constructors[ak.OBJECT_T][ak.OBJECT_T] = function(state, re, im) {
 var n = state.length;
 var ni = im.dims;
 var i;

 ni = (ak.nativeType(ni)===ak.FUNCTION_T) ? Number(ni()) : Number(ni);
 if(ni!==n) throw new Error('real and imaginary size mismatch in ak.complexVector');

 for(i=0;i<n;++i) state[i] = ak.complex(Number(re.at(i)), Number(im.at(i)));
};

constructors[ak.OBJECT_T][ak.UNDEFINED_T] = function(state, obj) {
 var n = state.length;
 var i;

 for(i=0;i<n;++i) state[i] = ak.complex(obj.at(i));
};

ak.complexVector Overloads

As previously stated, arithmetic with complex vectors simply requires that we use complex arithmetic operations upon their elements instead of the usual arithmetic operations. From an implementation perspective this means using our overloaded arithmetic operations instead of JavaScript's native ones. Note that this also means that we get arithmetic operations between real and complex vectors for free, as demonstrated for addition by listing 6.

Listing 6: ak.complexVector Addition
function add(z0, z1) {
 var n, i;

 z0 = z0.toArray();
 n = z0.length;
 if(z1.dims()!==n) throw new Error('dimensions mismatch in ak.complexVector add');

 for(i=0;i<n;++i) z0[i] = ak.add(z0[i], z1.at(i));
 return ak.complexVector(z0);
}

ak.overload(ak.add, [ak.COMPLEX_VECTOR_T, ak.COMPLEX_VECTOR_T], add);
ak.overload(ak.add, [ak.VECTOR_T,         ak.COMPLEX_VECTOR_T], add);
ak.overload(ak.add, [ak.COMPLEX_VECTOR_T, ak.VECTOR_T],         add);

The conjugate of a complex vector is simply the vector whose elements are the complex conjugates of its elements and, given that we have implemented addition of real and complex vectors, it makes sense to define the conjugate for both of them, as is done in listing 7.

Listing 7: ak.complexVector And ak.Vector Conjugates
function conj(z) {
 return ak.complexVector(z.toArray().map(ak.conj));
}

function conjV(v) {
 return v;
}

ak.overload(ak.conj, ak.COMPLEX_VECTOR_T, conj);
ak.overload(ak.conj, ak.VECTOR_T,         conjV);

Note that the conjugate of a real vector is trivially the vector itself since it has no complex part.

The special treatment required for the magnitude of complex vectors is given in listing 8.

Listing 8: ak.complexVector Magnitude
function abs(z) {
 var s = 0;
 var n = z.dims();
 var i;

 for(i=0;i<n;++i) s += Math.pow(ak.abs(z.at(i)), 2);
 return Math.sqrt(s);
}

ak.overload(ak.abs, ak.COMPLEX_VECTOR_T, abs);

Here we are taking the square root of the sum of the squares of the magnitudes of the elements, as calculated by ak.abs.

The remaining arithmetic overloads are given in listing 9 and simply replace JavaScript arithmetic operators with our own overloaded arithmetic operators, as can be seen in ComplexVector.js

Listing 9: ak.complexVector Overloads
var JACOBI_DECOMPOSITION_T = 'ak.jacobiDecomposition';

if(!ak.stableDiv) {
 ak.stableDiv = function(x0, x1, e) {
  return ak.stableDiv[ak.type(x0)][ak.type(x1)](x0, x1, e)
 };
}

ak.overload(ak.neg, ak.COMPLEX_VECTOR_T, neg);

ak.overload(ak.dist,      [ak.COMPLEX_VECTOR_T, ak.COMPLEX_VECTOR_T],    dist);
ak.overload(ak.dist,      [ak.VECTOR_T,         ak.COMPLEX_VECTOR_T],    dist);
ak.overload(ak.dist,      [ak.COMPLEX_VECTOR_T, ak.VECTOR_T],            dist);
ak.overload(ak.div,       [ak.COMPLEX_VECTOR_T, ak.COMPLEX_T],           div);
ak.overload(ak.div,       [ak.COMPLEX_VECTOR_T, ak.NUMBER_T],            div);
ak.overload(ak.div,       [ak.VECTOR_T,         ak.COMPLEX_T],           div);
ak.overload(ak.div,       [ak.COMPLEX_VECTOR_T, ak.MATRIX_T],            divM);
ak.overload(ak.div,       [ak.COMPLEX_VECTOR_T, JACOBI_DECOMPOSITION_T], divD);
ak.overload(ak.eq,        [ak.COMPLEX_VECTOR_T, ak.COMPLEX_VECTOR_T],    eq);
ak.overload(ak.eq,        [ak.VECTOR_T,         ak.COMPLEX_VECTOR_T],    eq);
ak.overload(ak.eq,        [ak.COMPLEX_VECTOR_T, ak.VECTOR_T],            eq);
ak.overload(ak.mul,       [ak.COMPLEX_VECTOR_T, ak.COMPLEX_VECTOR_T],    mul);
ak.overload(ak.mul,       [ak.VECTOR_T,         ak.COMPLEX_VECTOR_T],    mul);
ak.overload(ak.mul,       [ak.COMPLEX_VECTOR_T, ak.VECTOR_T],            mul);
ak.overload(ak.mul,       [ak.COMPLEX_T,        ak.COMPLEX_VECTOR_T],    mulRZ);
ak.overload(ak.mul,       [ak.COMPLEX_VECTOR_T, ak.COMPLEX_T],           mulZR);
ak.overload(ak.mul,       [ak.NUMBER_T,         ak.COMPLEX_VECTOR_T],    mulRZ);
ak.overload(ak.mul,       [ak.COMPLEX_VECTOR_T, ak.NUMBER_T],            mulZR);
ak.overload(ak.mul,       [ak.COMPLEX_T,        ak.VECTOR_T],            mulRZ);
ak.overload(ak.mul,       [ak.VECTOR_T,         ak.COMPLEX_T],           mulZR);
ak.overload(ak.mul,       [ak.MATRIX_T,         ak.COMPLEX_VECTOR_T],    mulMZ);
ak.overload(ak.mul,       [ak.COMPLEX_VECTOR_T, ak.MATRIX_T],            mulZM);
ak.overload(ak.ne,        [ak.COMPLEX_VECTOR_T, ak.COMPLEX_VECTOR_T],    ne);
ak.overload(ak.ne,        [ak.VECTOR_T,         ak.COMPLEX_VECTOR_T],    ne);
ak.overload(ak.ne,        [ak.COMPLEX_VECTOR_T, ak.VECTOR_T],            ne);
ak.overload(ak.sub,       [ak.COMPLEX_VECTOR_T, ak.COMPLEX_VECTOR_T],    sub);
ak.overload(ak.sub,       [ak.VECTOR_T,         ak.COMPLEX_VECTOR_T],    sub);
ak.overload(ak.sub,       [ak.COMPLEX_VECTOR_T, ak.VECTOR_T],            sub);
ak.overload(ak.stableDiv, [ak.COMPLEX_VECTOR_T, JACOBI_DECOMPOSITION_T], divD);

As we did with ak.complex, we support arithmetic involving both real and complex values such as the multiplication of complex vectors by real numbers, vectors and matrices and the multiplication of real vectors by complex numbers. We also allow division by real matrices and the Jacobi decompositions of real symmetric matrices, as implemented by our ak.matrix[3] and ak.jacobiDecomposition[4] classes respectively, to solve matrix vector equations such as
\[ \mathbf{M} \times \mathbf{v} = \mathbf{c} \]
with
\[ \mathbf{v} = \mathbf{c} / \mathbf{M} \]
where \(\mathbf{M}\) is a real matrix, \(\mathbf{c}\) is a complex vector and \(\mathbf{v}\) is an unknown complex vector.

Note that the overloads involving ak.jacobiDecomposition are something of a departure from our usual scheme in that the type is defined locally rather than as a member of ak. We must resort to this slightly less maintainable approach because we won't always need the Jacobi decomposition when using complex vectors and, if we don't, we should want to avoid the cost of loading JacobiDecomposition.js in which ak.JACOBI_DECOMPOSITION_T is defined.
In those cases where we do need both complex vectors and the Jacobi decomposition, we need simply load both files explicitly. We can get away with this because we don't actually need ak.jacobiDecomposition to be defined until we actually use it, which will necessarily be after we've loaded the files.

Finally, you can experiment with these operations in program 1.

Program 1: Complex Vector Operations

References

[1] Our Imaginary Friends, www.thusspakeak.com, 2014.

[2] What's Our Vector, Victor?, www.thusspakeak.com, 2014.

[3] The Matrix Recoded, www.thusspakeak.com, 2014.

[4] Conquering The Eigen, www.thusspakeak.com, 2014.

Leave a comment