Skip to content

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

Closed
wants to merge 17 commits into from

Conversation

mborgerding
Copy link

@mborgerding mborgerding commented Nov 2, 2019

Reference issue
See #7834

What does this implement/fix?

This implements the convolution_matrix function. It is similar to the Matlab/Octave convmtx function but not derived from their implementations in any way.
There is an additional "mode" argument analogous to that in numpy.convolve.

@mborgerding mborgerding changed the title add convmtx to scipy.linalg ENH: add convmtx to scipy.linalg Nov 2, 2019
@tylerjereddy tylerjereddy added enhancement A new feature or improvement scipy.linalg labels Nov 2, 2019
Copy link
Contributor

@tylerjereddy tylerjereddy left a 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?

Mark Borgerding added 2 commits November 3, 2019 09:31
Copy link
Member

@ilayn ilayn left a 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.

@mborgerding
Copy link
Author

@ilayn , @tylerjereddy
I believe all changes have been done except we have not resolved the discussion above about using itertools.product in a single fast test vs. nesting parameterize decorators to make multiple tests.
I tend to favor the itertools approach as I find it clearer to read and maintain the code, but I don't feel strongly about it. I defer to your judgement.

@mborgerding mborgerding changed the title ENH: add convmtx to scipy.linalg ENH: add scipy.linalg.convolution_matrix Nov 5, 2019
@mborgerding
Copy link
Author

bump

@ilayn
Copy link
Member

ilayn commented Nov 9, 2019

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.

@mborgerding
Copy link
Author

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.

Copy link
Member

@ilayn ilayn left a 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.

@mborgerding
Copy link
Author

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.
Apparently some of the checks are turned off by default.
I was able to turn on full pedantic mode with "--ignore ''"

@mborgerding
Copy link
Author

Is there anything more to be done for this PR?

@ilayn
Copy link
Member

ilayn commented Nov 16, 2019

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.

@ilayn ilayn added this to the 1.4.0 milestone Nov 16, 2019
@ilayn
Copy link
Member

ilayn commented Nov 16, 2019

@mborgerding

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.

Copy link
Contributor

@tylerjereddy tylerjereddy left a 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))\
Copy link
Contributor

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

Copy link
Member

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():
Copy link
Contributor

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

Copy link
Member

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

Copy link
Contributor

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.

Copy link
Member

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(
Copy link
Contributor

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..

@tylerjereddy
Copy link
Contributor

@rgommers Have you observed this Python 3.8/Windows Azure timeout issue before?

@tylerjereddy
Copy link
Contributor

The ppc64le failure probably isn't related

@tylerjereddy
Copy link
Contributor

There are no holes in the line coverage, which is good.

@rgommers
Copy link
Member

@rgommers Have you observed this Python 3.8/Windows Azure timeout issue before?

no I haven't

@tylerjereddy
Copy link
Contributor

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.

@tylerjereddy tylerjereddy modified the milestones: 1.4.0, 1.5.0 Nov 17, 2019
@ilayn
Copy link
Member

ilayn commented Nov 17, 2019

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):
Copy link
Contributor

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):
Copy link
Contributor

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

Copy link
Member

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.

Copy link
Member

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.

Copy link
Member

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.

Copy link
Member

@WarrenWeckesser WarrenWeckesser left a 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 be 3*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].
Copy link
Member

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.

Suggested change
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)``.
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
``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)``
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
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').
Copy link
Member

Choose a reason for hiding this comment

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

Spaces after commas:

Suggested change
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`
Copy link
Member

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?

Suggested change
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]])

Copy link
Member

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')")
Copy link
Member

Choose a reason for hiding this comment

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

Spaces after commas:

Suggested change
"'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))\
Copy link
Member

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()
Copy link
Member

@WarrenWeckesser WarrenWeckesser May 2, 2020

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().

Suggested change
.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':
Copy link
Member

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.

@tylerjereddy
Copy link
Contributor

Do you still have time and interest for working on it? Let us know. If not, we can take it over.

@WarrenWeckesser Are you going to have time to take over here before we branch?

@WarrenWeckesser
Copy link
Member

@tylerjereddy, thanks for the reminder. Yes, I'll take this over.

@WarrenWeckesser
Copy link
Member

WarrenWeckesser commented May 17, 2020

Updated pull request is in #12135.

@WarrenWeckesser
Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement A new feature or improvement scipy.linalg
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants