-
-
Notifications
You must be signed in to change notification settings - Fork 5.4k
ENH: add scipy.linalg.convolution_matrix #11003
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
Conversation
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.
Added a few comments.
Note that we normally discuss new features on the mailing list, but I see that this actually already has some positive feedback in the cross-linked issue. The longer-term issue was finding an implementation that was not in another license-restricted code base.
Is there a citation for this implementation?
…n the "full" and "valid" modes instead of trimming the "full" result.
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.
@mborgerding Thank you for your contribution and time. I've added some comments which all are of minor nature. My only major comment is the function name that it is probably better to spell out instead of the typical matlab cryptic naming scheme. I propose convolution_matrix
but maybe you or others have a better name proposal.
…g comment. test the equality of A[i,j] given in the docstring
…lightly refactored test
@ilayn , @tylerjereddy |
bump |
Hi @mborgerding instead of range(2,7) for both test cases please use [10, 100] such that at least two order of magnitudes tested. There is not much difference between 3 and 4 in terms of testing behavior. Some problems only reveal themselves when the values are large or arrays size are large. |
I expanded the range, but rather than [10,100], it tests the lowest meaningful number for convolution (2) and at least one odd number (9), and 100. |
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.
You can install pycodestyle and use that for trivial PEP8 checks or adjust your editor such that it does it for you.
I appreciate the tip, but the default pycodestyle options did not seem to catch some of the commas. |
Is there anything more to be done for this PR? |
If the CI is green this would be ready for 1.4.0. Please review if you would have time. I'll add the label prospectively. |
Just for the future reference, please make separate branches to work on new features and to send PRs such that your standard working repo does not interfere with the PRs you have submitted. Once a PR is merged you can safely delete that branch and keep working on other branches. |
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.
Was going to wait for Azure CI to finish to look for coverage holes, but if @ilayn is happy that should be good enough I suppose.
def test_vector(n, cpx): | ||
'make a complex or real test vector of length n' | ||
if cpx: | ||
return np.random.normal(size=(n, 2))\ |
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.
we try to avoid these slash continuations usually -- I guess that can be cleaned up later
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.
I'll be more insistent than @tylerjereddy: don't use a continuation. Fortunately, my next comment will make it unnecessary...
@@ -640,3 +642,45 @@ def test_fiedler_companion(): | |||
fc = fiedler_companion([1., -16., 86., -176., 105.]) | |||
assert_array_almost_equal(eigvals(fc), | |||
np.array([7., 5., 3., 1.])) | |||
|
|||
|
|||
def test_convolution_matrix(): |
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.
normally we'd use a class
and then place specific tests within that class for pytest namespace resolution
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.
It's a singleton so here a class is not needed
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.
I don't understand--there are actually several things being tested below--normally one would split each thing being tested into its own specific unit test classified within the same test class.
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.
I agree with @tylerjereddy, the tests should be split up, and the big loop at the end should use pytest's parametrize
feature. The increased granularity lets us know all the things that are going wrong if there is a failure, instead of just the first thing that goes wrong.
convolution_matrix((1, 1), 4, mode='invalid argument') | ||
|
||
array_sizes = (2, 9, 100) | ||
for cpx, na, nv, mode in itertools.product( |
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.
I suppose this is still used in our codebase a fair bit; at some number of permutations I think we should prefer to parametrize, but anyway..
@rgommers Have you observed this Python 3.8/Windows Azure timeout issue before? |
The |
There are no holes in the line coverage, which is good. |
no I haven't |
I'm bumping the milestone--we've already had more last-minute merges than I'd like & the CI is a little strange here, but this looks close to merging after we branch. |
The failures are not related to this PR. |
return np.random.normal(size=(n,)) | ||
|
||
# first arg must be a 1d array, otherwise ValueError | ||
with assert_raises(ValueError): |
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 one test
convolution_matrix(1, 4) | ||
|
||
# mode must be in ('full','valid','same') | ||
with assert_raises(ValueError): |
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 testing something else
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.
That's unnecessary granularity. And also if there is no scope nesting it's just a preference. If we write one test per assertion we will have millions of tests.
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.
See examples in the rest of the file for more examples.
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.
Unfortunately, there is a lot of code in our codebase that does not conform to what we now consider best practices. It is our job as reviewers to ensure the new code does so.
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.
@mborgerding, thanks for contributing this pull request. convolution_matrix
will be a nice addtion to the collection of special matrix functions, and I'd like to get it into version 1.5.0.
I made a bunch of comments in-line. Here are some additional comments:
-
Thanks for completing the table of special matrices in the tutorial!
-
There are some incorrect results.
In the following two examples, the output should be3*np.eye(5)
:In [114]: convolution_matrix([3], 5, mode='valid') Out[114]: array([], shape=(0, 0), dtype=int64) In [115]: convolution_matrix([3], 5, mode='same') Out[115]: array([], shape=(0, 5), dtype=int64)
In the next two examples, the output should be
np.array([[3], [4]])
:In [146]: convolution_matrix([3, 4], 1, mode='valid') Out[146]: array([], shape=(0, 0), dtype=int64) In [147]: convolution_matrix([3, 4], 1, mode='same') Out[147]: array([], shape=(0, 1), dtype=int64)
It has been about 5 months since there was activity in this pull request. Do you still have time and interest for working on it? Let us know. If not, we can take it over.
""" | ||
Construct a convolution matrix. | ||
|
||
Constructs the dense matrix representing convolution[1]. |
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.
Need a trailing _
for the citation.
Constructs the dense matrix representing convolution[1]. | |
Constructs the dense matrix representing convolution[1]_. |
``A = convolution_matrix(a, n[, mode])`` creates a matrix ``A`` such that | ||
``A @ v`` (or ``matmul(A, v)``) is equivalent to using | ||
``convolve(a, v[, mode])``. In the default 'full' mode, the entries of | ||
``A`` is given by ``A[i,j] == (a[i-j] if (0 <= (i-j) < m) else 0)``. |
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`` is given by ``A[i,j] == (a[i-j] if (0 <= (i-j) < m) else 0)``. | |
``A`` are given by ``A[i,j] == (a[i-j] if (0 <= (i-j) < m) else 0)``. |
The 1-D array to convolve. | ||
n : int | ||
The number of columns in the resulting matrix. | ||
This is analogous to the length of `v` in ``numpy.convolve(a,v)`` |
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 analogous to the length of `v` in ``numpy.convolve(a,v)`` | |
This is analogous to the length of `v` in ``numpy.convolve(a,v)``. |
This is analogous to the length of `v` in ``numpy.convolve(a,v)`` | ||
mode : str | ||
This is analogous to `mode` in numpy.convolve(v, a, mode). | ||
It must be one of ('full','valid','same'). |
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.
Spaces after commas:
It must be one of ('full','valid','same'). | |
It must be one of ('full', 'valid', 'same'). |
Returns | ||
------- | ||
A : (k, n) ndarray | ||
The convolution matrix whose row count `k` depends on `mode` |
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.
Might need ::
at the end?
The convolution matrix whose row count `k` depends on `mode` | |
The convolution matrix whose row count `k` depends on `mode`:: |
[ 0, -1, 2, -1, 0], | ||
[ 0, 0, -1, 2, -1], | ||
[ 0, 0, 0, -1, 2]]) | ||
|
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.
Let's add examples for mode='full'
and mode='valid'
, too.
|
||
if mode not in ('full', 'valid', 'same'): | ||
raise ValueError( | ||
"'mode' argument must be one of ('full','valid','same')") |
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.
Spaces after commas:
"'mode' argument must be one of ('full','valid','same')") | |
"'mode' argument must be one of ('full', 'valid', 'same')") |
def test_vector(n, cpx): | ||
'make a complex or real test vector of length n' | ||
if cpx: | ||
return np.random.normal(size=(n, 2))\ |
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.
I'll be more insistent than @tylerjereddy: don't use a continuation. Fortunately, my next comment will make it unnecessary...
'make a complex or real test vector of length n' | ||
if cpx: | ||
return np.random.normal(size=(n, 2))\ | ||
.astype(np.float64).view(np.complex128).ravel() |
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.
There is no need for .astype(np.float64)
here. That is already the return type of np.random.normal()
.
.astype(np.float64).view(np.complex128).ravel() | |
.view(np.complex128).ravel() |
A = convolution_matrix(a, nv, mode) | ||
y2 = A @ v | ||
assert_array_almost_equal(y1, y2) | ||
if mode == 'full': |
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.
The nested loop in this if
statement makes this part of the test very slow. It is checking element by element that A
is a Toeplitz matrix containing a
in the expected positions. I think we can simply delete this part of the test. If A
is not the correct Toeplitz matrix, the probability of the previous comparison of y1
and y2
passing is miniscule.
@WarrenWeckesser Are you going to have time to take over here before we branch? |
@tylerjereddy, thanks for the reminder. Yes, I'll take this over. |
Updated pull request is in #12135. |
This PR was continued in gh-12135, and that PR has been merged, so closing. Thanks @mborgerding for the contribution, and thanks @ilayn and @tylerjereddy for the reviews. |
Reference issue
See #7834
What does this implement/fix?
This implements the
convolution_matrix
function. It is similar to the Matlab/Octaveconvmtx
function but not derived from their implementations in any way.There is an additional "mode" argument analogous to that in numpy.convolve.