Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bivector and rotor splits for arbitrary GAs #398

Open
wants to merge 47 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
64a7131
Added bivector split for arbitrary GAs and tests thereof.
Jun 6, 2021
0b0c76a
Added rotor_split.
Jun 7, 2021
6a5dd22
Parameterized the tests, added testng for rotor_split.
Jun 7, 2021
f4e56da
Stop checking if a MV matrix is invertible and let numpy decide.
Jun 7, 2021
6d64658
Normalize complex simple rotors correctly
Jun 7, 2021
e823281
Added closed form rotor logarithm.
Jun 7, 2021
4d6fcbc
Added tests for closed form rotor logarithm.
Jun 7, 2021
1149fb2
Fixed flake8 errors.
Jun 7, 2021
8cdf6be
Improved comments in the tests.
Jun 7, 2021
5215235
Add a basic test to verify assumptions about the bivectors produced b…
hugohadfield Jun 7, 2021
5ebe817
Merge branch 'decomposition' of https://github.com/tBuLi/clifford int…
Jun 7, 2021
58e078c
Added check for simplicity to the unknown_split test
Jun 7, 2021
5404367
Added bivector split for arbitrary GAs and tests thereof.
Jun 6, 2021
68908ca
Added rotor_split.
Jun 7, 2021
3254634
Parameterized the tests, added testng for rotor_split.
Jun 7, 2021
3afcdd3
Stop checking if a MV matrix is invertible and let numpy decide.
Jun 7, 2021
b81e15d
Normalize complex simple rotors correctly
Jun 7, 2021
559773c
Added closed form rotor logarithm.
Jun 7, 2021
4372805
Added tests for closed form rotor logarithm.
Jun 7, 2021
5200950
Fixed flake8 errors.
Jun 7, 2021
d744819
Improved comments in the tests.
Jun 7, 2021
47c8e41
Add a basic test to verify assumptions about the bivectors produced b…
hugohadfield Jun 7, 2021
7e72138
Added check for simplicity to the unknown_split test
Jun 7, 2021
aa5d283
change assertions slighlty
hugohadfield Jun 8, 2021
341dd07
Prefer np.testing.allclose vs np.testing.assert_almost_equal
hugohadfield Jun 8, 2021
d9115d4
Failing tests were due to Bs and ls not always being returned in the …
Jun 8, 2021
647efdf
Removed print statement
Jun 8, 2021
d28fa34
Improve type stability of exp and log, reduce required tolerance a bi…
hugohadfield Jun 8, 2021
68a18bb
Tag the bivector split as too slow when v high dimensional
hugohadfield Jun 8, 2021
5d82fa3
Added Roelfs thesis to bibliography
Jun 13, 2021
1322ebf
Merge branch 'decomposition' of https://github.com/tBuLi/clifford int…
Jun 13, 2021
89ceada
Merge remote-tracking branch 'origin/master' into decomposition
hugohadfield Jun 14, 2021
087aea0
Sort eigenvalues from boost to rotation, implement the split for even…
Jun 15, 2021
9234594
Boosts should never hve a negative scalar part.
Jun 15, 2021
47166a6
Update known_splits to new sort order
Jun 15, 2021
18ee52a
Test that exp(log(R)) == R
Jun 15, 2021
139250c
Make the accuracy dependent on total dimension
hugohadfield Jul 5, 2021
21be96d
JIT the internal helper function
hugohadfield Jul 5, 2021
424ed7b
Fix linting
hugohadfield Jul 5, 2021
e8bb4ff
Tidy up the tests to be more granular and not impact the startup time…
eric-wieser Aug 12, 2021
90d232e
Merge remote-tracking branch 'upstream/master' into decomposition
eric-wieser Aug 12, 2021
ca30432
Ensure docs are generated
eric-wieser Aug 12, 2021
5c8e386
flake8
eric-wieser Aug 12, 2021
7d5d0ac
Fix docstrings, flake8
eric-wieser Aug 12, 2021
9bfe9cc
try to fix CI
eric-wieser Aug 12, 2021
025d624
Fix doctest
eric-wieser Aug 12, 2021
e206c24
Merge remote-tracking branch 'upstream/master' into decomposition
eric-wieser Sep 2, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions clifford/_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
)
from . import _numba_utils
from .io import read_ga_file
from . import _settings
from ._multivector import MultiVector
from ._layout_helpers import (
BasisBladeOrder, BasisVectorIds, canonical_reordering_sign_euclidean
Expand Down Expand Up @@ -685,8 +684,6 @@ def inv_func(self):
@_numba_utils.njit
def leftLaInvJIT(value):
intermed = _numba_val_get_left_gmt_matrix(value, k_list, l_list, m_list, mult_table_vals, n_dims)
if abs(np.linalg.det(intermed)) < _settings._eps:
raise ValueError("multivector has no left-inverse")
sol = np.linalg.solve(intermed, identity.astype(intermed.dtype))
return sol

Expand Down
185 changes: 185 additions & 0 deletions clifford/invariant_decomposition.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
"""
.. currentmodule:: clifford.invariant_decomposition

=====================================================
invariant_decomposition (:mod:`clifford.invariant_decomposition`)
=====================================================

.. versionadded:: 1.5.0


This file implements the invariant decomposition (aka bivector split) of bivectors into
mutually commuting orthogonal simple bivectors, based on the method of :cite:`roelfs2021thesis`, chapter 6.

The invariant decomposition enables closed form exponentials and logarithms, and the factorization of
rotors into simple rotors.

Example usage::

>>> from clifford.g4 import *
>>> B = 1*e12 + 2*e34
>>> bivector_split(B)
[((1+0j)^e12), ((2+0j)^e34)]

Implemented functions
---------------------

.. autofunction:: bivector_split
.. autofunction:: rotor_split
.. autofunction:: exp
.. autofunction:: log


Helper functions
----------------

.. autofunction:: _bivector_split
.. autofunction:: single_split

"""
import math
from functools import reduce

import numpy as np

from ._settings import _eps
from . import _numba_utils


@_numba_utils.njit(cache=True)
def _single_split_even_values(Wm_array, li, r):
ND = np.zeros((2, Wm_array[0, :].shape[0]), dtype=np.complex_)

Check warning on line 51 in clifford/invariant_decomposition.py

View check run for this annotation

Codecov / codecov/patch

clifford/invariant_decomposition.py#L51

Added line #L51 was not covered by tests
for i in range(0, Wm_array.shape[0]//2+1):
ND[0, :] += Wm_array[2*i, :] * li**(r - i)

Check warning on line 53 in clifford/invariant_decomposition.py

View check run for this annotation

Codecov / codecov/patch

clifford/invariant_decomposition.py#L53

Added line #L53 was not covered by tests
for i in range(0, Wm_array.shape[0]//2):
ND[1, :] += Wm_array[2*i+1, :] * li**(r - i - 1)
return ND

Check warning on line 56 in clifford/invariant_decomposition.py

View check run for this annotation

Codecov / codecov/patch

clifford/invariant_decomposition.py#L55-L56

Added lines #L55 - L56 were not covered by tests


@_numba_utils.njit(cache=True)
def _single_split_odd_values(Wm_array, li, r):
ND = np.zeros((2, Wm_array[0, :].shape[0]), dtype=np.complex_)

Check warning on line 61 in clifford/invariant_decomposition.py

View check run for this annotation

Codecov / codecov/patch

clifford/invariant_decomposition.py#L61

Added line #L61 was not covered by tests
for i in range(0, Wm_array.shape[0]//2):
ND[0, :] += Wm_array[2 * i + 1, :] * li ** (r - i)
ND[1, :] += Wm_array[2*i, :] * li**(r - i)
return ND

Check warning on line 65 in clifford/invariant_decomposition.py

View check run for this annotation

Codecov / codecov/patch

clifford/invariant_decomposition.py#L63-L65

Added lines #L63 - L65 were not covered by tests


def single_split_even(Wm, li, r):
"""Helper function to compute a single split for a given set of W_m and
eigenvalue lambda_i, when the total number of terms in the split is even.
"""
Wm_array = np.array([W.value for W in Wm])
ND = _single_split_even_values(Wm_array, li, r)
N = Wm[0].layout.MultiVector(ND[0, :])
D = Wm[0].layout.MultiVector(ND[1, :])
return N*D.leftLaInv()


def single_split_odd(Wm, li, r):
"""Helper function to compute a single split for a given set of W_m and
eigenvalue lambda_i, when the total number of terms in the split is odd.
"""
Wm_array = np.array([W.value for W in Wm])
ND = _single_split_odd_values(Wm_array, li, r)
N = Wm[0].layout.MultiVector(ND[0, :])
D = Wm[0].layout.MultiVector(ND[1, :])
return N*D.leftLaInv()

Check warning on line 87 in clifford/invariant_decomposition.py

View check run for this annotation

Codecov / codecov/patch

clifford/invariant_decomposition.py#L84-L87

Added lines #L84 - L87 were not covered by tests
Comment on lines +49 to +87
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hugohadfield, I assume this jitted code is your doing. Is there a reason you define the values function vs just using the multivector jit support?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe #412 will resolve this.



def _bivector_split(Wm, return_all=True):
"""Internal helper function to perform the decomposition, given a set of Wm.

Parameters
----------
return_all : bool, optional
If `True`, returns all the :math:`b_i`.
If `False`, return all :math:`b_i` except for the one with the smallest magnitude.
"""
# The effective value of k is determined by the largest non-zero W.
# remove the highest grade zeros to prevent meaningless lambda_i = 0 values.
for W in Wm[::-1]:
if np.linalg.norm(W.value) > _eps:
break
else:
Wm = Wm[:-1]

Check warning on line 105 in clifford/invariant_decomposition.py

View check run for this annotation

Codecov / codecov/patch

clifford/invariant_decomposition.py#L105

Added line #L105 was not covered by tests

k = (len(Wm) - 1)
r = k // 2
Wm_sq = np.array([(W ** 2).value[0] * (-1) ** (k - m) for m, W in enumerate(Wm)])
ls = np.roots(Wm_sq)

Bs = []
# Sort to have the value closest to zero last.
ls_sorted = sorted(ls, key=lambda li: -li)
# Exclude the smallest value if asked.
for li in (ls_sorted if return_all else ls_sorted[:-1]):
if k % 2 == 0:
Bs.append(single_split_even(Wm, li, r))
else:
Bs.append(single_split_odd(Wm, li, r))

Check warning on line 120 in clifford/invariant_decomposition.py

View check run for this annotation

Codecov / codecov/patch

clifford/invariant_decomposition.py#L120

Added line #L120 was not covered by tests
return (Bs, ls_sorted)


def bivector_split(B, k=None, roots=False):
r"""Bivector split of the bivector B based on the method of :cite:`roelfs2021thesis`, chapter 6.

Parameters
----------
roots : bool, optional
If `True`, return the values of the :math:`\lambda_i` in addition to the :math:`b_i`.
If `False`, return only the :math:`b_i`.
"""
dim = B.layout.dims
if k is None:
k = dim // 2

Wm = [(B**m)(2*m) / math.factorial(m) for m in range(0, k + 1)]
Bs, ls = _bivector_split(Wm, return_all=False)
Bs = Bs + [B - sum(Bs)]
return (Bs, ls) if roots else Bs


def rotor_split(R, k=None, roots=False):
dim = R.layout.dims

Check warning on line 144 in clifford/invariant_decomposition.py

View check run for this annotation

Codecov / codecov/patch

clifford/invariant_decomposition.py#L144

Added line #L144 was not covered by tests
if k is None:
k = dim // 2

Check warning on line 146 in clifford/invariant_decomposition.py

View check run for this annotation

Codecov / codecov/patch

clifford/invariant_decomposition.py#L146

Added line #L146 was not covered by tests

Wm = [R(2 * m) for m in range(0, k + 1)]
Ts, ls = _bivector_split(Wm, return_all=False)

Check warning on line 149 in clifford/invariant_decomposition.py

View check run for this annotation

Codecov / codecov/patch

clifford/invariant_decomposition.py#L149

Added line #L149 was not covered by tests

Rs = [(1 + ti) for ti in Ts]
Rs = [Ri.normal() if np.isreal((Ri*~Ri).value[0]) else Ri / np.sqrt((Ri*~Ri).value[0]) for Ri in Rs]
P = reduce(lambda tot, x: tot*x, Rs, 1.0 + 0.0*R)
Rs = Rs + [R*P.leftLaInv()]
return (Rs, ls) if roots else Rs

Check warning on line 155 in clifford/invariant_decomposition.py

View check run for this annotation

Codecov / codecov/patch

clifford/invariant_decomposition.py#L154-L155

Added lines #L154 - L155 were not covered by tests


def exp(B):
Bs, ls = bivector_split(B, roots=True)
R = B.layout.scalar

Check warning on line 160 in clifford/invariant_decomposition.py

View check run for this annotation

Codecov / codecov/patch

clifford/invariant_decomposition.py#L159-L160

Added lines #L159 - L160 were not covered by tests
for Bi, li in zip(Bs, ls):
if np.isreal(li) and li < 0:
beta_i = np.sqrt(-li)
R *= np.cos(beta_i) + (np.sin(beta_i) / beta_i) * Bi

Check warning on line 164 in clifford/invariant_decomposition.py

View check run for this annotation

Codecov / codecov/patch

clifford/invariant_decomposition.py#L163-L164

Added lines #L163 - L164 were not covered by tests
elif np.isreal(li) and np.abs(li) < _eps:
R *= 1 + Bi

Check warning on line 166 in clifford/invariant_decomposition.py

View check run for this annotation

Codecov / codecov/patch

clifford/invariant_decomposition.py#L166

Added line #L166 was not covered by tests
else:
beta_i = np.sqrt(li)
R *= np.cosh(beta_i) + (np.sinh(beta_i) / beta_i) * Bi
return R

Check warning on line 170 in clifford/invariant_decomposition.py

View check run for this annotation

Codecov / codecov/patch

clifford/invariant_decomposition.py#L168-L170

Added lines #L168 - L170 were not covered by tests


def log(R):
Rs, ls = rotor_split(R, roots=True)
logR = R.layout.MultiVector()

Check warning on line 175 in clifford/invariant_decomposition.py

View check run for this annotation

Codecov / codecov/patch

clifford/invariant_decomposition.py#L174-L175

Added lines #L174 - L175 were not covered by tests
for Ri, li in zip(Rs, ls):
if li < 0:
norm = np.sqrt(- (Ri(2) ** 2).value[0])
logR += np.arccos(Ri.value[0]) * Ri(2) / norm

Check warning on line 179 in clifford/invariant_decomposition.py

View check run for this annotation

Codecov / codecov/patch

clifford/invariant_decomposition.py#L178-L179

Added lines #L178 - L179 were not covered by tests
elif np.abs(li) < _eps:
logR += Ri(2)

Check warning on line 181 in clifford/invariant_decomposition.py

View check run for this annotation

Codecov / codecov/patch

clifford/invariant_decomposition.py#L181

Added line #L181 was not covered by tests
else:
norm = np.sqrt((Ri(2)**2).value[0])
logR += np.arccosh(Ri.value[0]) * Ri(2) / norm
return logR

Check warning on line 185 in clifford/invariant_decomposition.py

View check run for this annotation

Codecov / codecov/patch

clifford/invariant_decomposition.py#L183-L185

Added lines #L183 - L185 were not covered by tests
7 changes: 7 additions & 0 deletions clifford/test/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,20 @@
import pytest
import numpy as np

from clifford._numba_utils import DISABLE_JIT


@pytest.fixture
def rng():
default_test_seed = 1 # the default seed to start pseudo-random tests
return np.random.default_rng(default_test_seed)


too_slow_without_jit = pytest.mark.skipif(
DISABLE_JIT, reason="test is too slow without JIT"
)


def run_all_tests(*args):
""" Invoke pytest, forwarding options to pytest.main """
pytest.main([os.path.dirname(__file__)] + list(args))
Loading