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
ENH: sparse.linalg: Implement matrix_power() #18544
Conversation
Whitespace issues fixed. 37 tests fail expecting |
We decided to keep the default behaviour in both If we allowed the user to pass a negative |
Just to note - it looks like #513 introduced a recursive implementation that saves on the number of matmuls for larger exponents. It would be worthwhile to ensure there is a benchmark in place for sparse matrix power to ensure that there are no performance regressions with the updated implementation. @perimosocordiae is working on a benchmark in a separate PR! |
Happy to pull in the old implementation, too. I figured that the |
I just opened gh-18553 with a benchmark covering this case. Once that merges, it'll be really easy to evaluate the impact of this PR's changes using |
My benchmark PR is now merged. |
Yeah, this is not gonna fly. The
I'll swap back to the |
OK, this now uses the older recursive
|
Commenting here to say that I haven't forgotten about this PR! I still need to do one more read through the changes, but in the meantime, could you update the PR description to reflect the current state of the change? |
Great, the description of the PR and docstring is now fixed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you, @ljwolf!
Here are a few comments.
tmp = matrix_power(A, power // 2) | ||
if power % 2: | ||
return A @ tmp @ tmp | ||
else: | ||
return tmp @ tmp |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the benefit of proceeding as follow, here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is part of the existing matrix_power implementation, which (as I understand it) was used to reduce the number of required multiplications. This performance improvement was significant then, and seems so again.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yes, that's just a recursive implementation.
We could further reduce the cost of this function using inline computations with more or less np.log2(power)
iterations not to have the checks just above be rerun several times.
What do you think?
Co-authored-by: Julien Jerphanion <git@jjerphan.xyz>
Co-authored-by: Julien Jerphanion <git@jjerphan.xyz>
Co-authored-by: Julien Jerphanion <git@jjerphan.xyz>
Co-authored-by: Julien Jerphanion <git@jjerphan.xyz>
Co-authored-by: Julien Jerphanion <git@jjerphan.xyz>
OK, I took all of @jjerphan's suggestions, and clarified the comments! let me know where to take this further! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here is a second pass.
I just have another comment regarding the implementation of matrix_power
which I think can be improved.
tmp = matrix_power(A, power // 2) | ||
if power % 2: | ||
return A @ tmp @ tmp | ||
else: | ||
return tmp @ tmp |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yes, that's just a recursive implementation.
We could further reduce the cost of this function using inline computations with more or less np.log2(power)
iterations not to have the checks just above be rerun several times.
What do you think?
@@ -861,3 +862,47 @@ def _ell(A, m): | |||
log2_alpha_div_u = np.log2(alpha/u) | |||
value = int(np.ceil(log2_alpha_div_u / (2 * m))) | |||
return max(value, 0) | |||
|
|||
def matrix_power(A, power, structure=None): | |||
""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A couple docstring issues:
- As @jjerphan already noted,
structure
must be described in the "Parameters" section. If there is nothing implemented yet, then remove the parameter. - Add an "Examples" section (see DOC: Add "Examples" to docstrings #7168).
- Add a "Notes" section with the appropriate
versionadded
markup.
For comparison, see the expm
docstring in this file. A "Notes" section with just the versionadded
markup is fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Have done. "Structure" was a holdover from using the MatrixPowerOperator
implementation, and it has been removed. I also refer now to the idea of your stackoverflow comment in the notes... I think it's pretty important to disclose this to the user.
The recursive/logarithmic implementation is probably a good default, and I don't suggest we change it, but folks involved in this PR might be interested to know that it is not always the best approach. Its performance can be substantially worse than simple iteration, depending on how the number of nonzero values increases as powers are computed. See my answer to a question on stackoverflow for an example where the recursive calculation of |
thanks for the comment @WarrenWeckesser! I think I've addressed the concerns mentioned. The point on your stackoverflow post is useful to disclose (imho) so I've added the gist of the point to the notes. |
I should say, I'm happy to move forward with an inline-based computation, but my goal was only to bring forward the existing implementation. This recursive strategy was adopted in #513, and we probably should develop a more stringent benchmark to characterise the relative performance of a recursive vs. inline strategy over different sizes and sparsity levels. For now, we should probably proceed with the current |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM.
I am also in favor of studying both possible implementations in a dedicated PR.
I just have one comment regarding the docstring.
Co-authored-by: Julien Jerphanion <git@jjerphan.xyz>
As discussed, there are a few ways that this implementation could be made more efficient, but as it stands this is already a very nice improvement to the library: without a standalone Thanks for the PR, @ljwolf ! |
* add matrix_power() using MatrixPowerOperator() * remove unused imports in _matrix.py * move back to recursive matrix_power implementation * fix lint issues * add matrix_power to __init__ autosummary * fix docstring explaining negative powers * Update scipy/sparse/linalg/_matfuncs.py Co-authored-by: Julien Jerphanion <git@jjerphan.xyz> * Update scipy/sparse/_matrix.py Co-authored-by: Julien Jerphanion <git@jjerphan.xyz> * Update scipy/sparse/linalg/_matfuncs.py Co-authored-by: Julien Jerphanion <git@jjerphan.xyz> * Update scipy/sparse/_matrix.py Co-authored-by: Julien Jerphanion <git@jjerphan.xyz> * Update scipy/sparse/linalg/_matfuncs.py Co-authored-by: Julien Jerphanion <git@jjerphan.xyz> * update docstring and remove structure param * Update scipy/sparse/linalg/_matfuncs.py Co-authored-by: Julien Jerphanion <git@jjerphan.xyz> --------- Co-authored-by: Julien Jerphanion <git@jjerphan.xyz>
Reference issue
No specific reference issue
What does this implement/fix?
This adds a
scipy.sparse.linalg.matrix_power()
analogue tonumpy.linalg.matrix_power()
. It uses the old_spmatrix.__pow__()
recursive implementation, sinceMatrixPowerOperator()
was quite a bit slower.