Skip to content

Commit

Permalink
Release 0.1.1
Browse files Browse the repository at this point in the history
multivectors.py:
* Small fix in module docstring
+ Blade.__(eq|ne|[lg][te])__
  * __eq__ and __ne__ defined for all objects
  * __[lg][te]__ defined only for scalar blades
* Small fix in Blade.__rpow__ docstring: cannot use isclose on Blade
+ MultiVector.__(eq|ne)__
- doctest.testmod is done elsewhere

README.md:
* Follow module docstring

+docs.md:
* More formal documentation, mostly of geometric algebra
* Contains code too, which is doctested

+run_tests.py:
* Exits with a 1 status if doctesting the module or any MD has failures
* Used for workflow
  • Loading branch information
Kenny2github committed Jun 30, 2021
1 parent 401d02c commit 6468e68
Show file tree
Hide file tree
Showing 4 changed files with 305 additions and 8 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pip install multivectors
>>> v = 2*x + 3*y + 4*z
>>> print(v.rotate(math.pi/2, x * y))
(-3.00x + 2.00y + 4.00z)

```

For more see [the docs](https://github.com/Kenny2github/MultiVectors/wiki)
132 changes: 132 additions & 0 deletions docs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
Welcome to MultiVectors documentation!

- [Concepts](#concepts)
- [Bases and geometric products](#bases-and-geometric-products)
- [Blades and multivectors](#blades-and-multivectors)
- [The choose operator, inner (dot) and outer (wedge) products](#the-choose-operator-inner-dot-and-outer-wedge-products)
- [Euler's formula applied to multivectors](#eulers-formula-applied-to-multivectors)
- [Applied](#applied)
- [Blade](#blade)
- [MultiVector](#multivector)

## Concepts
Here are some concepts to bear in mind. This is a *very* brief introduction to geometric algebra; a longer one is [here](https://www.youtube.com/watch?v=60z_hpEAtD8).

### Bases and geometric products
* Every dimension of space comes with a *basis vector*: an arrow of length 1 unit pointed towards the positive end of the axis.
* In our 3-dimensional world, there are the basis vectors **, **, and **, which are pointed towards the positive ends of the *x*, *y*, and *z* axes respectively.
* The fourth dimensional basis vector is **. In higher dimensions, usually all bases are numbered instead of lettered: the fifth dimensional basis vectors are *ê₁*, *ê₂*, *ê₃*, *ê₄*, and *ê₅*.
* The *geometric product* of two basis vectors is their simple multiplication - not the dot or cross product! The geometric product of ** and ** is simply *x̂ŷ*.
* The geometric product of two basis vectors is a *basis plane*. *x̂ŷ* is the basis plane of the *x-y* plane. The other basis planes are *ŷẑ* and *x̂ẑ*.
* The geometric product of three basis vectors is a *basis volume*. *x̂ŷẑ* is the basis volume of 3D space, which only has one basis volume, but also one of the four basis volumes of 4D space.
* The geometric product of a basis vector with itself is 1. That is, *x̂x̂* = **² = *ŷŷ* = **² = *ẑẑ* = **² = 1
* The geometric product of different basis vectors *anticommutes*: *x̂ŷ* = -*ŷx̂* and *x̂ŷẑ* = -*x̂ẑŷ* = *ẑx̂ŷ* = -*ẑŷx̂*

### Blades and multivectors
* A *blade* is a *scaled basis*: a *scalar* (regular real number) multiplied by a *basis*. For example, 3*x̂ŷ* is a blade. Note that this means all bases are blades scaled by 1.
* A *k-blade* is a blade of *grade k*: the geometric product of a scalar and *k* different basis vectors. 3*x̂ŷ* has grade 2; it is a 2-blade.
* Scalars are 0-blades - blades consisting of *no* basis vectors.
* A *multivector* is a sum of multiple blades. For example, 1 + 2** - 3*ŷẑ* is a multivector.
* The sum of multiple (and only) 1-blades is usually called a simple *vector*. For example, 3** + 2** is a vector.
* The sum of multiple (and only) 2-blades is a *bivector*. Basis planes are also known as *basis bivectors*. For example, 3*x̂ŷ* is a bivector.
* The rules of linearity, associativity and distributivity in multiplication apply, as long as order of arguments is maintained:
* (**)(*aŷ*) = *ax̂ŷ* (linearity, for scalar *a*)
* (*x̂ŷ*)** = **(*ŷẑ*) (associativity)
* **(** + **) = *x̂ŷ* + *x̂ẑ* (distributivity)
* (** + **)(*ax̂*) = *a*(** + **)(**) (linearity) = a(*ŷx̂* + *ẑx̂*) (distributivity) = a(-*x̂ŷ* - *x̂ẑ*) (anticommutativity)
* However, some things which require commutativity break down, such as the binomial theorem.

### The choose operator, inner (dot) and outer (wedge) products
**V*⟩ₙ *chooses* all *n*-blades from the multivector *V*. For example, if *V* = 1 + 2** + 3** + 4*x̂ŷ* + 5*ŷẑ*, then ⟨*V*⟩₀ = 1 and ⟨*V*⟩₁ = 2** + 3** and ⟨*V*⟩₂ = 4*x̂ŷ* + 5*ŷẑ*
* *U* · *V* = ⟨*UV*⟩ₙ where *U* is of grade *r*, *V* is of grade *s*, and *n* = |*r - s*|. This is the *inner* or *dot product*.
* The dot product associates and distributes the same way the geometric product does.
* From this, for arbitrary vectors *ax̂* + *bŷ* and *cx̂* + *dŷ*, we recover the typical meaning of the dot product:<br/>![Derivation of normal vector dot product](https://cdn.discordapp.com/attachments/417244106876780544/859663520747356170/dot_product.png)
* *U**V* = ⟨*UV*⟩ₙ where *U* is of grade *r*, *V* is of grade *s*, and *n* = *r + s*. This is the *outer* or *wedge product*.
* The outer product associates and distributes the same way the geometric product does.
* From this, for arbitrary vectors *ax̂* + *bŷ* + *cẑ* and *dx̂* + *eŷ* + *fẑ*, we recover something that looks very much like a cross product:<br/>![Derivation of normal vector cross product](https://cdn.discordapp.com/attachments/417244106876780544/859663658762108968/wedge_product.png)

### Euler's formula applied to multivectors
* *e* to the power (*θB*) = cos(*θ*) + *B* sin(*θ*) where θ is a scalar in radians and *B* is a basis multivector.
* For reasons that are beyond my power to explain, the rotation of a multivector *V* by *θ* through the plane *B* is e\*\*(-*θB*/2) * V * e\*\*(*θB*/2)

## Applied
All of the above concepts are applied in this library.

### Blade
* `multivectors.Blade(*bases, scalar=a)` represents a blade with **0-indexed bases** `bases` multiplied by a real scalar `a`. For example, `Blade(0, 1, 3)` represents the basis volume *x̂ŷŵ*, with 0 meaning **.
* Basis names can be *swizzled* on the `Blade` class itself: the above could have been done with `Blade.xyw`. For basis vectors beyond **, use **ₙ: `Blade.e1e3e4e5` is a basis 4-vector in 5D space. `Blade._` is a 0-blade: a scalar, but with `Blade` type.
* Basis indices can also be used: `Blade[:4]` = `Blade[0, 1, 2, 3]` = `Blade(0, 1, 2, 3)` = `Blade.xyzw`
* To take it one step further, basis names can be swizzled on the module itself: `multivector.xyz` returns `multivectors.Blade.xyz`. This also works when importing: `from multivectors import x, y, z, xy, xz, yz` will work just fine.
* The rules of arithmetic with blades as described above apply:
```python
>>> from multivectors import x, y, z
>>> x * 2 + x * 3
5.0 * Blade.x
>>> x * 5 * y
5.0 * Blade.xy
>>> (x * y) * z == x * (y * z)
True

```
* You can query the grade of a blade: `Blade.xy.grade` is 2.

### MultiVector
* `multivectors.MultiVector.from_terms(*terms)` represents a **sum of `terms`**. You normally should not be constructing this class; it is created from summation involving `Blade`s.
* Basis names can be swizzled on class **instances** to get the coefficient of that basis:
```python
>>> from multivectors import x, y
>>> (x + 2*y).x
1.0

```
* Basis indices can also be used:
```python
>>> from multivectors import xy, yz
>>> (xy + 2*yz)[0, 1]
1.0

```
* Choosing by grade is supported:
```python
>>> from multivectors import x, y, xy, yz
>>> V = 1 + 2*x + 3*y + 4*xy + 5*yz
>>> V % 0
1.0
>>> V % 1
(2.0 * Blade.x + 3.0 * Blade.y)
>>> V % 2
(4.0 * Blade.xy + 5.0 * Blade.yz)

```
* The rules of arithmetic with multivectors as described above apply:
```python
>>> from multivectors import x, y, z
>>> x * (y + z)
(1.0 * Blade.xy + 1.0 * Blade.xz)
>>> (y + z) * (2 * x)
(-2.0 * Blade.xy + -2.0 * Blade.xz)
>>> (1*x + 2*y) * (3*x + 4*y)
(11.0 + -2.0 * Blade.xy)

```
* The extra products apply too:
```python
>>> from multivectors import x, y, z
>>> 1*3 + 2*4
11
>>> (1*x + 2*y) @ (3*x + 4*y)
11.0
>>> (1*5 - 2*4, 1*6 - 3*4, 2*6 - 3*5)
(-3, -6, -3)
>>> (1*x + 2*y + 3*z) ^ (4*x + 5*y + 6*z)
(-3.0 * Blade.xy + -6.0 * Blade.xz + -3.0 * Blade.yz)

```
* A convenience method is provided to rotate multivectors:
```python
>>> from math import radians
>>> from multivectors import x, y, z, xz
>>> round((3*x + 2*y + 4*z).rotate(radians(90), xz), 2)
(-4.0 * Blade.x + 2.0 * Blade.y + 3.0 * Blade.z)

```
171 changes: 163 additions & 8 deletions multivectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
>>> v = 2*x + 3*y + 4*z
>>> print(v.rotate(math.pi/2, x * y))
(-3.00x + 2.00y + 4.00z)
```
For more see [the docs](https://github.com/Kenny2github/MultiVectors/wiki)
Expand All @@ -34,7 +35,7 @@
'w'
]

__version__ = '0.1.0'
__version__ = '0.1.1'

NAMES = 'xyzw'

Expand Down Expand Up @@ -328,6 +329,129 @@ def __str__(self) -> str:
r = ''.join(NAMES[i] for i in self.bases)
return '%.2f%s' % (self.scalar, r)

# Relational operators

def __eq__(self, other: Simple) -> bool:
"""Compare equality of two objects.
Returns: True if this is a scalar blade equal to the real.
Returns: True if this blade's bases and scalar equal the other's.
Returns: False for all other cases or types.
```python
>>> Blade._ * 1 == 1
True
>>> Blade.xy == Blade.xy
True
>>> Blade.xy == Blade.x + Blade.y
False
```
"""
if isinstance(other, Real):
if self.bases != ():
return False
return self.scalar == other
if isinstance(other, Blade):
return (self.bases, self.scalar) == (other.bases, other.scalar)
return False

def __ne__(self, other: Simple) -> bool:
"""Compare inequality of two objects.
Returns: False if this is a scalar blade equal to the real.
Returns: False if this blade's bases and scalar equal the other's.
Returns: True for all other cases or types.
```python
>>> Blade._ * 1 != 2
True
>>> Blade.xy * 1 != Blade.xy * 2
True
>>> Blade.xy != Blade.x + Blade.y
True
```
"""
return not (self == other)

def __lt__(self, other: Real) -> bool:
"""Compare this blade less than an object.
Returns: True if this is a scalar blade less than the real.
Returns: NotImplemented for all other types.
```python
>>> Blade._ * 1 < 2
True
>>> Blade.x * 1 < 2
Traceback (most recent call last):
...
TypeError: '<' not supported between instances of 'Blade' and 'int'
```
"""
if not (isinstance(other, Real) and self.bases == ()):
return NotImplemented
return self.scalar < other

def __gt__(self, other: Real) -> bool:
"""Compare this blade greater than an object.
Returns: True if this is a scalar blade greater than the real.
Returns: NotImplemented for all other types.
```python
>>> Blade._ * 2 > 1
True
>>> Blade.x * 2 > 1
Traceback (most recent call last):
...
TypeError: '>' not supported between instances of 'Blade' and 'int'
"""
if not (isinstance(other, Real) and self.bases == ()):
return NotImplemented
return self != other and not (self < other)

def __le__(self, other: Real) -> bool:
"""Compare this blade less than or equal to an object.
Returns: True if this is a scalar blade not greater than the real.
Returns: NotImplemented for all other types.
```python
>>> Blade._ * 1 <= 2
True
>>> Blade.x * 1 <= 2
Traceback (most recent call last):
...
TypeError: '<=' not supported between instances of 'Blade' and 'int'
"""
if not (isinstance(other, Real) and self.bases == ()):
return NotImplemented
return self < other or self == other

def __ge__(self, other: Real) -> bool:
"""Compare this blade greater than or equal to an object.
Returns: True if this is a scalar blade not less than the real.
Returns: NotImplemented for all other types.
```python
>>> Blade._ * 2 >= 1
True
>>> Blade.x * 2 >= 1
Traceback (most recent call last):
...
TypeError: '>=' not supported between instances of 'Blade' and 'int'
"""
if not (isinstance(other, Real) and self.bases == ()):
return NotImplemented
return not (self < other)

# Binary operators

def __add__(self, other: MVV) -> MV:
Expand Down Expand Up @@ -571,12 +695,11 @@ def __rpow__(self, other: Real) -> Simple:
= cos(a ln x) + sin(a ln x) * I
```python
>>> from math import e, pi
>>> from cmath import isclose
>>> from cmath import e, pi, isclose
>>> round(e ** (pi / 4 * Blade.xy), 2)
(0.71 + 0.71 * Blade.xy)
>>> # in 2D, xy is isomorphic to i
>>> isclose(e ** (pi * Blade.xy), -1)
>>> round(e ** (pi * Blade.xy), 9) == -1
True
>>> isclose(e ** (pi * 1j), -1)
True
Expand Down Expand Up @@ -925,6 +1048,42 @@ def __str__(self) -> str:
"""
return '(' + ' + '.join(map(str, self.terms)) + ')'

# Relational operators

def __eq__(self, other: MultiVector) -> bool:
"""Compare equality of two objects.
Returns: True if all terms of this multivector are equal to the other.
Returns: False for all other cases or types.
```python
>>> Blade.x + Blade.y == Blade.y + Blade.x
True
>>> Blade.x + 2 * Blade.y == 2 * Blade.x + Blade.y
False
```
"""
if not isinstance(other, MultiVector):
return False
return self.termdict == other.termdict

def __ne__(self, other: MultiVector) -> bool:
"""Compare inequality of two objects.
Returns: False if all terms of this multivector are equal to the other.
Returns: True for all other cases or types.
```python
>>> Blade.x + Blade.y != Blade.y + Blade.x
False
>>> Blade.x + 2 * Blade.y != 2 * Blade.x + Blade.y
True
```
"""
return not (self == other)

# Binary operators

def __add__(self, other: MVV) -> MV:
Expand Down Expand Up @@ -1361,7 +1520,3 @@ def __getattr__(name: str) -> Blade:
Unlike Blade.__getattr__, this rejects invalid characters in the name.
"""
return Blade(*names_to_idxs(name, True))

if __name__ == "__main__":
import doctest
doctest.testmod()
9 changes: 9 additions & 0 deletions run_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import sys
import doctest
import multivectors

fails = doctest.testmod(multivectors)[0]
fails += doctest.testfile('docs.md')[0]
fails += doctest.testfile('README.md')[0]
if fails > 0:
sys.exit(1)

0 comments on commit 6468e68

Please sign in to comment.