Skip to content

Commit 40569c2

Browse files
stevengjStefanKarpinski
authored andcommitted
nonsquare (underdetermined) LQ solves (#34350)
* nonsquare (underdetermined) LQ solves * fix NEWS item * fix NEWS item * narrow method signature to eliminate ambiguity * fix docs -- under/overdetermined terms were swapped * Update stdlib/LinearAlgebra/test/lq.jl Co-Authored-By: Stefan Karpinski <stefan@karpinski.org> Co-authored-by: Stefan Karpinski <stefan@karpinski.org>
1 parent ce211b9 commit 40569c2

File tree

3 files changed

+61
-64
lines changed

3 files changed

+61
-64
lines changed

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Standard library changes
3636
#### LinearAlgebra
3737
* The BLAS submodule now supports the level-2 BLAS subroutine `hpmv!` ([#34211]).
3838
* `normalize` now supports multidimensional arrays ([#34239])
39+
* `lq` factorizations can now be used to compute the minimum-norm solution to under-determined systems ([#34350]).
3940

4041
#### Markdown
4142

stdlib/LinearAlgebra/src/lq.jl

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,9 @@ orthogonal/unitary component via `S.Q`, such that `A ≈ S.L*S.Q`.
8080
8181
Iterating the decomposition produces the components `S.L` and `S.Q`.
8282
83-
The LQ decomposition is the QR decomposition of `transpose(A)`.
83+
The LQ decomposition is the QR decomposition of `transpose(A)`, and it is useful
84+
in order to compute the minimum-norm solution `lq(A) \\ b` to an underdetermined
85+
system of equations (`A` has more columns than rows, but has full row rank).
8486
8587
# Examples
8688
```jldoctest
@@ -174,11 +176,13 @@ end
174176

175177

176178
## Multiplication by LQ
177-
lmul!(A::LQ, B::StridedVecOrMat) =
178-
lmul!(LowerTriangular(A.L), lmul!(A.Q, B))
179+
function lmul!(A::LQ, B::StridedVecOrMat)
180+
lmul!(LowerTriangular(A.L), view(lmul!(A.Q, B), 1:size(A,1), axes(B,2)))
181+
return B
182+
end
179183
function *(A::LQ{TA}, B::StridedVecOrMat{TB}) where {TA,TB}
180184
TAB = promote_type(TA, TB)
181-
lmul!(Factorization{TAB}(A), copy_oftype(B, TAB))
185+
_cut_B(lmul!(Factorization{TAB}(A), copy_oftype(B, TAB)), 1:size(A,1))
182186
end
183187

184188
## Multiplication by Q
@@ -303,36 +307,33 @@ _rightappdimmismatch(rowsorcols) =
303307
"(the factorization's originating matrix's number of rows)")))
304308

305309

306-
function (\)(A::LQ{TA}, b::StridedVector{Tb}) where {TA,Tb}
307-
S = promote_type(TA,Tb)
308-
m = checksquare(A)
309-
m == length(b) || throw(DimensionMismatch("left hand side has $m rows, but right hand side has length $(length(b))"))
310-
AA = Factorization{S}(A)
311-
x = ldiv!(AA, copy_oftype(b, S))
312-
return x
313-
end
314-
function (\)(A::LQ{TA},B::StridedMatrix{TB}) where {TA,TB}
310+
function (\)(A::LQ{TA},B::StridedVecOrMat{TB}) where {TA,TB}
315311
S = promote_type(TA,TB)
316-
m = checksquare(A)
317-
m == size(B,1) || throw(DimensionMismatch("left hand side has $m rows, but right hand side has $(size(B,1)) rows"))
312+
m, n = size(A)
313+
m n || throw(DimensionMismatch("LQ solver does not support overdetermined systems (more rows than columns)"))
314+
m == size(B,1) || throw(DimensionMismatch("Both inputs should have the same number of rows"))
318315
AA = Factorization{S}(A)
319-
X = ldiv!(AA, copy_oftype(B, S))
320-
return X
316+
X = _zeros(S, B, n)
317+
X[1:size(B, 1), :] = B
318+
return ldiv!(AA, X)
321319
end
322320
# With a real lhs and complex rhs with the same precision, we can reinterpret
323321
# the complex rhs as a real rhs with twice the number of columns
324322
function (\)(F::LQ{T}, B::VecOrMat{Complex{T}}) where T<:BlasReal
325323
require_one_based_indexing(B)
326-
c2r = reshape(copy(transpose(reinterpret(T, reshape(B, (1, length(B)))))), size(B, 1), 2*size(B, 2))
327-
x = ldiv!(F, c2r)
328-
return reshape(copy(reinterpret(Complex{T}, copy(transpose(reshape(x, div(length(x), 2), 2))))),
324+
X = zeros(T, size(F,2), 2*size(B,2))
325+
X[1:size(B,1), 1:size(B,2)] .= real.(B)
326+
X[1:size(B,1), size(B,2)+1:size(X,2)] .= imag.(B)
327+
ldiv!(F, X)
328+
return reshape(copy(reinterpret(Complex{T}, copy(transpose(reshape(X, div(length(X), 2), 2))))),
329329
isa(B, AbstractVector) ? (size(F,2),) : (size(F,2), size(B,2)))
330330
end
331331

332332

333333
function ldiv!(A::LQ{T}, B::StridedVecOrMat{T}) where T
334-
lmul!(adjoint(A.Q), ldiv!(LowerTriangular(A.L),B))
335-
return B
334+
require_one_based_indexing(B)
335+
ldiv!(LowerTriangular(A.L), view(B, 1:size(A,1), axes(B,2)))
336+
return lmul!(adjoint(A.Q), B)
336337
end
337338

338339

stdlib/LinearAlgebra/test/lq.jl

Lines changed: 37 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -5,48 +5,42 @@ module TestLQ
55
using Test, LinearAlgebra, Random
66
using LinearAlgebra: BlasComplex, BlasFloat, BlasReal, rmul!, lmul!
77

8-
n = 10
9-
10-
# Split n into 2 parts for tests needing two matrices
11-
n1 = div(n, 2)
12-
n2 = 2*n1
8+
m = 10
139

1410
Random.seed!(1234321)
1511

16-
areal = randn(n,n)/2
17-
aimg = randn(n,n)/2
18-
a2real = randn(n,n)/2
19-
a2img = randn(n,n)/2
20-
breal = randn(n,2)/2
21-
bimg = randn(n,2)/2
12+
asquare = randn(ComplexF64, m, m) / 2
13+
awide = randn(ComplexF64, m, m+3) / 2
14+
bcomplex = randn(ComplexF64, m, 2) / 2
2215

2316
# helper functions to unambiguously recover explicit forms of an LQPackedQ
2417
squareQ(Q::LinearAlgebra.LQPackedQ) = (n = size(Q.factors, 2); lmul!(Q, Matrix{eltype(Q)}(I, n, n)))
2518
rectangularQ(Q::LinearAlgebra.LQPackedQ) = convert(Array, Q)
2619

27-
@testset for eltya in (Float32, Float64, ComplexF32, ComplexF64)
28-
a = eltya == Int ? rand(1:7, n, n) : convert(Matrix{eltya}, eltya <: Complex ? complex.(areal, aimg) : areal)
29-
a2 = eltya == Int ? rand(1:7, n, n) : convert(Matrix{eltya}, eltya <: Complex ? complex.(a2real, a2img) : a2real)
30-
asym = a' + a # symmetric indefinite
31-
apd = a' * a # symmetric positive-definite
20+
@testset for eltya in (Float32, Float64, ComplexF32, ComplexF64), n in (m, size(awide, 2))
21+
adata = m == n ? asquare : awide
22+
a = convert(Matrix{eltya}, eltya <: Complex ? adata : real(adata))
3223
ε = εa = eps(abs(float(one(eltya))))
24+
n1 = n ÷ 2
25+
26+
α = rand(eltya)
27+
= fill(α,1,1)
28+
@test lq(α).L*lq(α).Q lq(aα).L*lq(aα).Q
29+
@test abs(lq(α).Q[1,1]) one(eltya)
3330

3431
@testset for eltyb in (Float32, Float64, ComplexF32, ComplexF64, Int)
35-
b = eltyb == Int ? rand(1:5, n, 2) : convert(Matrix{eltyb}, eltyb <: Complex ? complex.(breal, bimg) : breal)
32+
b = eltyb == Int ? rand(1:5, m, 2) : convert(Matrix{eltyb}, eltyb <: Complex ? bcomplex : real(bcomplex))
3633
εb = eps(abs(float(one(eltyb))))
3734
ε = max(εa,εb)
3835

39-
α = rand(eltya)
40-
= fill(α,1,1)
41-
@test lq(α).L*lq(α).Q lq(aα).L*lq(aα).Q
42-
@test abs(lq(α).Q[1,1]) one(eltya)
4336
tab = promote_type(eltya,eltyb)
4437

45-
for i = 1:2
46-
let a = i == 1 ? a : view(a, 1:n - 1, 1:n - 1), b = i == 1 ? b : view(b, 1:n - 1), n = i == 1 ? n : n - 1
38+
@testset for isview in (false,true)
39+
let a = isview ? view(a, 1:m - 1, 1:n - 1) : a, b = isview ? view(b, 1:m - 1) : b, m = m - isview, n = n - isview
4740
lqa = lq(a)
41+
x = lqa\b
4842
l,q = lqa.L, lqa.Q
49-
qra = qr(a)
43+
qra = qr(a, Val(true))
5044
@testset "Basic ops" begin
5145
@test size(lqa,1) == size(a,1)
5246
@test size(lqa,3) == 1
@@ -69,30 +63,31 @@ rectangularQ(Q::LinearAlgebra.LQPackedQ) = convert(Array, Q)
6963
@test Matrix{eltya}(q) isa Matrix{eltya}
7064
end
7165
@testset "Binary ops" begin
72-
@test a*(lqa\b) b atol=3000ε
73-
@test lqa*b qra.Q*qra.R*b atol=3000ε
74-
@test (sq = size(q.factors, 2); *(Matrix{eltyb}(I, sq, sq), adjoint(q))*squareQ(q)) Matrix(I, n, n) atol=5000ε
66+
@test a*x b rtol=3000ε
67+
@test x qra \ b rtol=3000ε
68+
@test lqa*x a*x rtol=3000ε
69+
@test (sq = size(q.factors, 2); *(Matrix{eltyb}(I, sq, sq), adjoint(q))*squareQ(q)) Matrix(I, n, n) rtol=5000ε
7570
if eltya != Int
7671
@test Matrix{eltyb}(I, n, n)*q convert(AbstractMatrix{tab},q)
7772
end
78-
@test q*b squareQ(q)*b atol=100ε
79-
@test transpose(q)*b transpose(squareQ(q))*b atol=100ε
80-
@test q'*b squareQ(q)'*b atol=100ε
81-
@test a*q a*squareQ(q) atol=100ε
82-
@test a*transpose(q) a*transpose(squareQ(q)) atol=100ε
83-
@test a*q' a*squareQ(q)' atol=100ε
84-
@test a'*q a'*squareQ(q) atol=100ε
85-
@test a'*q' a'*squareQ(q)' atol=100ε
86-
@test_throws DimensionMismatch q*b[1:n1 + 1]
87-
@test_throws DimensionMismatch adjoint(q) * Matrix{eltya}(undef,n+2,n+2)
88-
@test_throws DimensionMismatch Matrix{eltyb}(undef,n+2,n+2)*q
73+
@test q*x squareQ(q)*x rtol=100ε
74+
@test transpose(q)*x transpose(squareQ(q))*x rtol=100ε
75+
@test q'*x squareQ(q)'*x rtol=100ε
76+
@test a*q a*squareQ(q) rtol=100ε
77+
@test a*transpose(q) a*transpose(squareQ(q)) rtol=100ε
78+
@test a*q' a*squareQ(q)' rtol=100ε
79+
@test q*a' squareQ(q)*a' rtol=100ε
80+
@test q'*a' squareQ(q)'*a' rtol=100ε
81+
@test_throws DimensionMismatch q*x[1:n1 + 1]
82+
@test_throws DimensionMismatch adjoint(q) * Matrix{eltya}(undef,m+2,m+2)
83+
@test_throws DimensionMismatch Matrix{eltyb}(undef,m+2,m+2)*q
8984
if isa(a, DenseArray) && isa(b, DenseArray)
9085
# use this to test 2nd branch in mult code
9186
pad_a = vcat(I, a)
92-
pad_b = hcat(I, b)
93-
@test pad_a*q pad_a*squareQ(q) atol=100ε
94-
@test transpose(q)*pad_b transpose(squareQ(q))*pad_b atol=100ε
95-
@test q'*pad_b squareQ(q)'*pad_b atol=100ε
87+
pad_x = hcat(I, x)
88+
@test pad_a*q pad_a*squareQ(q) rtol=100ε
89+
@test transpose(q)*pad_x transpose(squareQ(q))*pad_x rtol=100ε
90+
@test q'*pad_x squareQ(q)'*pad_x rtol=100ε
9691
end
9792
end
9893
end

0 commit comments

Comments
 (0)