Skip to content

feat(linalg): add generalized least squares solver#1105

Open
aamrindersingh wants to merge 12 commits intofortran-lang:masterfrom
aamrindersingh:1047-feat-generalized-lstsq
Open

feat(linalg): add generalized least squares solver#1105
aamrindersingh wants to merge 12 commits intofortran-lang:masterfrom
aamrindersingh:1047-feat-generalized-lstsq

Conversation

@aamrindersingh
Copy link
Copy Markdown
Contributor

@aamrindersingh aamrindersingh commented Jan 24, 2026

Resolves #1047 (2 of 2)

This PR adds the generalized_lstsq interface to stdlib_linalg for least-squares problems with correlated errors described by a symmetric positive definite covariance matrix. Usage: x = generalized_lstsq(W, A, b). This minimizes the Mahalanobis distance (Ax-b)^T W^{-1} (Ax-b) using LAPACK's GGGLM.

The key design decisions are:

  1. generalized_lstsq always copies W internally to protect user data from GGGLM's destructive operations. This applies regardless of the prefactored_w flag, the input W matrix is never modified.
  2. The interface follows the overwrite_a pattern from solve_lu where copy_a defaults to .true. to preserve A unless the user explicitly opts into destruction for performance.
  3. A handle_ggglm_info error handler was added to parse LAPACK return codes, consistent with existing handle_gelsd_info and handle_gglse_info patterns.

Testing includes:

  • Basic GLS solves with correlated errors
  • Verification that GLS with identity covariance equals OLS
  • All real types (sp/dp/qp/xdp) covered following existing lstsq patterns

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements a generalized least-squares (GLS) solver for the stdlib_linalg module, addressing issue #1047 (2 of 2). The GLS solver handles least-squares problems with correlated errors described by a covariance matrix, using LAPACK's GGGLM routine to minimize the Mahalanobis distance (Ax-b)^T W^{-1} (Ax-b).

Changes:

  • Adds generalized_lstsq function interface supporting real and complex types with correlated error handling via SPD/Hermitian covariance matrices
  • Implements memory-safe design that always copies the covariance matrix W internally and follows the overwrite_a pattern from existing solvers
  • Provides comprehensive error handling via handle_ggglm_info consistent with existing LAPACK wrapper patterns

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/linalg/stdlib_linalg_least_squares.fypp Core implementation of generalized_lstsq with Cholesky factorization and GGGLM solver integration
src/linalg/stdlib_linalg.fypp Public interface declaration with documentation for the new generalized_lstsq function
src/lapack/stdlib_linalg_lapack_aux.fypp New handle_ggglm_info error handler following established patterns for LAPACK error processing
test/linalg/test_linalg_lstsq.fypp Test suite with basic GLS solver tests and identity covariance validation against OLS
example/linalg/example_generalized_lstsq.f90 Example program demonstrating GLS usage with correlated errors
example/linalg/CMakeLists.txt Build configuration update to include the new example
doc/specs/stdlib_linalg.md Comprehensive API documentation for generalized_lstsq with syntax, arguments, and usage example

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@codecov
Copy link
Copy Markdown

codecov bot commented Jan 25, 2026

Codecov Report

❌ Patch coverage is 0% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.98%. Comparing base (0ede301) to head (34e840d).
⚠️ Report is 101 commits behind head on master.

Files with missing lines Patch % Lines
example/linalg/example_generalized_lstsq.f90 0.00% 11 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1105      +/-   ##
==========================================
- Coverage   68.55%   67.98%   -0.58%     
==========================================
  Files         396      403       +7     
  Lines       12746    12850     +104     
  Branches     1376     1383       +7     
==========================================
- Hits         8738     8736       -2     
- Misses       4008     4114     +106     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@aamrindersingh
Copy link
Copy Markdown
Contributor Author

@jvdp1 Addressed all Copilot suggestions
Thanks

Copy link
Copy Markdown
Contributor

@loiseaujc loiseaujc left a comment

Choose a reason for hiding this comment

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

Again, I took a quick glance at your code and will try to go deeper into it by the end of the week. Looks pretty good so far.

@aamrindersingh
Copy link
Copy Markdown
Contributor Author

@loiseaujc Addressed all comments
Thanks for the review

@aamrindersingh aamrindersingh force-pushed the 1047-feat-generalized-lstsq branch from a73a5df to 296727a Compare January 27, 2026 14:36
@aamrindersingh
Copy link
Copy Markdown
Contributor Author

@loiseaujc Rebased onto master, CI should pass now.
Ready for review.

Copy link
Copy Markdown
Member

@perazz perazz left a comment

Choose a reason for hiding this comment

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

Thanks for this PR @aamrindersingh, I have added some comments.

`b`: Shall be a rank-1 array of the same kind as `a`, containing the right-hand-side vector. It is an `intent(in)` argument.

`prefactored_w` (optional): Shall be an input `logical` flag. If `.true.`, `w` is assumed to contain a matrix square root \( B \) such that \( W = B \cdot B^T \). This can be a Cholesky factor or any other valid square root (e.g., SVD-based). Default: `.false.`. This is an `intent(in)` argument.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You could mention here that, if the Cholesky factor is used, user need to have zeroed-out the other triangular part.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why asking that to the user, and not to do it internally?
If asked to the user, should there be an internal check to ensure that it is really the case?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The Cholesky factors are not the only matrix square-root one can use. If you use svd and take the square-root of the singular values, it'll be a valid square-root although it won't have the upper or lower triangular structure of the Cholesky factor. In order to check internally whether the upper (lower) triangular part has been zeroed-out by the user, we would need to know for sure that the pre-factored W matrix has been obtained with Cholesky. And even there, we would need to know whether U or L is being used.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

OK. thank you for the explanation. Could we then add something like that to warn the user that is up to its responsability:
"It is the user's responsibility to ensure that the prefactored matrix B is valid."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sure enough !

Copy link
Copy Markdown
Contributor

@loiseaujc loiseaujc left a comment

Choose a reason for hiding this comment

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

Here are some more minor comments. Really close.

@aamrindersingh
Copy link
Copy Markdown
Contributor Author

Done @loiseaujc

Copy link
Copy Markdown
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.

Overall it looks good to me. Here are a few suggestions.

`b`: Shall be a rank-1 array of the same kind as `a`, containing the right-hand-side vector. It is an `intent(in)` argument.

`prefactored_w` (optional): Shall be an input `logical` flag. If `.true.`, `w` is assumed to contain a matrix square root \( B \) such that \( W = B \cdot B^T \). This can be a Cholesky factor or any other valid square root (e.g., SVD-based). Default: `.false.`. This is an `intent(in)` argument.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why asking that to the user, and not to do it internally?
If asked to the user, should there be an internal check to ensure that it is really the case?

@aamrindersingh aamrindersingh requested a review from jvdp1 February 16, 2026 19:45
Copy link
Copy Markdown
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 @aamrindersingh . Here are some additional comments. It is taking a good shape!

W(3,:) = [0.25_dp, 0.5_dp, 1.0_dp]

! Solve generalized least-squares
call solve_generalized_lstsq(W, A, b, x)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

could this be combined in a single example file?

!> [optional] state return flag. On error if not requested, the code will stop
type(linalg_state_type), optional, intent(out) :: err
!> Result array x[n]
${rt}$, allocatable, target :: x(:)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
${rt}$, allocatable, target :: x(:)
${rt}$, allocatable :: x(:)

@aamrindersingh
Copy link
Copy Markdown
Contributor Author

@jvdp1 Done with the fixes
Thanks

Copy link
Copy Markdown
Contributor

@loiseaujc loiseaujc left a comment

Choose a reason for hiding this comment

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

It looks pretty good to me. Following @jvdp1 comment, just try to be consistent between [optional] and (optional) in the documentation (the parentheses form being preferred I guess).

Co-authored-by: Jeremie Vandenplas <jeremie.vandenplas@gmail.com>
Copy link
Copy Markdown
Member

@perazz perazz left a comment

Choose a reason for hiding this comment

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

Couple comments to remove an unnecessary allocation. lgtm otherwise

logical(lk) :: copy_a, copy_w, is_prefactored
${rt}$, pointer :: amat(:,:), lmat(:,:)
${rt}$, allocatable, target :: amat_alloc(:,:), lmat_alloc(:,:)
${rt}$, allocatable :: d(:), y(:), work(:)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Two work allocations will be too expensive. let's use a static variable for work size query

Suggested change
${rt}$, allocatable :: d(:), y(:), work(:)
${rt}$, allocatable :: d(:), y(:), work(:)
${rt}$ :: work_size(1)

Comment on lines +713 to +717
allocate(work(1))
call ggglm(m, n, p, amat, lda, lmat, ldb, d, x, y, work, -1_ilp, info)
lwork = ceiling(real(work(1), kind=${rk}$), kind=ilp)
deallocate(work)
allocate(work(lwork))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Avoid double allocation here

Suggested change
allocate(work(1))
call ggglm(m, n, p, amat, lda, lmat, ldb, d, x, y, work, -1_ilp, info)
lwork = ceiling(real(work(1), kind=${rk}$), kind=ilp)
deallocate(work)
allocate(work(lwork))
call ggglm(m, n, p, amat, lda, lmat, ldb, d, x, y, work_size, -1_ilp, info)
lwork = ceiling(real(work_size(1), kind=${rk}$), kind=ilp)
allocate(work(lwork))

@loiseaujc
Copy link
Copy Markdown
Contributor

@aamrindersingh : Would you have time to address the last comments and resolve the conflicts ?

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.

More least-squares variants

6 participants