From 36099904864263cc811178f710567d214cd03bb5 Mon Sep 17 00:00:00 2001 From: Yue Zhengyuan Date: Thu, 29 Oct 2026 13:21:04 +0800 Subject: [PATCH 01/10] R-only QR and LQ decompositions --- .gitignore | 1 + ...MatrixAlgebraKitGenericLinearAlgebraExt.jl | 2 +- src/common/gauge.jl | 12 +++-- src/implementations/lq.jl | 35 +++++-------- src/implementations/qr.jl | 23 +++++---- src/interface/lq.jl | 4 ++ src/interface/orthnull.jl | 8 +++ src/interface/qr.jl | 4 ++ src/pullbacks/lq.jl | 1 + src/pullbacks/qr.jl | 1 + src/pushforwards/lq.jl | 1 + src/pushforwards/qr.jl | 1 + test/decompositions/orth_factors.jl | 50 +++++++++++++++++++ 13 files changed, 105 insertions(+), 38 deletions(-) create mode 100644 test/decompositions/orth_factors.jl diff --git a/.gitignore b/.gitignore index 736645085..4fa90427c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ .*.swp Manifest.toml docs/build/ +.vscode diff --git a/ext/MatrixAlgebraKitGenericLinearAlgebraExt.jl b/ext/MatrixAlgebraKitGenericLinearAlgebraExt.jl index 93f846c81..954912c77 100644 --- a/ext/MatrixAlgebraKitGenericLinearAlgebraExt.jl +++ b/ext/MatrixAlgebraKitGenericLinearAlgebraExt.jl @@ -70,7 +70,7 @@ function qr_householder!( # compute QR Q̃, R̃ = qr!(A) - lmul!(Q̃, one!(Q)) + isempty(Q) || lmul!(Q̃, one!(Q)) if computeR copyto!(view(R, 1:minmn, :), R̃) diff --git a/src/common/gauge.jl b/src/common/gauge.jl index 2889c4e52..52a24bdd3 100644 --- a/src/common/gauge.jl +++ b/src/common/gauge.jl @@ -15,8 +15,10 @@ _largest(x::Complex, y::Complex) = abs2(x) < abs2(y) ? y : x function gaugefix!(::typeof(qr_householder!), Q, R, Rd) ax = Base.OneTo(length(Rd)) - Qf = view(Q, axes(Q, 1), ax) - Qf .*= sign_safe.(transpose(Rd)) + if !isempty(Q) + Qf = view(Q, axes(Q, 1), ax) + Qf .*= sign_safe.(transpose(Rd)) + end if !isnothing(R) Rf = view(R, ax, axes(R, 2)) Rf .*= conj.(sign_safe.(Rd)) @@ -26,8 +28,10 @@ end function gaugefix!(::typeof(lq_householder!), L, Q, Ld) ax = Base.OneTo(length(Ld)) - Qf = view(Q, ax, axes(Q, 2)) - Qf .*= sign_safe.(Ld) + if !isempty(Q) + Qf = view(Q, ax, axes(Q, 2)) + Qf .*= sign_safe.(Ld) + end if !isnothing(L) Lf = view(L, axes(L, 1), ax) Lf .*= conj.(sign_safe.(transpose(Ld))) diff --git a/src/implementations/lq.jl b/src/implementations/lq.jl index af2f799cb..d9962f001 100644 --- a/src/implementations/lq.jl +++ b/src/implementations/lq.jl @@ -12,7 +12,7 @@ function check_input(::typeof(lq_full!), A::AbstractMatrix, LQ, ::AbstractAlgori @assert L isa AbstractMatrix && Q isa AbstractMatrix isempty(L) || @check_size(L, (m, n)) @check_scalar(L, A) - @check_size(Q, (n, n)) + isempty(Q) || @check_size(Q, (n, n)) @check_scalar(Q, A) return nothing end @@ -23,7 +23,7 @@ function check_input(::typeof(lq_compact!), A::AbstractMatrix, LQ, ::AbstractAlg @assert L isa AbstractMatrix && Q isa AbstractMatrix isempty(L) || @check_size(L, (m, minmn)) @check_scalar(L, A) - @check_size(Q, (minmn, n)) + isempty(Q) || @check_size(Q, (minmn, n)) @check_scalar(Q, A) return nothing end @@ -42,7 +42,7 @@ function check_input(::typeof(lq_full!), A::AbstractMatrix, (L, Q), ::DiagonalAl @assert Q isa Diagonal && L isa Diagonal isempty(L) || @check_size(L, (m, n)) @check_scalar(L, A) - @check_size(Q, (n, n)) + isempty(Q) || @check_size(Q, (n, n)) @check_scalar(Q, A) return nothing end @@ -167,7 +167,7 @@ function lq_householder!( else A, T = gelqt!(driver, A, similar(A, mb, minmn)) end - Q = gemlqt!(driver, 'R', 'N', A, T, one!(Q)) + isempty(Q) || (Q = gemlqt!(driver, 'R', 'N', A, T, one!(Q))) computeL && _lq_copyL!(L, A) positive && gaugefix!(lq_householder!, computeL ? L : nothing, Q, diagview(A)) else @@ -179,7 +179,7 @@ function lq_householder!( unglq!(driver, A, τ) # Q === A, so no need to rebind Q positive && gaugefix!(lq_householder!, Lf, Q, Ld) else - Q = unmlq!(driver, 'R', 'N', A, τ, one!(Q)) + isempty(Q) || (Q = unmlq!(driver, 'R', 'N', A, τ, one!(Q))) positive && gaugefix!(lq_householder!, Lf, Q, diagview(A)) end end @@ -220,6 +220,7 @@ function lq_householder!( L[i, j] = A[i, j] end end + isempty(Q) && return L, Q # build Q one!(Q) @inbounds for i in minmn:-1:1 @@ -329,7 +330,7 @@ function lq_via_qr!( else Qt, Lt = qr_compact!(At, (Qt, Lt), qr_alg) end - adjoint!(Q, Qt) + isempty(Q) || adjoint!(Q, Qt) !isempty(L) && adjoint!(L, Lt) return L, Q end @@ -348,38 +349,28 @@ end function lq_full!(A::AbstractMatrix, LQ, alg::DiagonalAlgorithm) check_input(lq_full!, A, LQ, alg) L, Q = LQ - lq_diagonal!(A, L, Q; alg.kwargs...) + _lq_diagonal!(A, L, Q; alg.kwargs...) return L, Q end function lq_compact!(A::AbstractMatrix, LQ, alg::DiagonalAlgorithm) check_input(lq_compact!, A, LQ, alg) L, Q = LQ - lq_diagonal!(A, L, Q; alg.kwargs...) + _lq_diagonal!(A, L, Q; alg.kwargs...) return L, Q end function lq_null!(A::AbstractMatrix, N, alg::DiagonalAlgorithm) check_input(lq_null!, A, N, alg) - return lq_null_diagonal!(A, N; alg.kwargs...) + return _lq_null_diagonal!(A, N; alg.kwargs...) end -function lq_diagonal!( +function _lq_diagonal!( A::AbstractMatrix, L::AbstractMatrix, Q::AbstractMatrix; positive::Bool = true ) - # note: Ad and Qd might share memory here so order of operations is important - Ad = diagview(A) - Ld = diagview(L) - Qd = diagview(Q) - if positive - @. Ld = abs(Ad) - @. Qd = sign_safe(Ad) - else - Ld .= Ad - one!(Q) - end + Q, L = _qr_diagonal!(A, Q, L; positive) return L, Q end -lq_null_diagonal!(A::AbstractMatrix, N; positive::Bool = true) = N +_lq_null_diagonal!(A::AbstractMatrix, N; positive::Bool = true) = N # Deprecations # ------------ diff --git a/src/implementations/qr.jl b/src/implementations/qr.jl index 36453acc0..392d5efd4 100644 --- a/src/implementations/qr.jl +++ b/src/implementations/qr.jl @@ -10,7 +10,7 @@ function check_input(::typeof(qr_full!), A::AbstractMatrix, QR, ::AbstractAlgori m, n = size(A) Q, R = QR @assert Q isa AbstractMatrix && R isa AbstractMatrix - @check_size(Q, (m, m)) + isempty(Q) || @check_size(Q, (m, m)) @check_scalar(Q, A) isempty(R) || @check_size(R, (m, n)) @check_scalar(R, A) @@ -21,7 +21,7 @@ function check_input(::typeof(qr_compact!), A::AbstractMatrix, QR, ::AbstractAlg minmn = min(m, n) Q, R = QR @assert Q isa AbstractMatrix && R isa AbstractMatrix - @check_size(Q, (m, minmn)) + isempty(Q) || @check_size(Q, (m, minmn)) @check_scalar(Q, A) isempty(R) || @check_size(R, (minmn, n)) @check_scalar(R, A) @@ -40,7 +40,7 @@ function check_input(::typeof(qr_full!), A::AbstractMatrix, (Q, R), alg::Diagona m, n = size(A) @assert m == n && isdiag(A) @assert Q isa Diagonal && R isa Diagonal - @check_size(Q, (m, n)) + isempty(Q) || @check_size(Q, (m, n)) @check_scalar(Q, A) isempty(R) || @check_size(R, (m, n)) @check_scalar(R, A) @@ -190,7 +190,7 @@ function qr_householder!( else A, T = geqrt!(driver, A, similar(A, nb, minmn)) end - Q = gemqrt!(driver, 'L', 'N', A, T, one!(Q)) + isempty(Q) || (Q = gemqrt!(driver, 'L', 'N', A, T, one!(Q))) computeR && _qr_copyR!(R, A) positive && gaugefix!(qr_householder!, Q, computeR ? R : nothing, diagview(A)) else @@ -207,7 +207,7 @@ function qr_householder!( ungqr!(driver, A, τ) # Q === A, so no need to rebind Q positive && gaugefix!(qr_householder!, Q, Rf, Rd) else - Q = _qr_buildQ!(driver, Q, A, τ, minmn) + isempty(Q) || (Q = _qr_buildQ!(driver, Q, A, τ, minmn)) positive && gaugefix!(qr_householder!, Q, Rf, diagview(A)) end end @@ -248,6 +248,7 @@ function qr_householder!( R[i, j] = A[i, j] end end + isempty(Q) && return Q, R # build Q one!(Q) @inbounds for j in minmn:-1:1 @@ -330,22 +331,22 @@ end function qr_full!(A::AbstractMatrix, QR, alg::DiagonalAlgorithm) check_input(qr_full!, A, QR, alg) Q, R = QR - _diagonal_qr!(A, Q, R; alg.kwargs...) + _qr_diagonal!(A, Q, R; alg.kwargs...) return Q, R end function qr_compact!(A::AbstractMatrix, QR, alg::DiagonalAlgorithm) check_input(qr_compact!, A, QR, alg) Q, R = QR - _diagonal_qr!(A, Q, R; alg.kwargs...) + _qr_diagonal!(A, Q, R; alg.kwargs...) return Q, R end function qr_null!(A::AbstractMatrix, N, alg::DiagonalAlgorithm) check_input(qr_null!, A, N, alg) - _diagonal_qr_null!(A, N; alg.kwargs...) + _qr_null_diagonal!(A, N; alg.kwargs...) return N end -function _diagonal_qr!( +function _qr_diagonal!( A::AbstractMatrix, Q::AbstractMatrix, R::AbstractMatrix; positive::Bool = true ) # note: Ad and Qd might share memory here so order of operations is important @@ -354,7 +355,7 @@ function _diagonal_qr!( Rd = diagview(R) if positive @. Rd = abs(Ad) - @. Qd = sign_safe(Ad) + isempty(Q) || (@. Qd = sign_safe(Ad)) else Rd .= Ad one!(Q) @@ -362,7 +363,7 @@ function _diagonal_qr!( return Q, R end -_diagonal_qr_null!(A::AbstractMatrix, N; positive::Bool = true) = N +_qr_null_diagonal!(A::AbstractMatrix, N; positive::Bool = true) = N # Deprecations # ------------ diff --git a/src/interface/lq.jl b/src/interface/lq.jl index 1a805d6ac..66bdcda5f 100644 --- a/src/interface/lq.jl +++ b/src/interface/lq.jl @@ -10,6 +10,8 @@ Compute the full LQ decomposition of the rectangular matrix `A`, such that `A = where `Q` is a square unitary matrix with the same number of rows as `A` and `L` is a lower triangular matrix with the same size as `A`. +Pass an empty `Q` output to `lq_full!` to compute only `L` without constructing `Q`. + !!! note The bang method `lq_full!` optionally accepts the output structure and possibly destroys the input matrix `A`. Always use the return value of the function @@ -29,6 +31,8 @@ Compute the compact LQ decomposition of the rectangular matrix `A` of size `(m,n such that `A = L * Q` where the matrix `Q` of size `(min(m,n), n)` has orthogonal rows spanning the image of `A'`, and the matrix `L` of size `(m, min(m,n))` is lower triangular. +Pass an empty `Q` output to `lq_compact!` to compute only `L` without constructing `Q`. + !!! note The bang method `lq_compact!` optionally accepts the output structure and possibly destroys the input matrix `A`. Always use the return value of the function diff --git a/src/interface/orthnull.jl b/src/interface/orthnull.jl index 34537e078..741447ae2 100644 --- a/src/interface/orthnull.jl +++ b/src/interface/orthnull.jl @@ -76,6 +76,10 @@ deduce the decomposition kind with [`left_orth_alg(alg)`](@ref left_orth_alg). destroys the input matrix `A`. Always use the return value of the function as it may not always be possible to use the provided `CV` as output. +For QR-based decompositions, pass an empty basis matrix to compute only `C` (the `R` factor): +`left_orth!(A, (similar(A, 0, 0), C); alg = :qr)`. +The returned basis matrix remains empty, and its construction is skipped. + See also [`right_orth(!)`](@ref right_orth), [`left_null(!)`](@ref left_null) and [`right_null(!)`](@ref right_null). """ @@ -157,6 +161,10 @@ deduce the decomposition kind with [`right_orth_alg`](@ref). destroys the input matrix `A`. Always use the return value of the function as it may not always be possible to use the provided `CVᴴ` as output. +For LQ-based decompositions, pass an empty basis matrix to compute only `C` (the `L` factor): +`right_orth!(A, (C, similar(A, 0, 0)); alg = :lq)`. +The returned basis matrix remains empty, and its construction is skipped. + See also [`left_orth(!)`](@ref left_orth), [`left_null(!)`](@ref left_null) and [`right_null(!)`](@ref right_null). """ diff --git a/src/interface/qr.jl b/src/interface/qr.jl index 7ea5f1f41..bf621cca6 100644 --- a/src/interface/qr.jl +++ b/src/interface/qr.jl @@ -10,6 +10,8 @@ Compute the full QR decomposition of the rectangular matrix `A`, such that `A = where `Q` is a square unitary matrix with the same number of rows as `A` and `R` is an upper triangular matrix with the same size as `A`. +Pass an empty `Q` output to `qr_full!` to compute only `R` without constructing `Q`. + !!! note The bang method `qr_full!` optionally accepts the output structure and possibly destroys the input matrix `A`. Always use the return value of the function @@ -30,6 +32,8 @@ such that `A = Q * R` where the isometric matrix `Q` of size `(m, min(m,n))` has orthogonal columns spanning the image of `A`, and the matrix `R` of size `(min(m,n), n)` is upper triangular. +Pass an empty `Q` output to `qr_compact!` to compute only `R` without constructing `Q`. + !!! note The bang method `qr_compact!` optionally accepts the output structure and possibly destroys the input matrix `A`. Always use the return value of the function diff --git a/src/pullbacks/lq.jl b/src/pullbacks/lq.jl index 49fe6db7e..e3cac95a5 100644 --- a/src/pullbacks/lq.jl +++ b/src/pullbacks/lq.jl @@ -76,6 +76,7 @@ function lq_pullback!( gauge_atol::Real = default_pullback_gauge_atol(ΔLQ[2]) ) # process + # TODO: Support L-only `right_orth!` with an empty Q output; this rule currently requires Q. L, Q = LQ m = size(L, 1) n = size(Q, 2) diff --git a/src/pullbacks/qr.jl b/src/pullbacks/qr.jl index 5ca5a2c5e..3ff84b0b5 100644 --- a/src/pullbacks/qr.jl +++ b/src/pullbacks/qr.jl @@ -76,6 +76,7 @@ function qr_pullback!( gauge_atol::Real = default_pullback_gauge_atol(ΔQR[1]) ) # process + # TODO: Support R-only `left_orth!` with an empty Q output; this rule currently requires Q. Q, R = QR m = size(Q, 1) n = size(R, 2) diff --git a/src/pushforwards/lq.jl b/src/pushforwards/lq.jl index 9d1f82eed..574e10937 100644 --- a/src/pushforwards/lq.jl +++ b/src/pushforwards/lq.jl @@ -19,6 +19,7 @@ function lq_pushforward!( ΔA, A, LQ, ΔLQ; rank_atol::Real = default_pullback_rank_atol(LQ[1]), kwargs... ) + # TODO: Support L-only `right_orth!` with an empty Q output; this rule currently requires Q. L, Q = LQ ΔL, ΔQ = ΔLQ m = size(L, 1) diff --git a/src/pushforwards/qr.jl b/src/pushforwards/qr.jl index a4fd7fc16..0d1e90159 100644 --- a/src/pushforwards/qr.jl +++ b/src/pushforwards/qr.jl @@ -17,6 +17,7 @@ function qr_pushforward!( ΔA, A, QR, ΔQR; rank_atol::Real = default_pullback_rank_atol(QR[2]), kwargs... ) + # TODO: Support R-only `left_orth!` with an empty Q output; this rule currently requires Q. Q, R = QR ΔQ, ΔR = ΔQR m = size(Q, 1) diff --git a/test/decompositions/orth_factors.jl b/test/decompositions/orth_factors.jl new file mode 100644 index 000000000..d8db0e1b1 --- /dev/null +++ b/test/decompositions/orth_factors.jl @@ -0,0 +1,50 @@ +using MatrixAlgebraKit +using MatrixAlgebraKit: Native +using LinearAlgebra: Diagonal +using Test + +@testset "Orthogonalization without a basis" begin + for T in (Float64, ComplexF64), (m, n) in ((7, 4), (4, 4), (4, 7)) + A = randn(T, m, n) + Q0 = similar(A, 0, 0) + for positive in (false, true), alg in ( + Householder(; blocksize = 1, positive), + Householder(; blocksize = 2, positive), + Householder(; blocksize = 1, pivoted = true, positive), + Householder(; driver = Native(), positive), + ) + Q, R = left_orth(A; alg) + Q2, R2 = left_orth!(copy(A), (Q0, similar(R)), alg) + @test Q2 === Q0 + @test R2 ≈ R + @test Q * R2 ≈ A + + # LQ pivoting is supported through transposed QR. + lq_alg = LQViaTransposedQR(alg) + L, Q = right_orth(A; alg = lq_alg) + L2, Q2 = right_orth!(copy(A), (similar(L), Q0), lq_alg) + @test Q2 === Q0 + @test L2 ≈ L + @test L2 * Q ≈ A + end + for positive in (false, true), blocksize in (1, 2) + L, Q = right_orth(A; alg = :lq, positive, blocksize) + L2, Q2 = right_orth!(copy(A), (similar(L), Q0); alg = :lq, positive, blocksize) + @test Q2 === Q0 + @test L2 ≈ L + @test L2 * Q ≈ A + end + end + for T in (Float64, ComplexF64), positive in (false, true) + A = Diagonal(randn(T, 4)) + Q0 = Diagonal(T[]) + Q, R = left_orth(A; positive) + Q2, R2 = left_orth!(copy(A), (Q0, similar(R)); positive) + @test Q2 === Q0 + @test Q * R2 ≈ A + L, Q = right_orth(A; positive) + L2, Q2 = right_orth!(copy(A), (similar(L), Q0); positive) + @test Q2 === Q0 + @test L2 * Q ≈ A + end +end From 4684229fc3e19b1f405b5b23ca9c9557536c14c7 Mon Sep 17 00:00:00 2001 From: Yue Zhengyuan Date: Thu, 29 Oct 2026 19:49:03 +0800 Subject: [PATCH 02/10] Consistently use empty matrices instead of `nothing` for unneeded Q or R --- ...MatrixAlgebraKitGenericLinearAlgebraExt.jl | 7 ++-- src/common/gauge.jl | 4 +-- src/implementations/lq.jl | 21 +++++------- src/implementations/qr.jl | 22 +++++-------- test/decompositions/orth_factors.jl | 33 +++++++++++++++++++ 5 files changed, 54 insertions(+), 33 deletions(-) diff --git a/ext/MatrixAlgebraKitGenericLinearAlgebraExt.jl b/ext/MatrixAlgebraKitGenericLinearAlgebraExt.jl index 954912c77..bfecbf2a0 100644 --- a/ext/MatrixAlgebraKitGenericLinearAlgebraExt.jl +++ b/ext/MatrixAlgebraKitGenericLinearAlgebraExt.jl @@ -66,19 +66,16 @@ function qr_householder!( m, n = size(A) minmn = min(m, n) - computeR = length(R) > 0 # compute QR Q̃, R̃ = qr!(A) isempty(Q) || lmul!(Q̃, one!(Q)) - if computeR + if !isempty(R) copyto!(view(R, 1:minmn, :), R̃) zero!(view(R, (minmn + 1):size(R, 1), :)) - positive && gaugefix!(qr_householder!, Q, R, diagview(R̃)) - elseif positive - gaugefix!(qr_householder!, Q, nothing, diagview(R̃)) end + positive && gaugefix!(qr_householder!, Q, R, diagview(R̃)) return Q, R end diff --git a/src/common/gauge.jl b/src/common/gauge.jl index 52a24bdd3..9de1010e0 100644 --- a/src/common/gauge.jl +++ b/src/common/gauge.jl @@ -19,7 +19,7 @@ function gaugefix!(::typeof(qr_householder!), Q, R, Rd) Qf = view(Q, axes(Q, 1), ax) Qf .*= sign_safe.(transpose(Rd)) end - if !isnothing(R) + if !isempty(R) Rf = view(R, ax, axes(R, 2)) Rf .*= conj.(sign_safe.(Rd)) end @@ -32,7 +32,7 @@ function gaugefix!(::typeof(lq_householder!), L, Q, Ld) Qf = view(Q, ax, axes(Q, 2)) Qf .*= sign_safe.(Ld) end - if !isnothing(L) + if !isempty(L) Lf = view(L, axes(L, 1), ax) Lf .*= conj.(sign_safe.(transpose(Ld))) end diff --git a/src/implementations/lq.jl b/src/implementations/lq.jl index d9962f001..a7ae417bc 100644 --- a/src/implementations/lq.jl +++ b/src/implementations/lq.jl @@ -148,7 +148,7 @@ function lq_householder!( m, n = size(A) minmn = min(m, n) - computeL = length(L) > 0 + computeL = !isempty(L) inplaceQ = Q === A if inplaceQ @@ -159,30 +159,25 @@ function lq_householder!( throw(ArgumentError("in-place Q is only supported if L does not share memory with A")) end + Ld = diagview(A) if blocksize > 1 # L doubles as workspace for T, so Q is constructed before L is extracted mb = min(minmn, blocksize) - if computeL # first use L as space for T - A, T = gelqt!(driver, A, view(L, 1:mb, 1:minmn)) - else - A, T = gelqt!(driver, A, similar(A, mb, minmn)) - end + T = computeL ? view(L, 1:mb, 1:minmn) : similar(A, mb, minmn) + A, T = gelqt!(driver, A, T) isempty(Q) || (Q = gemlqt!(driver, 'R', 'N', A, T, one!(Q))) computeL && _lq_copyL!(L, A) - positive && gaugefix!(lq_householder!, computeL ? L : nothing, Q, diagview(A)) else A, τ = gelqf!(driver, A) computeL && _lq_copyL!(L, A) - Lf = computeL ? L : nothing if inplaceQ - Ld = positive ? copy(diagview(A)) : nothing # unglq! destroys the diagonal of A + positive && (Ld = copy(Ld)) # unglq! destroys the diagonal of A unglq!(driver, A, τ) # Q === A, so no need to rebind Q - positive && gaugefix!(lq_householder!, Lf, Q, Ld) - else - isempty(Q) || (Q = unmlq!(driver, 'R', 'N', A, τ, one!(Q))) - positive && gaugefix!(lq_householder!, Lf, Q, diagview(A)) + elseif !isempty(Q) + Q = unmlq!(driver, 'R', 'N', A, τ, one!(Q)) end end + positive && gaugefix!(lq_householder!, L, Q, Ld) return L, Q end diff --git a/src/implementations/qr.jl b/src/implementations/qr.jl index 392d5efd4..91d494edd 100644 --- a/src/implementations/qr.jl +++ b/src/implementations/qr.jl @@ -170,7 +170,7 @@ function qr_householder!( m, n = size(A) minmn = min(m, n) - computeR = length(R) > 0 + computeR = !isempty(R) inplaceQ = Q === A if inplaceQ @@ -182,17 +182,14 @@ function qr_householder!( end # Compute QR in packed form + Rd = diagview(A) if blocksize > 1 # R doubles as workspace for T, so Q is constructed before R is extracted nb = min(minmn, blocksize) - if computeR # first use R as space for T - A, T = geqrt!(driver, A, view(R, 1:nb, 1:minmn)) - else - A, T = geqrt!(driver, A, similar(A, nb, minmn)) - end + T = computeR ? view(R, 1:nb, 1:minmn) : similar(A, nb, minmn) + A, T = geqrt!(driver, A, T) isempty(Q) || (Q = gemqrt!(driver, 'L', 'N', A, T, one!(Q))) computeR && _qr_copyR!(R, A) - positive && gaugefix!(qr_householder!, Q, computeR ? R : nothing, diagview(A)) else if pivoted A, τ, jpvt = geqp3!(driver, A) @@ -201,16 +198,15 @@ function qr_householder!( A, τ = geqrf!(driver, A) computeR && _qr_copyR!(R, A) end - Rf = computeR ? R : nothing # gaugefix! rescales rows, which commutes with the pivoting if inplaceQ - Rd = positive ? copy(diagview(A)) : nothing # ungqr! destroys the diagonal of A + positive && (Rd = copy(Rd)) # ungqr! destroys the diagonal of A ungqr!(driver, A, τ) # Q === A, so no need to rebind Q - positive && gaugefix!(qr_householder!, Q, Rf, Rd) - else - isempty(Q) || (Q = _qr_buildQ!(driver, Q, A, τ, minmn)) - positive && gaugefix!(qr_householder!, Q, Rf, diagview(A)) + elseif !isempty(Q) + Q = _qr_buildQ!(driver, Q, A, τ, minmn) end end + # Gauge fixing rescales rows, which commutes with pivoting. + positive && gaugefix!(qr_householder!, Q, R, Rd) return Q, R end diff --git a/test/decompositions/orth_factors.jl b/test/decompositions/orth_factors.jl index d8db0e1b1..fb6904a15 100644 --- a/test/decompositions/orth_factors.jl +++ b/test/decompositions/orth_factors.jl @@ -48,3 +48,36 @@ using Test @test L2 * Q ≈ A end end + +@testset "Orthogonalization with only a basis" begin + for T in (Float64, ComplexF64), (m, n) in ((7, 4), (4, 4), (4, 7)), + positive in (false, true), blocksize in (1, 2) + A = randn(T, m, n) + empty_factor = similar(A, 0, 0) + Q, R = left_orth(A; positive, blocksize) + Q2, R2 = left_orth!(copy(A), (similar(Q), empty_factor); positive, blocksize) + @test R2 === empty_factor + @test Q2 ≈ Q + @test Q2 * R ≈ A + if m >= n && blocksize == 1 + Ac = copy(A) + Q2, R2 = left_orth!(Ac, (Ac, empty_factor); positive, blocksize) + @test Q2 === Ac + @test R2 === empty_factor + @test Q2 ≈ Q + end + + L, Q = right_orth(A; positive, blocksize) + L2, Q2 = right_orth!(copy(A), (empty_factor, similar(Q)); positive, blocksize) + @test L2 === empty_factor + @test Q2 ≈ Q + @test L * Q2 ≈ A + if n >= m && blocksize == 1 + Ac = copy(A) + L2, Q2 = right_orth!(Ac, (empty_factor, Ac); positive, blocksize) + @test Q2 === Ac + @test L2 === empty_factor + @test Q2 ≈ Q + end + end +end From 0bd63e102de8ec4bb3ab9f97ba740692dad5d526 Mon Sep 17 00:00:00 2001 From: Yue Zhengyuan Date: Thu, 29 Oct 2026 19:56:14 +0800 Subject: [PATCH 03/10] `lq_via_qr!` fix --- src/implementations/lq.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/implementations/lq.jl b/src/implementations/lq.jl index a7ae417bc..8ffde27c5 100644 --- a/src/implementations/lq.jl +++ b/src/implementations/lq.jl @@ -320,7 +320,7 @@ function lq_via_qr!( Qt = (A === Q) ? At : similar(Q') Lt = similar(L') n = size(A, 2) - if size(Q) == (n, n) + if size(Q) == (n, n) || size(L, 2) == n Qt, Lt = qr_full!(At, (Qt, Lt), qr_alg) else Qt, Lt = qr_compact!(At, (Qt, Lt), qr_alg) From 1c12957c34a879764959de5667fbdfdba4e1dbc7 Mon Sep 17 00:00:00 2001 From: Yue Zhengyuan Date: Thu, 29 Oct 2026 19:59:36 +0800 Subject: [PATCH 04/10] Test revision --- test/decompositions/orth_factors.jl | 41 +++++++++++++++++++---------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/test/decompositions/orth_factors.jl b/test/decompositions/orth_factors.jl index fb6904a15..893987576 100644 --- a/test/decompositions/orth_factors.jl +++ b/test/decompositions/orth_factors.jl @@ -34,6 +34,22 @@ using Test @test L2 ≈ L @test L2 * Q ≈ A end + + # Direct Native LQ has a separate implementation from LQ via QR. + alg = Householder(; driver = Native()) + L, _ = right_orth(A; alg) + L2, Q2 = right_orth!(copy(A), (similar(L), Q0), alg) + @test Q2 === Q0 + @test L2 ≈ L + + Q, R = qr_full(A) + Q2, R2 = qr_full!(copy(A), (Q0, similar(R))) + @test Q2 === Q0 + @test R2 ≈ R + L, Q = lq_full(A) + L2, Q2 = lq_full!(copy(A), (similar(L), Q0)) + @test Q2 === Q0 + @test L2 ≈ L end for T in (Float64, ComplexF64), positive in (false, true) A = Diagonal(randn(T, 4)) @@ -59,25 +75,22 @@ end @test R2 === empty_factor @test Q2 ≈ Q @test Q2 * R ≈ A - if m >= n && blocksize == 1 - Ac = copy(A) - Q2, R2 = left_orth!(Ac, (Ac, empty_factor); positive, blocksize) - @test Q2 === Ac - @test R2 === empty_factor - @test Q2 ≈ Q - end L, Q = right_orth(A; positive, blocksize) L2, Q2 = right_orth!(copy(A), (empty_factor, similar(Q)); positive, blocksize) @test L2 === empty_factor @test Q2 ≈ Q @test L * Q2 ≈ A - if n >= m && blocksize == 1 - Ac = copy(A) - L2, Q2 = right_orth!(Ac, (empty_factor, Ac); positive, blocksize) - @test Q2 === Ac - @test L2 === empty_factor - @test Q2 ≈ Q - end end end + +@testset "Full L-only LQ via transposed QR" begin + A = randn(4, 7) + alg = LQViaTransposedQR(Householder()) + L, Q = lq_full(A, alg) + Q0 = similar(A, 0, 0) + L2, Q2 = lq_full!(copy(A), (similar(L), Q0), alg) + @test Q2 === Q0 + @test L2 ≈ L + @test L2 * Q ≈ A +end From 576ed55b9f963a1f98ea786134ae5edc19527a63 Mon Sep 17 00:00:00 2001 From: Yue Zhengyuan Date: Thu, 29 Oct 2026 20:32:44 +0800 Subject: [PATCH 05/10] Follow testsuite structure --- src/implementations/lq.jl | 2 +- test/decompositions/orth_factors.jl | 110 ++++-------------- test/testsuite/TestSuite.jl | 1 + test/testsuite/decompositions/orth_factors.jl | 101 ++++++++++++++++ 4 files changed, 125 insertions(+), 89 deletions(-) create mode 100644 test/testsuite/decompositions/orth_factors.jl diff --git a/src/implementations/lq.jl b/src/implementations/lq.jl index 8ffde27c5..d0fca0fcf 100644 --- a/src/implementations/lq.jl +++ b/src/implementations/lq.jl @@ -326,7 +326,7 @@ function lq_via_qr!( Qt, Lt = qr_compact!(At, (Qt, Lt), qr_alg) end isempty(Q) || adjoint!(Q, Qt) - !isempty(L) && adjoint!(L, Lt) + isempty(L) || adjoint!(L, Lt) return L, Q end diff --git a/test/decompositions/orth_factors.jl b/test/decompositions/orth_factors.jl index 893987576..59ce45e0a 100644 --- a/test/decompositions/orth_factors.jl +++ b/test/decompositions/orth_factors.jl @@ -1,96 +1,30 @@ using MatrixAlgebraKit using MatrixAlgebraKit: Native using LinearAlgebra: Diagonal -using Test -@testset "Orthogonalization without a basis" begin - for T in (Float64, ComplexF64), (m, n) in ((7, 4), (4, 4), (4, 7)) - A = randn(T, m, n) - Q0 = similar(A, 0, 0) - for positive in (false, true), alg in ( - Householder(; blocksize = 1, positive), - Householder(; blocksize = 2, positive), - Householder(; blocksize = 1, pivoted = true, positive), - Householder(; driver = Native(), positive), - ) - Q, R = left_orth(A; alg) - Q2, R2 = left_orth!(copy(A), (Q0, similar(R)), alg) - @test Q2 === Q0 - @test R2 ≈ R - @test Q * R2 ≈ A +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite - # LQ pivoting is supported through transposed QR. - lq_alg = LQViaTransposedQR(alg) - L, Q = right_orth(A; alg = lq_alg) - L2, Q2 = right_orth!(copy(A), (similar(L), Q0), lq_alg) - @test Q2 === Q0 - @test L2 ≈ L - @test L2 * Q ≈ A - end - for positive in (false, true), blocksize in (1, 2) - L, Q = right_orth(A; alg = :lq, positive, blocksize) - L2, Q2 = right_orth!(copy(A), (similar(L), Q0); alg = :lq, positive, blocksize) - @test Q2 === Q0 - @test L2 ≈ L - @test L2 * Q ≈ A - end +qr_algs = Tuple( + alg for positive in (false, true) for alg in ( + Householder(; blocksize = 1, positive), + Householder(; blocksize = 2, positive), + Householder(; blocksize = 1, pivoted = true, positive), + Householder(; driver = Native(), positive), + ) +) +lq_algs = Tuple( + Householder(; positive, blocksize) for positive in (false, true) for blocksize in (1, 2) +) - # Direct Native LQ has a separate implementation from LQ via QR. - alg = Householder(; driver = Native()) - L, _ = right_orth(A; alg) - L2, Q2 = right_orth!(copy(A), (similar(L), Q0), alg) - @test Q2 === Q0 - @test L2 ≈ L - - Q, R = qr_full(A) - Q2, R2 = qr_full!(copy(A), (Q0, similar(R))) - @test Q2 === Q0 - @test R2 ≈ R - L, Q = lq_full(A) - L2, Q2 = lq_full!(copy(A), (similar(L), Q0)) - @test Q2 === Q0 - @test L2 ≈ L - end - for T in (Float64, ComplexF64), positive in (false, true) - A = Diagonal(randn(T, 4)) - Q0 = Diagonal(T[]) - Q, R = left_orth(A; positive) - Q2, R2 = left_orth!(copy(A), (Q0, similar(R)); positive) - @test Q2 === Q0 - @test Q * R2 ≈ A - L, Q = right_orth(A; positive) - L2, Q2 = right_orth!(copy(A), (similar(L), Q0); positive) - @test Q2 === Q0 - @test L2 * Q ≈ A - end +for T in (Float64, ComplexF64), sz in ((7, 4), (4, 4), (4, 7)) + TestSuite.seed_rng!(123) + TestSuite.test_orth_without_basis(T, sz, qr_algs, lq_algs) + TestSuite.test_orth_basis_only(T, sz) end - -@testset "Orthogonalization with only a basis" begin - for T in (Float64, ComplexF64), (m, n) in ((7, 4), (4, 4), (4, 7)), - positive in (false, true), blocksize in (1, 2) - A = randn(T, m, n) - empty_factor = similar(A, 0, 0) - Q, R = left_orth(A; positive, blocksize) - Q2, R2 = left_orth!(copy(A), (similar(Q), empty_factor); positive, blocksize) - @test R2 === empty_factor - @test Q2 ≈ Q - @test Q2 * R ≈ A - - L, Q = right_orth(A; positive, blocksize) - L2, Q2 = right_orth!(copy(A), (empty_factor, similar(Q)); positive, blocksize) - @test L2 === empty_factor - @test Q2 ≈ Q - @test L * Q2 ≈ A - end -end - -@testset "Full L-only LQ via transposed QR" begin - A = randn(4, 7) - alg = LQViaTransposedQR(Householder()) - L, Q = lq_full(A, alg) - Q0 = similar(A, 0, 0) - L2, Q2 = lq_full!(copy(A), (similar(L), Q0), alg) - @test Q2 === Q0 - @test L2 ≈ L - @test L2 * Q ≈ A +for T in (Float64, ComplexF64) + TestSuite.seed_rng!(123) + TestSuite.test_orth_without_basis(Diagonal{T, Vector{T}}, 4) end +TestSuite.seed_rng!(123) +TestSuite.test_lq_full_without_basis(Float64, (4, 7)) diff --git a/test/testsuite/TestSuite.jl b/test/testsuite/TestSuite.jl index 653d99f80..806bf5183 100644 --- a/test/testsuite/TestSuite.jl +++ b/test/testsuite/TestSuite.jl @@ -129,6 +129,7 @@ include("decompositions/schur.jl") include("decompositions/eig.jl") include("decompositions/eigh.jl") include("decompositions/orthnull.jl") +include("decompositions/orth_factors.jl") include("decompositions/svd.jl") # Mooncake diff --git a/test/testsuite/decompositions/orth_factors.jl b/test/testsuite/decompositions/orth_factors.jl new file mode 100644 index 000000000..beebc04f5 --- /dev/null +++ b/test/testsuite/decompositions/orth_factors.jl @@ -0,0 +1,101 @@ +"""Test omitted bases for QR/LQ algorithms and full factorizations.""" +function test_orth_without_basis(T::Type, sz, qr_algs, lq_algs) + summary_str = testargs_summary(T, sz) + return @testset "Orthogonalization without a basis $summary_str" begin + A = instantiate_matrix(T, sz) + Q0 = similar(A, 0, 0) + for alg in qr_algs + Q, R = left_orth(A; alg) + Q2, R2 = left_orth!(copy(A), (Q0, similar(R)), alg) + @test Q2 === Q0 + @test R2 ≈ R + @test Q * R2 ≈ A + + # LQ pivoting is supported through transposed QR. + lq_alg = LQViaTransposedQR(alg) + L, Q = right_orth(A; alg = lq_alg) + L2, Q2 = right_orth!(copy(A), (similar(L), Q0), lq_alg) + @test Q2 === Q0 + @test L2 ≈ L + @test L2 * Q ≈ A + end + for alg in lq_algs + L, Q = right_orth(A; alg) + L2, Q2 = right_orth!(copy(A), (similar(L), Q0), alg) + @test Q2 === Q0 + @test L2 ≈ L + @test L2 * Q ≈ A + end + + alg = Householder(; driver = MatrixAlgebraKit.Native()) + L, _ = right_orth(A; alg) + L2, Q2 = right_orth!(copy(A), (similar(L), Q0), alg) + @test Q2 === Q0 + @test L2 ≈ L + + Q, R = qr_full(A) + Q2, R2 = qr_full!(copy(A), (Q0, similar(R))) + @test Q2 === Q0 + @test R2 ≈ R + L, Q = lq_full(A) + L2, Q2 = lq_full!(copy(A), (similar(L), Q0)) + @test Q2 === Q0 + @test L2 ≈ L + end +end + +"""Test omitted bases for diagonal factorizations.""" +function test_orth_without_basis(T::Type{<:Diagonal}, sz) + summary_str = testargs_summary(T, sz) + return @testset "Diagonal orthogonalization without a basis $summary_str" begin + for positive in (false, true) + A = instantiate_matrix(T, sz) + Q0 = Diagonal(similar(parent(A), 0)) + Q, R = left_orth(A; positive) + Q2, R2 = left_orth!(copy(A), (Q0, similar(R)); positive) + @test Q2 === Q0 + @test Q * R2 ≈ A + L, Q = right_orth(A; positive) + L2, Q2 = right_orth!(copy(A), (similar(L), Q0); positive) + @test Q2 === Q0 + @test L2 * Q ≈ A + end + end +end + +"""Test omitted corestrictions for blocked and unblocked QR/LQ.""" +function test_orth_basis_only(T::Type, sz) + summary_str = testargs_summary(T, sz) + return @testset "Orthogonalization with only a basis $summary_str" begin + for positive in (false, true), blocksize in (1, 2) + A = instantiate_matrix(T, sz) + empty_factor = similar(A, 0, 0) + Q, R = left_orth(A; positive, blocksize) + Q2, R2 = left_orth!(copy(A), (similar(Q), empty_factor); positive, blocksize) + @test R2 === empty_factor + @test Q2 ≈ Q + @test Q2 * R ≈ A + + L, Q = right_orth(A; positive, blocksize) + L2, Q2 = right_orth!(copy(A), (empty_factor, similar(Q)); positive, blocksize) + @test L2 === empty_factor + @test Q2 ≈ Q + @test L * Q2 ≈ A + end + end +end + +"""Test full L-only LQ via transposed QR for a wide input.""" +function test_lq_full_without_basis(T::Type, sz) + summary_str = testargs_summary(T, sz) + return @testset "Full L-only LQ via transposed QR $summary_str" begin + A = instantiate_matrix(T, sz) + alg = LQViaTransposedQR(Householder()) + L, Q = lq_full(A, alg) + Q0 = similar(A, 0, 0) + L2, Q2 = lq_full!(copy(A), (similar(L), Q0), alg) + @test Q2 === Q0 + @test L2 ≈ L + @test L2 * Q ≈ A + end +end From 1d1589eadd01fb46e2967decc294fc9b8aa1afa3 Mon Sep 17 00:00:00 2001 From: Yue Zhengyuan Date: Thu, 29 Oct 2026 20:47:42 +0800 Subject: [PATCH 06/10] Apply docstring and TODO suggestions --- src/interface/lq.jl | 2 ++ src/interface/qr.jl | 2 ++ src/pullbacks/lq.jl | 2 ++ src/pullbacks/qr.jl | 2 ++ src/pushforwards/lq.jl | 2 ++ src/pushforwards/qr.jl | 2 ++ 6 files changed, 12 insertions(+) diff --git a/src/interface/lq.jl b/src/interface/lq.jl index 66bdcda5f..5144b2fce 100644 --- a/src/interface/lq.jl +++ b/src/interface/lq.jl @@ -11,6 +11,7 @@ where `Q` is a square unitary matrix with the same number of rows as `A` and `L` lower triangular matrix with the same size as `A`. Pass an empty `Q` output to `lq_full!` to compute only `L` without constructing `Q`. +Conversely, an empty `L` output requests only `Q`. !!! note The bang method `lq_full!` optionally accepts the output structure and @@ -32,6 +33,7 @@ such that `A = L * Q` where the matrix `Q` of size `(min(m,n), n)` has orthogona spanning the image of `A'`, and the matrix `L` of size `(m, min(m,n))` is lower triangular. Pass an empty `Q` output to `lq_compact!` to compute only `L` without constructing `Q`. +Conversely, an empty `L` output requests only `Q`. !!! note The bang method `lq_compact!` optionally accepts the output structure and diff --git a/src/interface/qr.jl b/src/interface/qr.jl index bf621cca6..fc3c75d79 100644 --- a/src/interface/qr.jl +++ b/src/interface/qr.jl @@ -11,6 +11,7 @@ where `Q` is a square unitary matrix with the same number of rows as `A` and `R` upper triangular matrix with the same size as `A`. Pass an empty `Q` output to `qr_full!` to compute only `R` without constructing `Q`. +Conversely, an empty `R` output requests only `Q`. !!! note The bang method `qr_full!` optionally accepts the output structure and @@ -33,6 +34,7 @@ orthogonal columns spanning the image of `A`, and the matrix `R` of size `(min(m is upper triangular. Pass an empty `Q` output to `qr_compact!` to compute only `R` without constructing `Q`. +Conversely, an empty `R` output requests only `Q`. !!! note The bang method `qr_compact!` optionally accepts the output structure and diff --git a/src/pullbacks/lq.jl b/src/pullbacks/lq.jl index e3cac95a5..bc2298954 100644 --- a/src/pullbacks/lq.jl +++ b/src/pullbacks/lq.jl @@ -78,6 +78,8 @@ function lq_pullback!( # process # TODO: Support L-only `right_orth!` with an empty Q output; this rule currently requires Q. L, Q = LQ + isempty(Q) && !isempty(L) && + throw(ArgumentError("LQ differentiation with an omitted Q output is not supported; compute both L and Q.")) m = size(L, 1) n = size(Q, 2) p = lq_rank(L; rank_atol) diff --git a/src/pullbacks/qr.jl b/src/pullbacks/qr.jl index 3ff84b0b5..e487d759a 100644 --- a/src/pullbacks/qr.jl +++ b/src/pullbacks/qr.jl @@ -78,6 +78,8 @@ function qr_pullback!( # process # TODO: Support R-only `left_orth!` with an empty Q output; this rule currently requires Q. Q, R = QR + isempty(Q) && !isempty(R) && + throw(ArgumentError("QR differentiation with an omitted Q output is not supported; compute both Q and R.")) m = size(Q, 1) n = size(R, 2) p = qr_rank(R; rank_atol) diff --git a/src/pushforwards/lq.jl b/src/pushforwards/lq.jl index 574e10937..bed3261a2 100644 --- a/src/pushforwards/lq.jl +++ b/src/pushforwards/lq.jl @@ -21,6 +21,8 @@ function lq_pushforward!( ) # TODO: Support L-only `right_orth!` with an empty Q output; this rule currently requires Q. L, Q = LQ + isempty(Q) && !isempty(L) && + throw(ArgumentError("LQ differentiation with an omitted Q output is not supported; compute both L and Q.")) ΔL, ΔQ = ΔLQ m = size(L, 1) n = size(Q, 2) diff --git a/src/pushforwards/qr.jl b/src/pushforwards/qr.jl index 0d1e90159..7c84ba0c2 100644 --- a/src/pushforwards/qr.jl +++ b/src/pushforwards/qr.jl @@ -19,6 +19,8 @@ function qr_pushforward!( ) # TODO: Support R-only `left_orth!` with an empty Q output; this rule currently requires Q. Q, R = QR + isempty(Q) && !isempty(R) && + throw(ArgumentError("QR differentiation with an omitted Q output is not supported; compute both Q and R.")) ΔQ, ΔR = ΔQR m = size(Q, 1) n = size(R, 2) From 7db929089e29b8150154cefeacce8c5eb30ed99e Mon Sep 17 00:00:00 2001 From: Yue Zhengyuan Date: Thu, 29 Oct 2026 20:54:47 +0800 Subject: [PATCH 07/10] Undo some unnecessary refactoring --- test/decompositions/orth_factors.jl | 11 ++++++----- test/testsuite/decompositions/orth_factors.jl | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/test/decompositions/orth_factors.jl b/test/decompositions/orth_factors.jl index 59ce45e0a..c959a8d39 100644 --- a/test/decompositions/orth_factors.jl +++ b/test/decompositions/orth_factors.jl @@ -7,11 +7,11 @@ using .TestSuite qr_algs = Tuple( alg for positive in (false, true) for alg in ( - Householder(; blocksize = 1, positive), - Householder(; blocksize = 2, positive), - Householder(; blocksize = 1, pivoted = true, positive), - Householder(; driver = Native(), positive), - ) + Householder(; blocksize = 1, positive), + Householder(; blocksize = 2, positive), + Householder(; blocksize = 1, pivoted = true, positive), + Householder(; driver = Native(), positive), + ) ) lq_algs = Tuple( Householder(; positive, blocksize) for positive in (false, true) for blocksize in (1, 2) @@ -25,6 +25,7 @@ end for T in (Float64, ComplexF64) TestSuite.seed_rng!(123) TestSuite.test_orth_without_basis(Diagonal{T, Vector{T}}, 4) + TestSuite.test_orth_without_basis_derivatives(T, (7, 4)) end TestSuite.seed_rng!(123) TestSuite.test_lq_full_without_basis(Float64, (4, 7)) diff --git a/test/testsuite/decompositions/orth_factors.jl b/test/testsuite/decompositions/orth_factors.jl index beebc04f5..9fa29451e 100644 --- a/test/testsuite/decompositions/orth_factors.jl +++ b/test/testsuite/decompositions/orth_factors.jl @@ -99,3 +99,20 @@ function test_lq_full_without_basis(T::Type, sz) @test L2 * Q ≈ A end end + +"""Check that differentiating an omitted basis gives an instructive error.""" +function test_orth_without_basis_derivatives(T::Type, sz) + summary_str = testargs_summary(T, sz) + return @testset "Omitted basis differentiation errors $summary_str" begin + A = instantiate_matrix(T, sz) + E = similar(A, 0, 0) + _, R = left_orth(A) + L, _ = right_orth(A) + for f! in (MatrixAlgebraKit.qr_pullback!, MatrixAlgebraKit.qr_pushforward!) + @test_throws r"omitted Q.*compute both Q and R" f!(zero(A), A, (E, R), (E, zero(R))) + end + for f! in (MatrixAlgebraKit.lq_pullback!, MatrixAlgebraKit.lq_pushforward!) + @test_throws r"omitted Q.*compute both L and Q" f!(zero(A), A, (L, E), (zero(L), E)) + end + end +end From f42eac9e54e2c878f712a3da8a45e4c7e209d1b1 Mon Sep 17 00:00:00 2001 From: Yue Zhengyuan Date: Thu, 29 Oct 2026 22:15:27 +0800 Subject: [PATCH 08/10] Rewrite docstring Co-authored-by: Lukas Devos --- src/interface/orthnull.jl | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/interface/orthnull.jl b/src/interface/orthnull.jl index 741447ae2..fd10f9398 100644 --- a/src/interface/orthnull.jl +++ b/src/interface/orthnull.jl @@ -76,8 +76,8 @@ deduce the decomposition kind with [`left_orth_alg(alg)`](@ref left_orth_alg). destroys the input matrix `A`. Always use the return value of the function as it may not always be possible to use the provided `CV` as output. -For QR-based decompositions, pass an empty basis matrix to compute only `C` (the `R` factor): -`left_orth!(A, (similar(A, 0, 0), C); alg = :qr)`. +For QR-based decompositions, pass an empty `V` output to `left_orth!` to compute only `C` without constructing `V`. +Conversely, an empty `C` output requests only `V`. The returned basis matrix remains empty, and its construction is skipped. See also [`right_orth(!)`](@ref right_orth), [`left_null(!)`](@ref left_null) and @@ -161,9 +161,8 @@ deduce the decomposition kind with [`right_orth_alg`](@ref). destroys the input matrix `A`. Always use the return value of the function as it may not always be possible to use the provided `CVᴴ` as output. -For LQ-based decompositions, pass an empty basis matrix to compute only `C` (the `L` factor): -`right_orth!(A, (C, similar(A, 0, 0)); alg = :lq)`. -The returned basis matrix remains empty, and its construction is skipped. +For LQ-based decompositions, pass an empty `Vᴴ` output to `right_orth!` to compute only `C` without constructing `Vᴴ`. +Conversely, an empty `C` output requests only `Vᴴ`. See also [`left_orth(!)`](@ref left_orth), [`left_null(!)`](@ref left_null) and [`right_null(!)`](@ref right_null). From af00a36d264ad4053facb3519cb9a32dddf72de2 Mon Sep 17 00:00:00 2001 From: Yue Zhengyuan Date: Thu, 29 Oct 2026 22:24:24 +0800 Subject: [PATCH 09/10] Integrate new tests into existing files --- test/decompositions/lq.jl | 1 + test/decompositions/orth_factors.jl | 31 ----- test/testsuite/TestSuite.jl | 1 - test/testsuite/decompositions/lq.jl | 20 +++ test/testsuite/decompositions/orth_factors.jl | 118 ------------------ test/testsuite/decompositions/orthnull.jl | 20 +++ test/testsuite/decompositions/qr.jl | 20 +++ 7 files changed, 61 insertions(+), 150 deletions(-) delete mode 100644 test/decompositions/orth_factors.jl delete mode 100644 test/testsuite/decompositions/orth_factors.jl diff --git a/test/decompositions/lq.jl b/test/decompositions/lq.jl index 2b13ee195..4051d3b02 100644 --- a/test/decompositions/lq.jl +++ b/test/decompositions/lq.jl @@ -46,6 +46,7 @@ for T in (BLASFloats..., GenericFloats...), n in (37, m, 63) if T ∈ BLASFloats TestSuite.test_lq(T, (m, n)) LAPACK_LQ_ALGS = ( + LQViaTransposedQR(Householder()), # includes full L-only LQ for wide inputs Householder(; positive = false, pivoted = false, blocksize = 1), Householder(; positive = false, pivoted = false, blocksize = 8), Householder(; positive = false, pivoted = true, blocksize = 1), diff --git a/test/decompositions/orth_factors.jl b/test/decompositions/orth_factors.jl deleted file mode 100644 index c959a8d39..000000000 --- a/test/decompositions/orth_factors.jl +++ /dev/null @@ -1,31 +0,0 @@ -using MatrixAlgebraKit -using MatrixAlgebraKit: Native -using LinearAlgebra: Diagonal - -@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") -using .TestSuite - -qr_algs = Tuple( - alg for positive in (false, true) for alg in ( - Householder(; blocksize = 1, positive), - Householder(; blocksize = 2, positive), - Householder(; blocksize = 1, pivoted = true, positive), - Householder(; driver = Native(), positive), - ) -) -lq_algs = Tuple( - Householder(; positive, blocksize) for positive in (false, true) for blocksize in (1, 2) -) - -for T in (Float64, ComplexF64), sz in ((7, 4), (4, 4), (4, 7)) - TestSuite.seed_rng!(123) - TestSuite.test_orth_without_basis(T, sz, qr_algs, lq_algs) - TestSuite.test_orth_basis_only(T, sz) -end -for T in (Float64, ComplexF64) - TestSuite.seed_rng!(123) - TestSuite.test_orth_without_basis(Diagonal{T, Vector{T}}, 4) - TestSuite.test_orth_without_basis_derivatives(T, (7, 4)) -end -TestSuite.seed_rng!(123) -TestSuite.test_lq_full_without_basis(Float64, (4, 7)) diff --git a/test/testsuite/TestSuite.jl b/test/testsuite/TestSuite.jl index 806bf5183..653d99f80 100644 --- a/test/testsuite/TestSuite.jl +++ b/test/testsuite/TestSuite.jl @@ -129,7 +129,6 @@ include("decompositions/schur.jl") include("decompositions/eig.jl") include("decompositions/eigh.jl") include("decompositions/orthnull.jl") -include("decompositions/orth_factors.jl") include("decompositions/svd.jl") # Mooncake diff --git a/test/testsuite/decompositions/lq.jl b/test/testsuite/decompositions/lq.jl index d50691bbb..6ab8c8f3a 100644 --- a/test/testsuite/decompositions/lq.jl +++ b/test/testsuite/decompositions/lq.jl @@ -70,6 +70,14 @@ function test_lq_compact( Lblocked, Qblocked = @testinferred lq_compact(A; blocksize = 2) @test Lblocked * Qblocked ≈ A @test isisometric(Qblocked; side = :right, atol, rtol) + + for positive in (false, true), blocksize in (1, 2) + _, Qref = lq_compact(A; positive, blocksize) + empty_factor = similar(A, 0, 0) + omitted, Qonly = lq_compact!(copy(A), (empty_factor, similar(Qref)); positive, blocksize) + @test omitted === empty_factor + @test Qonly ≈ Qref + end else @test_throws Exception lq_compact(A; blocksize = 2) end @@ -143,6 +151,12 @@ function test_lq_compact_algs( # does the elementary functionality work L, Q = @testinferred lq_compact(A; alg) + + # The triangular factor can be requested without constructing Q. + Q0 = Q isa Diagonal ? Diagonal(similar(parent(Q), 0)) : similar(Q, 0, 0) + L2, Q2 = lq_compact!(copy(A), (similar(L), Q0); alg) + @test Q2 === Q0 + @test L2 ≈ L @test L * Q ≈ A @test isisometric(Q; side = :right, atol, rtol) @test A == Ac @@ -235,6 +249,12 @@ function test_lq_full_algs( # does the elementary functionality work L, Q = @testinferred lq_full(A; alg) + + # The triangular factor can be requested without constructing Q. + Q0 = Q isa Diagonal ? Diagonal(similar(parent(Q), 0)) : similar(Q, 0, 0) + L2, Q2 = lq_full!(copy(A), (similar(L), Q0); alg) + @test Q2 === Q0 + @test L2 ≈ L @test L * Q ≈ A @test isisometric(Q; side = :right, atol, rtol) @test A == Ac diff --git a/test/testsuite/decompositions/orth_factors.jl b/test/testsuite/decompositions/orth_factors.jl deleted file mode 100644 index 9fa29451e..000000000 --- a/test/testsuite/decompositions/orth_factors.jl +++ /dev/null @@ -1,118 +0,0 @@ -"""Test omitted bases for QR/LQ algorithms and full factorizations.""" -function test_orth_without_basis(T::Type, sz, qr_algs, lq_algs) - summary_str = testargs_summary(T, sz) - return @testset "Orthogonalization without a basis $summary_str" begin - A = instantiate_matrix(T, sz) - Q0 = similar(A, 0, 0) - for alg in qr_algs - Q, R = left_orth(A; alg) - Q2, R2 = left_orth!(copy(A), (Q0, similar(R)), alg) - @test Q2 === Q0 - @test R2 ≈ R - @test Q * R2 ≈ A - - # LQ pivoting is supported through transposed QR. - lq_alg = LQViaTransposedQR(alg) - L, Q = right_orth(A; alg = lq_alg) - L2, Q2 = right_orth!(copy(A), (similar(L), Q0), lq_alg) - @test Q2 === Q0 - @test L2 ≈ L - @test L2 * Q ≈ A - end - for alg in lq_algs - L, Q = right_orth(A; alg) - L2, Q2 = right_orth!(copy(A), (similar(L), Q0), alg) - @test Q2 === Q0 - @test L2 ≈ L - @test L2 * Q ≈ A - end - - alg = Householder(; driver = MatrixAlgebraKit.Native()) - L, _ = right_orth(A; alg) - L2, Q2 = right_orth!(copy(A), (similar(L), Q0), alg) - @test Q2 === Q0 - @test L2 ≈ L - - Q, R = qr_full(A) - Q2, R2 = qr_full!(copy(A), (Q0, similar(R))) - @test Q2 === Q0 - @test R2 ≈ R - L, Q = lq_full(A) - L2, Q2 = lq_full!(copy(A), (similar(L), Q0)) - @test Q2 === Q0 - @test L2 ≈ L - end -end - -"""Test omitted bases for diagonal factorizations.""" -function test_orth_without_basis(T::Type{<:Diagonal}, sz) - summary_str = testargs_summary(T, sz) - return @testset "Diagonal orthogonalization without a basis $summary_str" begin - for positive in (false, true) - A = instantiate_matrix(T, sz) - Q0 = Diagonal(similar(parent(A), 0)) - Q, R = left_orth(A; positive) - Q2, R2 = left_orth!(copy(A), (Q0, similar(R)); positive) - @test Q2 === Q0 - @test Q * R2 ≈ A - L, Q = right_orth(A; positive) - L2, Q2 = right_orth!(copy(A), (similar(L), Q0); positive) - @test Q2 === Q0 - @test L2 * Q ≈ A - end - end -end - -"""Test omitted corestrictions for blocked and unblocked QR/LQ.""" -function test_orth_basis_only(T::Type, sz) - summary_str = testargs_summary(T, sz) - return @testset "Orthogonalization with only a basis $summary_str" begin - for positive in (false, true), blocksize in (1, 2) - A = instantiate_matrix(T, sz) - empty_factor = similar(A, 0, 0) - Q, R = left_orth(A; positive, blocksize) - Q2, R2 = left_orth!(copy(A), (similar(Q), empty_factor); positive, blocksize) - @test R2 === empty_factor - @test Q2 ≈ Q - @test Q2 * R ≈ A - - L, Q = right_orth(A; positive, blocksize) - L2, Q2 = right_orth!(copy(A), (empty_factor, similar(Q)); positive, blocksize) - @test L2 === empty_factor - @test Q2 ≈ Q - @test L * Q2 ≈ A - end - end -end - -"""Test full L-only LQ via transposed QR for a wide input.""" -function test_lq_full_without_basis(T::Type, sz) - summary_str = testargs_summary(T, sz) - return @testset "Full L-only LQ via transposed QR $summary_str" begin - A = instantiate_matrix(T, sz) - alg = LQViaTransposedQR(Householder()) - L, Q = lq_full(A, alg) - Q0 = similar(A, 0, 0) - L2, Q2 = lq_full!(copy(A), (similar(L), Q0), alg) - @test Q2 === Q0 - @test L2 ≈ L - @test L2 * Q ≈ A - end -end - -"""Check that differentiating an omitted basis gives an instructive error.""" -function test_orth_without_basis_derivatives(T::Type, sz) - summary_str = testargs_summary(T, sz) - return @testset "Omitted basis differentiation errors $summary_str" begin - A = instantiate_matrix(T, sz) - E = similar(A, 0, 0) - _, R = left_orth(A) - L, _ = right_orth(A) - for f! in (MatrixAlgebraKit.qr_pullback!, MatrixAlgebraKit.qr_pushforward!) - @test_throws r"omitted Q.*compute both Q and R" f!(zero(A), A, (E, R), (E, zero(R))) - end - for f! in (MatrixAlgebraKit.lq_pullback!, MatrixAlgebraKit.lq_pushforward!) - @test_throws r"omitted Q.*compute both L and Q" f!(zero(A), A, (L, E), (zero(L), E)) - end - end -end diff --git a/test/testsuite/decompositions/orthnull.jl b/test/testsuite/decompositions/orthnull.jl index 91a40ae27..6d0b90ac9 100644 --- a/test/testsuite/decompositions/orthnull.jl +++ b/test/testsuite/decompositions/orthnull.jl @@ -114,6 +114,16 @@ function test_left_orthnull( @test C isa typeof(A) && size(C) == (minmn, n) @test eltype(N) == eltype(A) && size(N) == (m, m - minmn) @test V * C ≈ A + + V0 = V isa Diagonal ? Diagonal(similar(parent(V), 0)) : similar(V, 0, 0) + V2, C2 = left_orth!(copy(A), (V0, similar(C))) + @test V2 === V0 + @test C2 ≈ C + if !isempty(C) + for f! in (MatrixAlgebraKit.qr_pullback!, MatrixAlgebraKit.qr_pushforward!) + @test_throws r"omitted Q.*compute both Q and R" f!(zero(A), A, (V0, C), (V0, zero(C))) + end + end @test isisometric(V) @test LinearAlgebra.norm(A' * N) ≈ 0 atol = MatrixAlgebraKit.defaulttol(T) @test isisometric(N) @@ -253,6 +263,16 @@ function test_right_orthnull( @test Vᴴ isa typeof(A) && size(Vᴴ) == (minmn, n) @test eltype(Nᴴ) == eltype(A) && size(Nᴴ) == (n - minmn, n) @test C * Vᴴ ≈ A + + V0 = Vᴴ isa Diagonal ? Diagonal(similar(parent(Vᴴ), 0)) : similar(Vᴴ, 0, 0) + C2, V2 = right_orth!(copy(A), (similar(C), V0)) + @test V2 === V0 + @test C2 ≈ C + if !isempty(C) + for f! in (MatrixAlgebraKit.lq_pullback!, MatrixAlgebraKit.lq_pushforward!) + @test_throws r"omitted Q.*compute both L and Q" f!(zero(A), A, (C, V0), (zero(C), V0)) + end + end @test isisometric(Vᴴ; side = :right) @test LinearAlgebra.norm(A * adjoint(Nᴴ)) ≈ 0 atol = MatrixAlgebraKit.defaulttol(eltype(T)) @test isisometric(Nᴴ; side = :right) diff --git a/test/testsuite/decompositions/qr.jl b/test/testsuite/decompositions/qr.jl index a8dbd9376..56bf6fb33 100644 --- a/test/testsuite/decompositions/qr.jl +++ b/test/testsuite/decompositions/qr.jl @@ -71,6 +71,14 @@ function test_qr_compact( Qblocked, Rblocked = @testinferred qr_compact(A; blocksize = 2) @test Qblocked * Rblocked ≈ A @test isisometric(Qblocked; atol, rtol) + + for positive in (false, true), blocksize in (1, 2) + Qref, _ = qr_compact(A; positive, blocksize) + empty_factor = similar(A, 0, 0) + Qonly, omitted = qr_compact!(copy(A), (similar(Qref), empty_factor); positive, blocksize) + @test omitted === empty_factor + @test Qonly ≈ Qref + end else @test_throws Exception qr_compact(A; blocksize = 2) end @@ -90,6 +98,12 @@ function test_qr_compact_algs( # does the elementary functionality work Q, R = @testinferred qr_compact(A; alg) + + # The triangular factor can be requested without constructing Q. + Q0 = Q isa Diagonal ? Diagonal(similar(parent(Q), 0)) : similar(Q, 0, 0) + Q2, R2 = qr_compact!(copy(A), (Q0, similar(R)); alg) + @test Q2 === Q0 + @test R2 ≈ R @test Q * R ≈ A @test isisometric(Q; atol, rtol) @test A == Ac @@ -242,6 +256,12 @@ function test_qr_full_algs( # does the elementary functionality work Q, R = @testinferred qr_full(A; alg) + + # The triangular factor can be requested without constructing Q. + Q0 = Q isa Diagonal ? Diagonal(similar(parent(Q), 0)) : similar(Q, 0, 0) + Q2, R2 = qr_full!(copy(A), (Q0, similar(R)); alg) + @test Q2 === Q0 + @test R2 ≈ R @test Q * R ≈ A @test isunitary(Q; atol, rtol) @test A == Ac From f50e9d6ad1d729fea44a718464074080e6a1dbf2 Mon Sep 17 00:00:00 2001 From: Yue Zhengyuan Date: Fri, 30 Oct 2026 06:20:55 +0800 Subject: [PATCH 10/10] Improve type stability of gauge fixing --- src/implementations/lq.jl | 17 +++++++++++------ src/implementations/qr.jl | 19 ++++++++++++------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/implementations/lq.jl b/src/implementations/lq.jl index d0fca0fcf..fce6533db 100644 --- a/src/implementations/lq.jl +++ b/src/implementations/lq.jl @@ -159,7 +159,6 @@ function lq_householder!( throw(ArgumentError("in-place Q is only supported if L does not share memory with A")) end - Ld = diagview(A) if blocksize > 1 # L doubles as workspace for T, so Q is constructed before L is extracted mb = min(minmn, blocksize) @@ -167,17 +166,23 @@ function lq_householder!( A, T = gelqt!(driver, A, T) isempty(Q) || (Q = gemlqt!(driver, 'R', 'N', A, T, one!(Q))) computeL && _lq_copyL!(L, A) + positive && gaugefix!(lq_householder!, L, Q, diagview(A)) else A, τ = gelqf!(driver, A) computeL && _lq_copyL!(L, A) if inplaceQ - positive && (Ld = copy(Ld)) # unglq! destroys the diagonal of A - unglq!(driver, A, τ) # Q === A, so no need to rebind Q - elseif !isempty(Q) - Q = unmlq!(driver, 'R', 'N', A, τ, one!(Q)) + if positive + Ld = copy(diagview(A)) # unglq! destroys the diagonal of A + unglq!(driver, A, τ) # Q === A, so no need to rebind Q + gaugefix!(lq_householder!, L, Q, Ld) + else + unglq!(driver, A, τ) + end + else + isempty(Q) || (Q = unmlq!(driver, 'R', 'N', A, τ, one!(Q))) + positive && gaugefix!(lq_householder!, L, Q, diagview(A)) end end - positive && gaugefix!(lq_householder!, L, Q, Ld) return L, Q end diff --git a/src/implementations/qr.jl b/src/implementations/qr.jl index 91d494edd..cfd1dd590 100644 --- a/src/implementations/qr.jl +++ b/src/implementations/qr.jl @@ -182,7 +182,6 @@ function qr_householder!( end # Compute QR in packed form - Rd = diagview(A) if blocksize > 1 # R doubles as workspace for T, so Q is constructed before R is extracted nb = min(minmn, blocksize) @@ -190,6 +189,7 @@ function qr_householder!( A, T = geqrt!(driver, A, T) isempty(Q) || (Q = gemqrt!(driver, 'L', 'N', A, T, one!(Q))) computeR && _qr_copyR!(R, A) + positive && gaugefix!(qr_householder!, Q, R, diagview(A)) else if pivoted A, τ, jpvt = geqp3!(driver, A) @@ -198,15 +198,20 @@ function qr_householder!( A, τ = geqrf!(driver, A) computeR && _qr_copyR!(R, A) end + # Gauge fixing rescales rows, which commutes with pivoting. if inplaceQ - positive && (Rd = copy(Rd)) # ungqr! destroys the diagonal of A - ungqr!(driver, A, τ) # Q === A, so no need to rebind Q - elseif !isempty(Q) - Q = _qr_buildQ!(driver, Q, A, τ, minmn) + if positive + Rd = copy(diagview(A)) # ungqr! destroys the diagonal of A + ungqr!(driver, A, τ) # Q === A, so no need to rebind Q + gaugefix!(qr_householder!, Q, R, Rd) + else + ungqr!(driver, A, τ) + end + else + isempty(Q) || (Q = _qr_buildQ!(driver, Q, A, τ, minmn)) + positive && gaugefix!(qr_householder!, Q, R, diagview(A)) end end - # Gauge fixing rescales rows, which commutes with pivoting. - positive && gaugefix!(qr_householder!, Q, R, Rd) return Q, R end