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 all 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
1 change: 1 addition & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ jobs:
matrix:
version:
- '1.6'
- '1.8'
- '1'
os:
- ubuntu-latest
Expand Down
2 changes: 2 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ version = "0.13.2"
[deps]
Compat = "34da2185-b29b-5c13-b0c7-acf172513d20"
PackageExtensionCompat = "65ce6f38-6b18-4e1d-a461-8949797d7930"
TestItems = "1c621080-faea-4a02-84b6-bbd5e436b8fe"
Tricks = "410a4b4d-49e4-4fbc-ab6d-cb71b17b3775"

[weakdeps]
Expand All @@ -26,6 +27,7 @@ LinearAlgebra = "1"
Measurements = "2"
PackageExtensionCompat = "1.0.2"
ScientificTypes = "3"
TestItems = "0.1"
Tricks = "0.1"
Unitful = "1"
julia = "1.6"
Expand Down
222 changes: 217 additions & 5 deletions ext/DynamicQuantitiesLinearAlgebraExt.jl
Original file line number Diff line number Diff line change
@@ -1,11 +1,223 @@
module DynamicQuantitiesLinearAlgebraExt

using DynamicQuantities: UnionAbstractQuantity, QuantityArray, ustrip, dimension, new_quantity
using LinearAlgebra: LinearAlgebra as LA
using Compat: allequal
using DynamicQuantities
using DynamicQuantities: DynamicQuantities as DQ, quantity_type, new_quantity, DimensionError
using TestItems: @testitem

import LinearAlgebra: norm
import DynamicQuantities: _norm
DQ.is_ext_loaded(::Val{:LinearAlgebra}) = true
DQ.norm(u) = LA.norm(u)
LA.norm(q::UnionAbstractQuantity, p::Real=2) = new_quantity(typeof(q), LA.norm(ustrip(q), p), dimension(q))

_norm(u::AbstractArray) = norm(u)
norm(q::UnionAbstractQuantity, p::Real=2) = new_quantity(typeof(q), norm(ustrip(q), p), dimension(q))
# Deal with ambiguous array operations:
for op in (:(Base.:*), :(Base.:/), :(Base.:\)),
Q_ARRAY_TYPE in (:(QuantityArray{<:Any,1}), :(QuantityArray{<:Any,2})),
ARRAY_TYPE in (
LA.Transpose{<:Any, <:AbstractVector},
LA.Transpose{<:Any, <:AbstractMatrix},
LA.Transpose{<:Any, <:LA.Bidiagonal},
LA.Adjoint{<:Any, <:AbstractVector},
LA.Adjoint{<:Any, <:AbstractMatrix},
LA.Adjoint{<:Any, <:LA.Bidiagonal},
LA.Adjoint{<:Number, <:AbstractVector},
Union{LA.Transpose{T,V}, LA.Adjoint{T,V}} where {T,V<:AbstractVector},
Union{LA.Transpose{T,V}, LA.Adjoint{T,V}} where {T,V<:LA.Bidiagonal},
LA.AbstractTriangular,
Union{LA.LowerTriangular,LA.UpperTriangular},
Union{LA.UnitLowerTriangular,LA.UnitUpperTriangular},
LA.Diagonal,
LA.Bidiagonal,
LA.SymTridiagonal,
Union{LA.Hermitian{T,S}, LA.Symmetric{T,S}} where {T,S},
),
MilesCranmer marked this conversation as resolved.
Show resolved Hide resolved
(L, R) in ((Q_ARRAY_TYPE, ARRAY_TYPE), (ARRAY_TYPE, Q_ARRAY_TYPE))

@eval $op(l::$L, r::$R) = DQ.array_op($op, l, r)
end

function Base.:*(
l::LA.Transpose{Q,<:AbstractVector},
r::DQ.QuantityArray{T2,1,D,Q,<:AbstractVector{T2}}
) where {
T2,D<:DQ.AbstractDimensions,Q<:DQ.AbstractRealQuantity{T2,D}
}
return array_op(Base.:*, l, r)
end


@static if VERSION >= v"1.8.0"
@eval function LA.svd(A::QuantityArray; full=false, alg::LA.Algorithm=LA.default_svd_alg(ustrip(A)))
F = LA.svd(ustrip(A), full=full, alg=alg)
S = QuantityArray(F.S, dimension(A), quantity_type(A))
return LA.SVD(F.U, S, F.Vt)
end
# TODO: functions on SVD type that are working: `size`, `adjoint`, partially working: `inv`, not working: `svdvals`, `ldiv!`.
end


@testitem "svd" begin
using DynamicQuantities, LinearAlgebra

if VERSION >= v"1.8.0"
A = [1. 0. 0. 0. 2.; 0. 0. 3. 0. 0.; 0. 0. 0. 0. 0.; 0. 2. 0. 0. 0.]
QA = QuantityArray(A, u"m/s")

F = svd(A)
FQ = svd(QA)

@test F.U ≈ FQ.U
@test F.S * u"m/s" ≈ FQ.S
@test F.Vt ≈ FQ.Vt
@test size(FQ) == size(F)

@test FQ.S ≈ [3.0u"m/s", 2.23606797749979u"m/s", 2.0u"m/s", 0.0u"m/s"]

@test adjoint(FQ).U ≈ adjoint(F).U
@test adjoint(FQ).S ≈ adjoint(F).S * u"m/s"
@test adjoint(FQ).Vt ≈ adjoint(F).Vt

@test QA ≈ FQ.U * Diagonal(FQ.S) * FQ.Vt
end
end

@static if VERSION >= v"1.8.0"
@eval function LA.inv(F::LA.SVD{T,Q,TU,TS}) where {T,Q<:UnionAbstractQuantity,TU,TS<:QuantityArray}
stripped_svd = LA.SVD(F.U, ustrip(F.S), F.Vt)
return QuantityArray(inv(stripped_svd), inv(dimension(F.S)), quantity_type(F.S))
end
end

@testitem "inv of svd" begin
using DynamicQuantities, LinearAlgebra

if VERSION >= v"1.8.0"
A = [
1.0 0.0 0.0
0.0 2.0 0.0
0.0 0.0 3.0
]
QA = QuantityArray(A, u"m/s")

F = svd(A)
FQ = svd(QA)

@test inv(FQ) ≈ inv(F) * inv(u"m/s")

# Should be a quantity array for speed:
@test inv(FQ) isa QuantityArray
end
end

LA.Diagonal(d::Vector{<:UnionAbstractQuantity}) = LA.Diagonal(QuantityArray(d))

# TODO: See https://github.com/JuliaLang/julia/pull/54440
LA.diagzero(D::LA.Diagonal{T}, _, _) where {T<:Quantity} = zero(first(D))
LA.fzero(S::LA.Diagonal{T}) where {T<:Quantity} = zero(first(S))

@testitem "Diagonal" begin
using DynamicQuantities, LinearAlgebra

d = [1.0, 2.0, 3.0]
QA = Diagonal(QuantityArray(d, u"m"))

@test QA isa Diagonal{<:UnionAbstractQuantity}
@test QA.diag isa QuantityArray

@test QA == Diagonal(d .* u"m")

QA_true = [
1.0u"m" 0.0u"m" 0.0u"m"
0.0u"m" 2.0u"m" 0.0u"m"
0.0u"m" 0.0u"m" 3.0u"m"
]

# This required the `diagzero` call:
@test QA == QA_true

# Need fzero for this to work
@test QA .* ones(3, 3) == QA_true .* ones(3, 3)

QA2 = Diagonal(d .* u"m")
@test QA2 isa Diagonal
@test QA2 == QA_true

# Throws an error if we pass mismatched elements
@test_throws DimensionError Diagonal([1.0u"m", 2.0u"s"])

# With *
QA3 = Diagonal([1.0u"m/s", 1.0u"m/s", 1.0u"m/s"])
v = QuantityArray([2.0u"s", 3.0u"s", 4.0u"s"])
@test QA3 * v == [2.0u"m", 3.0u"m", 4.0u"m"]
end


function LA.diagm(q::QuantityArray)
return QuantityArray(LA.diagm(ustrip(q)), dimension(q), quantity_type(q))
end
function LA.diagm(m::Integer, n::Integer, q::QuantityArray)
return QuantityArray(LA.diagm(m, n, ustrip(q)), dimension(q), quantity_type(q))
end
function LA.diagm(q::Vector{Q}) where {Q<:UnionAbstractQuantity}
allequal(dimension.(q)) || throw(DimensionError(first(q), q))
return QuantityArray(LA.diagm(ustrip.(q)), dimension(first(q)), Q)
end
function LA.diagm(m::Integer, n::Integer, q::Vector{Q}) where {Q<:UnionAbstractQuantity}
allequal(dimension.(q)) || throw(DimensionError(first(q), q))
return QuantityArray(LA.diagm(m, n, ustrip.(q)), dimension(first(q)), Q)
end


@testitem "diagm" begin
using DynamicQuantities, LinearAlgebra

A = diagm([1.0, 2.0, 3.0])
QA1 = diagm([1.0, 2.0, 3.0] .* u"m")
QA2 = diagm([1.0, 2.0, 3.0]) .* u"m"
QA3 = [
1.0u"m" 0.0u"m" 0.0u"m"
0.0u"m" 2.0u"m" 0.0u"m"
0.0u"m" 0.0u"m" 3.0u"m"
]
QA4 = diagm(QuantityArray([1.0, 2.0, 3.0], u"m"))
QA5 = diagm(3, 3, [1.0, 2.0, 3.0] .* u"m")
QA6 = diagm(3, 3, QuantityArray([1.0, 2.0, 3.0], u"m"))

@test A == ustrip(QA1)
@test QA1 == QA2
@test QA1 == QA3
@test QA1 == QA4
@test QA1 == QA5
@test QA1 == QA6
end

# function LA.eigen(A::QuantityArray; permute::Bool=true, scale::Bool=true, sortby::Union{Function,Nothing}=LA.eigsortby)
# F = LA.eigen(ustrip(A), permute=permute, scale=scale, sortby=sortby)
# return LA.Eigen(QuantityArray(F.values, dimension(A), quantity_type(A)), F.vectors)
# end
## functions available for Eigen objects: eigvals, det. Not implemented: inv, isposdef.

function LA.det(A::QuantityArray)
return new_quantity(quantity_type(A), LA.det(ustrip(A)), dimension(A)^(size(A,1)))
end


@testitem "det" begin
using DynamicQuantities, LinearAlgebra

A = [
1.0 0.0 0.0
0.0 2.0 0.0
0.0 0.0 3.0
]
QA = QuantityArray(A, u"m/s")

@test det(QA) == 6.0u"m^3/s^3"

QA2 = diagm([1.0u"m/s", 2.0u"m/s", 3.0u"m/s"])
@test det(QA2) == 6.0u"m^3/s^3"
end

# TODO: Tests from missing parts of LinearAlgebra interface

end
Loading
Loading