diff --git a/README.md b/README.md index a4c015e..024de22 100644 --- a/README.md +++ b/README.md @@ -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) \ No newline at end of file diff --git a/docs.md b/docs.md new file mode 100644 index 0000000..7168cf0 --- /dev/null +++ b/docs.md @@ -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 *x̂*, *ŷ*, 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 *x̂* 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̂* = *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*x̂* - 3*ŷẑ* is a multivector. + * The sum of multiple (and only) 1-blades is usually called a simple *vector*. For example, 3*x̂* + 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: + * (*x̂*)(*aŷ*) = *ax̂ŷ* (linearity, for scalar *a*) + * (*x̂ŷ*)*ẑ* = *x̂*(*ŷẑ*) (associativity) + * *x̂*(*ŷ* + *ẑ*) = *x̂ŷ* + *x̂ẑ* (distributivity) + * (*ŷ* + *ẑ*)(*ax̂*) = *a*(*ŷ* + *ẑ*)(*x̂*) (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*x̂* + 3*ŷ* + 4*x̂ŷ* + 5*ŷẑ*, then ⟨*V*⟩₀ = 1 and ⟨*V*⟩₁ = 2*x̂* + 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:
![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:
![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 *x̂*. +* 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) + +``` \ No newline at end of file diff --git a/multivectors.py b/multivectors.py index 6f34ae4..3a6c283 100644 --- a/multivectors.py +++ b/multivectors.py @@ -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) @@ -34,7 +35,7 @@ 'w' ] -__version__ = '0.1.0' +__version__ = '0.1.1' NAMES = 'xyzw' @@ -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: @@ -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 @@ -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: @@ -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() diff --git a/run_tests.py b/run_tests.py new file mode 100644 index 0000000..ad66883 --- /dev/null +++ b/run_tests.py @@ -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) \ No newline at end of file