Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
ea3c54c
Add gram_eigh_full / gram_eigh_full_with_pinv factorizations
mtfishman May 28, 2026
8e5152f
Switch gram_eigh_full to rank-first (A ≈ X'X) convention
mtfishman May 28, 2026
5ae8433
Tidy gram_eigh_full docstrings
mtfishman May 29, 2026
b170411
Use 'dimensions' instead of 'indices' in factorization docstrings
mtfishman May 29, 2026
b975b35
Promote gram_eigh_full helpers to matrix operations
mtfishman May 29, 2026
ca8d5b9
Refactor gram_eigh_full clamping helpers to pow_safe family
mtfishman May 29, 2026
93b98ff
Split clamping helpers into diag and Hermitian families
mtfishman May 29, 2026
1b6335f
Use copy instead of deepcopy in test_factorizations.jl
mtfishman May 29, 2026
1393746
Document atol/rtol in all clamping-family docstrings
mtfishman May 29, 2026
44b05d2
Fix interpolated docstring rendering for clamping kwargs
mtfishman May 29, 2026
27acfc7
Make clamping-kwargs doc a function over the variable name
mtfishman May 29, 2026
85be5f8
Add jldoctest examples to gram_eigh_full entries
mtfishman May 29, 2026
270e37e
Split joined julia> blocks in gram doctest examples
mtfishman May 29, 2026
ceeeaf9
Fix docstring attachments and signatures in MatrixAlgebra helpers
mtfishman May 29, 2026
72d5a67
Remove docstring from internal _clamp_kwargs_doc helper
mtfishman May 29, 2026
4bc5fd9
Generalize pow_diag_safe to AbstractMatrix via MAK.diagview/diagonal
mtfishman May 29, 2026
8ee9aa9
Cover new pow_diag_safe contract in MatrixAlgebra tests
mtfishman May 29, 2026
21e58fd
Cross-link MatrixAlgebra gram entries from the tensor-layer docstrings
mtfishman May 29, 2026
653badf
Stabilize gram tests against Float32 random rank deficiency
mtfishman May 29, 2026
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
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "TensorAlgebra"
uuid = "68bd88dc-f39d-4e12-b2ca-f046b68fcc6a"
version = "0.9.2"
version = "0.9.3"
authors = ["ITensor developers <support@itensor.org> and contributors"]

[workspace]
Expand Down
215 changes: 214 additions & 1 deletion src/MatrixAlgebra.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,31 @@ export eigen,
eigvals!!,
factorize,
factorize!!,
gram_eigh_full,
gram_eigh_full!!,
gram_eigh_full_with_pinv,
gram_eigh_full_with_pinv!!,
invsqrt_diag_safe,
invsqrth_safe,
lq,
lq!!,
orth,
orth!!,
polar,
polar!!,
pow_diag_safe,
powh_safe,
qr,
qr!!,
sqrt_diag_safe,
sqrth_safe,
svd,
svd!!,
svdvals,
svdvals!!

import MatrixAlgebraKit as MAK
using LinearAlgebra: LinearAlgebra, norm
using LinearAlgebra: LinearAlgebra, Diagonal, isdiag, norm

for (f, f_full, f_compact) in (
(:qr, :qr_full, :qr_compact),
Expand Down Expand Up @@ -74,6 +84,209 @@ for (eigvals, eigh_vals, eig_vals) in
end
end

function _clamp_kwargs_doc(arg::AbstractString)
return join(
(
" - `atol::Real`: absolute clamping threshold. Default `0`.",
" - `rtol::Real`: relative clamping threshold. Default `eps(real(eltype($arg)))^(3//4)` when `atol = 0`, else `0`.",
), "\n"
)
end

"""
pow_diag_safe(D::AbstractMatrix, p; atol=0, rtol=eps(real(eltype(D)))^(3//4)) -> D^p

Raise a diagonal-structured matrix `D` to the power `p`. Diagonal entries
`d` of `MAK.diagview(D)` with `abs(d) < tol` are clamped to zero before
exponentiation, where `tol = max(atol, rtol * maximum(abs, diagview(D)))`.
Negative `d` above `tol` cause `d^p` to error for fractional `p` (e.g.
`p = 1//2`) and pass through for integer `p`, so the operation itself
enforces the PSD precondition per-power. Errors if `isdiag(D)` is `false`.

The implementation extracts entries via `MAK.diagview` and rebuilds via
`MAK.diagonal`, so types extending those (e.g. graded or block diagonal)
automatically extend [`sqrt_diag_safe`](@ref), [`invsqrt_diag_safe`](@ref),
and the [`powh_safe`](@ref) family.

## Keyword arguments

$(_clamp_kwargs_doc("D"))
"""
function pow_diag_safe(
D::AbstractMatrix, p;
atol = zero(real(eltype(D))),
rtol = iszero(atol) ? eps(real(eltype(D)))^(3 // 4) :
zero(real(eltype(D)))
)
isdiag(D) || throw(
ArgumentError("pow_diag_safe requires a diagonal-structured matrix")
)
σ = MAK.diagview(D)
tol = max(atol, rtol * maximum(abs, σ; init = zero(real(eltype(D)))))
return MAK.diagonal(map(d -> abs(d) < tol ? zero(d) : real(d)^p, σ))
end

"""
sqrt_diag_safe(D::AbstractMatrix; atol=0, rtol=eps(real(eltype(D)))^(3//4)) -> D^(1//2)

Square root of a diagonal-structured matrix `D`, equivalent to
`pow_diag_safe(D, 1//2; atol, rtol)`.

## Keyword arguments

$(_clamp_kwargs_doc("D"))
"""
sqrt_diag_safe(D::AbstractMatrix; kwargs...) = pow_diag_safe(D, 1 // 2; kwargs...)

"""
invsqrt_diag_safe(D::AbstractMatrix; atol=0, rtol=eps(real(eltype(D)))^(3//4)) -> D^(-1//2)

Inverse square root of a diagonal-structured matrix `D`, treating diagonal
entries below tolerance as zero (Moore-Penrose convention). Equivalent to
`pow_diag_safe(D, -1//2; atol, rtol)`.

## Keyword arguments

$(_clamp_kwargs_doc("D"))
"""
invsqrt_diag_safe(D::AbstractMatrix; kwargs...) = pow_diag_safe(D, -1 // 2; kwargs...)

"""
powh_safe(M::AbstractMatrix, p; alg=nothing, atol=0, rtol=eps(real(eltype(M)))^(3//4)) -> M^p

Raise an approximately Hermitian positive semi-definite matrix to the
power `p`. For diagonal-structured `M` (`isdiag(M) == true`), dispatches
to [`pow_diag_safe`](@ref) and skips the eigendecomposition. Otherwise,
computes via `M = V * D * V'` as `V * pow_diag_safe(D, p; atol, rtol) * V'`.

## Keyword arguments

- `alg`: forwarded to `MatrixAlgebraKit.eigh_full`.

$(_clamp_kwargs_doc("M"))
"""
function powh_safe(M::AbstractMatrix, p; alg = nothing, kwargs...)
isdiag(M) && return pow_diag_safe(M, p; kwargs...)
D, V = MAK.eigh_full(M, MAK.select_algorithm(MAK.eigh_full, M, alg))
return V * pow_diag_safe(D, p; kwargs...) * V'
end

"""
sqrth_safe(M::AbstractMatrix; alg=nothing, atol=0, rtol=eps(real(eltype(M)))^(3//4)) -> M^(1//2)

Square root of an approximately Hermitian positive semi-definite matrix.
Equivalent to `powh_safe(M, 1//2; alg, atol, rtol)`.

## Keyword arguments

- `alg`: forwarded to `MatrixAlgebraKit.eigh_full`.

$(_clamp_kwargs_doc("M"))
"""
sqrth_safe(M::AbstractMatrix; kwargs...) = powh_safe(M, 1 // 2; kwargs...)

"""
invsqrth_safe(M::AbstractMatrix; alg=nothing, atol=0, rtol=eps(real(eltype(M)))^(3//4)) -> M^(-1//2)

Inverse square root of an approximately Hermitian positive semi-definite
matrix. Equivalent to `powh_safe(M, -1//2; alg, atol, rtol)`.

## Keyword arguments

- `alg`: forwarded to `MatrixAlgebraKit.eigh_full`.

$(_clamp_kwargs_doc("M"))
"""
invsqrth_safe(M::AbstractMatrix; kwargs...) = powh_safe(M, -1 // 2; kwargs...)

for (gram, gram_with_pinv, eigh_full) in (
(:gram_eigh_full, :gram_eigh_full_with_pinv, :eigh_full),
(:gram_eigh_full!!, :gram_eigh_full_with_pinv!!, :eigh_full!),
)
@eval begin
function $gram(A::AbstractMatrix; alg = nothing, kwargs...)
D, V = MAK.$eigh_full(A, MAK.select_algorithm(MAK.$eigh_full, A, alg))
return sqrth_safe(D; kwargs...) * V'
end
function $gram_with_pinv(A::AbstractMatrix; alg = nothing, kwargs...)
D, V = MAK.$eigh_full(A, MAK.select_algorithm(MAK.$eigh_full, A, alg))
return sqrth_safe(D; kwargs...) * V', V * invsqrth_safe(D; kwargs...)
end
end
end

"""
gram_eigh_full(A::AbstractMatrix; alg=nothing, atol=0, rtol=eps(real(eltype(A)))^(3//4)) -> X
gram_eigh_full!!(A::AbstractMatrix; alg=nothing, atol=0, rtol=eps(real(eltype(A)))^(3//4)) -> X

Gram factorization of a Hermitian positive semi-definite matrix via its
eigendecomposition: returns `X = sqrth_safe(D; atol, rtol) * V'` such
that `A ≈ X' * X`, where `A = V * D * V'`. Eigenvalues below `tol` (see
[`pow_diag_safe`](@ref)) are clamped to zero. The `!!` variant may
destroy `A`.

## Keyword arguments

- `alg`: forwarded to `MatrixAlgebraKit.eigh_full`.

$(_clamp_kwargs_doc("A"))

# Examples

```jldoctest
julia> using TensorAlgebra.MatrixAlgebra: gram_eigh_full

julia> B = [1.0 0.5; 0.5 2.0];

julia> A = B' * B;

julia> X = gram_eigh_full(A);

julia> X' * X ≈ A
true
```

See also [`gram_eigh_full_with_pinv`](@ref).
"""
gram_eigh_full, gram_eigh_full!!

"""
gram_eigh_full_with_pinv(A::AbstractMatrix; alg=nothing, atol=0, rtol=eps(real(eltype(A)))^(3//4)) -> X, Y
gram_eigh_full_with_pinv!!(A::AbstractMatrix; alg=nothing, atol=0, rtol=eps(real(eltype(A)))^(3//4)) -> X, Y

Like [`gram_eigh_full`](@ref), but additionally returns
`Y = V * invsqrth_safe(D; atol, rtol) ≈ pinv(X)` so that `X * Y ≈ I` on
the rank subspace. Eigenvalues below `tol` are clamped to zero in both
factors. The `!!` variant may destroy `A`.

## Keyword arguments

- `alg`: forwarded to `MatrixAlgebraKit.eigh_full`.

$(_clamp_kwargs_doc("A"))

# Examples

```jldoctest
julia> using LinearAlgebra: I

julia> using TensorAlgebra.MatrixAlgebra: gram_eigh_full_with_pinv

julia> B = [1.0 0.5; 0.5 2.0];

julia> A = B' * B;

julia> X, Y = gram_eigh_full_with_pinv(A);

julia> X' * X ≈ A
true

julia> X * Y ≈ I
true
```
"""
gram_eigh_full_with_pinv, gram_eigh_full_with_pinv!!

for (svd, svd_trunc, svd_full, svd_compact) in (
(:svd, :svd_trunc, :svd_full, :svd_compact),
(:svd!!, :svd_trunc!, :svd_full!, :svd_compact!),
Expand Down
5 changes: 3 additions & 2 deletions src/TensorAlgebra.jl
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
module TensorAlgebra

export contract, contract!, eigen, eigvals, factorize, left_null, left_orth, left_polar,
lq, qr, right_null, right_orth, right_polar, orth, polar, svd, svdvals
export contract, contract!, eigen, eigvals, factorize, gram_eigh_full,
gram_eigh_full_with_pinv, left_null, left_orth, left_polar, lq, qr,
right_null, right_orth, right_polar, orth, polar, svd, svdvals

if VERSION >= v"1.11.0-DEV.469"
eval(Meta.parse("public contractopadd!, matricizeop"))
Expand Down
Loading
Loading