clifford.py
author Robert Kern <robert.kern@gmail.com>
Tue Apr 15 14:50:27 2008 -0500 (4 years ago)
changeset 4 443a78ca5613
parent 39aaafe78e26c
permissions -rw-r--r--
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
     2 
     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.
     9 
    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
    14 
    15 Two classes, Layout and MultiVector, and several helper functions are 
    16 provided to implement the algebras.
    17 
    18 
    19 Helper Functions
    20 ================
    21 
    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.
    26 
    27   bases(layout) -- returns the dictionary of basis element names: instances
    28       as above
    29 
    30   randomMV(layout, grades=None) -- random MultiVector using layout.  If 
    31       grades is a sequence of integrers, only those grades will be non-zero
    32   
    33   pretty() -- set repr() to pretty-print MultiVectors
    34 
    35   ugly() -- set repr() to return eval-able strings
    36 
    37   eps(newEps) -- set _eps, the epsilon for comparisons and zero-testing
    38 
    39 
    40 Issues
    41 ======
    42 
    43  * Due to Python's order of operations, the bit operators & ^ << follow
    44    the normal arithmetic operators + - * /, so 
    45 
    46      1^e0 + 2^e1  !=  (1^e0) + (2^e1)
    47    
    48    as is probably intended.  Additionally,
    49 
    50      M = MultiVector(layout2D)  # null multivector
    51      M << 1^e0 << 2^e1 == 10.0^e1 + 1.0^e01
    52      M == 1.0
    53      e0 == 2 + 1^e0
    54 
    55    as is definitely not intended.  However,
    56    
    57      M = MultiVector(layout2D)
    58      M << (2^e0) << e1 << (3^e01) == M == 2^e0 + 1^e1 + 3^e01
    59      e0 == 1^e0
    60      e1 == 1^e1
    61      e01 == 1^e01
    62    
    63  * Since * is the inner product and the inner product with a scalar 
    64    vanishes by definition, an expression like 
    65 
    66      1*e0 + 2*e1
    67    
    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.
    74 
    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:
    78 
    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):
    82 
    83      m_i * g_ijk * n_k = v_j
    84    
    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
    91    that.
    92 
    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.
    96 
    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 
   100    with.
   101 
   102    Of course, a new issue arises with this method.  The inverses found
   103    are sometimes dependant on the order of multiplication.  That is:
   104 
   105      M.laInv() & M == 1.0
   106      M & M.laInv() != 1.0
   107    
   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).
   113    
   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
   118    like).
   119 
   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.
   125 
   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.
   131    
   132 Acknowledgements
   133 ----------------
   134 Konrad Hinsen fixed a few bugs in the conversion to numpy and adding some unit
   135 tests.
   136 
   137 
   138 Changes 0.6-0.7
   139 ===============
   140 
   141  * Added a real license.
   142  * Convert to numpy instead of Numeric.
   143    
   144 Changes 0.5-0.6
   145 ===============
   146 
   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
   160 
   161 Happy hacking!
   162 Robert Kern
   163 robert.kern@gmail.com
   164 """
   165 
   166 # Standard library imports.
   167 import math
   168 
   169 # Major library imports.
   170 import numpy as np
   171 from numpy import linalg
   172 
   173 
   174 class NoMorePermutations(StandardError):
   175     """ No more permutations can be generated.
   176     """
   177 
   178 
   179 _eps = 1e-15     # float epsilon for float comparisons
   180 _pretty = False  # pretty-print global
   181 
   182 def _myDot(a, b):
   183     """Returns the inner product as *I* learned it.
   184 
   185     a_i...k * b_k...m = c_i...m     in summation notation with the ...'s 
   186                                     representing arbitrary, omitted indices
   187 
   188     The sum is over the last axis of the first argument and the first axis 
   189     of the last axis.
   190 
   191     _myDot(a, b) --> NumPy array
   192     """
   193 
   194     a = np.asarray(a)
   195     b = np.asarray(b)
   196 
   197     tempAxes = tuple(range(1, len(b.shape)) + [0])
   198     newB = np.transpose(b, tempAxes)
   199 
   200     # innerproduct sums over the *last* axes of *both* arguments
   201     return np.inner(a, newB)
   202     
   203 
   204 class Layout(object):
   205     """ Layout stores information regarding the geometric algebra itself and the
   206     internal representation of multivectors.
   207     
   208     It is constructed like this:
   209 
   210       Layout(signature, bladeList, firstIdx=0, names=None)
   211 
   212     The arguments to be passed are interpreted as follows:
   213 
   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.
   220           
   221           Examples:
   222             signature = [+1, -1, -1, -1]  # Hestenes', et al. Space-Time Algebra
   223             signature = [+1, +1, +1]      # 3-D Euclidean signature
   224             
   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.
   230 
   231           Example:
   232             bladeList = [(), (0,), (1,), (0,1)]  # 2-D
   233       
   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.
   237       
   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.
   243 
   244           Example:
   245             names = ['', 's0', 's1', 'i']  # 2-D
   246 
   247       
   248     Layout's Members:
   249 
   250       dims -- dimensionality of vectors (== len(signature))
   251 
   252       sig -- normalized signature (i.e. all values are +1 or -1)
   253 
   254       firstIdx -- starting point for vector indices
   255 
   256       bladeList -- list of blades
   257 
   258       gradeList -- corresponding list of the grades of each blade
   259 
   260       gaDims -- 2**dims
   261 
   262       names -- pretty-printing symbols for the blades
   263 
   264       even -- dictionary of even permutations of blades to the canonical blades
   265 
   266       odd -- dictionary of odd permutations of blades to the canonical blades
   267 
   268       gmt -- multiplication table for geometric product [1]
   269 
   270       imt -- multiplication table for inner product [1]
   271 
   272       omt -- multiplication table for outer product [1]
   273 
   274       lcmt -- multiplication table for the left-contraction [1]
   275 
   276 
   277     [1] The multiplication tables are NumPy arrays of rank 3 with indices like 
   278         the tensor g_ijk discussed above.
   279     """
   280 
   281     def __init__(self, sig, bladeList, firstIdx=0, names=None):
   282         self.dims = len(sig)
   283         self.sig = np.divide(sig, np.absolute(sig)).astype(int)
   284         self.firstIdx = firstIdx
   285 
   286         self.bladeList = map(tuple, bladeList)
   287         self._checkList()
   288         
   289         self.gaDims = len(self.bladeList)
   290         self.gradeList = map(len, self.bladeList)
   291 
   292         if names is None:
   293             e = 'e'
   294             self.names = []
   295             
   296             for i in range(self.gaDims):
   297                 if self.gradeList[i] >= 1:
   298                     self.names.append(e + ''.join(map(str, self.bladeList[i])))
   299                 else:
   300                     self.names.append('')
   301             
   302         elif len(names) == self.gaDims:
   303             self.names = names
   304         else:
   305             raise ValueError, "names list of length %i needs to be of length %i"% (len(names), self.gaDims)
   306 
   307         self._genEvenOdd()
   308         self._genTables()
   309 
   310     def __repr__(self):
   311         s = ("Layout(%r, %r, firstIdx=%r, names=%r)" % (list(self.sig),
   312             self.bladeList, self.firstIdx, self.names))
   313         return s
   314 
   315     def _sign(self, seq, orig):
   316         """Determine {even,odd}-ness of permutation seq or orig.
   317 
   318         Returns 1 if even; -1 if odd.
   319         """
   320 
   321         sign = 1
   322         seq = list(seq)
   323 
   324         for i in range(len(seq)):
   325             if seq[i] != orig[i]:
   326                 j = seq.index(orig[i])
   327                 sign = -sign
   328                 seq[i], seq[j] = seq[j], seq[i]
   329         return sign
   330 
   331     def _containsDups(self, list):
   332         "Checks if list contains duplicates."
   333         
   334         for k in list:
   335             if list.count(k) != 1:
   336                 return 1
   337         return 0
   338 
   339     def _genEvenOdd(self):
   340         "Create mappings of even and odd permutations to their canonical blades."
   341         
   342         self.even = {}
   343         self.odd = {}
   344 
   345         for i in range(self.gaDims):
   346             blade = self.bladeList[i]
   347             grade = self.gradeList[i]
   348 
   349             if grade in (0, 1):
   350                 # handled easy cases
   351                 self.even[blade] = blade
   352                 continue
   353             elif grade == 2:
   354                 # another easy case
   355                 self.even[blade] = blade
   356                 self.odd[(blade[1], blade[0])] = blade
   357                 continue
   358             else:
   359                 # general case, lifted from Chooser.py released on 
   360                 # comp.lang.python by James Lehmann with permission.
   361                 idx = range(grade)
   362                 
   363                 try:
   364                     for i in range(np.multiply.reduce(range(1, grade+1))):
   365                         # grade! permutations
   366 
   367                         done = 0
   368                         j = grade - 1
   369 
   370                         while not done:
   371                             idx[j] = idx[j] + 1
   372 
   373                             while idx[j] == grade:
   374                                 idx[j] = 0
   375                                 j = j - 1
   376                                 idx[j] = idx[j] + 1
   377 
   378                                 if j == -1:
   379                                     raise NoMorePermutations()
   380                             j = grade - 1
   381 
   382                             if not self._containsDups(idx):
   383                                 done = 1
   384                                 
   385                         perm = []
   386 
   387                         for k in idx:
   388                             perm.append(blade[k])
   389                         
   390                         perm = tuple(perm)
   391                         
   392                         if self._sign(perm, blade) == 1:
   393                             self.even[perm] = blade
   394                         else:
   395                             self.odd[perm] = blade
   396 
   397                 except NoMorePermutations:
   398                     pass
   399 
   400                 self.even[blade] = blade
   401 
   402     def _checkList(self):
   403         "Ensure validity of arguments."
   404 
   405         # check for uniqueness
   406         for blade in self.bladeList:
   407             if self.bladeList.count(blade) != 1:
   408                 raise ValueError, "blades not unique"
   409 
   410         # check for right dimensionality
   411         if len(self.bladeList) != 2**self.dims:
   412             raise ValueError, "incorrect number of blades"
   413 
   414         # check for valid ranges of indices
   415         valid =  range(self.firstIdx, self.firstIdx+self.dims)
   416         try:
   417             for blade in self.bladeList:
   418                 for idx in blade:
   419                     if (idx not in valid) or (list(blade).count(idx) != 1):
   420                         raise ValueError
   421         except (ValueError, TypeError):
   422             raise ValueError, "invalid bladeList; must be a list of tuples"
   423     
   424     def _gmtElement(self, a, b):
   425         "Returns the element of the geometric multiplication table given blades a, b."
   426         
   427         mul = 1         # multiplier
   428 
   429         newBlade = list(a) + list(b)
   430         
   431         unique = 0
   432         while unique == 0:
   433             for i in range(len(newBlade)):
   434                 index = newBlade[i]
   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
   439                     # eo == -1 otherwise
   440                     
   441                     del newBlade[i+delta+1]
   442                     del newBlade[i]
   443 
   444                     mul = eo * mul * self.sig[index - self.firstIdx]
   445                     break
   446             else:
   447                 unique = 1
   448                 
   449         newBlade = tuple(newBlade)
   450         
   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():
   455             # even permutation
   456             idx = self.bladeList.index(self.even[newBlade])
   457         else:
   458             # odd permutation
   459             idx = self.bladeList.index(self.odd[newBlade])
   460             mul = -mul
   461     
   462         element = np.zeros((self.gaDims,), dtype=int)
   463         element[idx] = mul
   464 
   465         return element, idx
   466     
   467     def _genTables(self):
   468         "Generate the multiplication tables."
   469         
   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)
   478 
   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]))
   483 
   484                 if (self.gradeList[idx] == abs(self.gradeList[i] - self.gradeList[j]) 
   485                     and self.gradeList[i] != 0 
   486                     and self.gradeList[j] != 0):
   487                     
   488                     # A_r . B_s = <A_r B_s>_|r-s|
   489                     # if r,s != 0
   490                     imt[i,:,j] = gmt[i,:,j]
   491 
   492                 if self.gradeList[idx] == (self.gradeList[i] + self.gradeList[j]):
   493 
   494                     # A_r ^ B_s = <A_r B_s>_|r+s|
   495                     omt[i,:,j] = gmt[i,:,j]
   496 
   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]
   500                     
   501         self.gmt = gmt
   502         self.imt = imt
   503         self.omt = omt
   504         self.lcmt = lcmt
   505 
   506 class MultiVector(object):
   507     """ The elements of the algebras, the multivectors, are implemented in the
   508     MultiVector class.
   509     
   510     It has the following constructor:
   511 
   512       MultiVector(layout, value=None)
   513 
   514     The meaning of the arguments is as follows:
   515 
   516       layout -- instance of Layout
   517 
   518       value -- a sequence, of length layout.gaDims, of coefficients of the base
   519           blades
   520 
   521     MultiVector's Members
   522     =====================
   523 
   524       layout -- instance of Layout
   525 
   526       value -- a NumPy array, of length layout.gaDims, of coefficients of the base
   527           blades
   528     """
   529 
   530     def __init__(self, layout, value=None):
   531         """Constructor.
   532 
   533         MultiVector(layout, value=None) --> MultiVector
   534         """
   535         
   536         self.layout = layout
   537 
   538         if value is None:
   539             self.value = np.zeros((self.layout.gaDims,), dtype=float)
   540         else:
   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" % 
   544                     self.layout.gaDims)
   545 
   546     def _checkOther(self, other, coerce=1):
   547         """Ensure that the other argument has the same Layout or coerce value if 
   548         necessary/requested.
   549         
   550         _checkOther(other, coerce=1) --> newOther, isMultiVector
   551         """
   552 
   553         if isinstance(other, (int, float, long)):
   554             if coerce:
   555                 # numeric scalar
   556                 newOther = self._newMV()
   557                 newOther[()] = other
   558                 return newOther, True
   559             else:
   560                 return other, False
   561 
   562         elif (isinstance(other, self.__class__) 
   563             and other.layout is not self.layout):
   564             raise ValueError("cannot operate on MultiVectors with different Layouts")
   565 
   566         return other, True
   567 
   568     def _newMV(self, newValue=None):
   569         """Returns a new MultiVector (or derived class instance).
   570 
   571         _newMV(self, newValue=None)
   572         """
   573 
   574         return self.__class__(self.layout, newValue)
   575 
   576 
   577     ## numeric special methods
   578     # binary
   579     
   580     def __and__(self, other):
   581         """Geometric product
   582         
   583         M & N --> MN
   584         __and__(other) --> MultiVector
   585         """
   586         
   587         other, mv = self._checkOther(other, coerce=0)
   588         
   589         if mv:
   590             newValue = np.dot(self.value, np.dot(self.layout.gmt,
   591                                                            other.value))
   592         else:
   593             newValue = other * self.value
   594 
   595         return self._newMV(newValue)
   596         
   597     def __rand__(self, other):
   598         """Right-hand geometric product
   599         
   600         N & M --> NM
   601         __rand__(other) --> MultiVector
   602         """
   603         
   604         other, mv = self._checkOther(other, coerce=0)
   605 
   606         if mv:
   607             newValue = np.dot(other.value, np.dot(self.layout.gmt,
   608                                                             self.value))
   609         else:
   610             newValue = other * self.value
   611 
   612         return self._newMV(newValue)
   613     
   614     def __xor__(self, other):
   615         """Outer product
   616         
   617         M ^ N
   618         __xor__(other) --> MultiVector
   619         """
   620         
   621         other, mv = self._checkOther(other, coerce=0)
   622 
   623         if mv:
   624             newValue = np.dot(self.value, np.dot(self.layout.omt,
   625                                                            other.value))
   626         else:
   627             newValue = other * self.value
   628 
   629         return self._newMV(newValue)
   630     
   631     def __rxor__(self, other):
   632         """Right-hand outer product
   633         
   634         N ^ M
   635         __rxor__(other) --> MultiVector
   636         """
   637         
   638         other, mv = self._checkOther(other, coerce=0)
   639 
   640         if mv:
   641             newValue = np.dot(other.value, np.dot(self.layout.omt,
   642                                                             self.value))
   643         else:
   644             newValue = other * self.value
   645 
   646         return self._newMV(newValue)
   647         
   648     def __mul__(self, other):
   649         """Inner product
   650         
   651         M * N
   652         __mul__(other) --> MultiVector
   653         """
   654         
   655         other, mv = self._checkOther(other)
   656 
   657         if mv:
   658             newValue = np.dot(self.value, np.dot(self.layout.imt,
   659                                                            other.value))
   660         else:
   661             return self._newMV()  # l * M = M * l = 0 for scalar l
   662 
   663         return self._newMV(newValue)
   664 
   665     __rmul__ = __mul__
   666     
   667     def __add__(self, other):
   668         """Addition
   669         
   670         M + N
   671         __add__(other) --> MultiVector
   672         """
   673         
   674         other, mv = self._checkOther(other)
   675         newValue = self.value + other.value
   676 
   677         return self._newMV(newValue)
   678         
   679     __radd__ = __add__
   680 
   681     def __sub__(self, other):
   682         """Subtraction
   683         
   684         M - N
   685         __sub__(other) --> MultiVector
   686         """
   687         
   688         other, mv = self._checkOther(other)
   689         newValue = self.value - other.value
   690 
   691         return self._newMV(newValue)
   692 
   693     def __rsub__(self, other):
   694         """Right-hand subtraction
   695         
   696         N - M
   697         __rsub__(other) --> MultiVector
   698         """
   699         
   700         other, mv = self._checkOther(other)
   701         newValue = other.value - self.value
   702 
   703         return self._newMV(newValue)
   704         
   705     def __div__(self, other):
   706         """Division
   707                        -1
   708         M / N --> M & N
   709         __div__(other) --> MultiVector
   710         """
   711         
   712         other, mv = self._checkOther(other, coerce=0)
   713         
   714         if mv:
   715             return self & other.inv()
   716         else:
   717             newValue = self.value / other
   718             return self._newMV(newValue)
   719 
   720     def __rdiv__(self, other):
   721         """Right-hand division
   722                        -1
   723         N / M --> N & M
   724         __rdiv__(other) --> MultiVector
   725         """
   726         
   727         other, mv = self._checkOther(other)
   728 
   729         return other & self.inv()
   730 
   731     def __pow__(self, other):
   732         """Exponentiation of a multivector by an integer
   733                     n
   734         M ** n --> M
   735         __pow__(other) --> MultiVector
   736         """
   737         
   738         if not isinstance(other, (int, float)):
   739             raise ValueError, "exponent must be a Python int or float"
   740         
   741         if abs(round(other) - other) > _eps:
   742             raise ValueError, "exponent must have no fractional part"
   743 
   744         other = int(round(other))
   745 
   746         newMV = self._newMV(np.array(self.value))  # copy
   747 
   748         for i in range(1, other):
   749             newMV = newMV & self
   750 
   751         return newMV
   752 
   753     def __rpow__(self, other):
   754         """Exponentiation of a real by a multivector
   755                   M
   756         r**M --> r
   757         __rpow__(other) --> MultiVector
   758         """
   759 
   760         # Let math.log() check that other is a Python number, not something
   761         # else.
   762         intMV = math.log(other) & self
   763         # pow(x, y) == exp(y & log(x))
   764 
   765         newMV = self._newMV()  # null
   766 
   767         nextTerm = self._newMV()
   768         nextTerm[()] = 1  # order 0 term of exp(x) Taylor expansion
   769 
   770         n = 1
   771 
   772         while nextTerm != 0:
   773             # iterate until the added term is within _eps of 0
   774             newMV << nextTerm
   775             nextTerm = nextTerm & intMV / n
   776             n = n + 1
   777         else:
   778             # squeeze out that extra little bit of accuracy
   779             newMV << nextTerm
   780 
   781         return newMV
   782         
   783     def __iadd__(self, other):
   784         """In-place addition
   785         
   786         M << N --> M + N
   787         __iadd__(other) --> MultiVector
   788         """
   789         
   790         other, mv = self._checkOther(other)
   791         
   792         self.value = self.value + other.value
   793 
   794         return self
   795         
   796     
   797     # unary
   798 
   799     def __neg__(self):
   800         """Negation
   801         
   802         -M
   803         __neg__() --> MultiVector
   804         """
   805         
   806         newValue = -self.value
   807 
   808         return self._newMV(newValue)
   809 
   810     def __pos__(self):
   811         """Positive (just a copy)
   812 
   813         +M
   814         __pos__(self) --> MultiVector
   815         """
   816         
   817         newValue = self.value + 0  # copy
   818 
   819         return self._newMV(newValue)
   820 
   821     def mag2(self):
   822         """Magnitude (modulus) squared
   823            2
   824         |M|
   825         mag2() --> PyFloat | PyInt
   826         """
   827 
   828         return (~self & self)[()]
   829 
   830     def __abs__(self):
   831         """Magnitude (modulus)
   832         
   833         abs(M) --> |M|
   834         __abs__() --> PyFloat
   835         """
   836         
   837         return np.sqrt(self.mag2())
   838 
   839     def adjoint(self):
   840         """Adjoint / reversion
   841                _
   842         ~M --> M (any one of several conflicting notations)
   843         ~(N & M) --> ~M & ~N
   844         adjoint() --> MultiVector
   845         """
   846         # The multivector created by reversing all multiplications
   847 
   848         grades = np.array(self.layout.gradeList)
   849         signs = np.power(-1, grades*(grades-1)/2)
   850 
   851         newValue = signs * self.value  # elementwise multiplication
   852         
   853         return self._newMV(newValue)
   854 
   855     __invert__ = adjoint
   856 
   857 
   858     # builtin
   859 
   860     def __int__(self):
   861         """Coerce to an integer iff scalar.
   862 
   863         int(M)
   864         __int__() --> PyInt
   865         """
   866         
   867         return int(self.__float__())
   868 
   869     def __long__(self):
   870         """Coerce to a long iff scalar.
   871 
   872         long(M)
   873         __long__() --> PyLong
   874         """
   875         
   876         return long(self.__float__())
   877     
   878     def __float__(self):
   879         """"Coerce to a float iff scalar.
   880 
   881         float(M)
   882         __float__() --> PyFloat
   883         """
   884 
   885         if self.isScalar():
   886             return float(self[()])
   887         else:
   888             raise ValueError, "non-scalar coefficients are non-zero"
   889             
   890 
   891     ## sequence special methods
   892 
   893     def __len__(self):
   894         """Returns length of value array.
   895         
   896         len(M)
   897         __len__() --> PyInt
   898         """
   899         
   900         return self.layout.gaDims
   901 
   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.
   906         
   907         M[blade] --> PyFloat | PyInt
   908         M[index] --> PyFloat | PyInt
   909         __getitem__(key) --> PyFloat | PyInt
   910         """
   911            
   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])]
   918         else:
   919             return self.value[key]
   920 
   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.
   925 
   926         M[blade] = PyFloat | PyInt
   927         M[index] = PyFloat | PyInt
   928         __setitem__(key, value)
   929         """
   930         
   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
   937         else:
   938             self.value[key] = value
   939 
   940     def __delitem__(self, key):
   941         """Set the selected coefficient to 0.
   942         
   943         del M[blade]
   944         del M[index]
   945         __delitem__(key)
   946         """
   947         
   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
   954         else:
   955             self.value[key] = 0
   956 
   957     def __getslice__(self, i, j):
   958         """Return a copy with only the slice non-zero.
   959         
   960         M[i:j]
   961         __getslice__(i, j) --> MultiVector
   962         """
   963         
   964         newMV = self._newMV()
   965         newMV.value[i:j] = self.value[i:j]
   966 
   967         return newMV
   968 
   969     def __setslice__(self, i, j, sequence):
   970         """Paste a sequence into coefficients array.
   971         
   972         M[i:j] = sequence
   973         __setslice__(i, j, sequence)
   974         """
   975         
   976         self.value[i:j] = sequence
   977 
   978     def __delslice__(self, i, j):
   979         """Set slice to zeros.
   980         
   981         del M[i:j]
   982         __delslice__(i, j)
   983         """
   984         
   985         self.value[i:j] = 0
   986 
   987 
   988     ## grade projection
   989 
   990     def __call__(self, grade):
   991         """Return a new multi-vector projected onto the specified grade.
   992 
   993         M(grade) --> <M>
   994                         grade
   995         __call__(grade) --> MultiVector
   996         """
   997         
   998         if grade not in self.layout.gradeList:
   999             raise ValueError, "algebra does not have grade %s" % grade
  1000         
  1001         if not isinstance(grade, int):
  1002             raise ValueError, "grade must be an integer"
  1003 
  1004         mask = np.equal(grade, self.layout.gradeList)
  1005 
  1006         newValue = np.multiply(mask, self.value)
  1007 
  1008         return self._newMV(newValue)
  1009 
  1010     ## fundamental special methods
  1011 
  1012     def __str__(self):
  1013         """Return pretty-printed representation.
  1014 
  1015         str(M)
  1016         __str__() --> PyString
  1017         """
  1018         
  1019         s = ''
  1020         
  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
  1024             if s:
  1025                 seps = (' + ', ' - ')
  1026             else:
  1027                 seps = ('', '-')
  1028                 
  1029             if self.layout.gradeList[i] == 0:
  1030                 # scalar
  1031                 if abs(self.value[i]) >= _eps:
  1032                     if self.value[i] > 0:
  1033                         s = '%s%s%s' % (s, seps[0], self.value[i])
  1034                     else:
  1035                         s = '%s%s%s' % (s, seps[1], -self.value[i])
  1036                         
  1037             else:
  1038                 if abs(self.value[i]) >= _eps:
  1039                     # not a scalar
  1040                     if self.value[i] > 0:
  1041                         s = '%s%s(%s^%s)' % (s, seps[0], self.value[i], 
  1042                                               self.layout.names[i]) 
  1043                     else:
  1044                         s = '%s%s(%s^%s)' % (s, seps[1], -self.value[i], 
  1045                                               self.layout.names[i])
  1046         if s:
  1047             # non-zero
  1048             return s
  1049         else:
  1050             # return scalar 0
  1051             return '0'
  1052     
  1053     def __repr__(self):
  1054         """Return eval-able representation if global _pretty is false.  
  1055         Otherwise, return str(self).
  1056 
  1057         repr(M)
  1058         __repr__() --> PyString
  1059         """
  1060         
  1061         if _pretty:
  1062             return self.__str__()
  1063 
  1064         s = "MultiVector(%s, value=%s)" % \
  1065              (repr(self.layout), list(self.value))
  1066         return s
  1067 
  1068     def __nonzero__(self):
  1069         """Instance is nonzero iff at least one of the coefficients 
  1070         is nonzero.
  1071         
  1072         __nonzero() --> Boolean
  1073         """
  1074 
  1075         nonzeroes = np.absolute(self.value) > _eps
  1076 
  1077         if nonzeroes:
  1078             return True
  1079         else:
  1080             return False
  1081 
  1082     def __cmp__(self, other):
  1083         """Compares two multivectors.
  1084 
  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).  
  1092 
  1093         TODO: rich comparisons.
  1094         
  1095         M == N
  1096         __cmp__(other) --> -1|0|1
  1097         """
  1098 
  1099         other, mv = self._checkOther(other)
  1100         
  1101         if (np.absolute(self.value - other.value) < _eps).all():
  1102             # equal within epsilon
  1103             return 0
  1104         else:
  1105             return cmp(tuple(self.value), tuple(other.value))
  1106 
  1107     def clean(self, eps=None):
  1108         """Sets coefficients whose absolute value is < eps to exactly 0.
  1109 
  1110         eps defaults to the current value of the global _eps.
  1111 
  1112         clean(eps=None)
  1113         """
  1114 
  1115         if eps is None:
  1116             eps = _eps
  1117 
  1118         mask = np.absolute(self.value) > eps
  1119 
  1120         # note element-wise multiplication
  1121         self.value = mask * self.value
  1122 
  1123         return self
  1124 
  1125     def round(self, eps=None):
  1126         """Rounds all coefficients according to Python's rounding rules.
  1127 
  1128         eps defaults to the current value of the global _eps.
  1129 
  1130         round(eps=None)
  1131         """
  1132 
  1133         if eps is None:
  1134             eps = _eps
  1135         
  1136         self.value = np.around(self.value, eps)
  1137 
  1138         return self
  1139 
  1140     ## Geometric Algebraic functions
  1141 
  1142     def lc(self, other):
  1143         """Returns the left-contraction of two multivectors.
  1144 
  1145         M _| N
  1146         lc(other) --> MultiVector
  1147         """
  1148 
  1149         other, mv = self._checkOther(other, coerce=1)
  1150 
  1151         newValue = np.dot(self.value, np.dot(self.layout.lcmt, other.value))
  1152 
  1153         return self._newMV(newValue)
  1154 
  1155     def pseudoScalar(self):
  1156         "Returns a MultiVector that is the pseudoscalar of this space."
  1157 
  1158         psIdx = self.layout.gradeList.index(self.layout.dims)  
  1159         # pick out out blade with grade equal to dims
  1160 
  1161         pScalar = self._newMV()
  1162         pScalar.value[psIdx] = 1
  1163 
  1164         return pScalar
  1165 
  1166     def invPS(self):
  1167         "Returns the inverse of the pseudoscalar of the algebra."
  1168 
  1169         ps = self.pseudoScalar()
  1170 
  1171         return ps.inv()
  1172 
  1173     def isScalar(self):
  1174         """Returns true iff self is a scalar.
  1175         
  1176         isScalar() --> Boolean
  1177         """
  1178 
  1179         indices = range(self.layout.gaDims)
  1180         indices.remove(self.layout.gradeList.index(0))
  1181 
  1182         for i in indices:
  1183             if abs(self.value[i]) < _eps:
  1184                 continue
  1185             else:
  1186                 return False
  1187 
  1188         return True
  1189 
  1190     def isBlade(self):
  1191         """Returns true if multivector is a blade.
  1192 
  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.
  1196 
  1197         isBlade() --> Boolean
  1198         """
  1199 
  1200         grade = None
  1201 
  1202         for i in range(self.layout.gaDims):
  1203             if abs(self.value[i]) > _eps:
  1204                 if grade is None:
  1205                     grade = self.layout.gradeList[i]
  1206                 elif self.layout.gradeList[i] != grade:
  1207                     return 0
  1208 
  1209         return 1
  1210 
  1211     def grades(self):
  1212         """Return the grades contained in the multivector.
  1213 
  1214         grades() --> [ PyInt, PyInt, ... ]
  1215         """
  1216 
  1217         grades = []
  1218 
  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])
  1223 
  1224         return grades
  1225 
  1226     def normal(self):
  1227         """Return the (mostly) normalized multivector.
  1228 
  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
  1232         magnitude to +-1.
  1233         
  1234         M / |M|  up to a sign
  1235         normal() --> MultiVector
  1236         """
  1237 
  1238         return self / np.sqrt(abs(self.mag2()))
  1239         
  1240     def leftLaInv(self):
  1241         """Return left-inverse using a computational linear algebra method 
  1242         proposed by Christian Perwass.
  1243          -1         -1
  1244         M    where M  & M  == 1
  1245         leftLaInv() --> MultiVector
  1246         """
  1247         
  1248         identity = np.zeros((self.layout.gaDims,))
  1249         identity[self.layout.gradeList.index(0)] = 1
  1250 
  1251         intermed = np.dot(self.layout.gmt, self.value)
  1252         intermed = np.transpose(intermed)
  1253 
  1254         if abs(linalg.det(intermed)) < _eps:
  1255             raise ValueError("multivector has no left-inverse")
  1256 
  1257         sol = linalg.solve(intermed, identity)
  1258 
  1259         return self._newMV(sol)
  1260         
  1261     def rightLaInv(self):
  1262         """Return right-inverse using a computational linear algebra method 
  1263         proposed by Christian Perwass.
  1264          -1              -1
  1265         M    where M & M  == 1
  1266         rightLaInv() --> MultiVector
  1267         """
  1268 
  1269         identity = np.zeros((self.layout.gaDims,))
  1270         identity[self.layout.gradeList.index(0)] = 1
  1271 
  1272         intermed = _myDot(self.value, self.layout.gmt)
  1273 
  1274         if abs(linalg.det(intermed)) < _eps:
  1275             raise ValueError("multivector has no right-inverse")
  1276 
  1277         sol = linalg.solve(intermed, identity)
  1278 
  1279         return self._newMV(sol)
  1280 
  1281     def normalInv(self):
  1282         """Returns the inverse of itself if M&~M == |M|**2.
  1283          -1
  1284         M   = ~M / (M & ~M)
  1285         normalInv() --> MultiVector
  1286         """
  1287 
  1288         Madjoint = ~self
  1289         MadjointM = (Madjoint & self)
  1290 
  1291         if MadjointM.isScalar() and abs(MadjointM[()]) > _eps:
  1292             # inverse exists
  1293             return Madjoint / MadjointM[()]
  1294         else:
  1295             raise ValueError, "no inverse exists for this multivector"
  1296 
  1297     leftInv = leftLaInv
  1298     inv = rightInv = rightLaInv
  1299 
  1300     def dual(self, I=None):
  1301         """Returns the dual of the multivector against the given subspace I.
  1302         I defaults to the pseudoscalar.
  1303 
  1304         ~        -1
  1305         M = M * I
  1306         dual(I=None) --> MultiVector
  1307         """
  1308         
  1309         if I is None:
  1310             Iinv = self.layout.invPS()
  1311         else:
  1312             Iinv = I.inv()
  1313         
  1314         return self * Iinv
  1315 
  1316     def commutator(self, other):
  1317         """Returns the commutator product of two multivectors.
  1318 
  1319         [M, N] = M X N = (M&N - N&M)/2
  1320         commutator(other) --> MultiVector
  1321         """
  1322 
  1323         return ((self & other) - (other & self)) / 2
  1324 
  1325     def anticommutator(self, other):
  1326         """Returns the anti-commutator product of two multivectors.
  1327 
  1328         (M&N + N&M)/2
  1329         anticommutator(other) --> MultiVector
  1330         """
  1331 
  1332         return ((self & other) + (other & self)) / 2
  1333 
  1334     def gradeInvol(self):
  1335         """Returns the grade involution of the multivector.
  1336          *                    i
  1337         M  = Sum[i, dims, (-1)  <M>  ]
  1338                                    i
  1339         gradeInvol() --> MultiVector
  1340         """
  1341         
  1342         signs = np.power(-1, self.layout.gradeList)
  1343 
  1344         newValue = signs * self.value
  1345 
  1346         return self._newMV(newValue)
  1347 
  1348     def conjugate(self):
  1349         """Returns the Clifford conjugate (reversion and grade involution).
  1350          *
  1351         M  --> (~M).gradeInvol()
  1352         conjugate() --> MultiVector
  1353         """
  1354 
  1355         return (~self).gradeInvol()
  1356 
  1357     ## Subspace operations
  1358 
  1359     def project(self, other):
  1360         """Projects the multivector onto the subspace represented by this blade.
  1361                             -1
  1362         P (M) = (M _| A) & A
  1363          A
  1364         project(M) --> MultiVector
  1365         """
  1366 
  1367         other, mv = self._checkOther(other, coerce=1)
  1368 
  1369         if not self.isBlade():
  1370             raise ValueError, "self is not a blade"
  1371 
  1372         return other.lc(self) & self.inv()
  1373 
  1374     def basis(self):
  1375         """Finds a vector basis of this subspace.
  1376 
  1377         basis() --> [ MultiVector, MultiVector, ... ]
  1378         """
  1379 
  1380         gr = self.grades()
  1381 
  1382         if len(gr) != 1:
  1383             # FIXME: this is not a sufficient condition for a blade.
  1384             raise ValueError, "self is not a blade"
  1385 
  1386         selfInv = self.inv()
  1387 
  1388         selfInv.clean()
  1389 
  1390         wholeBasis = []  # vector basis of the whole space
  1391 
  1392         for i in range(self.layout.gaDims):
  1393             if self.layout.gradeList[i] == 1:
  1394                 v = np.zeros((self.layout.gaDims,), dtype=float)
  1395                 v[i] = 1.
  1396                 wholeBasis.append(self._newMV(v))
  1397 
  1398         thisBasis = []  # vector basis of this subspace
  1399 
  1400         J, mv = self._checkOther(1.)  # outer product of all of the vectors up 
  1401                              # to the point of iteration
  1402 
  1403         for ei in wholeBasis:
  1404             Pei = ei.lc(self) & selfInv
  1405 
  1406             J.clean()
  1407             
  1408             J2 = J ^ Pei
  1409 
  1410             if J2 != 0:
  1411                 J = J2
  1412                 thisBasis.append(Pei)
  1413                 if len(thisBasis) == gr[0]:  # we have a complete set
  1414                     break
  1415         
  1416         return thisBasis
  1417 
  1418     def join(self, other):
  1419         """Returns the join of two blades.
  1420               .
  1421         J = A ^ B
  1422         join(other) --> MultiVector
  1423         """
  1424 
  1425         other, mv = self._checkOther(other)
  1426 
  1427         grSelf = self.grades()
  1428         grOther = other.grades()
  1429 
  1430         if len(grSelf) == len(grOther) == 1:
  1431             # both blades
  1432             
  1433             # try the outer product first
  1434             J = self ^ other
  1435 
  1436             if J != 0:
  1437                 return J.normal()
  1438 
  1439             # try something else
  1440             M = (other & self.invPS()).lc(self)
  1441 
  1442             if M != 0:
  1443                 C = M.normal()
  1444                 J = (self & C.rightInv()) ^ other
  1445                 return J.normal()
  1446             
  1447             if grSelf[0] >= grOther[0]:
  1448                 A = self
  1449                 B = other
  1450             else:
  1451                 A = other
  1452                 B = self
  1453 
  1454             if (A & B) == (A * B):
  1455                 # B is a subspace of A or the same if grades are equal
  1456                 return A.normal()
  1457 
  1458             # ugly, but general way
  1459             # watch out for residues
  1460 
  1461             # A is still the larger-dimensional subspace
  1462 
  1463             Bbasis = B.basis()
  1464 
  1465             # add the basis vectors of B one by one to the larger 
  1466             # subspace except for the ones that make the outer
  1467             # product vanish
  1468 
  1469             J = A
  1470 
  1471             for ei in Bbasis:
  1472                 J.clean()
  1473                 J2 = J ^ ei
  1474 
  1475                 if J2 != 0:
  1476                     J = J2
  1477             
  1478             # for consistency's sake, we'll normalize the join
  1479             J = J.normal()
  1480                     
  1481             return J
  1482 
  1483         else:
  1484             raise ValueError, "not blades"
  1485 
  1486     def meet(self, other, subspace=None):
  1487         """Returns the meet of two blades.
  1488 
  1489         Computation is done with respect to a subspace that defaults to 
  1490         the join if none is given.
  1491                      -1
  1492         M \/i N = (Mi  ) * N
  1493         meet(other, subspace=None) --> MultiVector
  1494         """
  1495 
  1496         other, mv = self._checkOther(other)
  1497 
  1498         r = self.grades()
  1499         s = other.grades()
  1500 
  1501         if len(r) > 1 or len(s) > 1:
  1502             raise ValueError, "not blades"
  1503 
  1504         if subspace is None:
  1505             subspace = self.join(other)
  1506 
  1507         return (self & subspace.inv()) * other
  1508 
  1509 
  1510 def comb(n, k):
  1511     """\
  1512     Returns /n\\
  1513             \\k/
  1514     
  1515     comb(n, k) --> PyInt
  1516     """
  1517 
  1518     def fact(n):
  1519         if n == 0:
  1520             return 1
  1521         return np.multiply.reduce(range(1, n+1))
  1522 
  1523     return fact(n) / (fact(k) * fact(n-k))
  1524 
  1525 def elements(dims, firstIdx=0):
  1526     """Return a list of tuples representing all 2**dims of blades
  1527     in a dims-dimensional GA.
  1528     
  1529     elements(dims, firstIdx=0) --> bladeList
  1530     """
  1531 
  1532     indcs = range(firstIdx, firstIdx + dims)
  1533     
  1534     blades = [()]
  1535 
  1536     for k in range(1, dims+1):
  1537         # k = grade
  1538 
  1539         if k == 1:
  1540             for i in indcs:
  1541                 blades.append((i,))
  1542             continue
  1543 
  1544         curBladeX = indcs[:k]
  1545 
  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
  1551 
  1552             else:
  1553                 marker = -2
  1554                 tmp = curBladeX[:]  # copy
  1555                 tmp.reverse()
  1556                 
  1557                 # locate where the steady increase begins
  1558                 for j in range(k-1):
  1559                     if tmp[j] - tmp[j+1] == 1:
  1560                         marker = marker - 1
  1561                     else:
  1562                         break
  1563                         
  1564                 if marker < -k:
  1565                     blades.append(tuple(curBladeX))
  1566                     continue
  1567                     
  1568                 # replace
  1569                 blades.append(tuple(curBladeX))
  1570                 curBladeX[marker:] = range(curBladeX[marker]+1, 
  1571                                            curBladeX[marker]+1 - marker)
  1572 
  1573     return blades            
  1574                 
  1575 
  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.
  1578     
  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
  1581     negative.
  1582 
  1583     Cl(p, q=0, names=None, firstIdx=0) --> Layout, {'name': basisElement, ...}
  1584     """
  1585     
  1586     sig = [+1]*p + [-1]*q
  1587     bladeList = elements(p+q, firstIdx)
  1588     
  1589     layout = Layout(sig, bladeList, firstIdx=firstIdx, names=names)
  1590     blades = bases(layout, mvClass)
  1591 
  1592     return layout, blades
  1593 
  1594 
  1595 def bases(layout, mvClass=MultiVector):
  1596     """Returns a dictionary mapping basis element names to their MultiVector
  1597     instances.
  1598 
  1599     bases(layout) --> {'name': baseElement, ...}
  1600     """
  1601     
  1602     dict = {}
  1603     for i in range(layout.gaDims):
  1604         if layout.gradeList[i] != 0:
  1605             v = np.zeros((layout.gaDims,), dtype=int)
  1606             v[i] = 1
  1607             dict[layout.names[i]] = mvClass(layout, v)
  1608     return dict
  1609 
  1610 def randomMV(layout, min=-2.0, max=2.0, grades=None, mvClass=MultiVector,
  1611     uniform=None):
  1612     """Random MultiVector with given layout.
  1613     
  1614     Coefficients are between min and max, and if grades is a list of integers,
  1615     only those grades will be non-zero.
  1616 
  1617     randomMV(layout, min=-2.0, max=2.0, grades=None, uniform=None)
  1618     """
  1619     
  1620     if uniform is None:
  1621         uniform = np.random.uniform
  1622     
  1623     if grades is None:
  1624         return mvClass(layout, uniform(min, max, (layout.gaDims,)))
  1625     else:
  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)
  1631 
  1632 def pretty():
  1633     """Makes repr(M) default to pretty-print.
  1634 
  1635     pretty()
  1636     """
  1637     
  1638     global _pretty
  1639     _pretty = True
  1640 
  1641 def ugly():
  1642     """Makes repr(M) default to eval-able representation.
  1643 
  1644     ugly()
  1645     """
  1646     
  1647     global _pretty
  1648     _pretty = False
  1649 
  1650 def eps(newEps):
  1651     """Set the epsilon for float comparisons.
  1652 
  1653     eps(newEps)
  1654     """
  1655     
  1656     global _eps
  1657     _eps = newEps
  1658 
  1659