Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
6d43f15
`left_orth` and `right_orth` with `@functiondef`
lkdvos Oct 10, 2025
9c558d9
refactor truncationintersection for type stability
lkdvos Oct 11, 2025
651dca3
factor out `linearmap.jl`
lkdvos Oct 11, 2025
d4da1ec
refactor orth algorithm selection
lkdvos Oct 11, 2025
136cf62
add algorithm traits
lkdvos Oct 11, 2025
545ef5d
refactor left_orth and right_orth implementations
lkdvos Oct 11, 2025
1d06840
refactor null algorithm selection
lkdvos Oct 12, 2025
b68d8c8
refactor null implementation
lkdvos Oct 12, 2025
4c31a36
more algorithm traits
lkdvos Oct 12, 2025
18b5a94
`left_null` and `right_null` with `@functiondef`
lkdvos Oct 12, 2025
3b98df6
reorganize algorithm unions
lkdvos Oct 12, 2025
1541f9f
refactor null truncation
lkdvos Oct 12, 2025
08944ec
update docstrings
lkdvos Oct 12, 2025
67dc55a
disambiguate alg selection
lkdvos Oct 12, 2025
3fc6cbb
update tests
lkdvos Oct 12, 2025
fdbb86c
more docs updates
lkdvos Oct 12, 2025
0559aee
mark randomized SVD as unusable for nullspaces
lkdvos Oct 12, 2025
86f0c8d
fix JET complaint
lkdvos Oct 13, 2025
84537c7
docstring reorganization
lkdvos Oct 14, 2025
e8d8b12
improve truncation
lkdvos Oct 15, 2025
01ee394
headers
lkdvos Oct 15, 2025
c0fda8c
fix merge
lkdvos Oct 22, 2025
3bc0081
work out alternate proposition
lkdvos Oct 15, 2025
d3e155b
unpack algorithms
lkdvos Oct 22, 2025
437ed8c
rework traits
lkdvos Oct 23, 2025
f6266ea
also include null implementation
lkdvos Oct 23, 2025
6bc158a
maybeblasmat
lkdvos Oct 23, 2025
c7942c0
fix docs build
lkdvos Oct 23, 2025
f547ae0
some cleanup
lkdvos Oct 23, 2025
b3407a2
fix type stability again
lkdvos Oct 23, 2025
916fc28
update gpu tests
lkdvos Oct 23, 2025
14bc3c7
address some review comments
lkdvos Oct 23, 2025
d45cfa9
some AMD fixes
lkdvos Oct 23, 2025
2b62dfd
some more AMD fixes
lkdvos Oct 24, 2025
adc36bb
more more AMD fixes
lkdvos Oct 24, 2025
9c916af
move lqviatransposedqr
lkdvos Oct 25, 2025
185d939
no randomized svd for null
lkdvos Oct 25, 2025
f89a318
no initialization for orthnull with SVD
lkdvos Oct 25, 2025
878b428
fix docstring
lkdvos Oct 25, 2025
1744416
more more more AMD fixes
lkdvos Oct 25, 2025
54dda05
Revert "no randomized svd for null"
lkdvos Oct 25, 2025
15ff6ae
update syntax for leftorth
lkdvos Oct 30, 2025
aff5766
update algselector docstrings
lkdvos Oct 30, 2025
afa64bc
migrate logic from constructors to functions
lkdvos Oct 30, 2025
fb60fe3
also update docs
lkdvos Oct 30, 2025
a2b7e9a
remove unnecessary overloads
lkdvos Oct 30, 2025
f044228
apply suggestions
lkdvos Nov 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 187 additions & 5 deletions docs/src/user_interface/decompositions.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,23 +169,205 @@ PolarNewton

## Orthogonal Subspaces

Often it is useful to compute orthogonal bases for a particular subspace defined by a matrix.
Given a matrix `A` we can compute an orthonormal basis for its image or coimage, and factorize the matrix accordingly.
Often it is useful to compute orthogonal bases for particular subspaces defined by a matrix.
Given a matrix `A`, we can compute an orthonormal basis for its image or coimage, and factorize the matrix accordingly.
These bases are accessible through [`left_orth`](@ref) and [`right_orth`](@ref) respectively.
This is implemented through a combination of the decompositions mentioned above, and serves as a convenient interface to these operations.

### Overview

The [`left_orth`](@ref) function computes an orthonormal basis `V` for the image (column space) of `A`, along with a corestriction matrix `C` such that `A = V * C`.
The resulting `V` has orthonormal columns (`V' * V ≈ I` or `isisometric(V)`).

Similarly, [`right_orth`](@ref) computes an orthonormal basis for the coimage (row space) of `A`, i.e., the image of `A'`.
It returns matrices `C` and `Vᴴ` such that `A = C * Vᴴ`, where `V = (Vᴴ)'` has orthonormal columns (`isisometric(Vᴴ; side = :right)`).

These functions serve as high-level interfaces that automatically select the most appropriate decomposition based on the specified options, making them convenient for users who want orthonormalization without worrying about the underlying implementation details.

```@docs; canonical=false
left_orth
right_orth
```

### Algorithm Selection

Both functions support multiple decomposition drivers, which can be selected through the `alg` keyword argument:

**For `left_orth`:**
- `alg = :qr` (default without truncation): Uses QR decomposition via [`qr_compact`](@ref)
- `alg = :polar`: Uses polar decomposition via [`left_polar`](@ref)
- `alg = :svd` (default with truncation): Uses SVD via [`svd_compact`](@ref) or [`svd_trunc`](@ref)

**For `right_orth`:**
- `alg = :lq` (default without truncation): Uses LQ decomposition via [`lq_compact`](@ref)
- `alg = :polar`: Uses polar decomposition via [`right_polar`](@ref)
- `alg = :svd` (default with truncation): Uses SVD via [`svd_compact`](@ref) or [`svd_trunc`](@ref)

When `alg` is not specified, the function automatically selects `:qr`/`:lq` for exact orthogonalization, or `:svd` when a truncation strategy is provided.

### Extending with Custom Algorithms

To register a custom algorithm type for use with these functions, you need to define the appropriate conversion function, for example:

```julia
# For left_orth
MatrixAlgebraKit.left_orth_alg(alg::MyCustomAlgorithm) = LeftOrthAlgorithm{:qr}(alg)

# For right_orth
MatrixAlgebraKit.right_orth_alg(alg::MyCustomAlgorithm) = RightOrthAlgorithm{:lq}(alg)
```

The type parameter (`:qr`, `:lq`, `:polar`, or `:svd`) indicates which factorization backend will be used.
The wrapper algorithm types handle the dispatch to the appropriate implementation:

```@docs; canonical=false
left_orth_alg
right_orth_alg
LeftOrthAlgorithm
RightOrthAlgorithm
```

### Examples

Basic orthogonalization:

```jldoctest orthnull; output=false
using MatrixAlgebraKit
using LinearAlgebra

A = [1.0 2.0; 3.0 4.0; 5.0 6.0]
V, C = left_orth(A)
(V' * V) ≈ I && A ≈ V * C

# output
true
```

Using different algorithms:

```jldoctest orthnull; output=false
A = randn(4, 3)
V1, C1 = left_orth(A; alg = :qr)
V2, C2 = left_orth(A; alg = :polar)
V3, C3 = left_orth(A; alg = :svd)
A ≈ V1 * C1 ≈ V2 * C2 ≈ V3 * C3

# output
true
```

With truncation:

```jldoctest orthnull; output=false
A = [1.0 0.0; 0.0 1e-10; 0.0 0.0]
V, C = left_orth(A; trunc = (atol = 1e-8,))
size(V, 2) == 1 # Only one column retained

# output
true
```


## Null Spaces

Similarly, it can be convenient to obtain an orthogonal basis for the kernel or cokernel of a matrix.
These are the compliments of the coimage and image, respectively, and can be computed using the [`left_null`](@ref) and [`right_null`](@ref) functions.
Again, this is typically implemented through a combination of the decompositions mentioned above, and serves as a convenient interface to these operations.
These are the orthogonal complements of the coimage and image, respectively, and can be computed using the [`left_null`](@ref) and [`right_null`](@ref) functions.

### Overview

The [`left_null`](@ref) function computes an orthonormal basis `N` for the cokernel (left nullspace) of `A`, which is the nullspace of `A'`.
This means `A' * N ≈ 0` and `N' * N ≈ I`.

Similarly, [`right_null`](@ref) computes an orthonormal basis for the kernel (right nullspace) of `A`.
It returns `Nᴴ` such that `A * Nᴴ' ≈ 0` and `Nᴴ * Nᴴ' ≈ I`, where `N = (Nᴴ)'` has orthonormal columns.

These functions automatically handle rank determination and provide convenient access to nullspace computation without requiring detailed knowledge of the underlying decomposition methods.

```@docs; canonical=false
left_null
right_null
```

### Algorithm Selection

Both functions support multiple decomposition drivers, which can be selected through the `alg` keyword argument:

**For `left_null`:**
- `alg = :qr` (default without truncation): Uses QR-based nullspace computation via [`qr_null`](@ref)
- `alg = :svd` (default with truncation): Uses SVD via [`svd_full`](@ref) with appropriate truncation

**For `right_null`:**
- `alg = :lq` (default without truncation): Uses LQ-based nullspace computation via [`lq_null`](@ref)
- `alg = :svd` (default with truncation): Uses SVD via [`svd_full`](@ref) with appropriate truncation

When `alg` is not specified, the function automatically selects `:qr`/`:lq` for exact nullspace computation, or `:svd` when a truncation strategy is provided to handle numerical rank determination.

!!! note
For nullspace functions, [`notrunc`](@ref) has special meaning when used with the default QR/LQ algorithms.
It indicates that the nullspace should be computed from the exact zeros determined by the additional rows/columns of the extended matrix, without any tolerance-based truncation.

### Extending with Custom Algorithms

To register a custom algorithm type for use with these functions, you need to define the appropriate conversion function:

```julia
# For left_null
MatrixAlgebraKit.left_null_alg(alg::MyCustomAlgorithm) = LeftNullAlgorithm{:qr}(alg)

# For right_null
MatrixAlgebraKit.right_null_alg(alg::MyCustomAlgorithm) = RightNullAlgorithm{:lq}(alg)
```

The type parameter (`:qr`, `:lq`, or `:svd`) indicates which factorization backend will be used.
The wrapper algorithm types handle the dispatch to the appropriate implementation:

```@docs; canonical=false
LeftNullAlgorithm
RightNullAlgorithm
left_null_alg
right_null_alg
```

### Examples

Basic nullspace computation:

```jldoctest orthnull; output=false
A = [1.0 2.0 3.0; 4.0 5.0 6.0] # Rank 2 matrix
N = left_null(A)
size(N) == (2, 0)

# output
true
```

```jldoctest orthnull; output=false
Nᴴ = right_null(A)
size(Nᴴ) == (1, 3) && norm(A * Nᴴ') < 1e-14 && isisometric(Nᴴ; side = :right)

# output
true
```

Computing nullspace with rank detection:

```jldoctest orthnull; output=false
A = [1.0 2.0; 2.0 4.0; 3.0 6.0] # Rank 1 matrix (second column = 2*first)
N = left_null(A; alg = :svd, trunc = (atol = 1e-10,))
size(N) == (3, 2) && norm(A' * N) < 1e-12 && isisometric(N)

# output
true
```

Using different algorithms:

```jldoctest orthnull; output=false
A = [1.0 0.0 0.0; 0.0 1.0 0.0]
N1 = right_null(A; alg = :lq)
N2 = right_null(A; alg = :svd)
norm(A * N1') < 1e-14 && norm(A * N2') < 1e-14 &&
isisometric(N1; side = :right) && isisometric(N2; side = :right)

# output
true
```
33 changes: 31 additions & 2 deletions ext/MatrixAlgebraKitAMDGPUExt/MatrixAlgebraKitAMDGPUExt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ using MatrixAlgebraKit
using MatrixAlgebraKit: @algdef, Algorithm, check_input
using MatrixAlgebraKit: one!, zero!, uppertriangular!, lowertriangular!
using MatrixAlgebraKit: diagview, sign_safe
using MatrixAlgebraKit: LQViaTransposedQR, TruncationByValue, AbstractAlgorithm
using MatrixAlgebraKit: LQViaTransposedQR, TruncationStrategy, NoTruncation, TruncationByValue, AbstractAlgorithm
using MatrixAlgebraKit: default_qr_algorithm, default_lq_algorithm, default_svd_algorithm, default_eigh_algorithm
import MatrixAlgebraKit: _gpu_geqrf!, _gpu_ungqr!, _gpu_unmqr!, _gpu_gesvd!, _gpu_Xgesvdp!, _gpu_gesvdj!
import MatrixAlgebraKit: _gpu_heevj!, _gpu_heevd!, _gpu_heev!, _gpu_heevx!
Expand Down Expand Up @@ -161,7 +161,9 @@ function MatrixAlgebraKit._avgdiff!(A::StridedROCMatrix, B::StridedROCMatrix)
return A, B
end

function MatrixAlgebraKit.truncate(::typeof(MatrixAlgebraKit.left_null!), US::Tuple{TU, TS}, strategy::MatrixAlgebraKit.TruncationStrategy) where {TU <: ROCArray, TS}
function MatrixAlgebraKit.truncate(
::typeof(left_null!), US::Tuple{TU, TS}, strategy::TruncationStrategy
) where {TU <: ROCMatrix, TS}
# TODO: avoid allocation?
U, S = US
extended_S = vcat(diagview(S), zeros(eltype(S), max(0, size(S, 1) - size(S, 2))))
Expand All @@ -170,5 +172,32 @@ function MatrixAlgebraKit.truncate(::typeof(MatrixAlgebraKit.left_null!), US::Tu
Utrunc = U[:, trunc_cols]
return Utrunc, ind
end
function MatrixAlgebraKit.truncate(
::typeof(right_null!), SVᴴ::Tuple{TS, TVᴴ}, strategy::TruncationStrategy
) where {TS, TVᴴ <: ROCMatrix}
# TODO: avoid allocation?
S, Vᴴ = SVᴴ
extended_S = vcat(diagview(S), zeros(eltype(S), max(0, size(S, 2) - size(S, 1))))
ind = MatrixAlgebraKit.findtruncated(extended_S, strategy)
trunc_rows = collect(1:size(Vᴴ, 1))[ind]
Vᴴtrunc = Vᴴ[trunc_rows, :]
return Vᴴtrunc, ind
end

# disambiguate:
Copy link
Member

Choose a reason for hiding this comment

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

Are the methods below ambiguous with the methods above? They seem strictly more specific. Or are they ambiguous with methods that specify ::NoTruncation but not TU<:ROCMatrix?

Copy link
Member Author

Choose a reason for hiding this comment

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

The latter, I special-cased ::NoTruncation for the *_null implementations and this is ambiguous with the overall special-cased truncate defined above. I think in general these GPU methods still require scalar indexing, so it's not actually fully supported yet anyways, so we will probably revisit this and update that in the near future as well, but that doesn't have to be tackled in this PR

function MatrixAlgebraKit.truncate(
::typeof(left_null!), (U, S)::Tuple{TU, TS}, ::NoTruncation
) where {TU <: ROCMatrix, TS}
m, n = size(S)
ind = (n + 1):m
return U[:, ind], ind
end
function MatrixAlgebraKit.truncate(
::typeof(right_null!), (S, Vᴴ)::Tuple{TS, TVᴴ}, ::NoTruncation
) where {TS, TVᴴ <: ROCMatrix}
m, n = size(S)
ind = (m + 1):n
return Vᴴ[ind, :], ind
end

end
40 changes: 36 additions & 4 deletions src/algorithms.jl
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ function _show_alg(io::IO, alg::Algorithm)
return print(io, ")")
end

# Algorithm traits
# ----------------
"""
does_truncate(alg::AbstractAlgorithm) -> Bool

Indicate whether or not an algorithm will compute a truncated decomposition
(such that composing the factors only approximates the input up to some tolerance).
"""
does_truncate(alg::AbstractAlgorithm) = false

# Algorithm selection
# -------------------
@doc """
MatrixAlgebraKit.select_algorithm(f, A, alg::AbstractAlgorithm)
MatrixAlgebraKit.select_algorithm(f, A, alg::Symbol; kwargs...)
Expand Down Expand Up @@ -83,7 +95,7 @@ function select_algorithm(f::F, A, alg::Alg = nothing; kwargs...) where {F, Alg}
return Algorithm{alg}(; kwargs...)
elseif alg isa Type
return alg(; kwargs...)
elseif alg isa NamedTuple
elseif alg isa NamedTuple || alg isa Base.Pairs
isempty(kwargs) ||
throw(ArgumentError("Additional keyword arguments are not allowed when algorithm parameters are specified."))
return default_algorithm(f, A; alg...)
Expand Down Expand Up @@ -160,6 +172,24 @@ function select_truncation(trunc)
end
end

@doc """
MatrixAlgebraKit.select_null_truncation(trunc)

Construct a [`TruncationStrategy`](@ref) from the given `NamedTuple` of keywords or input strategy, to implement a nullspace selection.
""" select_null_truncation

function select_null_truncation(trunc)
if isnothing(trunc)
return NoTruncation()
elseif trunc isa NamedTuple
return null_truncation_strategy(; trunc...)
elseif trunc isa TruncationStrategy
return trunc
else
return throw(ArgumentError("Unknown truncation strategy: $trunc"))
end
end

@doc """
MatrixAlgebraKit.findtruncated(values::AbstractVector, strategy::TruncationStrategy)

Expand Down Expand Up @@ -200,6 +230,8 @@ struct TruncatedAlgorithm{A, T} <: AbstractAlgorithm
trunc::T
end

does_truncate(::TruncatedAlgorithm) = true

# Utility macros
# --------------

Expand Down Expand Up @@ -230,14 +262,14 @@ end

function _arg_expr(::Val{1}, f, f!)
return quote # out of place to inplace
$f(A; kwargs...) = $f!(copy_input($f, A); kwargs...)
@inline $f(A; alg = nothing, kwargs...) = $f(A, select_algorithm($f, A, alg; kwargs...))
$f(A, alg::AbstractAlgorithm) = $f!(copy_input($f, A), alg)

# fill in arguments
function $f!(A; alg = nothing, kwargs...)
@inline function $f!(A; alg = nothing, kwargs...)
return $f!(A, select_algorithm($f!, A, alg; kwargs...))
end
function $f!(A, out; alg = nothing, kwargs...)
@inline function $f!(A, out; alg = nothing, kwargs...)
return $f!(A, out, select_algorithm($f!, A, alg; kwargs...))
end
function $f!(A, alg::AbstractAlgorithm)
Expand Down
Loading