clifford
clifford copied to clipboard
Bivector and rotor splits for arbitrary GAs
This PR aims to add the bivector split of chapter 6 of my thesis to clifford
. When finished, this PR will add the decomposition of arbitrary bivectors into mutually committing orthogonal simple bivectors, and the decomposition of rotors into mutually commuting orthogonal simple rotors. Additionally, this enables us to implement closed form implementations for the exp/log function of simple bivectors/rotors respectively.
ToDo
- [x] Invariant decomposition of bivectors into simple bivectors
- [x] Closed form exponential of bivectors
- [x] Invariant decomposition of rotors into simple rotors
- [x] Logarithm of rotors
- [ ] Exception handling: the algorithm will fail when the eigenvalues are degenerate. We need to discuss how to handle these cases, although this cannot occur in any the popular small algebras of 3DCGA, 3DPGA, STA, and it might be okay to leave it up to the user.
- [x] Optimize tests: a lot of copy-pasting went into the current tests, pytest probably has a solution for this.
- [ ] Documentation.
Curious to hear your opinions on the work done so far.
This looks great, hopefully the inclusion of the general decomposition of bivectors means we can remove the various algebra specific bivector decompositions in other places in the code :) I'll have a dig for them later so we can do some spring cleaning
@hugohadfield that sounds great! Cleaning up the code base is what GA is for ;).
I have made one potentially contentious change to _layout.py
: I've removed the checking if a MV is invertible by removing the following lines:
if abs(np.linalg.det(intermed)) < _settings._eps:
raise ValueError("multivector has no left-inverse")
and instead will let numpy decide. Maybe there is a good reason this line was added that I'm not aware of, but in my experiments with bivector splits I've regularly walked into this condition whereas I knew the requested inversion was possible. Also decreasing _eps
further doesn't always work, whereas removing this check all together works like a charm. So I'm curious to hear what the reason was for adding this check. (If interested in a MWE, the r6 rotor_split
in the tests is uninvertible according to this check, but it isn't.)
This pull request introduces 1 alert when merging f4e56dafd2319bed44d4acb862ead62ac4449697 into 31651d095834ad4c2c5da0692414b55c88823840 - view on LGTM.com
new alerts:
- 1 for Unused import
@hugohadfield, when it comes to cleaning up the code base, should that be part of this PR or is it better to leave this one focused on adding the general solution and do the clean-up separately?
Good question, I think the file that is likely to be affected most by this merge is: https://github.com/pygae/clifford/blob/master/clifford/tools/g3c/rotor_parameterisation.py and the bivector split is specifically here: https://github.com/pygae/clifford/blob/31651d095834ad4c2c5da0692414b55c88823840/clifford/tools/g3c/rotor_parameterisation.py#L52 There is also some functionality from @arsenovic here: https://github.com/pygae/clifford/blob/31651d095834ad4c2c5da0692414b55c88823840/clifford/tools/init.py#L462 And the exponential is chosen currently as the taylor series by default here: https://github.com/pygae/clifford/blob/31651d095834ad4c2c5da0692414b55c88823840/clifford/_multivector.py#L101
It looks to me like the linear algebra method for inverting multivectors is not happy with the complex numbers and we might have to merge this: https://github.com/pygae/clifford/pull/373 first in order to get an inverse that is happy
This pull request introduces 6 alerts when merging 7e72138e37c8d4116bff3e818268190312630991 into 350c6938bab5e9f62630099ed56bd1f8ec33f217 - view on LGTM.com
new alerts:
- 5 for Module is imported with 'import' and 'import from'
- 1 for `__eq__` not overridden when adding attributes
Looking at the talking points on here, specifically:
Exception handling: the algorithm will fail when the eigenvalues are degenerate. We need to discuss how to handle these cases, although this cannot occur in any the popular small algebras of 3DCGA, 3DPGA, STA, and it might be okay to leave it up to the user.
How does this failure manifest itself? Like, does the algorithm just give the wrong result or do we get a divide by zero or something like that?
Can we construct a test case that doesn't work? If so we should include some test cases to ensure that we handle these failures in a reasonable way :)
I'm also tempted to build in something to test that exp and log definitely round trip for the principal logarithm of rotors in loads of different spaces
Due to numerical errors, there are two possibilities. In theory the degenerate cases lead to a "no inverse exists" error, although in practice I've had instances where they are different enough for the code to just continue anyway, especially if the eigenvalues pick up a small imaginary part like lets say, lambda_1 = 1.0 + 1e-8j
and lambda_2 = 1.0 - 1e-8j
. But despite not giving an error, the result will not be a 2-blade. The best would be to compute all abs((Bi**2)(4))
and verify that they are close to zero, but this is a bit expensive. Perhaps a warning when the eigenvalues start to become close will suffice?
I'll think about and implement a test case later.
I'm also tempted to build in something to test that exp and log definitely round trip for the principal logarithm of rotors in loads of different spaces
I agree with that, I think it would be a great idea to make some tests to verify that
R = exp(B)
assert R == exp(log(R))
if I understood you correctly.
So if I create the known_split
def r4_split():
alg = Layout([1, 1, 1, 1])
e1, e2, e3, e4 = alg.basis_vectors_lst
return {'B': 2*e1*e2 + 2*e3*e4,
'Bs': [2*e3*e4, 2*e1*e2],
'ls': [-4.0, -4.0],
'logR': 2*e1*e2 + 2*e3*e4}
then test_known_splits
errors with the traceback
clifford/test/test_invariant_decomposition.py:72 (TestInvariantDecomposition.test_known_splits[known_split0])
self = <clifford.test.test_invariant_decomposition.TestInvariantDecomposition object at 0x16e35fb20>
known_split = {'B': (2^e12) + (2^e34), 'Bs': [(2^e34), (2^e12)], 'logR': (2^e12) + (2^e34), 'ls': [-4.0, -4.0]}
@pytest.mark.parametrize('known_split', [r4_split()])
def test_known_splits(self, known_split):
B = known_split['B']
> Bs, ls = bivector_split(B, roots=True)
test/test_invariant_decomposition.py:76:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
invariant_decomposition.py:101: in bivector_split
Bs, ls = _bivector_split(Wm, return_all=False)
invariant_decomposition.py:82: in _bivector_split
Bs.append(single_split(Wm, li))
invariant_decomposition.py:53: in single_split
return N*D.inv()
_multivector.py:795: in inv
return self._pick_inv(fallback=True)
_multivector.py:767: in _pick_inv
return self.hitzer_inverse()
_multivector.py:741: in hitzer_inverse
return self.layout._hitzer_inverse(self)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
> raise ValueError('Multivector has no inverse')
E ValueError: Multivector has no inverse
_layout.py:634: ValueError
However, if I split B = 2*e1*e2 + (2+1e-7)*e3*e4
instead, I get the split
[-(0.06108^e12) + (2.06108^e34), (2.06108^e12) - (0.06108^e34)]
,
which both square to -4.2518 - (0.2518^e1234)
and thus aren't simple. So numerically speaking, the problems with the inverse being not defined already start a bit earlier. Perhaps it will be enough to give a warning when the eigenvalues start to become degenerate?
I've now added tests for exp(log(R)) == R
in higher dimensions as well. This slows down the tests significantly but was well worth it because it smoked out some bugs in the implementation. Most importantly, it made me realise that you have to sort from boost to rotations, i.e. from positive to negative eigenvalues, before performing the split, to prevent boosts from ever getting a negative scalar part. However, this means that zero could be somewhere in the middle, and thus I've had to implement the even and odd splits properly like in my thesis, whereas before I used a single form which is valid for non-zero eigenvalues and then computed the remaining split with B - sum(Bs)
.
In the spaces 3-5-1, 4-4-1, 5-3-0, and 5-3-1 I now get an error that the log doesn't work up to the desired precision, but that appears to be purely a numerical precision problem, its still correct-ish since np.linalg.norm(R.value - exp(log(R)).value) ~= 1e-7
. Perhaps we should make the demanded precision in the test dimension dependent?
@hugohadfield, is it possible to only iterate spaces with p >= q
? That saves some computation time and the even subalgebras of p,q
and q,p
spaces appear to be isomorphic anyway.
I think adding the dimension dependent accuracy check is a good idea, it would also be a good idea to profile the tests a little and see where the code is spending most of its time, unfortunately I suspect it is in algebra creation. We should also think about maybe numba JITing the various functions in this PR, that way users can include them inside jitted functions for speed etc.
Codecov Report
Merging #398 (e206c24) into master (e63f856) will decrease coverage by
38.77%
. The diff coverage is37.36%
.
@@ Coverage Diff @@
## master #398 +/- ##
===========================================
- Coverage 71.37% 32.59% -38.78%
===========================================
Files 73 75 +2
Lines 9459 9635 +176
Branches 1211 1244 +33
===========================================
- Hits 6751 3141 -3610
- Misses 2538 6381 +3843
+ Partials 170 113 -57
Impacted Files | Coverage Δ | |
---|---|---|
clifford/_layout.py | 56.13% <ø> (-40.83%) |
:arrow_down: |
clifford/test/test_invariant_decomposition.py | 31.81% <31.81%> (ø) |
|
clifford/invariant_decomposition.py | 40.65% <40.65%> (ø) |
|
clifford/test/__init__.py | 72.72% <100.00%> (-16.17%) |
:arrow_down: |
clifford/test/test_multivector_inverse.py | 42.30% <100.00%> (-57.70%) |
:arrow_down: |
clifford/test/test_dpga.py | 9.52% <0.00%> (-90.48%) |
:arrow_down: |
clifford/test/test_degenerate.py | 10.25% <0.00%> (-89.75%) |
:arrow_down: |
clifford/test/test_dg3c.py | 15.92% <0.00%> (-84.08%) |
:arrow_down: |
clifford/_parser.py | 16.39% <0.00%> (-83.61%) |
:arrow_down: |
clifford/test/test_clifford.py | 15.67% <0.00%> (-83.40%) |
:arrow_down: |
... and 51 more |
Continue to review full report at Codecov.
Legend - Click here to learn more
Δ = absolute <relative> (impact)
,ø = not affected
,? = missing data
Powered by Codecov. Last update e63f856...e206c24. Read the comment docs.
whats the status on this? any chance we could get it pushed in some form so i can use it? i mainly want to use in 4d.
just checking in again. this is a really great feature!