Skip to content

Commit 45bce81

Browse files
committed
Update null and docs
1 parent e0dd0ef commit 45bce81

File tree

4 files changed

+103
-79
lines changed

4 files changed

+103
-79
lines changed

src/implementations/orthnull.jl

Lines changed: 47 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -156,59 +156,70 @@ end
156156

157157
# Implementation of null functions
158158
# --------------------------------
159-
function left_null!(A::AbstractMatrix, N; kwargs...)
159+
function null_truncation_strategy(; atol=nothing, rtol=nothing, maxrank=nothing)
160+
if isnothing(maxrank) && isnothing(atol) && isnothing(rtol)
161+
return NoTruncation()
162+
end
163+
atol = @something atol 0
164+
rtol = @something rtol 0
165+
trunc = TruncationKeepBelow(atol, rtol)
166+
return !isnothing(maxrank) ? trunc & truncrank(maxrank; rev=false) : trunc
167+
end
168+
169+
function left_null!(A::AbstractMatrix, N; trunc=nothing,
170+
kind=isnothing(trunc) ? :qr : :svd, alg_qr=(; positive=true),
171+
alg_svd=(;))
160172
check_input(left_null!, A, N)
161-
atol = get(kwargs, :atol, 0)
162-
rtol = get(kwargs, :rtol, 0)
163-
kind = get(kwargs, :kind, iszero(atol) && iszero(rtol) ? :qrpos : :svd)
164-
if !(iszero(atol) && iszero(rtol)) && kind != :svd
165-
throw(ArgumentError("nonzero tolerance not supported for left_orth with kind=$kind"))
173+
if !isnothing(trunc) && kind != :svd
174+
throw(ArgumentError("truncation not supported for left_null with kind=$kind"))
166175
end
167176
if kind == :qr
168-
alg = get(kwargs, :alg, select_algorithm(qr_null!, A))
169-
return qr_null!(A, N, alg)
170-
elseif kind == :qrpos
171-
alg = get(kwargs, :alg, select_algorithm(qr_null!, A; positive=true))
172-
return qr_null!(A, N, alg)
173-
elseif kind == :svd && iszero(atol) && iszero(rtol)
174-
alg = get(kwargs, :alg, select_algorithm(svd_full!, A))
175-
U, _, _ = svd_full!(A, alg)
177+
alg_qr′ = algorithm_or_select_algorithm(qr_null!, A, alg_qr)
178+
return qr_null!(A, N, alg_qr′)
179+
elseif kind == :svd && isnothing(trunc)
180+
alg_svd′ = algorithm_or_select_algorithm(svd_full!, A, alg_svd)
181+
U, _, _ = svd_full!(A, alg_svd′)
182+
(m, n) = size(A)
183+
return copy!(N, view(U, 1:m, (n + 1):m))
184+
elseif kind == :svd && isnothing(trunc)
185+
alg_svd′ = algorithm_or_select_algorithm(svd_full!, A, alg_svd)
186+
U, _, _ = svd_full!(A, alg_svd′)
176187
(m, n) = size(A)
177188
return copy!(N, view(U, 1:m, (n + 1):m))
178189
elseif kind == :svd
179-
alg = get(kwargs, :alg, select_algorithm(svd_full!, A))
180-
U, S, _ = svd_full!(A, alg)
181-
trunc = TruncationKeepBelow(atol, rtol)
182-
return truncate!(left_null!, (U, S), trunc)
190+
alg_svd′ = algorithm_or_select_algorithm(svd_full!, A, alg_svd)
191+
U, S, _ = svd_full!(A, alg_svd′)
192+
trunc′ = trunc isa TruncationStrategy ? trunc :
193+
trunc isa NamedTuple ? null_truncation_strategy(; trunc...) :
194+
throw(ArgumentError("Unknown truncation strategy: $trunc"))
195+
return truncate!(left_null!, (U, S), trunc′)
183196
else
184197
throw(ArgumentError("`left_null!` received unknown value `kind = $kind`"))
185198
end
186199
end
187200

188-
function right_null!(A::AbstractMatrix, Nᴴ; kwargs...)
201+
function right_null!(A::AbstractMatrix, Nᴴ; trunc=nothing,
202+
kind=isnothing(trunc) ? :lq : :svd, alg_lq=(; positive=true),
203+
alg_svd=(;))
189204
check_input(right_null!, A, Nᴴ)
190-
atol = get(kwargs, :atol, 0)
191-
rtol = get(kwargs, :rtol, 0)
192-
kind = get(kwargs, :kind, iszero(atol) && iszero(rtol) ? :lqpos : :svd)
193-
if !(iszero(atol) && iszero(rtol)) && kind != :svd
194-
throw(ArgumentError("nonzero tolerance not supported for left_orth with kind=$kind"))
205+
if !isnothing(trunc) && kind != :svd
206+
throw(ArgumentError("truncation not supported for right_null with kind=$kind"))
195207
end
196208
if kind == :lq
197-
alg = get(kwargs, :alg, select_algorithm(lq_null!, A))
198-
return lq_null!(A, Nᴴ, alg)
199-
elseif kind == :lqpos
200-
alg = get(kwargs, :alg, select_algorithm(lq_null!, A; positive=true))
201-
return lq_null!(A, Nᴴ, alg)
202-
elseif kind == :svd && iszero(atol) && iszero(rtol)
203-
alg = get(kwargs, :alg, select_algorithm(svd_full!, A))
204-
_, _, Vᴴ = svd_full!(A, alg)
209+
alg_lq′ = algorithm_or_select_algorithm(lq_null!, A, alg_lq)
210+
return lq_null!(A, Nᴴ, alg_lq′)
211+
elseif kind == :svd && isnothing(trunc)
212+
alg_svd′ = algorithm_or_select_algorithm(svd_full!, A, alg_svd)
213+
_, _, Vᴴ = svd_full!(A, alg_svd′)
205214
(m, n) = size(A)
206215
return copy!(Nᴴ, view(Vᴴ, (m + 1):n, 1:n))
207216
elseif kind == :svd
208-
alg = get(kwargs, :alg, select_algorithm(svd_full!, A))
209-
_, S, Vᴴ = svd_full!(A, alg)
210-
trunc = TruncationKeepBelow(atol, rtol)
211-
return truncate!(right_null!, (S, Vᴴ), trunc)
217+
alg_svd′ = algorithm_or_select_algorithm(svd_full!, A, alg_svd)
218+
_, S, Vᴴ = svd_full!(A, alg_svd′)
219+
trunc′ = trunc isa TruncationStrategy ? trunc :
220+
trunc isa NamedTuple ? null_truncation_strategy(; trunc...) :
221+
throw(ArgumentError("Unknown truncation strategy: $trunc"))
222+
return truncate!(right_null!, (S, Vᴴ), trunc′)
212223
else
213224
throw(ArgumentError("`right_null!` received unknown value `kind = $kind`"))
214225
end

src/implementations/truncation.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,11 @@ TruncationKeepBelow(atol::Real, rtol::Real) = TruncationKeepBelow(promote(atol,
6969

7070
# TODO: better names for these functions of the above types
7171
"""
72-
truncrank(howmany::Int, by=abs, rev=true)
72+
truncrank(howmany::Int; by=abs, rev=true)
7373
7474
Truncation strategy to keep the first `howmany` values when sorted according to `by` or the last `howmany` if `rev` is true.
7575
"""
76-
truncrank(howmany::Int, by=abs, rev=true) = TruncationKeepSorted(howmany, by, rev)
76+
truncrank(howmany::Int; by=abs, rev=true) = TruncationKeepSorted(howmany, by, rev)
7777

7878
"""
7979
trunctol(atol::Real)

src/interface/orthnull.jl

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ The keyword argument `kind` can be used to specify the specific orthogonal decom
2828
that should be used to factor `A`, whereas `trunc` can be used to control the
2929
precision in determining the rank of `A` via its singular values.
3030
31+
`trunc` can either be a truncation strategy object or a NamedTuple with fields
32+
`atol`, `rtol`, and `maxrank`.
33+
3134
This is a high-level wrapper and will use one of the decompositions
3235
`qr_compact!`, `svd_compact!`/`svd_trunc!`, and `left_polar!` to compute the orthogonal basis `V`, as controlled
3336
by the keyword arguments.
@@ -75,15 +78,18 @@ function left_orth(A::AbstractMatrix; kwargs...)
7578
end
7679

7780
"""
78-
right_orth(A; [kind::Symbol, atol::Real=0, rtol::Real=0, alg]) -> C, Vᴴ
79-
right_orth!(A, [CVᴴ]; [kind::Symbol, atol::Real=0, rtol::Real=0, alg]) -> C, Vᴴ
81+
right_orth(A; [kind::Symbol, trunc, alg_lq, alg_polar, alg_svd]) -> C, Vᴴ
82+
right_orth!(A, [CVᴴ]; [kind::Symbol, trunc, alg_lq, alg_polar, alg_svd]) -> C, Vᴴ
8083
8184
Compute an orthonormal basis `V = adjoint(Vᴴ)` for the coimage of the matrix `A`, i.e.
8285
for the image of `adjoint(A)`, as well as a matrix `C` such that `A = C * Vᴴ`.
8386
The keyword argument `kind` can be used to specify the specific orthogonal decomposition
8487
that should be used to factor `A`, whereas `trunc` can be used to control the
8588
precision in determining the rank of `A` via its singular values.
8689
90+
`trunc` can either be a truncation strategy object or a NamedTuple with fields
91+
`atol`, `rtol`, and `maxrank`.
92+
8793
This is a high-level wrapper and will use call one of the decompositions
8894
`lq_compact!`, `svd_compact!`/`svd_trunc!`, and `right_polar!` to compute the
8995
orthogonal basis `V`, as controlled by the keyword arguments.
@@ -109,7 +115,7 @@ When `kind` is not provided, the default value is `:lq` when `isnothing(trunc)`
109115
and `:svd` otherwise. Finally, finer control is obtained by providing an explicit algorithm
110116
for backend factorizations through the `alg_lq`, `alg_polar`, and `alg_svd` keyword arguments,
111117
which will only be used if the corresponding factorization is called based on the other inputs.
112-
If NamedTuples are passed as `alg_lq`, `alg_polar`, or `alg_svd`, a default algorithm is chosen
118+
If `alg_lq`, `alg_polar`, or `alg_svd` are NamedTuples, a default algorithm is chosen
113119
with `select_algorithm` and the NamedTuple is passed as keyword arguments to that algorithm.
114120
`alg_lq` defaults to `(; positive=true)` so that by default a positive QR decomposition will
115121
be used.
@@ -133,36 +139,38 @@ end
133139
# Null functions
134140
# --------------
135141
"""
136-
left_null(A; [kind::Symbol, atol::Real=0, rtol::Real=0, alg]) -> N
137-
left_null!(A, [N]; [kind::Symbol, atol::Real=0, rtol::Real=0, alg]) -> N
142+
left_null(A; [kind::Symbol, trunc, alg_qr, alg_svd]) -> N
143+
left_null!(A, [N]; [kind::Symbol, alg_qr, alg_svd]) -> N
138144
139145
Compute an orthonormal basis `N` for the cokernel of the matrix `A` of size `(m, n)`, i.e.
140146
the nullspace of `adjoint(A)`, such that `adjoint(A)*N ≈ 0` and `N'*N ≈ I`.
141147
The keyword argument `kind` can be used to specify the specific orthogonal decomposition
142-
that should be used to factor `A`, whereas `atol` and `rtol` can be used to control the
143-
precision in determining the rank of `A` via its singular values.
148+
that should be used to factor `A`, whereas `trunc` can be used to control the
149+
the rank of `A` via its singular values.
150+
151+
`trunc` can either be a truncation strategy object or a NamedTuple with fields
152+
`atol`, `rtol`, and `maxrank`.
144153
145154
This is a high-level wrapper and will use one of the decompositions `qr!` or `svd!`
146155
to compute the orthogonal basis `N`, as controlled by the keyword arguments.
147156
148157
When `kind` is provided, its possible values are
149158
150-
* `kind == :qrpos`: `N` is computed using the positive QR decomposition.
151-
This requires `iszero(atol) && iszero(rtol)` and `left_null!(A, [N], kind=:qrpos)` is equivalent to
159+
* `kind == :qr`: `N` is computed using the QR decomposition.
160+
This requires `isnothing(trunc)` and `left_null!(A, [N], kind=:qr)` is equivalent to
152161
`qr_null!(A, [N], alg)` with a default value `alg = select_algorithm(qr_compact!, A; positive=true)`
153162
154-
* `kind == :qr`: `N` is computed using the (nonpositive) QR decomposition.
155-
This requires `iszero(atol) && iszero(rtol)` and `left_null!(A, [N], kind=:qr)` is equivalent to
156-
`qr_null!(A, [N], alg)` with a default value `alg = select_algorithm(qr_compact!, A)`
157-
158163
* `kind == :svd`: `N` is computed using the singular value decomposition and will contain
159-
the left singular vectors corresponding to the singular values that
160-
are smaller than `max(atol, rtol * σ₁)`, where `σ₁` is the largest singular value of `A`.
164+
the left singular vectors corresponding to either the zero singular values if `trunc`
165+
isn't specified or the singular values specified by `trunc`.
161166
162-
When `kind` is not provided, the default value is `:qrpos` when `iszero(atol) && iszero(rtol)`
167+
When `kind` is not provided, the default value is `:qr` when `isnothing(trunc)`
163168
and `:svd` otherwise. Finally, finer control is obtained by providing an explicit algorithm
164-
using the `alg` keyword argument, which should be compatible with the chosen or default value
165-
of `kind`.
169+
using the `alg_qr` and `alg_svd` keyword arguments, which will only be used by the corresponding
170+
factorization backend. If `alg_qr` or `alg_svd` are NamedTuples, a default algorithm is chosen
171+
with `select_algorithm` and the NamedTuple is passed as keyword arguments to that algorithm.
172+
`alg_qr` defaults to `(; positive=true)` so that by default a positive QR decomposition will
173+
be used.
166174
167175
!!! note
168176
The bang method `left_null!` optionally accepts the output structure and possibly destroys
@@ -181,33 +189,32 @@ function left_null(A::AbstractMatrix; kwargs...)
181189
end
182190

183191
"""
184-
right_null(A; [kind::Symbol, atol::Real=0, rtol::Real=0, alg]) -> Nᴴ
185-
right_null!(A, [Nᴴ]; [kind::Symbol, atol::Real=0, rtol::Real=0, alg]) -> Nᴴ
192+
right_null(A; [kind::Symbol, alg_lq, alg_svd]) -> Nᴴ
193+
right_null!(A, [Nᴴ]; [kind::Symbol, alg_lq, alg_svd]) -> Nᴴ
186194
187195
Compute an orthonormal basis `N = adjoint(Nᴴ)` for the kernel or nullspace of the matrix `A`
188196
of size `(m, n)`, such that `A*adjoint(Nᴴ) ≈ 0` and `Nᴴ*adjoint(Nᴴ) ≈ I`.
189197
The keyword argument `kind` can be used to specify the specific orthogonal decomposition
190-
that should be used to factor `A`, whereas `atol` and `rtol` can be used to control the
191-
precision in determining the rank of `A` via its singular values.
198+
that should be used to factor `A`, whereas `trunc` can be used to control the
199+
the rank of `A` via its singular values.
200+
201+
`trunc` can either be a truncation strategy object or a NamedTuple with fields
202+
`atol`, `rtol`, and `maxrank`.
192203
193204
This is a high-level wrapper and will use one of the decompositions `lq!` or `svd!`
194205
to compute the orthogonal basis `Nᴴ`, as controlled by the keyword arguments.
195206
196207
When `kind` is provided, its possible values are
197208
198-
* `kind == :lqpos`: `Nᴴ` is computed using the positive LQ decomposition.
199-
This requires `iszero(atol) && iszero(rtol)` and `right_null!(A, [Nᴴ], kind=:lqpos)` is equivalent to
200-
`lq_null!(A, [Nᴴ], alg)` with a default value `alg = select_algorithm(lq_compact!, A; positive=true)`
201-
202209
* `kind == :lq`: `Nᴴ` is computed using the (nonpositive) LQ decomposition.
203-
This requires `iszero(atol) && iszero(rtol)` and `right_null!(A, [Nᴴ], kind=:lq)` is equivalent to
204-
`lq_null!(A, [Nᴴ], alg)` with a default value `alg = select_algorithm(lq_compact!, A)`
210+
This requires `isnothing(trunc)` and `right_null!(A, [Nᴴ], kind=:lq)` is equivalent to
211+
`lq_null!(A, [Nᴴ], alg)` with a default value `alg = select_algorithm(lq_compact!, A; positive=true)`
205212
206213
* `kind == :svd`: `N` is computed using the singular value decomposition and will contain
207214
the left singular vectors corresponding to the singular values that
208215
are smaller than `max(atol, rtol * σ₁)`, where `σ₁` is the largest singular value of `A`.
209216
210-
When `kind` is not provided, the default value is `:lqpos` when `iszero(atol) && iszero(rtol)`
217+
When `kind` is not provided, the default value is `:lq` when `isnothing(trunc)`
211218
and `:svd` otherwise. Finally, finer control is obtained by providing an explicit algorithm
212219
using the `alg` keyword argument, which should be compatible with the chosen or default value
213220
of `kind`.

test/orthnull.jl

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ using LinearAlgebra: LinearAlgebra, I
3636

3737
atol = eps(real(T))
3838
V2, C2 = @constinferred left_orth!(copy!(Ac, A), (V, C); trunc=(; atol=atol))
39-
N2 = @constinferred left_null!(copy!(Ac, A), N; atol=atol)
39+
N2 = @constinferred left_null!(copy!(Ac, A), N; trunc=(; atol=atol))
4040
@test V2 !== V
4141
@test C2 !== C
4242
@test N2 !== C
@@ -48,7 +48,7 @@ using LinearAlgebra: LinearAlgebra, I
4848
4949
rtol = eps(real(T))
5050
V2, C2 = @constinferred left_orth!(copy!(Ac, A), (V, C); trunc=(; rtol=rtol))
51-
N2 = @constinferred left_null!(copy!(Ac, A), N; rtol=rtol)
51+
N2 = @constinferred left_null!(copy!(Ac, A), N; trunc=(; rtol=rtol))
5252
@test V2 !== V
5353
@test C2 !== C
5454
@test N2 !== C
@@ -77,7 +77,8 @@ using LinearAlgebra: LinearAlgebra, I
7777
if kind == :svd
7878
V2, C2 = @constinferred left_orth!(copy!(Ac, A), (V, C); kind=kind,
7979
trunc=(; atol=atol))
80-
N2 = @constinferred left_null!(copy!(Ac, A), N; kind=kind, atol=atol)
80+
N2 = @constinferred left_null!(copy!(Ac, A), N; kind=kind,
81+
trunc=(; atol=atol))
8182
@test V2 !== V
8283
@test C2 !== C
8384
@test N2 !== C
@@ -89,7 +90,8 @@ using LinearAlgebra: LinearAlgebra, I
8990

9091
V2, C2 = @constinferred left_orth!(copy!(Ac, A), (V, C); kind=kind,
9192
trunc=(; rtol=rtol))
92-
N2 = @constinferred left_null!(copy!(Ac, A), N; kind=kind, rtol=rtol)
93+
N2 = @constinferred left_null!(copy!(Ac, A), N; kind=kind,
94+
trunc=(; rtol=rtol))
9395
@test V2 !== V
9496
@test C2 !== C
9597
@test N2 !== C
@@ -103,8 +105,10 @@ using LinearAlgebra: LinearAlgebra, I
103105
trunc=(; atol=atol))
104106
@test_throws ArgumentError left_orth!(copy!(Ac, A), (V, C); kind=kind,
105107
trunc=(; rtol=rtol))
106-
@test_throws ArgumentError left_null!(copy!(Ac, A), N; kind=kind, atol=atol)
107-
@test_throws ArgumentError left_null!(copy!(Ac, A), N; kind=kind, rtol=rtol)
108+
@test_throws ArgumentError left_null!(copy!(Ac, A), N; kind=kind,
109+
trunc=(; atol=atol))
110+
@test_throws ArgumentError left_null!(copy!(Ac, A), N; kind=kind,
111+
trunc=(; rtol=rtol))
108112
end
109113
end
110114
end
@@ -142,7 +146,7 @@ end
142146
143147
atol = eps(real(T))
144148
C2, Vᴴ2 = @constinferred right_orth!(copy!(Ac, A), (C, Vᴴ); trunc=(; atol=atol))
145-
Nᴴ2 = @constinferred right_null!(copy!(Ac, A), Nᴴ; atol=atol)
149+
Nᴴ2 = @constinferred right_null!(copy!(Ac, A), Nᴴ; trunc=(; atol=atol))
146150
@test C2 !== C
147151
@test Vᴴ2 !== Vᴴ
148152
@test Nᴴ2 !== Nᴴ
@@ -154,7 +158,7 @@ end
154158
155159
rtol = eps(real(T))
156160
C2, Vᴴ2 = @constinferred right_orth!(copy!(Ac, A), (C, Vᴴ); trunc=(; rtol=rtol))
157-
Nᴴ2 = @constinferred right_null!(copy!(Ac, A), Nᴴ; rtol=rtol)
161+
Nᴴ2 = @constinferred right_null!(copy!(Ac, A), Nᴴ; trunc=(; rtol=rtol))
158162
@test C2 !== C
159163
@test Vᴴ2 !== Vᴴ
160164
@test Nᴴ2 !== Nᴴ
@@ -182,7 +186,8 @@ end
182186
if kind == :svd
183187
C2, Vᴴ2 = @constinferred right_orth!(copy!(Ac, A), (C, Vᴴ); kind=kind,
184188
trunc=(; atol=atol))
185-
Nᴴ2 = @constinferred right_null!(copy!(Ac, A), Nᴴ; kind=kind, atol=atol)
189+
Nᴴ2 = @constinferred right_null!(copy!(Ac, A), Nᴴ; kind=kind,
190+
trunc=(; atol=atol))
186191
@test C2 !== C
187192
@test Vᴴ2 !== Vᴴ
188193
@test Nᴴ2 !== Nᴴ
@@ -194,7 +199,8 @@ end
194199
195200
C2, Vᴴ2 = @constinferred right_orth!(copy!(Ac, A), (C, Vᴴ); kind=kind,
196201
trunc=(; rtol=rtol))
197-
Nᴴ2 = @constinferred right_null!(copy!(Ac, A), Nᴴ; kind=kind, rtol=rtol)
202+
Nᴴ2 = @constinferred right_null!(copy!(Ac, A), Nᴴ; kind=kind,
203+
trunc=(; rtol=rtol))
198204
@test C2 !== C
199205
@test Vᴴ2 !== Vᴴ
200206
@test Nᴴ2 !== Nᴴ
@@ -209,9 +215,9 @@ end
209215
@test_throws ArgumentError right_orth!(copy!(Ac, A), (C, Vᴴ); kind=kind,
210216
trunc=(; rtol=rtol))
211217
@test_throws ArgumentError right_null!(copy!(Ac, A), Nᴴ; kind=kind,
212-
atol=atol)
218+
trunc=(; atol=atol))
213219
@test_throws ArgumentError right_null!(copy!(Ac, A), Nᴴ; kind=kind,
214-
rtol=rtol)
220+
trunc=(; rtol=rtol))
215221
end
216222
end
217223
end

0 commit comments

Comments
 (0)