Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Linear Algebra extension: svd, inv, *, /, \ for QuantityArray #75

Merged
merged 52 commits into from
May 10, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
f833b0a
uniform matrix: backslash, inv, svd, *
ggebbie Oct 26, 2023
6b963b4
`QuantityArray` linalg functions from `UnitfulLinearAlgebra`
ggebbie Oct 26, 2023
2744c48
initial runtests for linear algebra
ggebbie Oct 26, 2023
b9b15dc
remove comments
ggebbie Oct 27, 2023
63d263e
run linear algebra tests before Aqua
ggebbie Oct 27, 2023
15c2122
extend * for `QuantityArray` in arrays.jl not Ext
ggebbie Oct 27, 2023
a80ef77
inv(dimension) > dimension^-1
ggebbie Oct 27, 2023
b9c58e8
fix julia 1.6 runtest fail due to SVD struct restriction
ggebbie Oct 27, 2023
04787c5
restrict (\) to 2D and 1D `QuantityArray`s
ggebbie Oct 27, 2023
f40c060
formatting, `diagm` added, ext Base in arrays.jl
ggebbie Oct 27, 2023
394a102
fix more formatting in `arrays.jl`
ggebbie Oct 28, 2023
4d517f5
restrict `svd` to 2D `QuantityArray`s
ggebbie Oct 28, 2023
71a8da4
Merge branch 'main' into uniform-matrix
MilesCranmer Nov 12, 2023
988ecd5
Make LinearAlgebra functions generic to abstract type
MilesCranmer Nov 12, 2023
b08a5d3
Fix constructor for `det`
MilesCranmer Nov 12, 2023
4261386
Improve basic array ops with promotion utilities
MilesCranmer Nov 12, 2023
b985292
Deal with ambiguities in abstract array interface
MilesCranmer Nov 12, 2023
9263331
Try to fix ambiguities
MilesCranmer Nov 12, 2023
2bfaeca
Merge branch 'main' into pr/ggebbie/75
MilesCranmer May 9, 2024
898a167
build: add TestItems dependency
MilesCranmer May 10, 2024
8e67510
feat: add missing isapprox for (Array, QuantityArray)
MilesCranmer May 10, 2024
442a67e
test: convert to testitem
MilesCranmer May 10, 2024
0ebbcef
test: expand linear algebra testing
MilesCranmer May 10, 2024
21640de
test: fix testitem syntax
MilesCranmer May 10, 2024
128912e
test: fix testitem syntax
MilesCranmer May 10, 2024
49ad304
fix: isapprox for regular arrays
MilesCranmer May 10, 2024
01a070d
refactor: greatly simplify linear algebra implementations
MilesCranmer May 10, 2024
33897fb
fix: add adjoint for dimensions
MilesCranmer May 10, 2024
81c63cb
refactor: cleaner way to overload extensions
MilesCranmer May 10, 2024
3dfa8b4
build: avoid triggering testitem locally
MilesCranmer May 10, 2024
c511535
refactor: more cleanup of linear algebra
MilesCranmer May 10, 2024
6f325c9
test: fix usage of norm
MilesCranmer May 10, 2024
77f80dc
fix: ambiguous linear algebra operations
MilesCranmer May 10, 2024
d04959b
docs: more helpful errors for passing values not types
MilesCranmer May 10, 2024
c405442
add many more parts of LinearAlgebra interface
MilesCranmer May 10, 2024
7a24f9f
add tests for diagm
MilesCranmer May 10, 2024
5f749fd
ci: allow unittests to be run repeatedly
MilesCranmer May 10, 2024
a68b598
refactor: clean up use of `first` in diagonal
MilesCranmer May 10, 2024
b5c29cd
revert: disable `eigen` until better testing
MilesCranmer May 10, 2024
62f2543
test: add unittests for determinant
MilesCranmer May 10, 2024
3235393
test: fix re-running test
MilesCranmer May 10, 2024
ffd382f
test: of initial LinearAlgebra imports
MilesCranmer May 10, 2024
5369056
test: remove old linear algebra tests
MilesCranmer May 10, 2024
e801bdd
Merge branch 'main' into uniform-matrix
MilesCranmer May 10, 2024
67445b0
test: remove old linear algebra tests
MilesCranmer May 10, 2024
529b595
Only define `inv` of SVD on Julia 1.8+
MilesCranmer May 10, 2024
027f920
test: skip ambiguities tests on Julia other than 1.10
MilesCranmer May 10, 2024
3873acc
build: add missing compat for Test
MilesCranmer May 10, 2024
5bf7f4c
refactor: removed unused type alias
MilesCranmer May 10, 2024
c6f5ce5
test: fix on 1.6
MilesCranmer May 10, 2024
699b60b
test: cover additional branches
MilesCranmer May 10, 2024
a7acc5a
fix!: add `ustrip` for all AbstractArray's
MilesCranmer May 10, 2024
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
65 changes: 63 additions & 2 deletions ext/DynamicQuantitiesLinearAlgebraExt.jl
Original file line number Diff line number Diff line change
@@ -1,8 +1,69 @@
module DynamicQuantitiesLinearAlgebraExt

import LinearAlgebra: norm
import DynamicQuantities: AbstractQuantity, ustrip, dimension, new_quantity
import Base: (*) #, transpose
import LinearAlgebra: norm, inv, (\), svd, Algorithm, default_svd_alg, SVD, Diagonal, Adjoint, Transpose, AbstractRotation, AbstractMatrix, eigen, eigsortby, Eigen, det
import DynamicQuantities: AbstractQuantity, ustrip, dimension, new_quantity, AbstractDimensions, QuantityArray, Quantity
MilesCranmer marked this conversation as resolved.
Show resolved Hide resolved

norm(q::AbstractQuantity, p::Real=2) = new_quantity(typeof(q), norm(ustrip(q), p), dimension(q))

# handle ambiguities
*(q::QuantityArray,r::QuantityArray) = QuantityArray(ustrip(q)*ustrip(r),dimension(q)*dimension(r))
MilesCranmer marked this conversation as resolved.
Show resolved Hide resolved
*(q::QuantityArray,r::AbstractMatrix) = QuantityArray(ustrip(q)*r,dimension(q))
*(q::QuantityArray,r::AbstractVector) = QuantityArray(ustrip(q)*r,dimension(q))
*(r::AbstractMatrix,q::QuantityArray) = QuantityArray(r*ustrip(q),dimension(q))
*(r::AbstractVector,q::QuantityArray) = QuantityArray(r*ustrip(q),dimension(q))

# a choice to make this function type stable (to be reviewed/revisited)
#transpose(q::QuantityArray) = QuantityArray(transpose(ustrip(q)),dimension(q))

\(q::QuantityArray,r::QuantityArray) = QuantityArray(ustrip(q)\ustrip(r),dimension(r)/dimension(q))
MilesCranmer marked this conversation as resolved.
Show resolved Hide resolved
\(q::QuantityArray,r::Union{AbstractVector,AbstractMatrix}) = QuantityArray(ustrip(q)\r,dimension(q)^-1)
MilesCranmer marked this conversation as resolved.
Show resolved Hide resolved
# not implemented, AbstractMatrix \ QuantityArray

inv(Q::QuantityArray) = QuantityArray(inv(ustrip(Q)),dimension(Q)^-1)
MilesCranmer marked this conversation as resolved.
Show resolved Hide resolved

"""
svd(A::QuantityArray; full::Bool = false, alg::Algorithm = default_svd_alg(A)) -> SVD

Singular value decomposition (SVD) of `QuantityArray`.
Exists for uniform matrices which includes all `QuantityArray`s (pp. 124, Hart, 1995).

Returns SVD factorization of parametric type: `SVD{T, Quantity{T, Dimensions{DynamicQuantities.FixedRational{Int32, 25200}}}, Matrix{T}, QuantityArray{T, 1, Dimensions{DynamicQuantities.FixedRational{Int32, 25200}}, Quantity{T, Dimensions{DynamicQuantities.FixedRational{Int32, 25200}}}, Vector{T}}}`.

Factorization `F` can be deconstructed: `U,σ,V = F`.

Functions working: , `size`, `adjoint`.
Functions partially working: `inv`
Functions not working: `svdvals`, `ldiv!`.
"""
MilesCranmer marked this conversation as resolved.
Show resolved Hide resolved
function svd(A::QuantityArray;full=false,alg::Algorithm = default_svd_alg(ustrip(A)))
MilesCranmer marked this conversation as resolved.
Show resolved Hide resolved
F = svd(ustrip(A), full=full, alg=alg)
return SVD(F.U,QuantityArray(F.S,dimension(A)),F.Vt)
end

Diagonal(q::QuantityArray) = QuantityArray(Diagonal(ustrip(q)),dimension(q))

"""
function eigen(A::T;permute::Bool=true, scale::Bool=true, sortby::Union{Function,Nothing}=eigsortby) where T <: AbstractMultipliableMatrix

Thin wrapper for `eigen` with same keyword arguments as `LinearAlgebra.eigen`.
Only squarable matrices have eigenstructure (pp. 96, Hart, 1995).
Eigenvalues have the same dimensions as A[1,1].
Eigenvectors are parallel to the domain and range.
There are multiple ways to distribute the units among the eigenvectors, however.
If 𝐀 is endomorphic (i.e., the dimensional domain and range are the same), then the dimensional domain should
be taken as the units of the eigenvectors (pp. 205, Hart, 1995).
In the general case, physical intuition and the equation 𝐀𝐱 = λ𝐱
dictate that the units of the eigenvectors are equal to the dimensional domain of 𝐀 (pp. 206, Hart, 1995).

The following functions are available for `Eigen(::QuantityArray)` objects: eigvals, [`det`](@ref).
Functions not working: [`inv`](@ref) and [`isposdef`](@ref).
"""
function eigen(A::QuantityArray;permute::Bool=true, scale::Bool=true, sortby::Union{Function,Nothing}=eigsortby)
F = eigen(ustrip(A), permute=permute, scale=scale, sortby=sortby)
return Eigen(QuantityArray(F.values,dimension(A)), F.vectors)
end

det(A::QuantityArray) = Quantity(det(ustrip(A)),dimension(A)^(size(A,1)))

end
3 changes: 3 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ else
@safetestset "Unit tests" begin
include("unittests.jl")
end
@safetestset "LinearAlgebra.jl integration tests" begin
include("test_linearalgebra.jl")
end
@safetestset "Aqua tests" begin
include("test_aqua.jl")
end
Expand Down
74 changes: 74 additions & 0 deletions test/test_linearalgebra.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#import Pkg; Pkg.activate(".")
#using Revise
using DynamicQuantities, LinearAlgebra

# checks for unit consistency with subtraction
within(A,B,tol) = maximum(abs.(ustrip(A - B))) < tol

v = randn(2,2)
d = 1u"m"
e = u"m"
A = QuantityArray(v, d) # Create a `QuantityArray` with value `value` and dimensions `dimensions`.

# vector multiplication
q = [1,2]
r = A\q
@test isequal(dimension(A*r),dimension(QuantityArray(q)))
@test within(ustrip(A*r),q,1e-10)

# test inv
B = inv(A)
@test isapprox(B*A,I(2),rtol = 1e-10)
@test isapprox(A*B,I(2),rtol = 1e-10)

vc = randn(2,2)
dc = 1u"kg"
C = QuantityArray(vc, dc) # Create a `QuantityArray` with v

D = A*C

@test within(A\D,C,1e-12)

F = svd(A)
U,σ,V = F

inv(F) # doesn't return a QuantityArray, could add a wrapper here

@test isequal(dimension(σ),dimension(A))

Σ = Diagonal(σ)
@test within(U*Σ*V',A,1e-10)

# SVD in modal form

# truncated SVD
K = 2
U[:,1:K]*Diagonal(σ[1:K])*V[:,1:K]'

K = 1
U[:,1:K]*Diagonal(σ[1:K])*V[:,1:K]'

# retain QuantityArray, best way to store this info
@test Σ*Σ isa QuantityArray
@test U*Σ isa QuantityArray
@test U'*Σ isa QuantityArray
@test transpose(U)*Σ isa QuantityArray
@test Σ*V isa QuantityArray
@test Σ*V' isa QuantityArray
@test Σ*transpose(V) isa QuantityArray
@test U*Σ*V' isa QuantityArray

F = eigen(A)
vals,vecs = F
@test isequal(dimension(vals),dimension(A))
# inv(F) # doesn't work
@test within(det(F),det(A),1e-10)

# eigenstructure satisfied?
@test within(A*vecs,vecs*Diagonal(vals),1e-10)

det(A) # does it run?

# svdvals(A) # error, eigen.jl doesn't handle it

# isapprox(ustrip(transpose(A)),transpose(ustrip(A))) # no show method for Transpose{QuantityArray}
Loading