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

ENH: Wrap LAPACK's dsytrd #7780

Merged
merged 19 commits into from
Sep 3, 2017
Merged

ENH: Wrap LAPACK's dsytrd #7780

merged 19 commits into from
Sep 3, 2017

Conversation

nschloe
Copy link
Contributor

@nschloe nschloe commented Aug 22, 2017

This PR adds the wrapper for LAPACK's dsytrd, a routine that takes a real symmetric matrix A and transforms it to a tridiagonal matrix by an orthogonal similarity tranformation

Q^T * A * Q = T

With test.

I would have wrapped its complex variant *HETRD as well, but I didn't know how to map the complex type <ctype2c> to its corresponding real type in the wrapper code. (HETRD transforms a complex Hermitian matrix to a real symmetric tridiagonal matrix.)

Fixes #7775.

@nschloe nschloe mentioned this pull request Aug 22, 2017
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.

I think once the <prefix> part is fixed this can build and the tests can be evaluated properly. One thing to look out for is not to cause segfaults even when used inappropriately. Recently we had a case with supplying np.empty((0,0)) was causing segfaults so better add it and possibly others if any, to the tests.

integer intent(in) :: n
integer optional,intent(in),check(lower==0||lower==1) :: lower = 0

integer intent(hide),depend(n) :: lda=n
Copy link
Member

Choose a reason for hiding this comment

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

Haven't checked if lwork=-1 causes a shortcut in the Fortran source, but still better to keep lda = MAX(n,1) to be on the safe side as it causes trouble on the Fortran side with zero-sized arrays. n=0 OK for LAPACK for which they have often a safeguard as a convention but lda=0 is not.

callstatement (*f2py_func)((lower?"L":"U"),&n,a,&lda,d,e,tau,work,&lwork,&info);
callprotoargument char*,int*,<ctype2>*,int*,<ctype2>*,<ctype2>*,<ctype2>*,<ctype2>*,int*,int*

integer intent(hide),depend(a) :: lda=shape(a,0)
Copy link
Member

Choose a reason for hiding this comment

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

Use lda = MAX(shape(a,0),1) to avoid lda=0

A = np.random.normal(size=(n, n))
A += A.T

sytrd, sytrd_lwork = get_lapack_funcs(('sytrd', 'sytrd_lwork'))
Copy link
Member

Choose a reason for hiding this comment

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

You would add the array you want to get the LAPACK routine for as get_lapack_funcs(('sytrd', 'sytrd_lwork'), (A,)) such that d is selected. Same should also test for s flavor with a float32 array.

<ftype2> intent(out) :: work
integer intent(hide) :: lwork = -1
integer intent(out) :: info
end subroutine <prefix>sytrd_lwork
Copy link
Member

Choose a reason for hiding this comment

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

This needs to be <prefix2> instead.


# query lwork
lwork, info = sytrd_lwork(n)
assert info == 0
Copy link
Member

Choose a reason for hiding this comment

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

Naked asserts are not allowed. You can use assert_equal from numpy.testing module

assert info == 0

data, d, e, tau, info = sytrd(A, lwork)
assert info == 0
Copy link
Member

Choose a reason for hiding this comment

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

Same comment about assert

np.random.seed(42)
n = 3
A = np.random.normal(size=(n, n))
A += A.T
Copy link
Member

Choose a reason for hiding this comment

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

Inplace addition of overlapping arrays is undefined in Numpy < 1.13

@nschloe
Copy link
Contributor Author

nschloe commented Aug 22, 2017

@ilayn When supplying a np.empty((0, 0)) matrix, the method fails with

ValueError: failed to create intent(cache|hide)|optional array-- must have defined dimensions but got (-1,)

that sounds pretty reasonable, right?

@nschloe
Copy link
Contributor Author

nschloe commented Aug 23, 2017

I'm going to need some help here. The error message is

E           AssertionError: 
E           Not equal to tolerance rtol=0.000119209, atol=0
E           
E           (mismatch 22.22222222222223%)
E            x: array([[  2.941178e-02,   3.823532e-01,  -2.384186e-07],
E                  [  3.823531e-01,   4.970589e+00,  -5.830952e+00],
E                  [ -2.384186e-07,  -5.830952e+00,   6.000000e+00]], dtype=float32)
E            y: array([[ 0.029412,  0.382353,  0.      ],
E                  [ 0.382353,  4.970587, -5.830952],
E                  [ 0.      , -5.830952,  6.      ]], dtype=float32)

and really x and y look equal. Why is there a mismatch?

@ev-br
Copy link
Member

ev-br commented Aug 23, 2017

What you can do is to drop into a pdb session (insert import pdb; pdb.set_trace() just before the assertion and print the difference

@nschloe
Copy link
Contributor Author

nschloe commented Aug 23, 2017

@ev-br How to I execute just the LAPACK tests locally? (Otherwise it always takes half an hour.)

@ev-br
Copy link
Member

ev-br commented Aug 23, 2017

$ python runtests.py -s linalg or $ python runtests.py -t /path/to/test/file

@rgommers rgommers added enhancement A new feature or improvement scipy.linalg labels Aug 24, 2017
@nschloe
Copy link
Contributor Author

nschloe commented Aug 24, 2017

@everyone Tests are passing now. Anything else for me to do?

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.

I've added some minor comments. I think when they are addressed, this is good to go.

integer intent(hide),depend(a) :: n=shape(a,1)
integer optional,intent(in),check(lower==0||lower==1) :: lower = 0

<ftype2> dimension(lda,n),intent(in,out,copy,out=c) :: a
Copy link
Member

Choose a reason for hiding this comment

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

This needs a squareness check before LAPACK chews it so it is best if we intervene

<ftype2> dimension(lda,n),check(shape(a,0)==shape(a,1)),intent(in,out,copy,out=c) :: a

<ftype2> dimension(n),intent(out),depend(n) :: d
<ftype2> dimension(n-1),intent(out),depend(n) :: e
<ftype2> dimension(n-1),intent(out),depend(n) :: tau
integer intent(in),optional,depend(n),check(lwork>=MAX(n,1)) :: lwork = MAX(n,1)
Copy link
Member

Choose a reason for hiding this comment

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

Lwork is not needed to be n or higher. It just needs to be greater than or equal to 1. For this you can keep the default but the check needs to be as

integer intent(in),optional,depend(n),check(lwork>=1||lwork==-1) :: lwork = MAX(n,1)

Copy link
Member

Choose a reason for hiding this comment

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

Here we already define sytrd_lwork for -1 query, but some people do it manually so better to keep it general.

def test_sytrd(self):
for dtype in REAL_DTYPES:
# "random" symmetric matrix
A = np.array([
Copy link
Member

Choose a reason for hiding this comment

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

If you create A via

n = 3
A = np.zeros((n, n), dtype=dtype)
A[np.triu_indices_from(A)] = np.arange(1, 2*n+1, dtype=dtype)

(or typing out manually if you wish), you can quickly test for lower=1 behavior for n of your choice since it will give the matrix and it's diagonal back together with zero arrays for a diagonal matrix. Then it can continue with lower=0 case for the precise calculation.

The comment can be worded as # some upper triangular array to avoid confusion about the randomness too.

# disable rtol here since some values in QTAQ and T are very close
# to 0.
assert_allclose(QTAQ, T, atol=5*np.finfo(dtype).eps, rtol=1.0)
return
Copy link
Member

Choose a reason for hiding this comment

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

I think we don't need this return here, no?

@ilayn
Copy link
Member

ilayn commented Aug 25, 2017

I have just seen your final remark in the original PR comment. Let's not waste this opportunity to wrap the complex versions. This should do the (C/Z)HETRD versions

   subroutine <prefix2c>hetrd(lower,n,a,lda,d,e,tau,work,lwork,info)
     ! Reduce a complex hermitian matrix A to real symmetric
     ! tridiagonal form T by an orthogonal similarity transformation:
     ! Q**H * A * Q = T.

     callstatement (*f2py_func)((lower?"L":"U"),&n,a,&lda,d,e,tau,work,&lwork,&info);
     callprotoargument char*,int*,<ctype2c>*,int*,<ctype2>*,<ctype2>*,<ctype2c>*,<ctype2c>*,int*,int*

     integer intent(hide),depend(a) :: lda=MAX(shape(a,0),1)
     integer intent(hide),depend(a) :: n=shape(a,1)
     integer optional,intent(in),check(lower==0||lower==1) :: lower = 0

     <ftype2c> dimension(lda,n),check(shape(a,0)==shape(a,1)),intent(in,out,copy,out=c) :: a
     <ftype2> dimension(n),intent(out),depend(n) :: d
     <ftype2> dimension(n-1),intent(out),depend(n) :: e
     <ftype2c> dimension(n-1),intent(out),depend(n) :: tau
     integer intent(in),optional,depend(n),check(lwork>=1||lwork==-1) :: lwork = MAX(n,1)
     <ftype2c> dimension(lwork),intent(cache,hide) :: work
     integer intent(out) :: info
   end subroutine <prefix2c>hetrd

   subroutine <prefix2c>hetrd_lwork(lower,n,a,lda,d,e,tau,work,lwork,info)
     ! lwork computation for hetrd
     fortranname <prefix2c>hetrd
     callstatement (*f2py_func)((lower?"L":"U"),&n,&a,&lda,&d,&e,&tau,&work,&lwork,&info);
     callprotoargument char*,int*,<ctype2c>*,int*,<ctype2>*,<ctype2>*,<ctype2c>*,<ctype2c>*,int*,int*

     integer intent(in) :: n
     integer optional,intent(in),check(lower==0||lower==1) :: lower = 0

     integer intent(hide),depend(n) :: lda=MAX(n,1)
     <ftype2c> intent(hide) :: a
     <ftype2> intent(hide) :: d
     <ftype2> intent(hide) :: e
     <ftype2c> intent(hide) :: tau
     <ftype2c> intent(out) :: work
     integer intent(hide) :: lwork = -1
     integer intent(out) :: info
   end subroutine <prefix2c>hetrd_lwork

together with their definitions in the lapack.py file

chetrd
zhetrd

chetrd_lwork
zhetrd_lwork

It would be great if you can extend the test to cover these too while we are at it.

@eric-wieser
Copy link
Contributor

What does this do on (0,0) arrays? Does it still error?

Having fixed this type of bug in numpy, the error is typically that it should be lda = max(n, 1) as it says in the docs, not lda = n.

@ilayn
Copy link
Member

ilayn commented Aug 27, 2017

@eric-wieser Yes it produces an error (also for 1x1 array). Since f2py needs to allocate arrays at the outset, the subdiagonals needs to be provided and in these cases subdiagonal sizes are -1 and 0 respectively which is problematic in both cases so it trips at the f2py check() condition with

ValueError: failed to create intent(cache|hide)|optional array-- must have defined dimensions but got (-1,) 

In the other routines, we have also fixed a similar bug recently with the same conclusion (#7633)

@eric-wieser
Copy link
Contributor

Doesn't seem like (1,1) should be a problem to me - isn't (0,) a valid size? I see your point about (0,0) though.

Either way, it might be nice to add tests for these cases.

@nschloe
Copy link
Contributor Author

nschloe commented Aug 27, 2017

Doesn't seem like (1,1) should be a problem to me

I agree. I'll check it out tomorrow.

@nschloe
Copy link
Contributor Author

nschloe commented Aug 28, 2017

The error that is produced when dealing with a 1x1 array is

>           data, d, e, tau, info = sytrd(A, lower=1)
E           ValueError: failed to create intent(cache|hide)|optional array-- must have defined dimensions but got (0,)

I don't understand what variable gets (0,) – everything is protected with MAX(n, 1). Any ideas?

@nschloe
Copy link
Contributor Author

nschloe commented Aug 28, 2017

Okay, so it's apparently about the arrays

     <ftype2> dimension(n-1),intent(out),depend(n) :: e
     <ftype2> dimension(n-1),intent(out),depend(n) :: tau

(representing the super/subdiagonal, e, and a list of Householder coefficients, tau). If n==1, it tries to allocate arrays of dimension 0, which as far as I know is alright with Fortran. SciPy's preprocessor seems to disagree though.

@nschloe
Copy link
Contributor Author

nschloe commented Aug 28, 2017

Alright, so this appears to be a NumPy bug (numpy/numpy#9617). It wouldn't make much sense to add a test for n==1 at this point since the behavior will soon be corrected (I'm working on a NumPy PR right now). I've added a test case for n==0 to make sure that a ValueError is raised.

@eric-wieser
Copy link
Contributor

I'd maybe argue for a dec.knownFailure test, or at least a commented out assertion with a TODO (referencing the numpy issue), so that testing it isn't completely forgotten.

@nschloe
Copy link
Contributor Author

nschloe commented Aug 31, 2017

I've added a TODO note (although I'd consider this an upstream issue).

@ilayn
Copy link
Member

ilayn commented Aug 31, 2017

Alright, so this appears to be a NumPy bug (numpy/numpy#9617). It wouldn't make much sense to add a test for n==1 at this point since the behavior will soon be corrected

Is it going to be backported?

@nschloe
Copy link
Contributor Author

nschloe commented Aug 31, 2017

Is it going to be backported?

Probably a question to ask on the numpy side. Either way, no change is required on scipy for this to work properly.

@nschloe nschloe changed the title Wrap LAPACK's dsytrd ENH: Wrap LAPACK's dsytrd Aug 31, 2017
@pv pv merged commit 892a9c4 into scipy:master Sep 3, 2017
@pv
Copy link
Member

pv commented Sep 3, 2017

thanks, lgtm. I don't see a nice way to support the 1x1 case before f2py supports size-0 arrays.

@pv pv added this to the 1.0.0 milestone Sep 3, 2017
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.

6 participants