- Sponsor
-
Notifications
You must be signed in to change notification settings - Fork 5.6k
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
make Symmetric/Hermitian recursive #25688
Conversation
@@ -1,7 +1,7 @@ | |||
# This file is a part of Julia. License is MIT: https://julialang.org/license | |||
|
|||
# Symmetric and Hermitian matrices | |||
struct Symmetric{T,S<:AbstractMatrix{T}} <: AbstractMatrix{T} | |||
struct Symmetric{T,S<:AbstractMatrix{<:T}} <: AbstractMatrix{T} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you explain this change here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The type of transpose(A.data[i, j])
may not be the same as that of A.data[i, j]
, so in order to not lie about the return type of getindex(A, i, j)
, eltype(A)
may need to be widened with respect to eltype(A.data)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’m not sure this is quite right. The Adjoint
stuff might not even match this signature. IIRC, for Transpose
and Adjoint
we leave this free and check it later (in constructors, getindex, etc).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confused, maybe I'm misunderstanding you. If A
is the argument to the Symmetric
outer constructor, then T == Union{eltype(A),promote_op(transpose, eltype(A))}
should always be at least eltype(A) == eltype(S)
(so that eltype(S) <: T
), right?
Do you mean that it would be possible to call the inner constructor with bad parameters? That I agree with. Could make the current outer constructor an inner constructor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I second @tkoolen's explanation and share his confusion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This definition now clicks on this end. Thanks for the explanations! :)
I was just thinking through JuliaLang/LinearAlgebra.jl#497 some more, and I think the approach in this PR is better. Case 1:
|
This PR | JuliaLang/LinearAlgebra.jl#497 | |
---|---|---|
eltype(A) |
Union{Matrix{T}, Transpose{T, Matrix{T}}} |
Matrix{T} |
eltype(A) concrete |
❌ | ✅ |
getindex(A, i, j) allocations |
O(1) | O(n^2) |
Here eltype(A)
is a non-isbits union, but I think this is outweighed by the getindex
allocations, and perhaps the allocations for this PR can even go away entirely in the future.
Case 2: A = Symmetric([rand(SMatrix{n, n, T}) for i = 1 : m, j = 1 : m])
This PR | JuliaLang/LinearAlgebra.jl#497 | |
---|---|---|
eltype(A) |
SMatrix{n, n, T, n^2} |
SMatrix{n, n, T, n^2} |
eltype(A) concrete |
✅ | ✅ |
getindex(A, i, j) allocations |
0 | 0 |
At least right now, StaticArrays defines a non-lazy transpose
, so transposed elements of A.data
have the same type as non-transposed elements (my mistake in JuliaLang/LinearAlgebra.jl#497), and both approaches result in the same behavior.
Case 3: same as case 2, but hypothetically, transpose(::SMatrix}
uses Transpose
This PR | JuliaLang/LinearAlgebra.jl#497 | |
---|---|---|
eltype(A) |
Union{Transpose{SMatrix{n, n, T, n^2}}, SMatrix{n, n, T, n^2}} |
SMatrix{n, n, T, n^2} |
eltype(A) concrete |
❌ | ✅ |
getindex(A, i, j) allocations |
0 | 0 |
I believe there would not be any allocations for the approach in this PR given the union-of-bitstypes optimization.
Case 4: A = Symmetric([rand(SVector{n, T}) for i = 1 : m, j = 1 : m])
This PR | JuliaLang/LinearAlgebra.jl#497 | |
---|---|---|
eltype(A) |
Union{SMatrix{n, 1, T, n}, SVector{n, T}} |
N/A |
eltype(A) concrete |
❌ | N/A |
getindex(A, i, j) allocations |
0 | N/A |
JuliaLang/LinearAlgebra.jl#497 doesn't do this case, because you can't convert an SMatrix{n, 1, T, n}
(the transpose element type) to an SVector{n, T}
.
end | ||
end | ||
@inline function getindex(A::Hermitian, i::Integer, j::Integer) | ||
@boundscheck checkbounds(A, i, j) | ||
@inbounds if (A.uplo == 'U') == (i < j) | ||
return A.data[i, j] | ||
elseif i == j | ||
return eltype(A)(real(A.data[i, j])) | ||
return convert(typeof(A.data[i, j]), real(A.data[i, j])) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, this is probably wrong if A.data[i, j]
is a matrix: should probably rather be Hermitian(...)
then..
Just noticed that julia> Symmetric(hcat([[1 2; 3 4]]))
1×1 Symmetric{Union{Array{Int64,2}, Transpose{Int64,Array{Int64,2}}},Array{Array{Int64,2},2}}:
[1 3; 2 4] isn't quite symmetric and julia> Hermitian(hcat([[1 2im; -2im 3]]))
1×1 Hermitian{Union{Array{Complex{Int64},2}, Adjoint{Complex{Int64},Array{Complex{Int64},2}}},Array{Array{Complex{Int64},2},2}}:
[1+0im 0+0im; 0+0im 3+0im] looks pretty wrong. Probably |
e77b3a5
to
9ab815d
Compare
Updated to fix these cases. I added (non-exported) functions |
Not sure if we should care, but with 9ab815d, julia> data = [[Complex(row * i, col * i + 1) for i = 1 : 2] for row = 1 : 3, col = 1 : 3]
3×3 Array{Array{Complex{Int64},1},2}:
[1+2im, 2+3im] [1+3im, 2+5im] [1+4im, 2+7im]
[2+2im, 4+3im] [2+3im, 4+5im] [2+4im, 4+7im]
[3+2im, 6+3im] [3+3im, 6+5im] [3+4im, 6+7im]
julia> Symmetric(data)
ERROR: MethodError: no method matching symmetric_type(::Type{Array{Complex{Int64},1}})
Closest candidates are:
symmetric_type(::Type{T<:AbstractArray{S,2}}) where {S, T<:AbstractArray{S,2}} at /Users/twan/code/julia/julia/usr/share/julia/site/v0.7/LinearAlgebra/src/symmetric.jl:51
symmetric_type(::Type{T<:Number}) where T<:Number at /Users/twan/code/julia/julia/usr/share/julia/site/v0.7/LinearAlgebra/src/symmetric.jl:53
Stacktrace:
[1] symmetric_type(::Type{Array{Array{Complex{Int64},1},2}}) at /Users/twan/code/julia/julia/usr/share/julia/site/v0.7/LinearAlgebra/src/symmetric.jl:51
[2] Type at /Users/twan/code/julia/julia/usr/share/julia/site/v0.7/LinearAlgebra/src/symmetric.jl:45 [inlined]
[3] Symmetric(::Array{Array{Complex{Int64},1},2}) at /Users/twan/code/julia/julia/usr/share/julia/site/v0.7/LinearAlgebra/src/symmetric.jl:44
[4] top-level scope
julia> Hermitian(data)
ERROR: MethodError: no method matching hermitian_type(::Type{Array{Complex{Int64},1}})
Closest candidates are:
hermitian_type(::Type{T<:AbstractArray{S,2}}) where {S, T<:AbstractArray{S,2}} at /Users/twan/code/julia/julia/usr/share/julia/site/v0.7/LinearAlgebra/src/symmetric.jl:102
hermitian_type(::Type{T<:Number}) where T<:Number at /Users/twan/code/julia/julia/usr/share/julia/site/v0.7/LinearAlgebra/src/symmetric.jl:104
Stacktrace:
[1] hermitian_type(::Type{Array{Array{Complex{Int64},1},2}}) at /Users/twan/code/julia/julia/usr/share/julia/site/v0.7/LinearAlgebra/src/symmetric.jl:102
[2] Type at /Users/twan/code/julia/julia/usr/share/julia/site/v0.7/LinearAlgebra/src/symmetric.jl:96 [inlined]
[3] Hermitian(::Array{Array{Complex{Int64},1},2}) at /Users/twan/code/julia/julia/usr/share/julia/site/v0.7/LinearAlgebra/src/symmetric.jl:95
[4] top-level scope |
Does it make sense to talk about a symmetric matrix of vectors? You also used that example in JuliaLang/LinearAlgebra.jl#497 so is it a real use case? |
What should be on the diagonal in that case? What would be a reasonable transformation to turn |
I don't have a use case for that, no. It's not completely inconceivable that someone would want it, but I'm fine with not supporting it, as long as it's thought through and documented. Here's a more realistic case though: as it stands, wrapping a matrix of e.g. Should that be handled by fallbacks for In the former case, (basically widening the |
@@ -162,11 +181,17 @@ end | |||
similar(A::Union{Symmetric,Hermitian}, ::Type{T}, dims::Dims{N}) where {T,N} = similar(parent(A), T, dims) | |||
|
|||
# Conversion | |||
Matrix(A::Symmetric) = copytri!(convert(Matrix, copy(A.data)), A.uplo) | |||
function Matrix(A::Symmetric) | |||
B = copytri!(convert(Matrix, copy(A.data)), A.uplo) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can convert(Matrix, copy(A.data))
be simplified to Matrix(A.data)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That sounds plausible, but it was like that before, so I don't know what considerations led to the copy
+convert
construct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically (i.e. not speaking to broader design questions) this looks great! Thanks @martinholters! :)
I think it is fair to require that if a new type doesn't want to be a subtype of, say, |
symmetric(A::T, ::Symbol) where {T} = convert(T, 0.5*(A + transpose(A)))
symmetric_type(::Type{T}) where {T} = T might make a reasonable fallback. (Similar for But more generally, I'm doubtful whether we should do this. What we get is that instead of S::Symmetric == permutedims(S)
H::Hermitian == conj(permutedims(H)) we get S::Symmetric == transpose(S)
H::Hermitian == adjoint(H) Now at first glance, it looks like a no-brainer we want this. But it comes at the expense of additional complexity. Further, if we go with this change, we have nothing to express the former behavior. So before I try to wrap this up by adding docs---are we sure we prefer the latter behavior? Is that more useful in practice? Is it worth it? |
I don't have a strong opinion either way. The original issue didn't arise from a practical use case on my end, but from a perceived discrepancy between |
@martinholters I'm pretty sure I prefer the latter version. |
Yes, the latter looks correct to me. |
9ab815d
to
04ea22e
Compare
Ok, I've added docstrings which I hope are enough documentation here. The need to create a |
Should this be nodded off by someone else or is good to go? |
Testing the waters here. I have no clue what a
Symmetric(::Matrix{Matrix})
might be used for in practice, but I'd say the additional tests look like we'd want them to pass. The alternative would be to makea::Symmetric
obeya == permutedims(a)
anda::Hermitian
obeya == conj(permutedims(a))
, where especially the latter looks extremely artificial.(I admit to having used
promote_op
here but that's whatTranspose
/Adjoint
do and I'm afraid I have to be consistent with those...)Fixes JuliaLang/LinearAlgebra.jl#497.
Note: This is on top of #25687 because it triggered the bug fixed therein.