Ad some unit tests from Konrad. Fix some typechecks. Fix factorial of 0 to return an integer 1 instead of 1.0.
1 """ Geometric Algebra for Python
3 This module implements geometric algebras (a.k.a. Clifford algebras). For the
4 uninitiated, a geometric algebra is an algebra of vectors of given dimensions
5 and signature. The familiar inner (dot) product and the outer product,
6 a generalized relative of the three-dimensional cross product, are unified in an
7 invertible geometric product. Scalars, vectors, and higher-grade entities can
8 be mixed freely and consistently in the form of mixed-grade multivectors.
10 For more information, see the Cambridge Clifford Algebras website:
11 http://www.mrao.cam.ac.uk/~clifford
12 and David Hestenes' website:
13 http://modelingnts.la.asu.edu/GC_R&D.html
15 Two classes, Layout and MultiVector, and several helper functions are
16 provided to implement the algebras.
22 Cl(p, q=0, names=None, firstIdx=0, mvClass=MultiVector) -- generates
23 the geometric algebra Cl_p,q and returns the appropriate Layout
24 and dictionary of basis element names to their MultiVector instances.
25 Uses mvClass if provided.
27 bases(layout) -- returns the dictionary of basis element names: instances
30 randomMV(layout, grades=None) -- random MultiVector using layout. If
31 grades is a sequence of integrers, only those grades will be non-zero
33 pretty() -- set repr() to pretty-print MultiVectors
35 ugly() -- set repr() to return eval-able strings
37 eps(newEps) -- set _eps, the epsilon for comparisons and zero-testing
43 * Due to Python's order of operations, the bit operators & ^ << follow
44 the normal arithmetic operators + - * /, so
46 1^e0 + 2^e1 != (1^e0) + (2^e1)
48 as is probably intended. Additionally,
50 M = MultiVector(layout2D) # null multivector
51 M << 1^e0 << 2^e1 == 10.0^e1 + 1.0^e01
55 as is definitely not intended. However,
57 M = MultiVector(layout2D)
58 M << (2^e0) << e1 << (3^e01) == M == 2^e0 + 1^e1 + 3^e01
63 * Since * is the inner product and the inner product with a scalar
64 vanishes by definition, an expression like
68 is null. Use the outer product or full geometric product, to
69 multiply scalars with MultiVectors. This can cause problems if
70 one has code that mixes Python numbers and MultiVectors. If the
71 code multiplies two values that can each be either type without
72 checking, one can run into problems as "1 & 2" has a very different
73 result from the same multiplication with scalar MultiVectors.
75 * Taking the inverse of a MultiVector will use a method proposed by
76 Christian Perwass that involves the solution of a matrix equation.
77 A description of that method follows:
79 Representing multivectors as 2**dims vectors (in the matrix sense),
80 we can carry out the geometric product with a multiplication table.
81 In pseudo-tensorish language (using summation notation):
83 m_i * g_ijk * n_k = v_j
85 Suppose m_i are known (M is the vector we are taking the inverse of),
86 the g_ijk have been computed for this algebra, and v_j = 1 if the
87 j'th element is the scalar element and 0 otherwise, we can compute the
88 dot product m_i * g_ijk. This yields a rank-2 matrix. We can
89 then use well-established computational linear algebra techniques
90 to solve this matrix equation for n_k. The laInv method does precisely
93 The usual, analytic, method for computing inverses [M**-1 = ~M/(M&~M) iff
94 M&~M == |M|**2] fails for those multivectors where M&~M is not a scalar.
95 It is only used if the inv method is manually set to point to normalInv.
97 My testing suggests that laInv works. In the cases where normalInv works,
98 laInv returns the same result (within _eps). In all cases,
99 M & M.laInv() == 1.0 (within _eps). Use whichever you feel comfortable
102 Of course, a new issue arises with this method. The inverses found
103 are sometimes dependant on the order of multiplication. That is:
108 XXX Thus, there are two other methods defined, leftInv and rightInv which
109 point to leftLaInv and rightLaInv. The method inv points to rightInv.
110 Should the user choose, leftInv and rightInv will both point to normalInv,
111 which yields a left- and right-inverse that are the same should either exist
112 (the proof is fairly simple).
114 * The basis vectors of any algebra will be orthonormal unless you supply
115 your own multiplication tables (which you are free to do after the Layout
116 constructor is called). A derived class could be made to calculate these
117 tables for you (and include methods for generating reciprocal bases and the
120 * No care is taken to preserve the dtype of the arrays. The purpose
121 of this module is pedagogical. If your application requires so many
122 multivectors that storage becomes important, the class structure here
123 is unsuitable for you anyways. Instead, use the algorithms from this
124 module and implement application-specific data structures.
126 * Conversely, explicit typecasting is rare. MultiVectors will have
127 integer coefficients if you instantiate them that way. Dividing them
128 by Python integers will have the same consequences as normal integer
129 division. Public outcry will convince me to add the explicit casts
130 if this becomes a problem.
134 Konrad Hinsen fixed a few bugs in the conversion to numpy and adding some unit
141 * Added a real license.
142 * Convert to numpy instead of Numeric.
147 * join() and meet() actually work now, but have numerical accuracy problems
148 * added clean() to MultiVector
149 * added leftInv() and rightInv() to MultiVector
150 * moved pseudoScalar() and invPS() to MultiVector (so we can derive
151 new classes from MultiVector)
152 * changed all of the instances of creating a new MultiVector to create
153 an instance of self.__class__ for proper inheritance
154 * fixed bug in laInv()
155 * fixed the massive confusion about how dot() works
156 * added left-contraction
157 * fixed embarassing bug in gmt generation
158 * added normal() and anticommutator() methods
159 * fixed dumb bug in elements() that limited it to 4 dimensions
163 robert.kern@gmail.com
166 # Standard library imports.
169 # Major library imports.
171 from numpy import linalg
174 class NoMorePermutations(StandardError):
175 """ No more permutations can be generated.
179 _eps = 1e-15 # float epsilon for float comparisons
180 _pretty = False # pretty-print global
183 """Returns the inner product as *I* learned it.
185 a_i...k * b_k...m = c_i...m in summation notation with the ...'s
186 representing arbitrary, omitted indices
188 The sum is over the last axis of the first argument and the first axis
191 _myDot(a, b) --> NumPy array
197 tempAxes = tuple(range(1, len(b.shape)) + [0])
198 newB = np.transpose(b, tempAxes)
200 # innerproduct sums over the *last* axes of *both* arguments
201 return np.inner(a, newB)
204 class Layout(object):
205 """ Layout stores information regarding the geometric algebra itself and the
206 internal representation of multivectors.
208 It is constructed like this:
210 Layout(signature, bladeList, firstIdx=0, names=None)
212 The arguments to be passed are interpreted as follows:
214 signature -- the signature of the vector space. This should be
215 a list of positive and negative numbers where the sign determines
216 the sign of the inner product of the corresponding vector with itself.
217 The values are irrelevant except for sign. This list also determines
218 the dimensionality of the vectors. Signatures with zeroes are not
219 permitted at this time.
222 signature = [+1, -1, -1, -1] # Hestenes', et al. Space-Time Algebra
223 signature = [+1, +1, +1] # 3-D Euclidean signature
225 bladeList -- list of tuples corresponding to the blades in the whole
226 algebra. This list determines the order of coefficients in the
227 internal representation of multivectors. The entry for the scalar
228 must be an empty tuple, and the entries for grade-1 vectors must be
229 singleton tuples. Remember, the length of the list will be 2**dims.
232 bladeList = [(), (0,), (1,), (0,1)] # 2-D
234 firstIdx -- the index of the first vector. That is, some systems number
235 the base vectors starting with 0, some with 1. Choose by passing
236 the correct number as firstIdx. 0 is the default.
238 names -- list of names of each blade. When pretty-printing multivectors,
239 use these symbols for the blades. names should be in the same order
240 as bladeList. You may use an empty string for scalars. By default,
241 the name for each non-scalar blade is 'e' plus the indices of the blade
242 as given in bladeList.
245 names = ['', 's0', 's1', 'i'] # 2-D
250 dims -- dimensionality of vectors (== len(signature))
252 sig -- normalized signature (i.e. all values are +1 or -1)
254 firstIdx -- starting point for vector indices
256 bladeList -- list of blades
258 gradeList -- corresponding list of the grades of each blade
262 names -- pretty-printing symbols for the blades
264 even -- dictionary of even permutations of blades to the canonical blades
266 odd -- dictionary of odd permutations of blades to the canonical blades
268 gmt -- multiplication table for geometric product [1]
270 imt -- multiplication table for inner product [1]
272 omt -- multiplication table for outer product [1]
274 lcmt -- multiplication table for the left-contraction [1]
277 [1] The multiplication tables are NumPy arrays of rank 3 with indices like
278 the tensor g_ijk discussed above.
281 def __init__(self, sig, bladeList, firstIdx=0, names=None):
283 self.sig = np.divide(sig, np.absolute(sig)).astype(int)
284 self.firstIdx = firstIdx
286 self.bladeList = map(tuple, bladeList)
289 self.gaDims = len(self.bladeList)
290 self.gradeList = map(len, self.bladeList)
296 for i in range(self.gaDims):
297 if self.gradeList[i] >= 1:
298 self.names.append(e + ''.join(map(str, self.bladeList[i])))
300 self.names.append('')
302 elif len(names) == self.gaDims:
305 raise ValueError, "names list of length %i needs to be of length %i"% (len(names), self.gaDims)
311 s = ("Layout(%r, %r, firstIdx=%r, names=%r)" % (list(self.sig),
312 self.bladeList, self.firstIdx, self.names))
315 def _sign(self, seq, orig):
316 """Determine {even,odd}-ness of permutation seq or orig.
318 Returns 1 if even; -1 if odd.
324 for i in range(len(seq)):
325 if seq[i] != orig[i]:
326 j = seq.index(orig[i])
328 seq[i], seq[j] = seq[j], seq[i]
331 def _containsDups(self, list):
332 "Checks if list contains duplicates."
335 if list.count(k) != 1:
339 def _genEvenOdd(self):
340 "Create mappings of even and odd permutations to their canonical blades."
345 for i in range(self.gaDims):
346 blade = self.bladeList[i]
347 grade = self.gradeList[i]
351 self.even[blade] = blade
355 self.even[blade] = blade
356 self.odd[(blade[1], blade[0])] = blade
359 # general case, lifted from Chooser.py released on
360 # comp.lang.python by James Lehmann with permission.
364 for i in range(np.multiply.reduce(range(1, grade+1))):
365 # grade! permutations
373 while idx[j] == grade:
379 raise NoMorePermutations()
382 if not self._containsDups(idx):
388 perm.append(blade[k])
392 if self._sign(perm, blade) == 1:
393 self.even[perm] = blade
395 self.odd[perm] = blade
397 except NoMorePermutations:
400 self.even[blade] = blade
402 def _checkList(self):
403 "Ensure validity of arguments."
405 # check for uniqueness
406 for blade in self.bladeList:
407 if self.bladeList.count(blade) != 1:
408 raise ValueError, "blades not unique"
410 # check for right dimensionality
411 if len(self.bladeList) != 2**self.dims:
412 raise ValueError, "incorrect number of blades"
414 # check for valid ranges of indices
415 valid = range(self.firstIdx, self.firstIdx+self.dims)
417 for blade in self.bladeList:
419 if (idx not in valid) or (list(blade).count(idx) != 1):
421 except (ValueError, TypeError):
422 raise ValueError, "invalid bladeList; must be a list of tuples"
424 def _gmtElement(self, a, b):
425 "Returns the element of the geometric multiplication table given blades a, b."
429 newBlade = list(a) + list(b)
433 for i in range(len(newBlade)):
435 if newBlade.count(index) != 1:
436 delta = newBlade[i+1:].index(index)
437 eo = 1 - 2*(delta % 2)
438 # eo == 1 if the elements are an even number of flips away
441 del newBlade[i+delta+1]
444 mul = eo * mul * self.sig[index - self.firstIdx]
449 newBlade = tuple(newBlade)
451 if newBlade in self.bladeList:
452 idx = self.bladeList.index(newBlade)
453 # index of the product blade in the bladeList
454 elif newBlade in self.even.keys():
456 idx = self.bladeList.index(self.even[newBlade])
459 idx = self.bladeList.index(self.odd[newBlade])
462 element = np.zeros((self.gaDims,), dtype=int)
467 def _genTables(self):
468 "Generate the multiplication tables."
470 # geometric multiplication table
471 gmt = np.zeros((self.gaDims, self.gaDims, self.gaDims), dtype=int)
472 # inner product table
473 imt = np.zeros((self.gaDims, self.gaDims, self.gaDims), dtype=int)
474 # outer product table
475 omt = np.zeros((self.gaDims, self.gaDims, self.gaDims), dtype=int)
476 # left-contraction table
477 lcmt = np.zeros((self.gaDims, self.gaDims, self.gaDims), dtype=int)
479 for i in range(self.gaDims):
480 for j in range(self.gaDims):
481 gmt[i,:,j], idx = self._gmtElement(list(self.bladeList[i]),
482 list(self.bladeList[j]))
484 if (self.gradeList[idx] == abs(self.gradeList[i] - self.gradeList[j])
485 and self.gradeList[i] != 0
486 and self.gradeList[j] != 0):
488 # A_r . B_s = <A_r B_s>_|r-s|
490 imt[i,:,j] = gmt[i,:,j]
492 if self.gradeList[idx] == (self.gradeList[i] + self.gradeList[j]):
494 # A_r ^ B_s = <A_r B_s>_|r+s|
495 omt[i,:,j] = gmt[i,:,j]
497 if self.gradeList[idx] == (self.gradeList[j] - self.gradeList[i]):
498 # A_r _| B_s = <A_r B_s>_(s-r) if s-r >= 0
499 lcmt[i,:,j] = gmt[i,:,j]
506 class MultiVector(object):
507 """ The elements of the algebras, the multivectors, are implemented in the
510 It has the following constructor:
512 MultiVector(layout, value=None)
514 The meaning of the arguments is as follows:
516 layout -- instance of Layout
518 value -- a sequence, of length layout.gaDims, of coefficients of the base
521 MultiVector's Members
522 =====================
524 layout -- instance of Layout
526 value -- a NumPy array, of length layout.gaDims, of coefficients of the base
530 def __init__(self, layout, value=None):
533 MultiVector(layout, value=None) --> MultiVector
539 self.value = np.zeros((self.layout.gaDims,), dtype=float)
541 self.value = np.array(value)
542 if self.value.shape != (self.layout.gaDims,):
543 raise ValueError("value must be a sequence of length %s" %
546 def _checkOther(self, other, coerce=1):
547 """Ensure that the other argument has the same Layout or coerce value if
550 _checkOther(other, coerce=1) --> newOther, isMultiVector
553 if isinstance(other, (int, float, long)):
556 newOther = self._newMV()
558 return newOther, True
562 elif (isinstance(other, self.__class__)
563 and other.layout is not self.layout):
564 raise ValueError("cannot operate on MultiVectors with different Layouts")
568 def _newMV(self, newValue=None):
569 """Returns a new MultiVector (or derived class instance).
571 _newMV(self, newValue=None)
574 return self.__class__(self.layout, newValue)
577 ## numeric special methods
580 def __and__(self, other):
584 __and__(other) --> MultiVector
587 other, mv = self._checkOther(other, coerce=0)
590 newValue = np.dot(self.value, np.dot(self.layout.gmt,
593 newValue = other * self.value
595 return self._newMV(newValue)
597 def __rand__(self, other):
598 """Right-hand geometric product
601 __rand__(other) --> MultiVector
604 other, mv = self._checkOther(other, coerce=0)
607 newValue = np.dot(other.value, np.dot(self.layout.gmt,
610 newValue = other * self.value
612 return self._newMV(newValue)
614 def __xor__(self, other):
618 __xor__(other) --> MultiVector
621 other, mv = self._checkOther(other, coerce=0)
624 newValue = np.dot(self.value, np.dot(self.layout.omt,
627 newValue = other * self.value
629 return self._newMV(newValue)
631 def __rxor__(self, other):
632 """Right-hand outer product
635 __rxor__(other) --> MultiVector
638 other, mv = self._checkOther(other, coerce=0)
641 newValue = np.dot(other.value, np.dot(self.layout.omt,
644 newValue = other * self.value
646 return self._newMV(newValue)
648 def __mul__(self, other):
652 __mul__(other) --> MultiVector
655 other, mv = self._checkOther(other)
658 newValue = np.dot(self.value, np.dot(self.layout.imt,
661 return self._newMV() # l * M = M * l = 0 for scalar l
663 return self._newMV(newValue)
667 def __add__(self, other):
671 __add__(other) --> MultiVector
674 other, mv = self._checkOther(other)
675 newValue = self.value + other.value
677 return self._newMV(newValue)
681 def __sub__(self, other):
685 __sub__(other) --> MultiVector
688 other, mv = self._checkOther(other)
689 newValue = self.value - other.value
691 return self._newMV(newValue)
693 def __rsub__(self, other):
694 """Right-hand subtraction
697 __rsub__(other) --> MultiVector
700 other, mv = self._checkOther(other)
701 newValue = other.value - self.value
703 return self._newMV(newValue)
705 def __div__(self, other):
709 __div__(other) --> MultiVector
712 other, mv = self._checkOther(other, coerce=0)
715 return self & other.inv()
717 newValue = self.value / other
718 return self._newMV(newValue)
720 def __rdiv__(self, other):
721 """Right-hand division
724 __rdiv__(other) --> MultiVector
727 other, mv = self._checkOther(other)
729 return other & self.inv()
731 def __pow__(self, other):
732 """Exponentiation of a multivector by an integer
735 __pow__(other) --> MultiVector
738 if not isinstance(other, (int, float)):
739 raise ValueError, "exponent must be a Python int or float"
741 if abs(round(other) - other) > _eps:
742 raise ValueError, "exponent must have no fractional part"
744 other = int(round(other))
746 newMV = self._newMV(np.array(self.value)) # copy
748 for i in range(1, other):
753 def __rpow__(self, other):
754 """Exponentiation of a real by a multivector
757 __rpow__(other) --> MultiVector
760 # Let math.log() check that other is a Python number, not something
762 intMV = math.log(other) & self
763 # pow(x, y) == exp(y & log(x))
765 newMV = self._newMV() # null
767 nextTerm = self._newMV()
768 nextTerm[()] = 1 # order 0 term of exp(x) Taylor expansion
773 # iterate until the added term is within _eps of 0
775 nextTerm = nextTerm & intMV / n
778 # squeeze out that extra little bit of accuracy
783 def __iadd__(self, other):
787 __iadd__(other) --> MultiVector
790 other, mv = self._checkOther(other)
792 self.value = self.value + other.value
803 __neg__() --> MultiVector
806 newValue = -self.value
808 return self._newMV(newValue)
811 """Positive (just a copy)
814 __pos__(self) --> MultiVector
817 newValue = self.value + 0 # copy
819 return self._newMV(newValue)
822 """Magnitude (modulus) squared
825 mag2() --> PyFloat | PyInt
828 return (~self & self)[()]
831 """Magnitude (modulus)
834 __abs__() --> PyFloat
837 return np.sqrt(self.mag2())
840 """Adjoint / reversion
842 ~M --> M (any one of several conflicting notations)
844 adjoint() --> MultiVector
846 # The multivector created by reversing all multiplications
848 grades = np.array(self.layout.gradeList)
849 signs = np.power(-1, grades*(grades-1)/2)
851 newValue = signs * self.value # elementwise multiplication
853 return self._newMV(newValue)
861 """Coerce to an integer iff scalar.
867 return int(self.__float__())
870 """Coerce to a long iff scalar.
873 __long__() --> PyLong
876 return long(self.__float__())
879 """"Coerce to a float iff scalar.
882 __float__() --> PyFloat
886 return float(self[()])
888 raise ValueError, "non-scalar coefficients are non-zero"
891 ## sequence special methods
894 """Returns length of value array.
900 return self.layout.gaDims
902 def __getitem__(self, key):
903 """If key is a blade tuple (e.g. (0,1) or (1,3)), then return
904 the (real) value of that blade's coefficient.
905 Otherwise, treat key as an index into the list of coefficients.
907 M[blade] --> PyFloat | PyInt
908 M[index] --> PyFloat | PyInt
909 __getitem__(key) --> PyFloat | PyInt
912 if key in self.layout.bladeList:
913 return self.value[self.layout.bladeList.index(key)]
914 elif key in self.layout.even:
915 return self.value[self.layout.bladeList.index(self.layout.even[key])]
916 elif key in self.layout.odd:
917 return -self.value[self.layout.bladeList.index(self.layout.odd[key])]
919 return self.value[key]
921 def __setitem__(self, key, value):
922 """If key is a blade tuple (e.g. (0,1) or (1,3)), then set
923 the (real) value of that blade's coeficient.
924 Otherwise treat key as an index into the list of coefficients.
926 M[blade] = PyFloat | PyInt
927 M[index] = PyFloat | PyInt
928 __setitem__(key, value)
931 if key in self.layout.bladeList:
932 self.value[self.layout.bladeList.index(key)] = value
933 elif key in self.layout.even:
934 self.value[self.layout.bladeList.index(self.layout.even[key])] = value
935 elif key in self.layout.odd:
936 self.value[self.layout.bladeList.index(self.layout.odd[key])] = -value
938 self.value[key] = value
940 def __delitem__(self, key):
941 """Set the selected coefficient to 0.
948 if key in self.layout.bladeList:
949 self.value[self.layout.bladeList.index(key)] = 0
950 elif key in self.layout.even:
951 self.value[self.layout.bladeList.index(self.layout.even[key])] = 0
952 elif key in self.layout.odd:
953 self.value[self.layout.bladeList.index(self.layout.odd[key])] = 0
957 def __getslice__(self, i, j):
958 """Return a copy with only the slice non-zero.
961 __getslice__(i, j) --> MultiVector
964 newMV = self._newMV()
965 newMV.value[i:j] = self.value[i:j]
969 def __setslice__(self, i, j, sequence):
970 """Paste a sequence into coefficients array.
973 __setslice__(i, j, sequence)
976 self.value[i:j] = sequence
978 def __delslice__(self, i, j):
979 """Set slice to zeros.
990 def __call__(self, grade):
991 """Return a new multi-vector projected onto the specified grade.
995 __call__(grade) --> MultiVector
998 if grade not in self.layout.gradeList:
999 raise ValueError, "algebra does not have grade %s" % grade
1001 if not isinstance(grade, int):
1002 raise ValueError, "grade must be an integer"
1004 mask = np.equal(grade, self.layout.gradeList)
1006 newValue = np.multiply(mask, self.value)
1008 return self._newMV(newValue)
1010 ## fundamental special methods
1013 """Return pretty-printed representation.
1016 __str__() --> PyString
1021 for i in range(self.layout.gaDims):
1022 # if we have nothing yet, don't use + and - as operators but
1023 # use - as an unary prefix if necessary
1025 seps = (' + ', ' - ')
1029 if self.layout.gradeList[i] == 0:
1031 if abs(self.value[i]) >= _eps:
1032 if self.value[i] > 0:
1033 s = '%s%s%s' % (s, seps[0], self.value[i])
1035 s = '%s%s%s' % (s, seps[1], -self.value[i])
1038 if abs(self.value[i]) >= _eps:
1040 if self.value[i] > 0:
1041 s = '%s%s(%s^%s)' % (s, seps[0], self.value[i],
1042 self.layout.names[i])
1044 s = '%s%s(%s^%s)' % (s, seps[1], -self.value[i],
1045 self.layout.names[i])
1054 """Return eval-able representation if global _pretty is false.
1055 Otherwise, return str(self).
1058 __repr__() --> PyString
1062 return self.__str__()
1064 s = "MultiVector(%s, value=%s)" % \
1065 (repr(self.layout), list(self.value))
1068 def __nonzero__(self):
1069 """Instance is nonzero iff at least one of the coefficients
1072 __nonzero() --> Boolean
1075 nonzeroes = np.absolute(self.value) > _eps
1082 def __cmp__(self, other):
1083 """Compares two multivectors.
1085 This is mostly defined for equality testing (within epsilon).
1086 In the case that the two multivectors have different Layouts,
1087 we will raise an error. In the case that they are not equal,
1088 we will compare the tuple represenations of the coefficients
1089 lists just so as to return something valid. Therefore,
1090 inequalities are well-nigh meaningless (since they are
1091 meaningless for multivectors while equality is meaningful).
1093 TODO: rich comparisons.
1096 __cmp__(other) --> -1|0|1
1099 other, mv = self._checkOther(other)
1101 if (np.absolute(self.value - other.value) < _eps).all():
1102 # equal within epsilon
1105 return cmp(tuple(self.value), tuple(other.value))
1107 def clean(self, eps=None):
1108 """Sets coefficients whose absolute value is < eps to exactly 0.
1110 eps defaults to the current value of the global _eps.
1118 mask = np.absolute(self.value) > eps
1120 # note element-wise multiplication
1121 self.value = mask * self.value
1125 def round(self, eps=None):
1126 """Rounds all coefficients according to Python's rounding rules.
1128 eps defaults to the current value of the global _eps.
1136 self.value = np.around(self.value, eps)
1140 ## Geometric Algebraic functions
1142 def lc(self, other):
1143 """Returns the left-contraction of two multivectors.
1146 lc(other) --> MultiVector
1149 other, mv = self._checkOther(other, coerce=1)
1151 newValue = np.dot(self.value, np.dot(self.layout.lcmt, other.value))
1153 return self._newMV(newValue)
1155 def pseudoScalar(self):
1156 "Returns a MultiVector that is the pseudoscalar of this space."
1158 psIdx = self.layout.gradeList.index(self.layout.dims)
1159 # pick out out blade with grade equal to dims
1161 pScalar = self._newMV()
1162 pScalar.value[psIdx] = 1
1167 "Returns the inverse of the pseudoscalar of the algebra."
1169 ps = self.pseudoScalar()
1174 """Returns true iff self is a scalar.
1176 isScalar() --> Boolean
1179 indices = range(self.layout.gaDims)
1180 indices.remove(self.layout.gradeList.index(0))
1183 if abs(self.value[i]) < _eps:
1191 """Returns true if multivector is a blade.
1193 FIXME: Apparently, not all single-grade multivectors are blades. E.g. in
1194 the 4-D Euclidean space, a=(e1^e2 + e3^e4) is not a blade. There is no
1195 vector v such that v^a=0.
1197 isBlade() --> Boolean
1202 for i in range(self.layout.gaDims):
1203 if abs(self.value[i]) > _eps:
1205 grade = self.layout.gradeList[i]
1206 elif self.layout.gradeList[i] != grade:
1212 """Return the grades contained in the multivector.
1214 grades() --> [ PyInt, PyInt, ... ]
1219 for i in range(self.layout.gaDims):
1220 if abs(self.value[i]) > _eps and \
1221 self.layout.gradeList[i] not in grades:
1222 grades.append(self.layout.gradeList[i])
1227 """Return the (mostly) normalized multivector.
1229 The _mostly_ comes from the fact that some multivectors have a
1230 negative squared-magnitude. So, without introducing formally
1231 imaginary numbers, we can only fix the normalized multivector's
1234 M / |M| up to a sign
1235 normal() --> MultiVector
1238 return self / np.sqrt(abs(self.mag2()))
1240 def leftLaInv(self):
1241 """Return left-inverse using a computational linear algebra method
1242 proposed by Christian Perwass.
1245 leftLaInv() --> MultiVector
1248 identity = np.zeros((self.layout.gaDims,))
1249 identity[self.layout.gradeList.index(0)] = 1
1251 intermed = np.dot(self.layout.gmt, self.value)
1252 intermed = np.transpose(intermed)
1254 if abs(linalg.det(intermed)) < _eps:
1255 raise ValueError("multivector has no left-inverse")
1257 sol = linalg.solve(intermed, identity)
1259 return self._newMV(sol)
1261 def rightLaInv(self):
1262 """Return right-inverse using a computational linear algebra method
1263 proposed by Christian Perwass.
1266 rightLaInv() --> MultiVector
1269 identity = np.zeros((self.layout.gaDims,))
1270 identity[self.layout.gradeList.index(0)] = 1
1272 intermed = _myDot(self.value, self.layout.gmt)
1274 if abs(linalg.det(intermed)) < _eps:
1275 raise ValueError("multivector has no right-inverse")
1277 sol = linalg.solve(intermed, identity)
1279 return self._newMV(sol)
1281 def normalInv(self):
1282 """Returns the inverse of itself if M&~M == |M|**2.
1285 normalInv() --> MultiVector
1289 MadjointM = (Madjoint & self)
1291 if MadjointM.isScalar() and abs(MadjointM[()]) > _eps:
1293 return Madjoint / MadjointM[()]
1295 raise ValueError, "no inverse exists for this multivector"
1298 inv = rightInv = rightLaInv
1300 def dual(self, I=None):
1301 """Returns the dual of the multivector against the given subspace I.
1302 I defaults to the pseudoscalar.
1306 dual(I=None) --> MultiVector
1310 Iinv = self.layout.invPS()
1316 def commutator(self, other):
1317 """Returns the commutator product of two multivectors.
1319 [M, N] = M X N = (M&N - N&M)/2
1320 commutator(other) --> MultiVector
1323 return ((self & other) - (other & self)) / 2
1325 def anticommutator(self, other):
1326 """Returns the anti-commutator product of two multivectors.
1329 anticommutator(other) --> MultiVector
1332 return ((self & other) + (other & self)) / 2
1334 def gradeInvol(self):
1335 """Returns the grade involution of the multivector.
1337 M = Sum[i, dims, (-1) <M> ]
1339 gradeInvol() --> MultiVector
1342 signs = np.power(-1, self.layout.gradeList)
1344 newValue = signs * self.value
1346 return self._newMV(newValue)
1348 def conjugate(self):
1349 """Returns the Clifford conjugate (reversion and grade involution).
1351 M --> (~M).gradeInvol()
1352 conjugate() --> MultiVector
1355 return (~self).gradeInvol()
1357 ## Subspace operations
1359 def project(self, other):
1360 """Projects the multivector onto the subspace represented by this blade.
1362 P (M) = (M _| A) & A
1364 project(M) --> MultiVector
1367 other, mv = self._checkOther(other, coerce=1)
1369 if not self.isBlade():
1370 raise ValueError, "self is not a blade"
1372 return other.lc(self) & self.inv()
1375 """Finds a vector basis of this subspace.
1377 basis() --> [ MultiVector, MultiVector, ... ]
1383 # FIXME: this is not a sufficient condition for a blade.
1384 raise ValueError, "self is not a blade"
1386 selfInv = self.inv()
1390 wholeBasis = [] # vector basis of the whole space
1392 for i in range(self.layout.gaDims):
1393 if self.layout.gradeList[i] == 1:
1394 v = np.zeros((self.layout.gaDims,), dtype=float)
1396 wholeBasis.append(self._newMV(v))
1398 thisBasis = [] # vector basis of this subspace
1400 J, mv = self._checkOther(1.) # outer product of all of the vectors up
1401 # to the point of iteration
1403 for ei in wholeBasis:
1404 Pei = ei.lc(self) & selfInv
1412 thisBasis.append(Pei)
1413 if len(thisBasis) == gr[0]: # we have a complete set
1418 def join(self, other):
1419 """Returns the join of two blades.
1422 join(other) --> MultiVector
1425 other, mv = self._checkOther(other)
1427 grSelf = self.grades()
1428 grOther = other.grades()
1430 if len(grSelf) == len(grOther) == 1:
1433 # try the outer product first
1439 # try something else
1440 M = (other & self.invPS()).lc(self)
1444 J = (self & C.rightInv()) ^ other
1447 if grSelf[0] >= grOther[0]:
1454 if (A & B) == (A * B):
1455 # B is a subspace of A or the same if grades are equal
1458 # ugly, but general way
1459 # watch out for residues
1461 # A is still the larger-dimensional subspace
1465 # add the basis vectors of B one by one to the larger
1466 # subspace except for the ones that make the outer
1478 # for consistency's sake, we'll normalize the join
1484 raise ValueError, "not blades"
1486 def meet(self, other, subspace=None):
1487 """Returns the meet of two blades.
1489 Computation is done with respect to a subspace that defaults to
1490 the join if none is given.
1493 meet(other, subspace=None) --> MultiVector
1496 other, mv = self._checkOther(other)
1501 if len(r) > 1 or len(s) > 1:
1502 raise ValueError, "not blades"
1504 if subspace is None:
1505 subspace = self.join(other)
1507 return (self & subspace.inv()) * other
1515 comb(n, k) --> PyInt
1521 return np.multiply.reduce(range(1, n+1))
1523 return fact(n) / (fact(k) * fact(n-k))
1525 def elements(dims, firstIdx=0):
1526 """Return a list of tuples representing all 2**dims of blades
1527 in a dims-dimensional GA.
1529 elements(dims, firstIdx=0) --> bladeList
1532 indcs = range(firstIdx, firstIdx + dims)
1536 for k in range(1, dims+1):
1544 curBladeX = indcs[:k]
1546 for i in range(comb(dims, k)):
1547 if curBladeX[-1] < firstIdx+dims-1:
1548 # increment last index
1549 blades.append(tuple(curBladeX))
1550 curBladeX[-1] = curBladeX[-1] + 1
1554 tmp = curBladeX[:] # copy
1557 # locate where the steady increase begins
1558 for j in range(k-1):
1559 if tmp[j] - tmp[j+1] == 1:
1565 blades.append(tuple(curBladeX))
1569 blades.append(tuple(curBladeX))
1570 curBladeX[marker:] = range(curBladeX[marker]+1,
1571 curBladeX[marker]+1 - marker)
1576 def Cl(p, q=0, names=None, firstIdx=0, mvClass=MultiVector):
1577 """Returns a Layout and basis blades for the geometric algebra Cl_p,q.
1579 The notation Cl_p,q means that the algebra is p+q dimensional, with
1580 the first p vectors with positive signature and the final q vectors
1583 Cl(p, q=0, names=None, firstIdx=0) --> Layout, {'name': basisElement, ...}
1586 sig = [+1]*p + [-1]*q
1587 bladeList = elements(p+q, firstIdx)
1589 layout = Layout(sig, bladeList, firstIdx=firstIdx, names=names)
1590 blades = bases(layout, mvClass)
1592 return layout, blades
1595 def bases(layout, mvClass=MultiVector):
1596 """Returns a dictionary mapping basis element names to their MultiVector
1599 bases(layout) --> {'name': baseElement, ...}
1603 for i in range(layout.gaDims):
1604 if layout.gradeList[i] != 0:
1605 v = np.zeros((layout.gaDims,), dtype=int)
1607 dict[layout.names[i]] = mvClass(layout, v)
1610 def randomMV(layout, min=-2.0, max=2.0, grades=None, mvClass=MultiVector,
1612 """Random MultiVector with given layout.
1614 Coefficients are between min and max, and if grades is a list of integers,
1615 only those grades will be non-zero.
1617 randomMV(layout, min=-2.0, max=2.0, grades=None, uniform=None)
1621 uniform = np.random.uniform
1624 return mvClass(layout, uniform(min, max, (layout.gaDims,)))
1626 newValue = np.zeros((layout.gaDims,))
1627 for i in range(layout.gaDims):
1628 if layout.gradeList[i] in grades:
1629 newValue[i] = uniform(min, max)
1630 return mvClass(layout, newValue)
1633 """Makes repr(M) default to pretty-print.
1642 """Makes repr(M) default to eval-able representation.
1651 """Set the epsilon for float comparisons.