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

linalg: Eigenvalues and Eigenvectors #816

Open
wants to merge 21 commits into
base: master
Choose a base branch
from

Conversation

perazz
Copy link
Contributor

@perazz perazz commented May 16, 2024

Computing eigenvalues and eigenvectors of a square matrix: $A \cdot \bar{v} - \lambda \cdot \bar{v} = 0$ .
Based on LAPACK General (*GEEV) or real symmetric (*SYEV) / complex Hermitian (*HEEV) operations.

  • base implementation
  • tests
  • exclude unsupported xdp
  • documentation
  • submodule
  • examples
  • subroutine interface

General matrix

Prior art

  • Numpy: eigenvalues, eigenvectors = eig(a)
  • Numpy: eigenvalues = eigvals(a)
  • Scipy: eig(a, b=None, left=False, right=True, overwrite_a=False, overwrite_b=False, check_finite=True, homogeneous_eigvals=False)

Proposed implementation

  • Eigenvalues only: two function interfaces lambda = eigvals(A [, err])

  • Eigendecomposition: subroutine interface call eig(a,lambda [,right] [,left] [,overwrite_a] [,err])

    • Eigenvalues of real matrices can be complex-conjugate. To avoid confusion, right, left are postprocessed and always returned as complex arrays. LAPACK instead stores (re,im) of conjugates as consecutive real values [j, j+1] in a real array, this would be very confusing and hard to track down.

    • The eigendecomposition can be used either for eigenvalues only, or to retrieve left or right eigenvectors.

Real Symmetric / Complex Hermitian

Prior art

  • Numpy: eigenvalues, eigenvectors = eigh(a, uplo='L')
  • Numpy: eigenvalues = eigvalsh(a)
  • Scipy: eigh(a, b=None, *, lower=True, eigvals_only=False, overwrite_a=False, overwrite_b=False, turbo=<object object>, eigvals=<object object>, type=1, check_finite=True, subset_by_index=None, subset_by_value=None, driver=None)

Proposed implementation

  • Eigenvalues only: two function interfaces lambda = eigvalsh(A [, err])

  • Eigendecomposition: subroutine interface call eigh(a,lambda [,vectors] [,upper_a] [,overwrite_a] [,err] )

    • option to access lower/upper triangle part only
    • Avoid allocation when possible e.g. when providing vectors that can be used as temporary storage for a.

Note: the LAPACK backends are not pure due to functions with intent(out) arguments. The current proposed interfaces are ready to be made pure (e.g., function interface with all intent(in) arguments) to make it easy when the backend will be pure.

I believe this is ready for consideration, thank you @jvdp1 @jalvesz @fortran-lang/stdlib

@perazz perazz marked this pull request as ready for review May 16, 2024 10:31
@jvdp1 jvdp1 requested a review from a team May 27, 2024 20:11
Copy link
Member

@jvdp1 jvdp1 left a comment

Choose a reason for hiding this comment

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

Thank you. Here are some comments regarding the specs. I'll focus on the code later

doc/specs/stdlib_linalg.md Outdated Show resolved Hide resolved
doc/specs/stdlib_linalg.md Outdated Show resolved Hide resolved
doc/specs/stdlib_linalg.md Outdated Show resolved Hide resolved
doc/specs/stdlib_linalg.md Outdated Show resolved Hide resolved
doc/specs/stdlib_linalg.md Show resolved Hide resolved
doc/specs/stdlib_linalg.md Outdated Show resolved Hide resolved
doc/specs/stdlib_linalg.md Outdated Show resolved Hide resolved
doc/specs/stdlib_linalg.md Outdated Show resolved Hide resolved
doc/specs/stdlib_linalg.md Outdated Show resolved Hide resolved
example/linalg/example_eig.f90 Outdated Show resolved Hide resolved
@perazz
Copy link
Contributor Author

perazz commented Jun 4, 2024

From Fortran Monthly call:

  • For general matrix, option to also return real array of singular values (it is complex by default). But, stop on errors if the matrix had complex eigenvalue pairs.

For example, NumPy returns real eigenvalues when all imaginary parts are zero:

    if not isComplexType(t) and all(w.imag == 0.0):
        w = w.real
        vt = vt.real
        result_t = _realType(result_t)
    else:
        result_t = _complexType(result_t)

Thank you @jvdp1 for the reviews. As pointed out during our last Monthly call with @everythingfunctional @jalvesz, I've added the option to return the real part of the eigenvalues only. In this latest case, we must check that there are no meaningful imaginary components, and raise an error if so:

! Check that no eigenvalues have meaningful imaginary part
if (err0%ok() .and. any(aimag(clambda)>atol+rtol*abs(abs(clambda)))) then
err0 = linalg_state_type(this,LINALG_VALUE_ERROR, &
'complex eigenvalues detected: max(imag(lambda))=',maxval(aimag(clambda)))
endif

perazz and others added 10 commits June 5, 2024 08:31
Co-authored-by: Jeremie Vandenplas <jeremie.vandenplas@gmail.com>
Co-authored-by: Jeremie Vandenplas <jeremie.vandenplas@gmail.com>
Co-authored-by: Jeremie Vandenplas <jeremie.vandenplas@gmail.com>
Co-authored-by: Jeremie Vandenplas <jeremie.vandenplas@gmail.com>
Comment on lines +24 to +28
if (present(upper)) then
symmetric_triangle_task = merge('U','L',upper)
else
symmetric_triangle_task = 'L'
endif
Copy link
Contributor

Choose a reason for hiding this comment

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

Would suggest to put the default value explicitly outside the conditional evaluation for optionals.

Suggested change
if (present(upper)) then
symmetric_triangle_task = merge('U','L',upper)
else
symmetric_triangle_task = 'L'
endif
symmetric_triangle_task = 'L'
if (present(upper)) symmetric_triangle_task = merge('U','L',upper)

Copy link
Member

Choose a reason for hiding this comment

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

I also prefer @jalvesz 's notation.

neig = size(lambda,kind=ilp)
lda = m

if (.not.(k>0 .and. m==n)) then
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this conditional can be represented this way:

Suggested change
if (.not.(k>0 .and. m==n)) then
if (k<=0 .or. m/=n) then

'invalid or matrix size a=',[m,n],', must be square.')
call linalg_error_handling(err0,err)
return
elseif (.not.neig>=k) then
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not simply?:

Suggested change
elseif (.not.neig>=k) then
elseif (neig<k) then

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My coding practice is that checking for .not.verified is always better because it includes the edge cases (e.g. what if neig is NaN because we provided an unallocated array?
But, we're talking about tiny details, so I'm also ok with checking if (verified)!

Copy link
Contributor

Choose a reason for hiding this comment

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

If what is written here https://fortran-lang.discourse.group/t/how-to-fill-an-integer-array-with-some-nans/3696 is correct, an integer cannot be NaN. What might cause issues is that this query neig = size(lambda,kind=ilp) can return 1 with some compilers if the allocatable has not been allocated. To be in the safe side I would actually change in line 180 for

neig = 0
if(allocated(lambda)) neig = size(lambda,kind=ilp) 

Comment on lines +197 to +201
if (present(overwrite_a)) then
copy_a = .not.overwrite_a
else
copy_a = .true._lk
endif
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if (present(overwrite_a)) then
copy_a = .not.overwrite_a
else
copy_a = .true._lk
endif
copy_a = .true._lk
if (present(overwrite_a)) copy_a = .not.overwrite_a

k = min(m,n)
neig = size(lambda,kind=ilp)

if (.not.(k>0 .and. m==n)) then
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if (.not.(k>0 .and. m==n)) then
if (k<=0 .or. m/=n) then

', must be square.')
call linalg_error_handling(err0,err)
return
elseif (.not.neig>=k) then
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
elseif (.not.neig>=k) then
elseif (neig<k) then

Comment on lines +460 to +467
if (present(vectors)) then
! No need to copy A anyways
copy_a = .false.
elseif (present(overwrite_a)) then
copy_a = .not.overwrite_a
else
copy_a = .true._lk
endif
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if (present(vectors)) then
! No need to copy A anyways
copy_a = .false.
elseif (present(overwrite_a)) then
copy_a = .not.overwrite_a
else
copy_a = .true._lk
endif
copy_a = .true._lk
if (present(vectors)) then
! No need to copy A anyways
copy_a = .false.
elseif (present(overwrite_a)) then
copy_a = .not.overwrite_a
endif

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants