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

Faster matrix exponentiation #18595

Merged
merged 3 commits into from
Feb 13, 2020
Merged

Faster matrix exponentiation #18595

merged 3 commits into from
Feb 13, 2020

Conversation

abhinav-anand-addepar
Copy link
Member

  • matrices
    • Faster Matrix exponentiation using Cayley Hamilton Theorem

@sympy-bot
Copy link

sympy-bot commented Feb 7, 2020

Hi, I am the SymPy bot (v149). I'm here to help you write a release notes entry. Please read the guide on how to write release notes.

Your release notes are in good order.

Here is what the release notes will look like:

This will be added to https://github.com/sympy/sympy/wiki/Release-Notes-for-1.6.

Note: This comment will be updated with the latest check if you edit the pull request. You need to reload the page to see it.

Click here to see the pull request description that was parsed.

<!-- BEGIN RELEASE NOTES -->
* matrices
  * Faster Matrix exponentiation using Cayley Hamilton Theorem
<!-- END RELEASE NOTES -->

Update

The release notes on the wiki have been updated.

@abhinav-anand-addepar
Copy link
Member Author

When A = Matrix([[2, 3, 4, 4, 5], [2, 1, 2, 3, 1], [4, 3, 5, 0, 2], [3, 1, 4, 0, 3], [3, 2, 3, 4, 3]])
exp = 200000
Using Cayley 2.937811851501465
Without Using Cayley 16.406556844711304
exp = 500000
Using Cayley 10.280319690704346
Without Using Cayley 69.17847156524658
exp = 1000000
Using Cayley 26.187591552734375
Without Using Cayley 208.7181031703949
[Finished in 334.3s]

@abhinav-anand-addepar
Copy link
Member Author

abhinav-anand-addepar commented Feb 7, 2020

I think the condition check for n can be removed. I will have to do the timing analysis for different rows

@abhinav-anand-addepar
Copy link
Member Author

When A = Matrix([[2, 1, 3, 0, 0, 2, 3], [2, 3, 1, 3, 3, 2, 3], [3, 3, 2, 3, 1, 2, 2], [2, 0, 0, 0, 3, 0, 1], [2, 2, 3, 0, 0, 3, 3], [0, 2, 2, 3, 1, 3, 1], [3, 3, 3, 3, 0, 0, 1]])
exp = 200000
Using Cayley 6.235580921173096
Without Using Cayley 42.61778020858765
exp = 500000
Using Cayley 20.59529948234558
Without Using Cayley 174.21074295043945
exp = 1000000
Using Cayley 49.71791124343872
Without Using Cayley 519.055638551712
[Finished in 813.1s]

@sylee957
Copy link
Member

sylee957 commented Feb 7, 2020

Generally, I don't want to see another polynomial long division reimplemented here
Why not use https://docs.sympy.org/latest/modules/polys/reference.html#sympy.polys.polytools.rem

Is it slow?

@abhinav-anand-addepar
Copy link
Member Author

Generally, I don't want to see another polynomial long division reimplemented here
Why not use https://docs.sympy.org/latest/modules/polys/reference.html#sympy.polys.polytools.rem

Is it slow?

I am not using polynomial long division

@sylee957
Copy link
Member

sylee957 commented Feb 7, 2020

But I mean what about contributing to the polynomial for more general use

I've already found some bug with a matrix with complex entries
Matrix([[2+I, 3, 4, 4, 5], [2, 1, 2, 3, 1], [4, 3, 5, 0, 2], [3, 1, 4, 0, 3], [3, 2, 3, 4, 3]])
And I doubt that you can use cayley exponential with a matrix of symbolic coefficients unless it's a special case

@sylee957
Copy link
Member

sylee957 commented Feb 7, 2020

And I ask you what kind of method did you use for computing the polynomial remainer

@abhinav-anand-addepar
Copy link
Member Author

And I ask you what kind of method did you use for computing the polynomial remainer

There is no polynomial remainder here. I am using this algorithm.

@abhinav-anand-addepar
Copy link
Member Author

But I mean what about contributing to the polynomial for more general use

I've already found some bug with a matrix with complex entries
Matrix([[2+I, 3, 4, 4, 5], [2, 1, 2, 3, 1], [4, 3, 5, 0, 2], [3, 1, 4, 0, 3], [3, 2, 3, 4, 3]])
And I doubt that you can use cayley exponential with a matrix of symbolic coefficients unless it's a special case

It's working fine.
The code i used is:
sym1
Can you tell what exponent you used.

@sylee957
Copy link
Member

sylee957 commented Feb 7, 2020

from sympy import *

A = Matrix([[2+I, 3, 4, 4, 5], [2, 1, 2, 3, 1], [4, 3, 5, 0, 2], [3, 1, 4, 0, 3], [3, 2, 3, 4, 3]])
A._pow_cayley(100)

I'm calling from the private function

In my second thought, I suppose it can be used for symbolic or complex coefficients, but it should be fixed.

@sylee957
Copy link
Member

sylee957 commented Feb 7, 2020

And I still think that what you're doing here is computing the remainder of x**n with the characteristic polynomial of the matrix.
So if that is the case, I'd expect that this can be made to use less symbolic substitutions, but more direct coefficient manipulation, if this is the reason it's causing the problems.

@abhinav-anand-addepar
Copy link
Member Author

And I still think that what you're doing here is computing the remainder of x**n with the characteristic polynomial of the matrix.
So if that is the case, I'd expect that this can be made to use less symbolic substitutions, but more direct coefficient manipulation, if this is the reason it's causing the problems.

the problem was that i initialised the polynomial without telling the variable. I used poly(x**4). I should have used poly(x**4,x). It will cause problem when there is another symbol in the charpoly or A. Now it should be working fine.
But there is still a problem , that is that i used {x, c1 , c2......} symbols in the function so the matrix should not contain any of these symbols. Is there any symbol in sympy that can be initialised only within a function and not by the user?

@abhinav-anand-addepar
Copy link
Member Author

And I still think that what you're doing here is computing the remainder of x**n with the characteristic polynomial of the matrix.
So if that is the case, I'd expect that this can be made to use less symbolic substitutions, but more direct coefficient manipulation, if this is the reason it's causing the problems.

Yes i think you are right, let me check which is faster.

@abhinav-anand-addepar
Copy link
Member Author

@sylee957 i checked. polynomial division is too slow to be used here. where exp > 1000, i found that there was significant drop in performance when we use polynomial division.

@sylee957
Copy link
Member

sylee957 commented Feb 7, 2020

In my opinion, I think that the idea from here can be generalized to improve the polynomial remainder with a monic polynomial.

@abhinav-anand-addepar
Copy link
Member Author

In my opinion, I think that the idea from here can be generalized to improve the polynomial remainder with a monic polynomial.

this is a special case. here x**n can be divided by a multivariate polynomial.

@sylee957
Copy link
Member

sylee957 commented Feb 7, 2020

But there is still a problem , that is that i used {x, c1 , c2......} symbols in the function so the matrix should not contain any of these symbols. Is there any symbol in sympy that can be initialised only within a function and not by the user?

You should use dummy if you are going to use symbols in library code. But generally, I don't think that it's a right direction unless the problem is too difficult.

sympy/matrices/common.py Outdated Show resolved Hide resolved
sympy/matrices/common.py Outdated Show resolved Hide resolved
sympy/matrices/common.py Outdated Show resolved Hide resolved
@@ -2377,7 +2454,9 @@ def __pow__(self, exp):

return self.pow(exp)

def pow(self, exp, jordan=None):

def pow(self, exp, jordan=None, cayley=False):
Copy link
Contributor

Choose a reason for hiding this comment

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

Having method='jordan' is better than two boolean arguments

Copy link
Contributor

Choose a reason for hiding this comment

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

The pow method here is new since the last release of sympy so the signature can be changed.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, I’d also want to use a single keyword to control every methods

@jksuom
Copy link
Member

jksuom commented Feb 7, 2020

This looks like solving the linear recursion defined by the characteristic function. The function linrec was designed for numerical sequences but it seems to work with more general objects that have the relevant arithmetic methods defined. For matrices, it is necessary to define addition with zero. I have tested with this quick fix:

--- a/sympy/matrices/common.py
+++ b/sympy/matrices/common.py
@@ -2234,6 +2234,8 @@ def __abs__(self):
     @call_highest_priority('__radd__')
     def __add__(self, other):
         """Return self + other, raising ShapeError if shapes don't match."""
+        if not other:
+            return self
         other = _matrixify(other)
         # matrix-like objects can have shapes.  This is
         # our first sanity check.

Then

>>> from sympy import Matrix, eye
>>> from sympy.discrete.recurrences import linrec
>>> A = Matrix([[1,2],[4,5]])
>>> p = A.charpoly()
>>> coeffs = (-p).all_coeffs()[1:]
>>> init = [eye(2), A]
>>> linrec(coeffs, init, 20)
Matrix([
[3428578511656497,  4683525345792216],
[9367050691584432, 12795629203240929]])
>>> _ == A**20
True

@@ -6,7 +6,7 @@
from sympy.core import S, sympify
from sympy.core.compatibility import as_int, iterable

def linrec(coeffs, init, n):
def linrec(coeffs, init, n, final_expr=False):
Copy link
Member

Choose a reason for hiding this comment

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

How about having a separate linrec_coeffs(coeffs, n) that could also be used by linrec?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, I would also agree on @jksuom 's idea to have a separate function than adding up a keyword.
It is not a good design because setting the keyword True would make the variable init obscure.

Copy link
Member Author

Choose a reason for hiding this comment

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

can you look at the error?

Copy link
Member Author

Choose a reason for hiding this comment

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

got it

@codecov
Copy link

codecov bot commented Feb 9, 2020

Codecov Report

Merging #18595 into master will decrease coverage by 0.005%.
The diff coverage is 51.063%.

@@             Coverage Diff              @@
##            master   #18595       +/-   ##
============================================
- Coverage   75.585%   75.58%   -0.006%     
============================================
  Files          644      644               
  Lines       167452   167573      +121     
  Branches     39462    39502       +40     
============================================
+ Hits        126570   126652       +82     
- Misses       35364    35391       +27     
- Partials      5518     5530       +12


return b[n] if n < k else sum(u*v for u, v in zip(_linrec_coeffs(c, n), b))

def _linrec_coeffs(c, n):
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps this could be explicitly public (no _) though not necessarily imported in __init__.py. The docstring could refer to linrec.

be used if possible and False means it should not be used unless
it is the only way to calculate the power.

method : jordon, cayley
Copy link
Contributor

Choose a reason for hiding this comment

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

It should also be possible to specify recursion

Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps recursion should be specified as "multiply" though since I think that makes more sense to users.

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually since dotprodsimp is only used for recursion it should probably be:

        method : multiply, mulsimp, jordon, cayley

Then mulsimp corresponds to using recursion with dotprodsimp and the dotprodsimp parameter can be removed (it was added since last release).

Copy link
Member Author

Choose a reason for hiding this comment

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

I have made the changes. Is is good now?

def pow(self, exp, jordan=None, dotprodsimp=None):

def pow(self, exp, method=None):

Copy link
Member

Choose a reason for hiding this comment

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

Suggested change

If left as None then Jordan form exponentiation will be used under
certain conditions, True specifies that jordan_pow should always
be used if possible and False means it should not be used unless
it is the only way to calculate the power.

Copy link
Member

Choose a reason for hiding this comment

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

Suggested change

@abhinav-anand-addepar
Copy link
Member Author

I don't know why the tests are failing, could anyone take a look

@abhinav-anand-addepar
Copy link
Member Author

got it

raise

if _get_intermediate_simp_bool(True, dotprodsimp):
if method == 'cayley':
return a._eval_pow_by_cayley(exp)
Copy link
Contributor

Choose a reason for hiding this comment

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

These things should outside the enclosing if

Copy link
Member Author

Choose a reason for hiding this comment

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

Done. Is this okay?

return a._eval_pow_by_recursion_dotprodsimp(exp)
elif method == 'cayley':
if not exp.is_Number or exp % 1 != 0:
raise ValueError("cayley method is only valid for integer powers")
Copy link
Member

Choose a reason for hiding this comment

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

nonnegative integer powers?

Copy link
Member Author

Choose a reason for hiding this comment

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

when the power is negative, then the matrix is inverted and exp *= -1 is done in previous step.
So, even method = multiply works for negative powers.

@sylee957 sylee957 merged commit dc48d83 into sympy:master Feb 13, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants