├── .github └── workflows │ ├── publish-to-pypi.yml │ └── publish-to-testpypi.yml ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── LICENSE.txt ├── README.md ├── diagram ├── diagram.html ├── diagram.png ├── diagram.svg └── raw_diagram.png ├── examples_to_markdown.py ├── hypercomplex ├── __init__.py ├── examples.py ├── hypercomplex.py └── test_hypercomplex.py ├── setup.py └── tox.ini /.github/workflows/publish-to-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Build and Upload Python Package to PyPI if Tagged 2 | 3 | on: create 4 | 5 | jobs: 6 | publish-to-pypi: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Check out repo 10 | uses: actions/checkout@v2 11 | - name: Set up Python 12 | uses: actions/setup-python@v2 13 | with: 14 | python-version: "3.9" 15 | - name: Install wheel 16 | run: python -m pip install wheel --user 17 | - name: Install setuptools 18 | run: python -m pip install setuptools --user 19 | - name: Build a binary wheel and a source tarball 20 | run: python setup.py sdist bdist_wheel 21 | - name: Upload to PyPI 22 | uses: pypa/gh-action-pypi-publish@release/v1 23 | with: 24 | user: __token__ 25 | password: ${{ secrets.PYPI_API_TOKEN }} 26 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-testpypi.yml: -------------------------------------------------------------------------------- 1 | name: Build and Upload Python Package to TestPyPI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | publish-to-test-pypi: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check out repo 13 | uses: actions/checkout@v2 14 | - name: Set up Python 15 | uses: actions/setup-python@v2 16 | with: 17 | python-version: "3.9" 18 | - name: Install wheel 19 | run: python -m pip install wheel --user 20 | - name: Install setuptools 21 | run: python -m pip install setuptools --user 22 | - name: Build a binary wheel and a source tarball 23 | run: python setup.py sdist bdist_wheel 24 | - name: Upload to TestPyPI 25 | uses: pypa/gh-action-pypi-publish@release/v1 26 | with: 27 | user: __token__ 28 | password: ${{ secrets.TEST_PYPI_API_TOKEN }} 29 | repository_url: https://test.pypi.org/legacy/ 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | MANIFEST 2 | desktop.ini 3 | tempCodeRunnerFile.py 4 | __pycache__/ 5 | hypercomplex.egg-info/ 6 | dist/ 7 | build/ 8 | examples.md 9 | .idea/ 10 | .tox/ -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: Current File", 9 | "type": "python", 10 | "request": "launch", 11 | "program": "${file}", 12 | "console": "integratedTerminal" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Cayley", 4 | "chingon", 5 | "chingons", 6 | "hypercomplex", 7 | "imag", 8 | "octonion", 9 | "octonions", 10 | "pathion", 11 | "pathions", 12 | "quaternion", 13 | "quaternions", 14 | "routon", 15 | "routons", 16 | "sedenion", 17 | "sedenions", 18 | "voudon", 19 | "voudons" 20 | ], 21 | "python.pythonPath": "C:\\Users\\r\\AppData\\Local\\Programs\\Python\\Python39\\python.exe", 22 | "python.formatting.autopep8Args": ["--ignore=E402"] 23 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 discretegames 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hypercomplex 2 | 3 | **A Python library for working with quaternions, octonions, sedenions, and beyond following the Cayley-Dickson construction of hypercomplex numbers.** 4 | 5 | The [complex numbers](https://en.wikipedia.org/wiki/Complex_number) may be viewed as an extension of the everyday [real numbers](https://en.wikipedia.org/wiki/Real_number). A complex number has two real-number coefficients, one multiplied by 1, the other multiplied by [i](https://en.wikipedia.org/wiki/Imaginary_unit). 6 | 7 | In a similar way, a [quaternion](https://en.wikipedia.org/wiki/Quaternion), which has 4 components, can be constructed by combining two complex numbers. Likewise, two quaternions can construct an [octonion](https://en.wikipedia.org/wiki/Octonion) (8 components), and two octonions can construct a [sedenion](https://en.wikipedia.org/wiki/Sedenion) (16 components). 8 | 9 | The method for this construction is known as the [Cayley-Dickson construction][2] and the resulting classes of numbers are types of [hypercomplex numbers][1]. There is no limit to the number of times you can repeat the Cayley-Dickson construction to create new types of hypercomplex numbers, doubling the number of components each time. 10 | 11 | This Python 3 package allows the creation of number classes at any repetition level of Cayley-Dickson constructions, and has built-ins for the lower, named levels such as quaternion, octonion, and sedenion. 12 | 13 | [![Hypercomplex numbers containment diagram][8]][8] 14 | 15 | ## Installation 16 | 17 | ```text 18 | pip install hypercomplex 19 | ``` 20 | 21 | [View on PyPI](https://pypi.org/project/hypercomplex) - [View on GitHub](https://github.com/discretegames/hypercomplex) 22 | 23 | This package was built in Python 3.9.6 and has been tested to be compatible with python 3.6 through 3.10. 24 | 25 | ## Basic Usage 26 | 27 | ```py 28 | from hypercomplex import Complex, Quaternion, Octonion, Voudon, cayley_dickson_construction 29 | 30 | c = Complex(0, 7) 31 | print(c) # -> (0 7) 32 | print(c == 7j) # -> True 33 | 34 | q = Quaternion(1.1, 2.2, 3.3, 4.4) 35 | print(2 * q) # -> (2.2 4.4 6.6 8.8) 36 | 37 | print(Quaternion.e_matrix()) # -> e0 e1 e2 e3 38 | # e1 -e0 e3 -e2 39 | # e2 -e3 -e0 e1 40 | # e3 e2 -e1 -e0 41 | 42 | o = Octonion(0, 0, 0, 0, 8, 8, 9, 9) 43 | print(o + q) # -> (1.1 2.2 3.3 4.4 8 8 9 9) 44 | 45 | v = Voudon() 46 | print(v == 0) # -> True 47 | print(len(v)) # -> 256 48 | 49 | BeyondVoudon = cayley_dickson_construction(Voudon) 50 | print(len(BeyondVoudon())) # -> 512 51 | ``` 52 | 53 | For more snippets see the Thorough Usage Examples section below. 54 | 55 | ## Package Contents 56 | 57 | Three functions form the core of the package: 58 | 59 | - `reals(base)` - Given a base type (`float` by default), generates a class that represents numbers with 1 hypercomplex dimension, i.e. [real numbers](https://en.wikipedia.org/wiki/Real_number). This class can then be extended into complex numbers and beyond with `cayley_dickson_construction`. 60 | 61 | Any usual math operations on instances of the class returned by `reals` behave as instances of `base` would but their type remains the reals class. By default they are printed with the `g` [format-spec][7] and surrounded by parentheses, e.g. `(1)`, to remain consistent with the format of higher dimension hypercomplex numbers. 62 | 63 | Python's [`decimal.Decimal`](https://docs.python.org/3/library/decimal.html) might be another likely choice for `base`. 64 | 65 | ```py 66 | # reals example: 67 | from hypercomplex import reals 68 | from decimal import Decimal 69 | 70 | D = reals(Decimal) 71 | print(D(10) / 4) # -> (2.5) 72 | print(D(3) * D(9)) # -> (27) 73 | ``` 74 | 75 | - `cayley_dickson_construction(basis)` (alias `cd_construction`) generates a new class of hypercomplex numbers with twice the dimension of the given `basis`, which must be another hypercomplex number class or class returned from `reals`. The new class of numbers is defined recursively on the basis according the [Cayley-Dickson construction][2]. Normal math operations may be done upon its instances and with instances of other numeric types. 76 | 77 | ```py 78 | # cayley_dickson_construction example: 79 | from hypercomplex import * 80 | RealNum = reals() 81 | ComplexNum = cayley_dickson_construction(RealNum) 82 | QuaternionNum = cayley_dickson_construction(ComplexNum) 83 | 84 | q = QuaternionNum(1, 2, 3, 4) 85 | print(q) # -> (1 2 3 4) 86 | print(1 / q) # -> (0.0333333 -0.0666667 -0.1 -0.133333) 87 | print(q + 1+2j) # -> (2 4 3 4) 88 | ``` 89 | 90 | - `cayley_dickson_algebra(level, base)` (alias `cd_algebra`) is a helper function that repeatedly applies `cayley_dickson_construction` to the given `base` type (`float` by default) `level` number of times. That is, `cayley_dickson_algebra` returns the class for the Cayley-Dickson algebra of hypercomplex numbers with `2**level` dimensions. 91 | 92 | ```py 93 | # cayley_dickson_algebra example: 94 | from hypercomplex import * 95 | OctonionNum = cayley_dickson_algebra(3) 96 | 97 | o = OctonionNum(8, 7, 6, 5, 4, 3, 2, 1) 98 | print(o) # -> (8 7 6 5 4 3 2 1) 99 | print(2 * o) # -> (16 14 12 10 8 6 4 2) 100 | print(o.conjugate()) # -> (8 -7 -6 -5 -4 -3 -2 -1) 101 | ``` 102 | 103 | For convenience, nine internal number types are already defined, built off of each other: 104 | 105 | | Name | Aliases | Description | 106 | | ------------ | --------------------- | ----------------------------------------------------------------------------------------------------------------- | 107 | | `Real` | `R`, `CD1`, `CD[0]` | [Real numbers](https://en.wikipedia.org/wiki/Real_number) with 1 hypercomplex dimension based on `float`. | 108 | | `Complex` | `C`, `CD2`, `CD[1]` | [Complex numbers](https://en.wikipedia.org/wiki/Complex_number) with 2 hypercomplex dimensions based on `Real`. | 109 | | `Quaternion` | `Q`, `CD4`, `CD[2]` | [Quaternion numbers](https://en.wikipedia.org/wiki/Quaternion) with 4 hypercomplex dimensions based on `Complex`. | 110 | | `Octonion` | `O`, `CD8`, `CD[3]` | [Octonion numbers](https://en.wikipedia.org/wiki/Octonion) with 8 hypercomplex dimensions based on `Quaternion`. | 111 | | `Sedenion` | `S`, `CD16`, `CD[4]` | [Sedenion numbers](https://en.wikipedia.org/wiki/Sedenion) with 16 hypercomplex dimensions based on `Octonion`. | 112 | | `Pathion` | `P`, `CD32`, `CD[5]` | Pathion numbers with 32 hypercomplex dimensions based on `Sedenion`. | 113 | | `Chingon` | `X`, `CD64`, `CD[6]` | Chingon numbers with 64 hypercomplex dimensions based on `Pathion`. | 114 | | `Routon` | `U`, `CD128`, `CD[7]` | Routon numbers with 128 hypercomplex dimensions based on `Chingon`. | 115 | | `Voudon` | `V`, `CD256`, `CD[8]` | Voudon numbers with 256 hypercomplex dimensions based on `Routon`. | 116 | 117 | ```py 118 | # built-in types example: 119 | from hypercomplex import * 120 | print(Real(4)) # -> (4) 121 | print(C(3-7j)) # -> (3 -7) 122 | print(CD4(.1, -2.2, 3.3e3)) # -> (0.1 -2.2 3300 0) 123 | print(CD[3](1, 0, 2, 0, 3)) # -> (1 0 2 0 3 0 0 0) 124 | ``` 125 | 126 | The names and letter-abbreviations were taken from [this image][3] ([mirror][4]) found in Micheal Carter's paper [_Visualization of the Cayley-Dickson Hypercomplex Numbers Up to the Chingons (64D)_](https://www.mapleprimes.com/posts/124913-Visualization-Of-The-CayleyDickson), but they also may be known according to their [Latin naming conventions][6]. 127 | 128 | ## Thorough Usage Examples 129 | 130 | This list follows [examples.py](https://github.com/discretegames/hypercomplex/blob/main/hypercomplex/examples.py) exactly and documents nearly all the things you can do with the hypercomplex numbers created by this package. 131 | 132 | Every example assumes the appropriate imports are already done, e.g. `from hypercomplex import *`. 133 | 134 | 1. Initialization can be done in various ways, including using Python's built in complex numbers. Unspecified coefficients become 0. 135 | 136 | ```py 137 | print(R(-1.5)) # -> (-1.5) 138 | print(C(2, 3)) # -> (2 3) 139 | print(C(2 + 3j)) # -> (2 3) 140 | print(Q(4, 5, 6, 7)) # -> (4 5 6 7) 141 | print(Q(4 + 5j, C(6, 7), pair=True)) # -> (4 5 6 7) 142 | print(P()) # -> (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0) 143 | ``` 144 | 145 | 2. Numbers can be added and subtracted. The result will be the type with more dimensions. 146 | 147 | ```py 148 | print(Q(0, 1, 2, 2) + C(9, -1)) # -> (9 0 2 2) 149 | print(100.1 - O(0, 0, 0, 0, 1.1, 2.2, 3.3, 4.4)) # -> (100.1 0 0 0 -1.1 -2.2 -3.3 -4.4) 150 | ``` 151 | 152 | 3. Numbers can be multiplied. The result will be the type with more dimensions. 153 | 154 | ```py 155 | print(10 * S(1, 2, 3)) # -> (10 20 30 0 0 0 0 0 0 0 0 0 0 0 0 0) 156 | print(Q(1.5, 2.0) * O(0, -1)) # -> (2 -1.5 0 0 0 0 0 0) 157 | 158 | # notice quaternions are non-commutative 159 | print(Q(1, 2, 3, 4) * Q(1, 0, 0, 1)) # -> (-3 5 1 5) 160 | print(Q(1, 0, 0, 1) * Q(1, 2, 3, 4)) # -> (-3 -1 5 5) 161 | ``` 162 | 163 | 4. Numbers can be divided and `inverse` gives the multiplicative inverse. 164 | 165 | ```py 166 | print(100 / C(0, 2)) # -> (0 -50) 167 | print(C(2, 2) / Q(1, 2, 3, 4)) # -> (0.2 -0.0666667 0.0666667 -0.466667) 168 | print(C(2, 2) * Q(1, 2, 3, 4).inverse()) # -> (0.2 -0.0666667 0.0666667 -0.466667) 169 | print(R(2).inverse(), 1 / R(2)) # -> (0.5) (0.5) 170 | ``` 171 | 172 | 5. Numbers can be raised to integer powers, a shortcut for repeated multiplication or division. 173 | 174 | ```py 175 | q = Q(0, 3, 4, 0) 176 | print(q**5) # -> (0 1875 2500 0) 177 | print(q * q * q * q * q) # -> (0 1875 2500 0) 178 | print(q**-1) # -> (0 -0.12 -0.16 0) 179 | print(1 / q) # -> (0 -0.12 -0.16 0) 180 | print(q**0) # -> (1 0 0 0) 181 | ``` 182 | 183 | 6. `conjugate` gives the conjugate of the number. 184 | 185 | ```py 186 | print(R(9).conjugate()) # -> (9) 187 | print(C(9, 8).conjugate()) # -> (9 -8) 188 | print(Q(9, 8, 7, 6).conjugate()) # -> (9 -8 -7 -6) 189 | ``` 190 | 191 | 7. `norm` gives the absolute value as the base type (`float` by default). There is also `norm_squared`. 192 | 193 | ```py 194 | print(O(3, 4).norm(), type(O(3, 4).norm())) # -> 5.0 195 | print(abs(O(3, 4))) # -> 5.0 196 | print(O(3, 4).norm_squared()) # -> 25.0 197 | ``` 198 | 199 | 8. Numbers are considered equal if their coefficients all match. Non-existent coefficients are 0. 200 | 201 | ```py 202 | print(R(999) == V(999)) # -> True 203 | print(C(1, 2) == Q(1, 2)) # -> True 204 | print(C(1, 2) == Q(1, 2, 0.1)) # -> False 205 | ``` 206 | 207 | 9. `coefficients` gives a tuple of the components of the number in their base type (`float` by default). The properties `real` and `imag` are shortcuts for the first two components. Indexing can also be used (but is inefficient). 208 | 209 | ```py 210 | print(R(100).coefficients()) # -> (100.0,) 211 | q = Q(2, 3, 4, 5) 212 | print(q.coefficients()) # -> (2.0, 3.0, 4.0, 5.0) 213 | print(q.real, q.imag) # -> 2.0 3.0 214 | print(q[0], q[1], q[2], q[3]) # -> 2.0 3.0 4.0 5.0 215 | ``` 216 | 217 | 10. `e(index)` of a number class gives the unit hypercomplex number where the index coefficient is 1 and all others are 0. 218 | 219 | ```py 220 | print(C.e(0)) # -> (1 0) 221 | print(C.e(1)) # -> (0 1) 222 | print(O.e(3)) # -> (0 0 0 1 0 0 0 0) 223 | ``` 224 | 225 | 11. `e_matrix` of a number class gives the multiplication table of `e(i)*e(j)`. Set `string=False` to get a 2D list instead of a string. Set `raw=True` to get the raw hypercomplex numbers. 226 | 227 | ```py 228 | print(O.e_matrix()) # -> e1 e2 e3 e4 e5 e6 e7 229 | # -e0 e3 -e2 e5 -e4 -e7 e6 230 | # -e3 -e0 e1 e6 e7 -e4 -e5 231 | # e2 -e1 -e0 e7 -e6 e5 -e4 232 | # -e5 -e6 -e7 -e0 e1 e2 e3 233 | # e4 -e7 e6 -e1 -e0 -e3 e2 234 | # e7 e4 -e5 -e2 e3 -e0 -e1 235 | # -e6 e5 e4 -e3 -e2 e1 -e0 236 | # 237 | print(C.e_matrix(string=False, raw=True)) # -> [[(1 0), (0 1)], [(0 1), (-1 0)]] 238 | ``` 239 | 240 | 12. A number is considered truthy if it has has non-zero coefficients. Conversion to `int`, `float` and `complex` are only valid when the coefficients beyond the dimension of those types are all 0. 241 | 242 | ```py 243 | print(bool(Q())) # -> False 244 | print(bool(Q(0, 0, 0.01, 0))) # -> True 245 | 246 | print(complex(Q(5, 5))) # -> (5+5j) 247 | print(int(V(9.9))) # -> 9 248 | # print(float(C(1, 2))) <- invalid 249 | ``` 250 | 251 | 13. Any usual format spec for the base type can be given in an f-string. 252 | 253 | ```py 254 | o = O(0.001, 1, -2, 3.3333, 4e5) 255 | print(f"{o:.2f}") # -> (0.00 1.00 -2.00 3.33 400000.00 0.00 0.00 0.00) 256 | print(f"{R(23.9):04.0f}") # -> (0024) 257 | ``` 258 | 259 | 14. The `len` of a number is its hypercomplex dimension, i.e. the number of components or coefficients it has. 260 | 261 | ```py 262 | print(len(R())) # -> 1 263 | print(len(C(7, 7))) # -> 2 264 | print(len(U())) # -> 128 265 | ``` 266 | 267 | 15. Using `in` behaves the same as if the number were a tuple of its coefficients. 268 | 269 | ```py 270 | print(3 in Q(1, 2, 3, 4)) # -> True 271 | print(5 in Q(1, 2, 3, 4)) # -> False 272 | ``` 273 | 274 | 16. `copy` can be used to duplicate a number (but should generally never be needed as all operations create a new number). 275 | 276 | ```py 277 | x = O(9, 8, 7) 278 | y = x.copy() 279 | print(x == y) # -> True 280 | print(x is y) # -> False 281 | ``` 282 | 283 | 17. `base` on a number class will return the base type the entire numbers are built upon. 284 | 285 | ```py 286 | print(R.base()) # -> 287 | print(V.base()) # -> 288 | A = cayley_dickson_algebra(20, int) 289 | print(A.base()) # -> 290 | ``` 291 | 292 | 18. Hypercomplex numbers are weird, so be careful! Here two non-zero sedenions multiply to give zero because sedenions and beyond have zero divisors. 293 | 294 | ```py 295 | s1 = S.e(5) + S.e(10) 296 | s2 = S.e(6) + S.e(9) 297 | print(s1) # -> (0 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0) 298 | print(s2) # -> (0 0 0 0 0 0 1 0 0 1 0 0 0 0 0 0) 299 | print(s1 * s2) # -> (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0) 300 | print((1 / s1) * (1 / s2)) # -> (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0) 301 | # print(1/(s1 * s2)) <- zero division error 302 | ``` 303 | 304 | ## About 305 | 306 | I wrote this package for the novelty of it and as a math and programming exercise. The operations it can perform on hypercomplex numbers are not particularly efficient due to the recursive nature of the Cayley-Dickson construction. 307 | 308 | I am not a mathematician, only a math hobbyist, and apologize if there are issues with the implementations or descriptions I have provided. 309 | 310 | [1]: https://en.wikipedia.org/wiki/Hypercomplex_number 311 | [2]: https://en.wikipedia.org/wiki/Cayley%E2%80%93Dickson_construction 312 | [3]: https://www.mapleprimes.com/DocumentFiles/124913/419426/Figure1.JPG 313 | [4]: https://github.com/discretegames/hypercomplex/blob/ed3c47fb909e85736b7b5a147a39981e6e87fa57/hypercomplex_names.png 314 | [5]: https://www.mapleprimes.com/posts/124913-Visualization-Of-The-CayleyDickson 315 | [6]: https://english.stackexchange.com/q/234607 316 | [7]: https://docs.python.org/3/library/string.html#format-specification-mini-language 317 | [8]: https://raw.githubusercontent.com/discretegames/hypercomplex/main/diagram/diagram.png 318 | -------------------------------------------------------------------------------- /diagram/diagram.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Hypercomplex Numbers Diagram 9 | 10 | 11 | 33 | 34 | 35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |

Real

45 |
46 |

Complex

47 |
48 |

Quaternion

49 |
50 |

Octonion

51 |
52 |

Sedenion

53 |
54 |

Pathion

55 |
56 |

Chingon

57 |
58 |

Routon

59 |
60 |

Voudon

61 |
62 | 63 | 64 | -------------------------------------------------------------------------------- /diagram/diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/discretegames/hypercomplex/d026ee205a4bf41f93b5c67df17e212ca3be5de2/diagram/diagram.png -------------------------------------------------------------------------------- /diagram/diagram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 25 | 28 | 37 | 38 | 39 | 58 | 60 | 61 | 63 | image/svg+xml 64 | 66 | 67 | 68 | 69 | 70 | 75 | 1954 | 1955 | 1956 | -------------------------------------------------------------------------------- /diagram/raw_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/discretegames/hypercomplex/d026ee205a4bf41f93b5c67df17e212ca3be5de2/diagram/raw_diagram.png -------------------------------------------------------------------------------- /examples_to_markdown.py: -------------------------------------------------------------------------------- 1 | """Puts hypercomplex/examples.py into markdown format for easy use in README.md""" 2 | 3 | import os 4 | import io 5 | import sys 6 | 7 | 8 | def get_print_results(lines): 9 | header = "from hypercomplex import *\n" 10 | code = header + '\n'.join(lines) 11 | stdout = sys.stdout 12 | sys.stdout = io.StringIO() 13 | exec(code) 14 | output = sys.stdout.getvalue().splitlines() 15 | sys.stdout = stdout 16 | results = [] 17 | i = 0 18 | for line in lines: # Assumes each print remained on one line. 19 | out = None 20 | if line.strip().startswith('print'): 21 | out = output[i] 22 | i += 1 23 | results.append((line, out)) 24 | return results 25 | 26 | 27 | def example_to_markdown(example): 28 | lines = example.splitlines() 29 | start = f'{lines[0]}\n\n ```py\n' 30 | lines = lines[1:] 31 | longest = max(map(len, lines)) 32 | middle = [] 33 | for line, output in get_print_results(lines): 34 | if output is not None: 35 | padding = longest - len(line) 36 | line += ' ' * padding + f' # -> {output}' 37 | middle.append(f' {line}') 38 | end = '\n ```\n' 39 | return start + '\n'.join(middle) + end 40 | 41 | 42 | def examples_to_markdown(): 43 | with open(os.path.join('hypercomplex', 'examples.py')) as f: 44 | examples = [e.strip() for e in f.read().split('# %%')] 45 | examples = [e for e in examples if e][1:] 46 | markdown = '\n'.join(map(example_to_markdown, examples)) 47 | with open('examples.md', 'w') as f: 48 | f.write(markdown) 49 | print(f"Converted {len(examples)} examples to markdown.") 50 | # print(markdown) 51 | 52 | 53 | if __name__ == "__main__": 54 | examples_to_markdown() 55 | 56 | # This is the only multiline example and examples_to_markdown can't handle it. Saving it here to avoid manually making it again. 57 | e_matrix_example = """ 58 | 59 | 11. `e_matrix` of a number class gives the multiplication table of `e(i)*e(j)`. Set `string=False` to get a 2D list instead of a string. Set `raw=True` to get the raw hypercomplex numbers. 60 | 61 | ```py 62 | print(O.e_matrix()) # -> e1 e2 e3 e4 e5 e6 e7 63 | # -e0 e3 -e2 e5 -e4 -e7 e6 64 | # -e3 -e0 e1 e6 e7 -e4 -e5 65 | # e2 -e1 -e0 e7 -e6 e5 -e4 66 | # -e5 -e6 -e7 -e0 e1 e2 e3 67 | # e4 -e7 e6 -e1 -e0 -e3 e2 68 | # e7 e4 -e5 -e2 e3 -e0 -e1 69 | # -e6 e5 e4 -e3 -e2 e1 -e0 70 | # 71 | print(C.e_matrix(string=False, raw=True)) # -> [[(1 0), (0 1)], [(0 1), (-1 0)]] 72 | ``` 73 | 74 | """ 75 | -------------------------------------------------------------------------------- /hypercomplex/__init__.py: -------------------------------------------------------------------------------- 1 | """This package provides a way to work with hypercomplex number algebras following the Cayley-Dickson construction.""" 2 | 3 | __all__ = """ 4 | reals 5 | cayley_dickson_construction cd_construction 6 | cayley_dickson_algebra cd_algebra 7 | CD1 R Real 8 | CD2 C Complex 9 | CD4 Q Quaternion 10 | CD8 O Octonion 11 | CD16 S Sedenion 12 | CD32 P Pathion 13 | CD64 X Chingon 14 | CD128 U Routon 15 | CD256 V Voudon 16 | CD 17 | """.split() 18 | 19 | from hypercomplex.hypercomplex import \ 20 | reals, \ 21 | cayley_dickson_construction, cd_construction, \ 22 | cayley_dickson_algebra, cd_algebra, \ 23 | CD1, R, Real,\ 24 | CD2, C, Complex, \ 25 | CD4, Q, Quaternion, \ 26 | CD8, O, Octonion, \ 27 | CD16, S, Sedenion, \ 28 | CD32, P, Pathion, \ 29 | CD64, X, Chingon, \ 30 | CD128, U, Routon, \ 31 | CD256, V, Voudon, \ 32 | CD 33 | -------------------------------------------------------------------------------- /hypercomplex/examples.py: -------------------------------------------------------------------------------- 1 | # %% 0. Imports 2 | from hypercomplex import \ 3 | reals, \ 4 | cayley_dickson_construction, cd_construction, \ 5 | cayley_dickson_algebra, cd_algebra, \ 6 | CD1, R, Real,\ 7 | CD2, C, Complex, \ 8 | CD4, Q, Quaternion, \ 9 | CD8, O, Octonion, \ 10 | CD16, S, Sedenion, \ 11 | CD32, P, Pathion, \ 12 | CD64, X, Chingon, \ 13 | CD128, U, Routon, \ 14 | CD256, V, Voudon, \ 15 | CD 16 | 17 | 18 | # %% 1. Initialization can be done in various ways, including using Python's built in complex numbers. Unspecified coefficients become 0. 19 | print(R(-1.5)) 20 | print(C(2, 3)) 21 | print(C(2 + 3j)) 22 | print(Q(4, 5, 6, 7)) 23 | print(Q(4 + 5j, C(6, 7), pair=True)) 24 | print(P()) 25 | 26 | 27 | # %% 2. Numbers can be added and subtracted. The result will be the type with more dimensions. 28 | print(Q(0, 1, 2, 2) + C(9, -1)) 29 | print(100.1 - O(0, 0, 0, 0, 1.1, 2.2, 3.3, 4.4)) 30 | 31 | 32 | # %% 3. Numbers can be multiplied. The result will be the type with more dimensions. 33 | print(10 * S(1, 2, 3)) 34 | print(Q(1.5, 2.0) * O(0, -1)) 35 | 36 | # notice quaternions are non-commutative 37 | print(Q(1, 2, 3, 4) * Q(1, 0, 0, 1)) 38 | print(Q(1, 0, 0, 1) * Q(1, 2, 3, 4)) 39 | 40 | 41 | # %% 4. Numbers can be divided and `inverse` gives the multiplicative inverse. 42 | print(100 / C(0, 2)) 43 | print(C(2, 2) / Q(1, 2, 3, 4)) 44 | print(C(2, 2) * Q(1, 2, 3, 4).inverse()) 45 | print(R(2).inverse(), 1 / R(2)) 46 | 47 | 48 | # %% 5. Numbers can be raised to integer powers, a shortcut for repeated multiplication or division. 49 | q = Q(0, 3, 4, 0) 50 | print(q**5) 51 | print(q * q * q * q * q) 52 | print(q**-1) 53 | print(1 / q) 54 | print(q**0) 55 | 56 | 57 | # %% 6. `conjugate` gives the conjugate of the number. 58 | print(R(9).conjugate()) 59 | print(C(9, 8).conjugate()) 60 | print(Q(9, 8, 7, 6).conjugate()) 61 | 62 | 63 | # %% 7. `norm` gives the absolute value as the base type (`float` by default). There is also `norm_squared`. 64 | print(O(3, 4).norm(), type(O(3, 4).norm())) 65 | print(abs(O(3, 4))) 66 | print(O(3, 4).norm_squared()) 67 | 68 | 69 | # %% 8. Numbers are considered equal if their coefficients all match. Non-existent coefficients are 0. 70 | print(R(999) == V(999)) 71 | print(C(1, 2) == Q(1, 2)) 72 | print(C(1, 2) == Q(1, 2, 0.1)) 73 | 74 | 75 | # %% 9. `coefficients` gives a tuple of the components of the number in their base type (`float` by default). The properties `real` and `imag` are shortcuts for the first two components. Indexing can also be used (but is inefficient). 76 | print(R(100).coefficients()) 77 | q = Q(2, 3, 4, 5) 78 | print(q.coefficients()) 79 | print(q.real, q.imag) 80 | print(q[0], q[1], q[2], q[3]) 81 | 82 | 83 | # %% 10. `e(index)` of a number class gives the unit hypercomplex number where the index coefficient is 1 and all others are 0. 84 | print(C.e(0)) 85 | print(C.e(1)) 86 | print(O.e(3)) 87 | 88 | 89 | # %% 11. `e_matrix` of a number class gives the multiplication table of `e(i)*e(j)`. Set `string=False` to get a 2D list instead of a string. Set `raw=True` to get the raw hypercomplex numbers. 90 | print(O.e_matrix()) 91 | print(C.e_matrix(string=False, raw=True)) 92 | 93 | 94 | # %% 12. A number is considered truthy if it has has non-zero coefficients. Conversion to `int`, `float` and `complex` are only valid when the coefficients beyond the dimension of those types are all 0. 95 | print(bool(Q())) 96 | print(bool(Q(0, 0, 0.01, 0))) 97 | 98 | print(complex(Q(5, 5))) 99 | print(int(V(9.9))) 100 | # print(float(C(1, 2))) <- invalid 101 | 102 | 103 | # %% 13. Any usual format spec for the base type can be given in an f-string. 104 | o = O(0.001, 1, -2, 3.3333, 4e5) 105 | print(f"{o:.2f}") 106 | print(f"{R(23.9):04.0f}") 107 | 108 | 109 | # %% 14. The `len` of a number is its hypercomplex dimension, i.e. the number of components or coefficients it has. 110 | print(len(R())) 111 | print(len(C(7, 7))) 112 | print(len(U())) 113 | 114 | 115 | # %% 15. Using `in` behaves the same as if the number were a tuple of its coefficients. 116 | print(3 in Q(1, 2, 3, 4)) 117 | print(5 in Q(1, 2, 3, 4)) 118 | 119 | 120 | # %% 16. `copy` can be used to duplicate a number (but should generally never be needed as all operations create a new number). 121 | x = O(9, 8, 7) 122 | y = x.copy() 123 | print(x == y) 124 | print(x is y) 125 | 126 | 127 | # %% 17. `base` on a number class will return the base type the entire numbers are built upon. 128 | print(R.base()) 129 | print(V.base()) 130 | A = cayley_dickson_algebra(20, int) 131 | print(A.base()) 132 | 133 | 134 | # %% 18. Hypercomplex numbers are weird, so be careful! Here two non-zero sedenions multiply to give zero because sedenions and beyond have zero devisors. 135 | s1 = S.e(5) + S.e(10) 136 | s2 = S.e(6) + S.e(9) 137 | print(s1) 138 | print(s2) 139 | print(s1 * s2) 140 | print((1 / s1) * (1 / s2)) 141 | # print(1/(s1 * s2)) <- zero division error 142 | 143 | 144 | # %% 145 | -------------------------------------------------------------------------------- /hypercomplex/hypercomplex.py: -------------------------------------------------------------------------------- 1 | """Provides the types and tools to create arbitrary-dimension hypercomplex numbers following the Cayley-Dickson construction.""" 2 | 3 | from mathdunders import mathdunders 4 | from numbers import Number 5 | from math import sqrt 6 | 7 | 8 | class Numeric(Number): 9 | """A parent class for Real and Hypercomplex for shared behaviors.""" 10 | 11 | def copy(self): 12 | return self.__class__(self) 13 | 14 | def inverse(self): 15 | """Returns the multiplicative inverse of the number.""" 16 | return self.conjugate() / self.norm_squared() 17 | 18 | def norm_squared(self): # Returns base type. 19 | """Returns the square of the norm of the number as the base type.""" 20 | return (self.conjugate() * self).real_coefficient() 21 | 22 | def norm(self): # Returns base type. 23 | """Returns the norm of the number as the base type.""" 24 | return sqrt(self.norm_squared()) 25 | 26 | def __abs__(self): # Returns base type. 27 | return self.norm() 28 | 29 | def __len__(self): 30 | return self.dimensions 31 | 32 | def __getitem__(self, index): 33 | return self.coefficients()[index] 34 | 35 | def __contains__(self, obj): 36 | return obj in self.coefficients() 37 | 38 | def __str__(self): 39 | return format(self) 40 | 41 | def __repr__(self): 42 | return str(self) 43 | 44 | def __format__(self, format_spec): 45 | if not format_spec: 46 | format_spec = "g" 47 | coefficients = [f"{c:{format_spec}}" for c in self.coefficients()] 48 | return "(" + ' '.join(coefficients) + ")" 49 | 50 | @classmethod 51 | def e(cls, index): 52 | """Returns the unit hypercomplex number at the given subscript index.""" 53 | base = cls.base() 54 | coefficients = [base()] * cls.dimensions 55 | coefficients[index] = base(1) 56 | return cls(*coefficients) 57 | 58 | @classmethod 59 | def e_matrix(cls, string=True, raw=False, e="e"): 60 | """Creates a table of e(i)*e(j)'s akin to the ones found e.g. at wikipedia.org/wiki/Octonion.""" 61 | def format_cell(cell): 62 | if not raw: 63 | i, c = next(((i, c) 64 | for i, c in enumerate(cell.coefficients()) if c)) 65 | cell = f"{e}{i}" 66 | if c < 0: 67 | cell = "-" + cell 68 | return cell 69 | 70 | ees = list(map(cls.e, range(cls.dimensions))) 71 | matrix = [[format_cell(i * j) for j in ees] for i in ees] 72 | 73 | if string: 74 | matrix = [list(map(str, row)) for row in matrix] 75 | length = max(len(cell) for row in matrix for cell in row) 76 | offset = length - max(len(row[0]) for row in matrix) 77 | rows = [' '.join(cell.rjust(length) 78 | for cell in row)[offset:] for row in matrix] 79 | return '\n'.join(rows) + '\n' 80 | return matrix 81 | 82 | 83 | def reals(base=float): 84 | """Creates a type that represents real numbers based on a numeric type base.""" 85 | if not issubclass(base, Number): 86 | raise TypeError("The base type must be derived from numbers.Number.") 87 | 88 | @mathdunders(base=base) 89 | class Real(Numeric, base): 90 | """A class that represents a real number, level 0 of the Cayley-Dickson construction.""" 91 | dimensions = 1 92 | 93 | @staticmethod 94 | def base(): 95 | """Returns the base type these numbers were based on.""" 96 | return base 97 | 98 | def real_coefficient(self): # Returns base type. 99 | """Returns the real (leftmost) coefficient of the hypercomplex number as the base type.""" 100 | return base(self) 101 | 102 | def coefficients(self): # Returns tuple of base types. 103 | """Returns a tuple of base types of all the coefficients of the hypercomplex number.""" 104 | return (self.real_coefficient(),) 105 | 106 | def conjugate(self): 107 | """Returns the conjugate of the hypercomplex number.""" 108 | return Real(self) 109 | 110 | # For simplicity, use the base's hash rather than hash of coefficients tuple. 111 | def __hash__(self): 112 | return hash(base(self)) 113 | 114 | return Real 115 | 116 | 117 | def cayley_dickson_construction(basis): 118 | """Creates a type for the Cayley-Dickson algebra with twice the dimensions of the given Hypercomplex or Real basis.""" 119 | if not hasattr(basis, 'coefficients'): 120 | raise ValueError( 121 | "The basis type must be Real or Hypercomplex. (No coefficients found.)") 122 | 123 | class Hypercomplex(Numeric): 124 | """A class that represents a hypercomplex number, level > 0 of the Cayley-Dickson construction.""" 125 | dimensions = 2 * basis.dimensions 126 | 127 | def __init__(self, *args, pair=False): 128 | if pair: 129 | # a is the "real" left half. b is the "imaginary" right half. 130 | self.a, self.b = map(basis, args) 131 | else: 132 | if len(args) == 1: 133 | if hasattr(args[0], 'coefficients'): 134 | args = args[0].coefficients() 135 | elif isinstance(args[0], complex): 136 | args = args[0].real, args[0].imag 137 | if len(args) > len(self): 138 | raise TypeError( 139 | f"Too many args. Got {len(args)} expecting at most {len(self)}.") 140 | if len(self) != len(args): 141 | args += (Hypercomplex.base()(),) * (len(self) - len(args)) 142 | self.a = basis(*args[:len(self) // 2]) 143 | self.b = basis(*args[len(self) // 2:]) 144 | 145 | @staticmethod 146 | def coerce(other): 147 | """Attempts to coerce other to this Hypercomplex type.""" 148 | try: 149 | return Hypercomplex(other) 150 | except TypeError: 151 | return None 152 | 153 | @staticmethod 154 | def base(): 155 | """Returns the base type these numbers were based on.""" 156 | return basis.base() 157 | 158 | @property 159 | # Added so Hypercomplex numbers behave like other Python number types. 160 | def real(self): 161 | """The real (leftmost) coefficient of the hypercomplex number as the base type.""" 162 | return self.real_coefficient() 163 | 164 | @property 165 | # Added so Hypercomplex numbers behave like other Python number types. 166 | def imag(self): 167 | """Returns the imaginary (second leftmost) coefficient of the hypercomplex number as the base type.""" 168 | if len(self) == 2: 169 | return Hypercomplex.base()(self.b) 170 | return self.a.imag 171 | 172 | def real_coefficient(self): # Returns base type. 173 | """Returns the real (leftmost) coefficient of the hypercomplex number as the base type.""" 174 | return self.a.real_coefficient() 175 | 176 | def coefficients(self): # Returns tuple of base types. 177 | """Returns a tuple of base types of all the coefficients of the hypercomplex number.""" 178 | return self.a.coefficients() + self.b.coefficients() 179 | 180 | def conjugate(self): 181 | """Returns the conjugate of the hypercomplex number.""" 182 | return Hypercomplex(self.a.conjugate(), -self.b, pair=True) 183 | 184 | def __hash__(self): 185 | return hash(self.coefficients()) 186 | 187 | def __bool__(self): 188 | return bool(self.a) or bool(self.b) 189 | 190 | def convert(self, to_type, dimensions=1): 191 | """Attempts to convert self to to_type. Raises an error if the conversion is impossible.""" 192 | coefficients = self.coefficients() 193 | if any(coefficients[dimensions:]): 194 | raise TypeError( 195 | f"Can't convert {self.__class__.__name__}[{self.dimensions}] to {to_type.__name__} when there are non-zero incompatible coefficients.") 196 | return to_type(*coefficients[:dimensions]) 197 | 198 | def __int__(self): 199 | return self.convert(int) 200 | 201 | def __float__(self): 202 | return self.convert(float) 203 | 204 | def __complex__(self): 205 | return self.convert(complex, 2) 206 | 207 | def __eq__(self, other): 208 | coerced = Hypercomplex.coerce(other) 209 | if coerced is None: 210 | self = other.__class__.coerce(self) 211 | else: 212 | other = coerced 213 | return self.a == other.a and self.b == other.b 214 | 215 | # Unary Math Dunders: 216 | 217 | def __neg__(self): 218 | return Hypercomplex(-self.a, -self.b, pair=True) 219 | 220 | def __pos__(self): 221 | return Hypercomplex(+self.a, +self.b, pair=True) 222 | 223 | # Binary Math Dunders: 224 | 225 | def __add__(self, other): 226 | other = Hypercomplex.coerce(other) 227 | if other is None: 228 | return NotImplemented 229 | return Hypercomplex(self.a + other.a, self.b + other.b, pair=True) 230 | 231 | def __radd__(self, other): 232 | # Should never encounter a TypeError. 233 | return Hypercomplex(other) + self 234 | 235 | def __mul__(self, other): 236 | other = Hypercomplex.coerce(other) 237 | if other is None: 238 | return NotImplemented 239 | a = self.a * other.a - other.b.conjugate() * self.b 240 | b = other.b * self.a + self.b * other.a.conjugate() 241 | return Hypercomplex(a, b, pair=True) 242 | 243 | def __rmul__(self, other): 244 | return Hypercomplex(other) * self 245 | 246 | def __pow__(self, other): # Only valid if other is an integer. 247 | if not isinstance(other, int): 248 | return NotImplemented 249 | 250 | value = Hypercomplex(Hypercomplex.base()(1)) 251 | if other: 252 | multiplier = self if other > 0 else self.inverse() 253 | for _ in range(abs(other)): 254 | value *= multiplier 255 | return value 256 | 257 | def __sub__(self, other): 258 | other = Hypercomplex.coerce(other) 259 | if other is None: 260 | return NotImplemented 261 | return Hypercomplex(self.a - other.a, self.b - other.b, pair=True) 262 | 263 | def __rsub__(self, other): 264 | return Hypercomplex(other) - self 265 | 266 | def __truediv__(self, other): 267 | base = Hypercomplex.base() 268 | # Short circuit base type to avoid infinite recursion in inverse(). 269 | if isinstance(other, base): 270 | other = base(1) / other 271 | else: 272 | other = Hypercomplex.coerce(other) 273 | if other is None: 274 | return NotImplemented 275 | other = other.inverse() 276 | return self * other 277 | 278 | def __rtruediv__(self, other): 279 | return Hypercomplex(other) / self 280 | 281 | return Hypercomplex 282 | 283 | 284 | def cayley_dickson_algebra(level, base=float): 285 | """Creates the type for the Cayley-Dickson algebra with 2**level dimensions. e.g. 0 for Real, 1 for Complex, 2 for Quaternion.""" 286 | if not isinstance(level, int) or level < 0: 287 | raise ValueError("The level must be a positive integer.") 288 | numbers = reals(base) 289 | for _ in range(level): 290 | numbers = cayley_dickson_construction(numbers) 291 | return numbers 292 | 293 | 294 | cd_construction = cayley_dickson_construction 295 | cd_algebra = cayley_dickson_algebra 296 | 297 | # Names and letters taken from https://www.mapleprimes.com/DocumentFiles/124913/419426/Figure1.JPG 298 | CD1 = R = Real = reals() 299 | CD2 = C = Complex = cayley_dickson_construction(CD1) 300 | CD4 = Q = Quaternion = cayley_dickson_construction(CD2) 301 | CD8 = O = Octonion = cayley_dickson_construction(CD4) 302 | CD16 = S = Sedenion = cayley_dickson_construction(CD8) 303 | CD32 = P = Pathion = cayley_dickson_construction(CD16) 304 | CD64 = X = Chingon = cayley_dickson_construction(CD32) 305 | CD128 = U = Routon = cayley_dickson_construction(CD64) 306 | CD256 = V = Voudon = cayley_dickson_construction(CD128) 307 | CD = CD1, CD2, CD4, CD8, CD16, CD32, CD64, CD128, CD256 308 | -------------------------------------------------------------------------------- /hypercomplex/test_hypercomplex.py: -------------------------------------------------------------------------------- 1 | """Test suite for hypercomplex.py based on code from README.md and examples.py.""" 2 | 3 | import unittest 4 | from hypercomplex import \ 5 | reals, \ 6 | cayley_dickson_construction, cd_construction, \ 7 | cayley_dickson_algebra, cd_algebra, \ 8 | CD1, R, Real,\ 9 | CD2, C, Complex, \ 10 | CD4, Q, Quaternion, \ 11 | CD8, O, Octonion, \ 12 | CD16, S, Sedenion, \ 13 | CD32, P, Pathion, \ 14 | CD64, X, Chingon, \ 15 | CD128, U, Routon, \ 16 | CD256, V, Voudon, \ 17 | CD 18 | 19 | 20 | # This isn't the most thorough test suite ever but it covers the basics in Python 3.6+ with tox. 21 | class TestHypercomplex(unittest.TestCase): 22 | 23 | def assertEqualT(self, a, b): 24 | self.assertEqual(a, b) 25 | self.assertEqual(type(a), type(b)) 26 | 27 | # Tests from README.md: 28 | 29 | def test_basics(self): 30 | c = Complex(0, 7) 31 | self.assertEqual(str(c), "(0 7)") 32 | self.assertEqual(c, 7j) 33 | 34 | q = Quaternion(1.1, 2.2, 3.3, 4.4) 35 | self.assertEqual(str(2 * q), "(2.2 4.4 6.6 8.8)") 36 | 37 | self.assertEqual(Quaternion.e_matrix(), 38 | "e0 e1 e2 e3\ne1 -e0 e3 -e2\ne2 -e3 -e0 e1\ne3 e2 -e1 -e0\n") 39 | 40 | o = Octonion(0, 0, 0, 0, 8, 8, 9, 9) 41 | self.assertEqual(o + q, O(1.1, 2.2, 3.3, 4.4, 8, 8, 9, 9)) 42 | 43 | v = Voudon() 44 | self.assertEqual(v, 0) 45 | self.assertEqual(len(v), 256) 46 | 47 | BeyondVoudon = cayley_dickson_construction(Voudon) 48 | self.assertEqual(len(BeyondVoudon()), 512) 49 | 50 | def test_reals(self): 51 | from decimal import Decimal 52 | D = reals(Decimal) 53 | self.assertEqual(D(10) / 4, 2.5) 54 | self.assertEqual(D(3) * D(9), 27) 55 | 56 | def test_cd_construction(self): 57 | RealNum = reals() 58 | ComplexNum = cayley_dickson_construction(RealNum) 59 | QuaternionNum = cd_construction(ComplexNum) 60 | 61 | q = QuaternionNum(1, 2, 3, 4) 62 | self.assertEqual(q, Q(1, 2, 3, 4)) 63 | self.assertEqual(str(1 / q), "(0.0333333 -0.0666667 -0.1 -0.133333)") 64 | self.assertEqual(q + 1 + 2j, Q(2, 4, 3, 4)) 65 | 66 | def test_cd_algebra(self): 67 | OctonionNum = cayley_dickson_algebra(3) 68 | o = OctonionNum(8, 7, 6, 5, 4, 3, 2, 1) 69 | self.assertEqual(o, O(8, 7, 6, 5, 4, 3, 2, 1)) 70 | self.assertEqual(2 * o, O(16, 14, 12, 10, 8, 6, 4, 2)) 71 | self.assertEqual(o.conjugate(), O(8, -7, -6, -5, -4, -3, -2, -1)) 72 | 73 | def test_types(self): 74 | self.assertEqual(Real(4), cd_algebra(0)(4)) 75 | self.assertEqual(C(3 - 7j), cd_algebra(1)(3 - 7j)) 76 | self.assertEqual(CD4(.1, -2.2, 3.3e3), cd_algebra(2)(.1, -2.2, 3.3e3)) 77 | self.assertEqual(CD[3](1, 0, 2, 0, 3), cd_algebra(3)(1, 0, 2, 0, 3)) 78 | self.assertEqual(len(CD), 9) 79 | 80 | # Tests from examples.py: 81 | 82 | def test_init(self): 83 | self.assertEqual(R(-1.5), -1.5) 84 | self.assertEqual(C(2, 3), 2 + 3j) 85 | self.assertEqual(C(2 + 3j), 2 + 3j) 86 | self.assertEqual(repr(Q(4, 5, 6, 7)), "(4 5 6 7)") 87 | self.assertEqual(Q(4 + 5j, C(6, 7), pair=True), Q(4, 5, 6, 7)) 88 | self.assertEqual(P(), 0) 89 | self.assertRaises(TypeError, C, 1, 2, 3) 90 | 91 | def test_add_subtract(self): 92 | self.assertEqual(Q(0, 1, 2, 2) + C(9, -1), Q(9, 0, 2, 2)) 93 | self.assertEqual(100.1 - O(0, 0, 0, 0, 1.1, 2.2, 3.3, 4.4), 94 | O(100.1, 0, 0, 0, -1.1, -2.2, -3.3, -4.4)) 95 | 96 | def test_multiply(self): 97 | self.assertEqual(10 * S(1, 2, 3), Q(10, 20, 30)) 98 | self.assertEqual(Q(1.5, 2.0) * O(0, -1), C(2, -1.5)) 99 | self.assertEqual(Q(1, 2, 3, 4) * Q(1, 0, 0, 1), Q(-3, 5, 1, 5)) 100 | self.assertEqual(Q(1, 0, 0, 1) * Q(1, 2, 3, 4), Q(-3, -1, 5, 5)) 101 | 102 | def test_divide(self): 103 | self.assertEqual(100 / C(0, 2), C(0, -50)) 104 | inv = Q(1 / 5, -1 / 15, 1 / 15, -7 / 15) 105 | self.assertAlmostEqual(C(2, 2) / Q(1, 2, 3, 4), inv) 106 | self.assertAlmostEqual(C(2, 2) * Q(1, 2, 3, 4).inverse(), inv) 107 | self.assertEqual(R(2).inverse(), 0.5) 108 | self.assertEqual(1 / R(2), 0.5) 109 | 110 | def test_power(self): 111 | q = Q(0, 3, 4, 0) 112 | q5 = Q(0, 1875, 2500, 0) 113 | self.assertEqual(q**5, q5) 114 | self.assertEqual(q * q * q * q * q, q5) 115 | q1 = Q(0, -0.12, -0.16, 0) 116 | self.assertEqual(q**-1, q1) 117 | self.assertEqual(1 / q, q1) 118 | self.assertEqual(q**0, Q(1, 0, 0, 0)) 119 | 120 | def test_conjugate(self): 121 | self.assertEqual(R(9).conjugate(), (9).conjugate()) 122 | self.assertEqual(C(9, 8).conjugate(), (9 + 8j).conjugate()) 123 | self.assertEqual(Q(9, 8, 7, 6).conjugate(), Q(9, -8, -7, -6)) 124 | 125 | def test_norm(self): 126 | o = O(3, 4) 127 | self.assertEqualT(o.norm(), 5.0) 128 | self.assertEqual(abs(o), 5) 129 | self.assertEqual(o.norm_squared(), 25.0) 130 | 131 | def test_equals(self): 132 | self.assertEqual(R(999), V(999)) 133 | self.assertEqual(C(1, 2), Q(1, 2)) 134 | self.assertNotEqual(C(1, 2), Q(1, 2, 0.1)) 135 | self.assertNotEqual(C(0, 0.1), C(0.1, 1)) 136 | 137 | def test_coefficients(self): 138 | self.assertEqual(R(100).coefficients(), (100.0,)) 139 | q = Q(2, 3, 4, 5) 140 | self.assertEqual(q.coefficients(), (2, 3, 4, 5)) 141 | self.assertEqual(q.real, 2.0) 142 | self.assertEqual(q.imag, 3.0) 143 | self.assertEqual((q[0], q[1], q[2], q[3]), (2, 3, 4, 5)) 144 | self.assertEqual(tuple(q), (2, 3, 4, 5)) 145 | self.assertEqual(list(q), [2, 3, 4, 5]) 146 | 147 | def test_e(self): 148 | self.assertEqual(C.e(0), C(1, 0)) 149 | self.assertEqual(C.e(1), C(0, 1)) 150 | self.assertEqual(O.e(3), O(0, 0, 0, 1, 0, 0, 0, 0)) 151 | 152 | def test_e_matrix(self): 153 | self.assertEqual(R.e_matrix(), 'e0\n') 154 | self.assertEqual(C.e_matrix(False, True), [[1, 1j], [1j, -1]]) 155 | 156 | def test_conversion(self): 157 | self.assertFalse(bool(Q())) 158 | self.assertTrue(bool(Q(0, 0, 0.01, 0))) 159 | self.assertEqualT(complex(Q(5, 5)), 5 + 5j) 160 | self.assertEqualT(int(V(9.9)), 9) 161 | self.assertRaises(TypeError, float, C(1, 2)) 162 | 163 | def test_format(self): 164 | o = O(0.001, 1, -2, 3.3333, 4e5) 165 | self.assertEqual( 166 | f"{o:.2f}", "(0.00 1.00 -2.00 3.33 400000.00 0.00 0.00 0.00)") 167 | self.assertEqual(f"{R(23.9):04.0f}", "(0024)") 168 | 169 | def test_len(self): 170 | self.assertEqual(len(R()), 1) 171 | self.assertEqual(len(C(7, 7)), 2) 172 | self.assertEqual(len(U()), 128) 173 | 174 | def test_in(self): 175 | self.assertTrue(3 in Q(1, 2, 3, 4)) 176 | self.assertFalse(5 in Q(1, 2, 3, 4)) 177 | 178 | def test_copy(self): 179 | x = O(9, 8, 7) 180 | y = x.copy() 181 | self.assertTrue(x == y) 182 | self.assertFalse(x is y) 183 | 184 | def test_base(self): 185 | self.assertEqual(R.base(), float) 186 | self.assertEqual(V.base(), float) 187 | A = cayley_dickson_algebra(20, int) 188 | self.assertEqual(A.base(), int) 189 | 190 | def test_zero_divisors(self): 191 | s1 = S.e(5) + S.e(10) 192 | s2 = S.e(6) + S.e(9) 193 | self.assertEqual(s1, S(0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0)) 194 | self.assertEqual(s2, S(0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0)) 195 | self.assertEqual(s1 * s2, 0) 196 | self.assertEqual((1 / s1) * (1 / s2), 0) 197 | self.assertRaises(ZeroDivisionError, lambda: 1 / (s1 * s2)) 198 | 199 | 200 | if __name__ == "__main__": 201 | print('Running tests from main...') 202 | try: 203 | unittest.main() 204 | except SystemExit: # So debugger doesn't trigger. 205 | pass 206 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from os import path 2 | from setuptools import setup 3 | 4 | version = "0.3.4" 5 | 6 | directory = path.abspath(path.dirname(__file__)) 7 | with open(path.join(directory, 'README.md'), encoding='utf-8') as file: 8 | long_description = file.read() 9 | 10 | setup( 11 | name='hypercomplex', 12 | version=version, 13 | author='discretegames', 14 | author_email='discretizedgames@gmail.com', 15 | description="Library for arbitrary-dimension hypercomplex numbers following the Cayley-Dickson construction.", 16 | long_description=long_description, 17 | long_description_content_type='text/markdown', 18 | url='https://github.com/discretegames/hypercomplex', 19 | project_urls={"GitHub": "https://github.com/discretegames/hypercomplex", 20 | "PyPI": "https://pypi.org/project/hypercomplex", 21 | "TestPyPI": "https://test.pypi.org/project/hypercomplex"}, 22 | packages=['hypercomplex'], 23 | python_requires='>=3.6', 24 | install_requires=['mathdunders>=0.4.1'], 25 | license="MIT", 26 | keywords=['python', 'math', 'complex', 'number', 'hypercomplex', 'Cayley', 'Dickson', 'construction', 27 | 'algebra', 'quaternion', 'octonion', 'sedenion', 'pathion', 'chingon', 'routon', 'voudon'], 28 | classifiers=[ 29 | "Development Status :: 5 - Production/Stable", 30 | "Intended Audience :: Education", 31 | "Intended Audience :: Science/Research", 32 | "Topic :: Scientific/Engineering :: Mathematics", 33 | "License :: OSI Approved :: MIT License", 34 | "Programming Language :: Python :: 3", 35 | "Programming Language :: Python :: 3.6", 36 | "Programming Language :: Python :: 3.7", 37 | "Programming Language :: Python :: 3.8", 38 | "Programming Language :: Python :: 3.9", 39 | "Programming Language :: Python :: 3.10" 40 | ] 41 | ) 42 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py39, py38, py37, py36, py310 3 | 4 | [testenv] 5 | deps = 6 | 7 | commands = 8 | python -m unittest discover -s ./hypercomplex 9 | --------------------------------------------------------------------------------