[ruby-talk:4595] Re: ANN: Clifford Algebras in Python
From:
Conrad Schneiker <schneik@...>
Date:
2000-08-27 01:10:02 UTC
List:
ruby-talk #4595
FYI -- in case someone is interested in doing a Ruby variant....
Even if not, the URLs cited below will be interesting for many of you
with an interest in math and physics.
In my non-expert opinion, Geometric calculus:multidimensional math =~=
Ruby:OOP
kern@caltech.edu wrote:
>
> Hello all three of you who would care about this module!
>
> At long last, I felt confident enough in my understanding of
> Geometric (Clifford) Algebras to code up an implementation in
> Python (gracefully assisted by NumPy) which I call pyCA.
> (Note: this is numerical, not symbolic.)
>
> The documentation strings are fairly complete (read: I was too
> lazy to write more documentation without a good reason). pyCA
> is a fairly general framework for doing work in geometric algebras.
> It's not especially fast for large numbers of computations or
> efficient for many multivectors. It also concedes to Python's
> operator precedence. I plan to add modules over the framework
> to implement a convenient shell and implement specific algebras
> and subalgebras (3D Euclidean, quaternions, STA, PGA, etc.).
>
> Well, have fun with the code below. I'll place it on the Starship
> once I get around to getting my account re-activated.
>
> Apologies for the line-breaks.
>
> ----------%<-------------------------------------------------------
> """\
> pyCA 0.5 -- Geometric Algebra for Python
> Robert Kern
> kern@caltech.edu
>
> This module implements geometric algebras (a.k.a. Clifford algebras).
> For the uninitiated, a geometric algebra is an algebra of vectors of
> given dimensions and signature. The familiar inner (dot) product and the
> outer product, a generalized relative of the three-dimensional cross
> product are unified in an invertible geometric product. Scalars, vectors,
> and higher-grade entities can be mixed freely and consistently in the
> form of mixed-grade multivectors.
>
> For more information, see the Cambridge Clifford Algebras website:
> http://www.mrao.cam.ac.uk/~clifford
> and David Hestenes' website:
> http://modelingnts.la.asu.edu/GC_R&D.html
>
> This implementation is inspired by Christian Perwass' XGAlib, a C++
> library for GA. Although there is practically no code shared with
> that LGPLed library, I have still obtained permission from Mr. Perwass
> to release this code into the public domain. In addition, I have
> obtained permission from James Lehmann to use a snippet of his code
> posted on comp.lang.python . Thus:
>
> I hereby release this code into the PUBLIC DOMAIN AS IS. There is no
> support, warranty, or guarantee. I will gladly accept comments, bug
> reports, and patches, however.
>
> Two classes, Layout and MultiVector, and several helper functions are
> provided to implement the algebras.
>
> Helper Functions
> ================
>
> Cl(p, q=0, names=None, firstIdx=0) -- generates the geometric algebra
> Cl_p,q and returns the appropriate Layout and dictionary of basis
> element names to their MultiVector instances.
>
> bases(layout) -- returns the dictionary of basis element names: instances
> as above
>
> randomMV(layout, grades=None) -- random MultiVector using layout. If
> grades is a sequence of integrers, only those grades will be non-zero
>
> pretty() -- set repr() to pretty-print MultiVectors
>
> ugly() -- set repr() to return eval-able strings
>
> eps(newEps) -- set _eps, the epsilon for comparisons and zero-testing
>
> Issues
> ======
>
> * Due to Python's order of operations, the bit operators & ^ << follow
> the normal arithmetic operators + - * /, so
>
> 1^e0 + 2^e1 != (1^e0) + (2^e1)
>
> as is probably intended. Additionally (comments on this pun will be
> summarily ignored),
>
> M = MultiVector(layout2D) # null multivector
> M << 1^e0 << 2^e1 == 10.0^e1 + 1.0^e01
> M == 1.0
> e0 == 2 + 1^e0
>
> as is definitely not intended. However,
>
> M = MultiVector(layout2D)
> M << (2^e0) << e1 << (3^e01) == M == 2^e0 + 1^e1 + 3^e01
> e0 == 1^e0
> e1 == 1^e1
> e01 == 1^e01
>
> A shell that reorders the operators and adds a few conveniences is in
> the works.
>
> * Since * is the inner product and the inner product with a scalar
> vanishes by definition, an expression like
>
> 1*e0 + 2*e1
>
> is null. Use the outer product or full geometric product, to
> multiply scalars with MultiVectors. This can cause problems if
> one has code that mixes Python numbers and MultiVectors. If the
> code multiplies two values that can each be either type without
> checking, one can run into problems as "1 & 2" has a very different
> result from the same multiplication with scalar MultiVectors.
>
> * If the LinearAlgebra module from the NumPy distribution is available,
> taking the inverse of a MultiVector will use a method proposed by
> Christian Perwass that involves the solution of a matrix equation.
> A description of that method follows:
>
> Representing multivectors as 2**dims vectors (in the matrix sense),
> we can carry out the geometric product with a multiplication table.
> In pseudo-tensorish language (using summation notation):
>
> m_i * g_ijk * n_k = v_j
>
> Suppose m_i are known (M is the vector we are taking the inverse of),
> the g_ijk have been computed for this algebra, and v_j = 1 if the
> j'th element is the scalar element and 0 otherwise, we can compute the
> dot product m_i * g_ijk. This yields a rank-2 matrix. We can
> then use well-established computational linear algebra techniques
> to solve this matrix equation for n_k. The laInv method does precisely
> that.
>
> The usual, analytic, method for computing inverses [M**-1 = ~M/(M&~M) iff
> M&~M == |M|**2] fails for those multivectors where M&~M is not a scalar.
> It is only used if the inv method is manually set to point to normalInv
> or the module LinearAlgebra from NumPy is not available.
>
> My testing suggests that laInv works. In the cases where normalInv works,
> laInv returns the same result (within _eps). In all cases,
> M & M.laInv() == 1.0 (within _eps). Use whichever you feel comfortable
> with.
>
> * The basis vectors of any algebra will be orthonormal unless you supply
> your own multiplication tables (which you are free to do after
> the Layout constructor is called). A derived class is in preparation
> that will calculate these tables for you (and include methods for
> generating reciprocal bases and the like). I would greatly appreciate
> help on the algorithm to generate these tables from an arbitrary
> bilinear form.
>
> * No care is taken to preserve the typecode of the arrays. The purpose
> of this module is pedagogical. If your application requires so many
> multivectors that storage becomes important, the class structure here
> is unsuitable for you anyways. Instead, use the algorithms from this
> module and implement application-specific data structures.
>
> * Conversely, explicit typecasting is rare. MultiVectors will have
> integer coefficients if you instantiate them that way. Dividing them
> by Python integers will have the same consequences as normal integer
> division. Public outcry will convince me to add the explicit casts
> if this becomes a problem.
>
> * The way I make variables of the bases is by .update()'ing the global
> (or local for functions) namespace dictionary. I use a convenient
> function for that in the global case. I have not included it here
> as I don't want to canonize something that is, well, let's face it,
> evil. But it's a venial kind of evil, so do it anyways.
>
> * Integration with PyOpenGL for some kick-ass demonstrations are left
> as an exercise for the reader.
>
> Happy hacking!
> Robert Kern
> kern@caltech.edu
> """
>
> import Numeric
> import types
> import operator
> import math
>
> from Numeric import pi, e
>
> try:
> import LinearAlgebra
> LA = LinearAlgebra
> except ImportError:
> LA = None
>
> _eps = 1e-15 # float epsilon for float comparisons
> _pretty = 0 # pretty-print global
>
> class Layout:
> """\
> Layout stores information regarding the geometric algebra itself and
> the internal representation of multivectors. It is constructed like this:
>
> Layout(signature, bladeList, firstIdx=0, names=None)
>
> The arguments to be passed are interpreted as follows:
>
> signature -- the signature of the vector space. This should be
> a list of positive and negative numbers where the sign determines
> the sign of the inner product of the corresponding vector with itself.
> The values are irrelevant except for sign. This list also determines
> the dimensionality of the vectors. Signatures with zeroes are not
> permitted at this time.
>
> Examples:
> signature = [+1, -1, -1, -1] # Hestenes', et al. Space-Time Algebra
> signature = [+1, +1, +1] # 3-D Euclidean signature
>
> bladeList -- list of tuples corresponding to the blades in the whole
> algebra. This list determines the order of coefficients in the
> internal representation of multivectors. The entry for the scalar
> must be an empty tuple, and the entries for grade-1 vectors must be
> singleton tuples. Remember, the length of the list will be 2**dims.
>
> Example:
> bladeList = [(), (0,), (1,), (0,1)] # 2-D
>
> firstIdx -- the index of the first vector. That is, some systems number
> the base vectors starting with 0, some with 1. Choose by passing
> the correct number as firstIdx. 0 is the default.
>
> names -- list of names of each blade. When pretty-printing multivectors,
> use these symbols for the blades. names should be in the same order
> as bladeList. You may use an empty string for scalars. By default,
> the name for each non-scalar blade is 'e' plus the indices of the blade
> as given in bladeList.
>
> Example:
> names = ['', 's0', 's1', 'i'] # 2-D
>
>
> Layout's Members:
>
> dims -- dimensionality of vectors (== len(signature))
>
> sig -- normalized signature (i.e. all values are +1 or -1)
>
> firstIdx -- starting point for vector indices
>
> bladeList -- list of blades
>
> gradeList -- corresponding list of the grades of each blade
>
> gaDims -- 2**dims
>
> names -- pretty-printing symbols for the blades
>
> even -- dictionary of even permutations of blades to the canonical blades
>
> odd -- dictionary of odd permutations of blades to the canonical blades
>
> gmt -- multiplication table for geometric product [1]
>
> imt -- multiplication table for inner product [1]
>
> omt -- multiplication table for outer product [1]
>
> [1] The multiplication tables are NumPy arrays of rank 3 with indices like
> the tensor g_ijk discussed above.
> """
>
> def __init__(self, sig, bladeList, firstIdx=0, names=None):
> self.dims = len(sig)
> self.sig = Numeric.divide(sig,
> Numeric.absolute(sig)).astype(Numeric.Int)
> self.firstIdx = firstIdx
>
> self.bladeList = map(tuple, bladeList)
> self._checkList()
>
> self.gaDims = len(self.bladeList)
> self.gradeList = map(len, self.bladeList)
>
> if names is None:
> e = 'e'
> self.names = []
>
> for i in range(self.gaDims):
> if self.gradeList[i] > 1:
> self.names.append(e + str(Numeric.add.reduce(map(str, self.bladeList[i]))))
> elif self.gradeList[i] == 1:
> self.names.append(e + str(self.bladeList[i][0]))
> else:
> self.names.append('')
>
> elif len(names) == self.gaDims:
> self.names = names
> else:
> raise ValueError, "names list of length %i needs to be of length %i" % \
> (len(names), self.gaDims)
>
> self._genEvenOdd()
> self._genTables()
>
> def __repr__(self):
> s = "Layout(%s, %s, firstIdx=%s, names=%s)" % (list(self.sig),
> self.bladeList,
> self.firstIdx,
> self.names)
> return s
>
> def _sign(self, seq, orig):
> """Determine {even,odd}-ness of permutation seq or orig.
>
> Returns 1 if even; -1 if odd.
> """
>
> sign = 1
> seq = list(seq)
>
> for i in range(len(seq)):
> if seq[i] != orig[i]:
> j = seq.index(orig[i])
> sign = -sign
> seq[i], seq[j] = seq[j], seq[i]
> return sign
>
> def _containsDups(self, list):
> "Checks if list contains duplicates."
>
> for k in list:
> if list.count(k) != 1:
> return 1
> return 0
>
> def _genEvenOdd(self):
> "Create mappings of even and odd permutations to their canonical blades."
>
> self.even = {}
> self.odd = {}
>
> class NoMorePermutations(StandardError):
> pass
>
> for i in range(self.gaDims):
> blade = self.bladeList[i]
> grade = self.gradeList[i]
>
> if grade in (0, 1):
> # handled easy cases
> self.even[blade] = blade
> continue
> elif grade == 2:
> # another easy case
> self.even[blade] = blade
> self.odd[(blade[1], blade[0])] = blade
> continue
> else:
> # general case, lifted from Chooser.py released on
> # comp.lang.python by James Lehmann.
> idx = range(grade)
>
> try:
> for i in range(Numeric.multiply.reduce(range(1, grade+1))):
> # grade! permutations
>
> done = 0
> j = grade - 1
>
> while not done:
> idx[j] = idx[j] + 1
>
> while idx[j] == grade:
> idx[j] = 0
> j = j - 1
> idx[j] = idx[j] + 1
>
> if j == -1:
> raise NoMorePermutations()
> j = grade - 1
>
> if not self._containsDups(idx):
> done = 1
>
> perm = []
>
> for k in idx:
> perm.append(blade[k])
>
> perm = tuple(perm)
>
> if self._sign(perm, blade) == 1:
> self.even[perm] = blade
> else:
> self.odd[perm] = blade
>
> except NoMorePermutations:
> pass
>
> self.even[blade] = blade
>
> def _checkList(self):
> "Ensure validity of arguments."
>
> # check for uniqueness
> for blade in self.bladeList:
> if self.bladeList.count(blade) != 1:
> raise ValueError, "blades not unique"
>
> # check for right dimensionality
> if len(self.bladeList) != 2**self.dims:
> raise ValueError, "incorrect number of blades"
>
> # check for valid ranges of indices
> valid = range(self.firstIdx, self.firstIdx+self.dims)
> try:
> for blade in self.bladeList:
> for idx in blade:
> if (idx not in valid) or (list(blade).count(idx) != 1):
> raise ValueError
> except (ValueError, TypeError):
> raise ValueError, "invalid bladeList; must be a list of tuples"
>
> def _gmtElement(self, a, b):
> "Returns the element of the geometric multiplication table given blades a, b."
>
> mul = 1 # multiplier
>
> newBlade = list(a) + list(b)
>
> unique = 0
> while unique == 0:
> for i in range(len(newBlade)):
> if newBlade.count(newBlade[i]) != 1:
> delta = newBlade[i+1:].index(newBlade[i])
> eo = 1 - 2*(delta % 2)
> # eo == 1 if the elements are an even number of flips away
> # eo == -1 otherwise
>
> del newBlade[i+delta+1]
> del newBlade[i]
> mul = eo * mul * self.sig[a[i] - self.firstIdx]
> break
> else:
> unique = 1
>
> newBlade = tuple(newBlade)
>
> if newBlade in self.bladeList:
> idx = self.bladeList.index(newBlade)
> # index of the product blade in the bladeList
> elif newBlade in self.even.keys():
> # even permutation
> idx = self.bladeList.index(self.even[newBlade])
> else:
> # odd permutation
> idx = self.bladeList.index(self.odd[newBlade])
> mul = -mul
>
> element = Numeric.zeros((self.gaDims,), Numeric.Int)
> element[idx] = mul
>
> return element, idx
>
> def _genTables(self):
> "Generate the multiplication tables."
>
> # geometric multiplication table
> gmt = Numeric.zeros((self.gaDims, self.gaDims, self.gaDims),
> Numeric.Int)
> # inner product table
> imt = Numeric.zeros((self.gaDims, self.gaDims, self.gaDims),
> Numeric.Int)
>
> # outer product table
> omt = Numeric.zeros((self.gaDims, self.gaDims, self.gaDims),
> Numeric.Int)
>
> for i in range(self.gaDims):
> for j in range(self.gaDims):
> gmt[i,:,j], idx = self._gmtElement(list(self.bladeList[i]),
> list(self.bladeList[j]))
>
> if self.gradeList[idx] == abs(self.gradeList[i] - self.gradeList[j]) and \
> self.gradeList[i] != 0 and self.gradeList[j] != 0:
>
> # A_r . B_s = <A_r B_s>_|r-s|
> # if r,s != 0
> imt[i,:,j] = gmt[i,:,j]
>
> if self.gradeList[idx] == (self.gradeList[i] + self.gradeList[j]):
>
> # A_r ^ B_s = <A_r B_s>_|r+s|
> omt[i,:,j] = gmt[i,:,j]
>
> self.gmt = gmt
> self.imt = imt
> self.omt = omt
>
> def pseudoScalar(self):
> "Returns a MultiVector that is the pseudoscalar of this space."
>
> psIdx = self.gradeList.index(self.dims)
> # pick out out blade with grade equal to dims
>
> pScalar = MultiVector(self)
> pScalar.value[psIdx] = 1
>
> return pScalar
>
> def invPS(self):
> "Returns the inverse of the pseudoscalar of the algebra."
>
> ps = self.pseudoScalar()
>
> return ps.inv()
>
> class MultiVector:
> """\
> The elements of the algebras, the multivectors, are implemented in the
> MultiVector class. It has the following constructor:
>
> MultiVector(layout, value=None)
>
> The meaning of the arguments is as follows:
>
> layout -- instance of Layout
>
> value -- a sequence, of length layout.gaDims, of coefficients of the base
> blades
>
> MultiVector's Members
> =====================
>
> layout -- instance of Layout
>
> value -- a NumPy array, of length layout.gaDims, of coefficients of the base
> blades
>
> MultiVector's Public Methods
> ============================
>
> __and__, __rand__ -- geometric product. M & N
>
> __xor__, __rxor__ -- outer product. M ^ N
>
> __mul__, __rmul__ -- inner product. M * N
>
> __add__, __radd__ -- addition. M + N
>
> __sub__, __rsub__ -- subtraction. M - N
>
> __div__, __rdiv__ -- division. M / N <==> M & (N.inv())
>
> __pow__ -- exponentiation of a multivector by an integer. M ** n
>
> __rpow__ -- exponentiation of a scalar by a multivector. x ** M
>
> __lshift__ -- in-place addition. M << N
> Adds N to M, modifying M and returning M.
>
> mag2() -- magnitude squared. (M & ~M)[()] == |M|**2
>
> __abs__ -- magnitude. abs(M) == |M|
>
> adjoint(), __invert__ -- adjoint/reversion. ~M
>
> __int__, __long__, __float__ -- cast to scalar Python number iff only the
> multivector's scalar coefficient is non-zero
>
>
> __getitem__, __setitem__ -- get/set coefficient to/from a Python number.
> Item can be an index into self.value or a
> blade (or a permutation of a blade).
>
> __delitem__ -- sets coefficient of item (same rules as above) to 0
>
> __getslice__ -- returns a new MultiVector with only the selected slice
> non-zero
>
> __setslice__ -- sets the slice of self.value to a value or values
>
> __delslice__ -- sets the slice of self.value to 0
>
> __call__ -- projects multivector onto the specified grade. M(grade)
>
> __cmp__ -- returns 0 if the multivectors' coefficients are each equal to
> within epsilon. Otherwise, returns a well-defined, but
> meaningless, ordering.
>
> __str__ -- pretty-printed representation
>
> __repr__ -- ugly, but eval-able by default, but pretty if global variable
> _pretty is true
>
> isScalar() -- returns 1 if multivector is a scalar
>
> isBlade() -- returns 1 if multivector is a blade
>
> grades() -- returns the grades contained in the multivector
>
> normalInv() -- multivector inverse iff (M & ~M) == |M|**2. ~M/|M|**2
>
> laInv() -- experimental, more general inverse using computational
> linear algebra to solve for the inverse.
>
> inv() -- one of normalInv or laInv. By default, inv is laInv if the
> LinearAlgebra module is available, normalInv otherwise.
>
> dual() -- returns the dual of the multivector
>
> commutator(other) -- returns the commutator of two multivectors
>
> join(other) -- returns the join of two blades if it exists
>
> meet(other, subspace=None) -- returns the meet of two blades wrt some
> subspace which defaults to the pseudoscalar
>
> gradeInvol() -- returns the grade involution of the multivector
>
> conjugate() -- returns the Clifford conjugate of the multivector
>
> """
>
> def __init__(self, layout, value=None):
> """Constructor.
>
> MultiVector(layout, value=None) --> MultiVector
> """
>
> self.layout = layout
>
> if value is None:
> self.value = Numeric.zeros((self.layout.gaDims,), Numeric.Float)
> else:
> self.value = Numeric.array(value)
> if self.value.shape != (self.layout.gaDims,):
> raise ValueError, "value must be a sequence of length %s" % self.layout.gaDims
>
> def _checkOther(self, other, coerce=1):
> """Ensure that the other argument has the same Layout or coerce value if
> necessary/requested.
>
> _checkOther(other, coerce=1) --> newOther, isMultiVector
> """
>
> if type(other) in (types.IntType, types.FloatType, types.LongType):
> if coerce:
> # numeric scalar
> newOther = MultiVector(self.layout)
> newOther[()] = other
> return newOther, 1
> else:
> return other, 0
>
> elif isinstance(other, self.__class__) and \
> other.layout is not self.layout:
> raise ValueError, "cannot operate on MultiVectors with different Layouts"
>
> return other, 1
>
> ## numeric special methods
> # binary
>
> def __and__(self, other):
> """Geometric product
>
> M & N --> MN
> __and__(other) --> MultiVector
> """
>
> other, mv = self._checkOther(other, coerce=0)
>
> if mv:
> newValue = Numeric.dot(self.value, Numeric.dot(self.layout.gmt,
> other.value))
> else:
> newValue = other * self.value
>
> return MultiVector(self.layout, newValue)
>
> def __rand__(self, other):
> """Right-hand geometric product
>
> N & M --> NM
> __rand__(other) --> MultiVector
> """
>
> other, mv = self._checkOther(other, coerce=0)
>
> if mv:
> newValue = Numeric.dot(other.value, Numeric.dot(self.layout.gmt,
> self.value))
> else:
> newValue = other * self.value
>
> return MultiVector(self.layout, newValue)
>
> def __xor__(self, other):
> """Outer product
>
> M ^ N
> __xor__(other) --> MultiVector
> """
>
> other, mv = self._checkOther(other, coerce=0)
>
> if mv:
> newValue = Numeric.dot(self.value, Numeric.dot(self.layout.omt,
> other.value))
> else:
> newValue = other * self.value
>
> return MultiVector(self.layout, newValue)
>
> def __rxor__(self, other):
> """Right-hand outer product
>
> N ^ M
> __rxor__(other) --> MultiVector
> """
>
> other, mv = self._checkOther(other, coerce=0)
>
> if mv:
> newValue = Numeric.dot(other.value, Numeric.dot(self.layout.omt,
> self.value))
> else:
> newValue = other * self.value
>
> return MultiVector(self.layout, newValue)
>
> def __mul__(self, other):
> """Inner product
>
> M * N
> __mul__(other) --> MultiVector
> """
>
> other, mv = self._checkOther(other)
>
> if mv:
> newValue = Numeric.dot(self.value, Numeric.dot(self.layout.imt,
> other.value))
> else:
> return MultiVector(self.layout) # l * M = M * l = 0 for scalar l
>
> return MultiVector(self.layout, newValue)
>
> __rmul__ = __mul__
>
> def __add__(self, other):
> """Addition
>
> M + N
> __add__(other) --> MultiVector
> """
>
> other, mv = self._checkOther(other)
> newValue = self.value + other.value
>
> return MultiVector(self.layout, newValue)
>
> __radd__ = __add__
>
> def __sub__(self, other):
> """Subtraction
>
> M - N
> __sub__(other) --> MultiVector
> """
>
> other, mv = self._checkOther(other)
> newValue = self.value - other.value
>
> return MultiVector(self.layout, newValue)
>
> def __rsub__(self, other):
> """Right-hand subtraction
>
> N - M
> __rsub__(other) --> MultiVector
> """
>
> other, mv = self._checkOther(other)
> newValue = other.value - self.value
>
> return MultiVector(self.layout, newValue)
>
> def __div__(self, other):
> """Division
> -1
> M / N --> M & N
> __div__(other) --> MultiVector
> """
>
> other, mv = self._checkOther(other, coerce=0)
>
> if mv:
> return self & other.inv()
> else:
> newValue = self.value / other
> return MultiVector(self.layout, newValue)
>
> def __rdiv__(self, other):
> """Right-hand division
> -1
> N / M --> N & M
> __rdiv__(other) --> MultiVector
> """
>
> other, mv = self._checkOther(other)
>
> return other & self.inv()
>
> def __pow__(self, other):
> """Exponentiation of a multivector by an integer
> n
> M ** n --> M
> __pow__(other) --> MultiVector
> """
>
> if type(other) not in (types.IntType, types.FloatType):
> raise ValueError, "exponent must be a Python Int or Float"
>
> if abs(round(other) - other) > _eps:
> raise ValueError, "exponent must have no fractional part"
>
> other = int(round(other))
>
> newMV = MultiVector(self.layout, list(self.value))
>
> for i in range(1, other):
> newMV = newMV & self
>
> return newMV
>
> def __rpow__(self, other):
> """Exponentiation of a real by a multivector
> M
> r**M --> r
> __rpow__(other) --> MultiVector
> """
>
> # check that other is a Python number, not a MultiVector
>
> if type(other) not in (types.IntType, types.FloatType, types.LongType):
> raise ValueError, "base must be a Python number"
>
> intMV = math.log(other) & self
> # pow(x, y) == exp(y & log(x))
>
> newMV = MultiVector(self.layout) # null
>
> nextTerm = MultiVector(self.layout)
> nextTerm[()] = 1 # order 0 term of exp(x) Taylor expansion
>
> n = 1
>
> while nextTerm != 0:
> # iterate until the added term is within _eps of 0
> newMV << nextTerm
> nextTerm = nextTerm & intMV / n
> n = n + 1
> else:
> # squeeze out that extra little bit of accuracy
> newMV << nextTerm
>
> return newMV
>
> def __lshift__(self, other):
> """In-place addition
>
> M << N --> M + N
> __lshift__(other) --> MultiVector
> """
>
> other, mv = self._checkOther(other)
>
> self.value = self.value + other.value
>
> return self
>
>
> # unary
>
> def __neg__(self):
> """Negation
>
> -M
> __neg__() --> MultiVector
> """
>
> newValue = -self.value
>
> return MultiVector(self.layout, newValue)
>
> def __pos__(self):
> """Positive (just a copy)
>
> +M
> __pos__(self) --> MultiVector
> """
>
> newValue = self.value + 0 # copy
>
> return MultiVector(self.layout, newValue)
>
> def mag2(self):
> """Magnitude (modulus) squared
> 2
> |M|
> mag2() --> PyFloat | PyInt
> """
>
> return (~self & self)[()]
>
> def __abs__(self):
> """Magnitude (modulus)
>
> abs(M) --> |M|
> __abs__() --> PyFloat
> """
>
> return Numeric.sqrt(self.mag2())
>
> def adjoint(self):
> """Adjoint / reversion
> _
> ~M --> M (any one of several conflicting notations)
> ~(N & M) --> ~M & ~N
> adjoint() --> MultiVector
> """
> # The multivector created by reversing all multiplications
>
> newMV = MultiVector(self.layout)
>
> grades = Numeric.array(self.layout.gradeList)
> signs = Numeric.power(-1, grades*(grades-1)/2)
>
> newMV.value = signs * self.value
>
> return newMV
>
> __invert__ = adjoint
>
> # builtin
>
> def __int__(self):
> """Coerce to an integer iff scalar.
>
> int(M)
> __int__() --> PyInt
> """
>
> return int(self.__float__())
>
> def __long__(self):
> """Coerce to a long iff scalar.
>
> long(M)
> __long__() --> PyLong
> """
>
> return long(self.__float__())
>
> def __float__(self):
> """"Coerce to a float iff scalar.
>
> float(M)
> __float__() --> PyFloat
> """
>
> if self.isScalar():
> return float(self[()])
> else:
> raise ValueError, "non-scalar coefficients are non-zero"
>
>
> ## sequence special methods
>
> def __len__(self):
> """Returns length of value array.
>
> len(M)
> __len__() --> PyInt
> """
>
> return self.layout.gaDims
>
> def __getitem__(self, key):
> """If key is a blade tuple (e.g. (0,1) or (1,3)), then return
> the (real) value of that blade's coefficient.
> Otherwise, treat key as an index into the list of coefficients.
>
> M[blade] --> PyFloat | PyInt
> M[index] --> PyFloat | PyInt
> __getitem__(key) --> PyFloat | PyInt
> """
>
> if key in self.layout.bladeList:
> return self.value[self.layout.bladeList.index(key)]
> elif key in self.layout.even.keys():
> return self.value[self.layout.bladeList.index(self.layout.even[key])]
> elif key in self.layout.odd.keys():
> return -self.value[self.layout.bladeList.index(self.layout.odd[key])]
> else:
> return self.value[key]
>
> def __setitem__(self, key, value):
> """If key is a blade tuple (e.g. (0,1) or (1,3)), then set
> the (real) value of that blade's coeficient.
> Otherwise treat key as an index into the list of coefficients.
>
> M[blade] = PyFloat | PyInt
> M[index] = PyFloat | PyInt
> __setitem__(key, value)
> """
>
> if key in self.layout.bladeList:
> self.value[self.layout.bladeList.index(key)] = value
> elif key in self.layout.even.keys():
> self.value[self.layout.bladeList.index(self.layout.even[key])] = value
> elif key in self.layout.odd.keys():
> self.value[self.layout.bladeList.index(self.layout.odd[key])] = -value
> else:
> self.value[key] = value
>
> def __delitem__(self, key):
> """Set the selected coefficient to 0.
>
> del M[blade]
> del M[index]
> __delitem__(key)
> """
>
> if key in self.layout.bladeList:
> self.value[self.layout.bladeList.index(key)] = 0
> elif key in self.layout.even.keys():
> self.value[self.layout.bladeList.index(self.layout.even[key])] = 0
> elif key in self.layout.odd.keys():
> self.value[self.layout.bladeList.index(self.layout.odd[key])] = 0
> else:
> self.value[key] = 0
>
> def __getslice__(self, i, j):
> """Return a copy with only the slice non-zero.
>
> M[i:j]
> __getslice__(i, j) --> MultiVector
> """
>
> newMV = MultiVector(self.layout)
> newMV.value[i:j] = self.value[i:j]
>
> return newMV
>
> def __setslice__(self, i, j, sequence):
> """Paste a sequence into coefficients array.
>
> M[i:j] = sequence
> __setslice__(i, j, sequence)
> """
>
> self.value[i:j] = sequence
>
> def __delslice__(self, i, j):
> """Set slice to zeros.
>
> del M[i:j]
> __delslice__(i, j)
> """
>
> self.value[i:j] = 0
>
> ## grade projection
>
> def __call__(self, grade):
> """Return a new multi-vector projected onto the specified grade.
>
> M(grade) --> <M>
> grade
> __call__(grade) --> MultiVector
> """
>
> if grade not in self.layout.gradeList:
> raise ValueError, "algebra does not have grade %s" % grade
>
> if type(grade) is not types.IntType:
> raise ValueError, "grade must be an integer"
>
> mask = Numeric.equal(grade, self.layout.gradeList)
>
> newValue = Numeric.multiply(mask, self.value)
>
> return MultiVector(self.layout, newValue)
>
> ## fundamental special methods
>
> def __str__(self):
> """Return pretty-printed representation.
>
> str(M)
> __str__() --> PyString
> """
>
> s = ''
>
> for i in range(self.layout.gaDims):
> # if we have nothing yet, don't use + and - as operators but
> # use - as an unary prefix if necessary
> if s:
> seps = (' + ', ' - ')
> else:
> seps = ('', '-')
>
> if self.layout.gradeList[i] == 0:
> # scalar
> if abs(self.value[i]) >= _eps:
> if self.value[i] > 0:
> s = '%s%s%s' % (s, seps[0], self.value[i])
> else:
> s = '%s%s%s' % (s, seps[1], -self.value[i])
>
> else:
> if abs(self.value[i]) >= _eps:
> # not a scalar
> if self.value[i] > 0:
> s = '%s%s%s^%s' % (s, seps[0], self.value[i],
> self.layout.names[i])
> else:
> s = '%s%s%s^%s' % (s, seps[1], -self.value[i],
> self.layout.names[i])
> if s:
> # non-zero
> return s
> else:
> # return scalar 0
> return '0'
>
> def __repr__(self):
> """Return eval-able representation if global _pretty is false.
> Otherwise, return str(self).
>
> repr(M)
> __repr__() --> PyString
> """
>
> if _pretty:
> return self.__str__()
>
> s = "MultiVector(%s, value=%s)" % \
> (repr(self.layout), list(self.value))
> return s
>
> def __nonzero__(self):
> """Instance is nonzero iff at least one of the coefficients
> is nonzero.
>
> __nonzero() --> Boolean
> """
>
> nonzeroes = Numeric.greater(Numeric.absolute(self.value), _eps)
>
> if nonzeroes:
> return 1
> else:
> return 0
>
> def __cmp__(self, other):
> """Compares two multivectors.
>
> This is mostly defined for equality testing (within epsilon).
> In the case that the two multivectors have different Layouts,
> we will raise an error. In the case that they are not equal,
> we will compare the tuple represenations of the coefficients
> lists just so as to return something valid. Therefore,
> inequalities are well-nigh meaningless (since they are
> meaningless for multivectors while equality is meaningful).
> Oh, how I wish for rich comparisons.
>
> M == N
> __cmp__(other) --> -1|0|1
> """
>
> other, mv = self._checkOther(other)
>
> if Numeric.alltrue(Numeric.less(Numeric.absolute( \
> self.value - other.value), _eps)):
> # equal within epsilon
> return 0
> else:
> return cmp(tuple(self.value), tuple(other.value))
>
> ## Geometric Algebraic functions
>
> def isScalar(self):
> """Returns true iff self is a scalar.
>
> isScalar() --> Boolean
> """
>
> indices = range(self.layout.gaDims)
> indices.remove(self.layout.gradeList.index(0))
>
> for i in indices:
> if abs(self.value[i]) < _eps:
> continue
> else:
> return 0
>
> return 1
>
> def isBlade(self):
> """Returns true if multivector is a blade.
>
> isBlade() --> Boolean
> """
>
> grade = None
>
> for i in range(self.layout.gaDims):
> if abs(self.value[i]) > _eps:
> if grade is None:
> grade = self.layout.gradeList[i]
> elif self.layout.gradeList[i] != grade:
> return 0
>
> return 1
>
> def grades(self):
> """Return the grades contained in the multivector.
>
> grades() --> PyInt"""
>
> grades = []
>
> for i in range(self.layout.gaDims):
> if abs(self.value[i]) > _eps and \
> self.layout.gradeList[i] not in grades:
> grades.append(self.layout.gradeList[i])
>
> return grades
>
> def laInv(self):
> """Return inverse using a computational linear algebra method
> proposed by Christian Perwass.
> -1
> M
> laInv() --> MultiVector
> """
>
> identity = Numeric.zeros((self.layout.gaDims,))
> identity[self.layout.gradeList.index(0)] = 1
>
> intermed = Numeric.dot(self.value, self.layout.gmt)
> intermed = Numeric.transpose(intermed)
>
> if abs(LA.determinant(intermed)) < _eps:
> raise ValueError, "multivector has no inverse"
>
> sol = LA.solve_linear_equations(intermed, identity)
>
> return MultiVector(self.layout, sol)
>
>
> def normalInv(self):
> """Returns the inverse of itself if M&~M == |M|**2.
> -1
> M = ~M / (M & ~M)
> normalInv() --> MultiVector
> """
>
> Madjoint = ~self
> MadjointM = (Madjoint & self)
>
> if MadjointM.isScalar() and abs(MadjointM[()]) > _eps:
> # inverse exists
> return Madjoint / MadjointM[()]
> else:
> raise ValueError, "no inverse exists for this multivector"
> if LA:
> inv = laInv
> else:
> inv = normalInv
>
> def dual(self):
> """Returns the dual of the multivector.
>
> ~ -1
> M = M * I where I is the pseudoscalar.
> dual() --> MultiVector
> """
>
> return self * self.layout.invPS()
>
> def commutator(self, other):
> """Returns the commutator product of two multivectors.
>
> [M, N] = M X N = (M&N - N&M)/2
> commutator(other) --> MultiVector
> """
>
> return ((self & other) - (other & self)) / 2
>
> def join(self, other):
> """Returns the join of two blades.
>
> J = M ^ N iff M, N are blades and M^N != 0
> join(other) --> MultiVector
> """
>
> other, mv = self._checkOther(other)
>
> if self.isBlade() and other.isBlade():
> J = self ^ other
>
> if J:
> return J
> else:
> raise ValueError, "join does not exist, blades share common factors"
> else:
> raise ValueError, "not blades"
>
> def meet(self, other, subspace=None):
> """Returns the meet of an r-blade and an s-blade iff r+s >= dims.
>
> Computation is done with respect to a subspace that defaults to
> the pseudo scalar if none is given.
> -1
> M \/i N = (Mi ) * N
> meet(other, subspace=None) --> MultiVector
> """
>
> other, mv = self._checkOther(other)
>
> r = self.grades()
> s = other.grades()
>
> if len(r) > 1 or len(s) > 1:
> raise ValueError, "not blades"
>
> r = r[0]
> s = s[0]
>
> if subspace is None:
> subspace = self.layout.pseudoScalar()
>
> dims = subspace.grades()[0]
>
> if r + s < dims:
> raise ValueError, "r+s < dims; meet not defined"
> elif r + s == dims:
> # special case; M v N == 0
> return MultiVector(self.layout)
> else:
> return (self & subspace.inv()) * other
>
> def gradeInvol(self):
> """Returns the grade involution of the multivector.
> * i
> M = Sum[i, dims, (-1) <M> ]
> i
> gradeInvol() --> MultiVector
> """
>
> signs = Numeric.power(-1, self.layout.gradeList)
>
> newValue = signs * self.value
>
> return MultiVector(self.layout, newValue)
>
> def conjugate(self):
> """Returns the Clifford conjugate (reversion and grade involution).
> *
> M --> (~M).gradeInvol()
> conjugate() --> MultiVector
> """
>
> return (~self).gradeInvol()
>
> def comb(n, k):
> """\
> Returns /n\\
> \\k/
>
> comb(n, k) --> PyInt"""
>
> def fact(n):
> return Numeric.multiply.reduce(range(1, n+1))
>
> return fact(n) / (fact(k) * fact(n-k))
>
> def elements(dims, firstIdx=0):
> """Return a list of tuples representing all 2**dims of blades
> in a dims-dimensional GA.
>
> elements(dims, firstIdx=0) --> bladeList"""
>
> indcs = range(firstIdx, firstIdx + dims)
>
> blades = [()]
>
> for k in range(1, dims+1):
> # k = grade
>
> if k == 1:
> for i in indcs:
> blades.append((i,))
> continue
>
> curBladeX = indcs[:k]
>
> marker = -2
> # curBladeX[marker:] will be replaced with a range each time
> # the final element overflows; you'll see
>
> for i in range(comb(dims, k)):
> if curBladeX[-1] < firstIdx+dims-1:
> # increment last index
> blades.append(tuple(curBladeX))
> curBladeX[-1] = curBladeX[-1] + 1
>
> else:
> if curBladeX[marker:] == range(firstIdx+dims+marker,
> firstIdx+dims):
> # decrement marker
> marker = marker - 1
> if marker < -k:
> blades.append(tuple(curBladeX))
> continue
>
> # replace
> blades.append(tuple(curBladeX))
> curBladeX[marker:] = range(curBladeX[marker]+1,
> curBladeX[marker]+1 - marker)
>
> return blades
>
>
> def Cl(p, q=0, names=None, firstIdx=0):
> """Returns a Layout and basis blades for the geometric algebra Cl_p,q.
>
> The notation Cl_p,q means that the algebra is p+q dimensional, with
> the first p vectors with positive signature and the final q vectors
> negative.
>
> Cl(p, q=0, names=None, firstIdx=0) --> Layout, {'name': basisElement, ...}"""
>
> sig = [+1]*p + [-1]*q
> bladeList = elements(p+q, firstIdx)
>
> layout = Layout(sig, bladeList, firstIdx=firstIdx, names=names)
> blades = bases(layout)
>
> return layout, blades
>
> def bases(layout):
> """Returns a dictionary mapping basis element names to their MultiVector
> instances.
>
> bases(layout) --> {'name': baseElement, ...}"""
>
> dict = {}
> for i in range(layout.gaDims):
> if layout.gradeList[i] != 0:
> v = Numeric.zeros((layout.gaDims,))
> v[i] = 1
> dict[layout.names[i]] = MultiVector(layout, v)
> return dict
>
> def randomMV(layout, min=-2.0, max=2.0, grades=None):
> """Random MultiVector with given layout.
>
> Coefficients are between min and max, and if grades is a list of integers,
> only those grades will be non-zero. Uses the RandomArray module.
>
> randomMV(layout, min=-2.0, max=2.0, grades=None)"""
>
> from RandomArray import uniform
>
> if grades is None:
> return MultiVector(layout, uniform(min, max, (layout.gaDims,)))
> else:
> newValue = Numeric.zeros((layout.gaDims,)).astype(Numeric.Float)
> for i in range(layout.gaDims):
> if layout.gradeList[i] in grades:
> newValue[i] = uniform(min, max)
> return MultiVector(layout, newValue)
>
> def pretty():
> """Makes repr(M) default to pretty-print.
>
> pretty()"""
>
> global _pretty
> _pretty = 1
>
> def ugly():
> """Makes repr(M) default to eval-able representation.
>
> ugly()"""
>
> global _pretty
> _pretty = 0
>
> def eps(newEps):
> """Set the epsilon for float comparisons.
>
> eps(newEps)"""
>
> global _eps
> _eps = newEps
>
> ------------%<------------------------------------------
>
> --
> Robert Kern
> kern@caltech.edu
>
> "In the fields of hell where the grass grows high
> Are the graves of dreams allowed to die."
> -- Richard Harter
--
Conrad Schneiker
(This note is unofficial and subject to improvement without notice.)