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

Define copy!, setindex!, and arithmetic for Symmetric and Hermitian #19228

Merged
merged 4 commits into from
Nov 10, 2016

Conversation

ararslan
Copy link
Member

@ararslan ararslan commented Nov 5, 2016

Fixes #19225

This implements copy! for Symmetric and Hermitian types, as well as + and - for Symmetric and Hermitian with UniformScaling.

@@ -133,6 +133,8 @@ convert{T}(::Type{AbstractMatrix{T}}, A::Hermitian) = Hermitian(convert(Abstract

copy{T,S}(A::Symmetric{T,S}) = (B = copy(A.data); Symmetric{T,typeof(B)}(B,A.uplo))
copy{T,S}(A::Hermitian{T,S}) = (B = copy(A.data); Hermitian{T,typeof(B)}(B,A.uplo))
copy!(dest::Symmetric, src::Symmetric) = copy!(dest.data, src.data)
copy!(dest::Hermitian, src::Hermitian) = copy!(dest.data, src.data)
Copy link
Contributor

Choose a reason for hiding this comment

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

this is not correct if uplo differs

Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch! Will fix, thanks!

Copy link
Contributor

Choose a reason for hiding this comment

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

should write a test too that would have caught that

@@ -56,6 +56,10 @@ ishermitian(J::UniformScaling) = isreal(J.λ)
(-)(B::BitArray{2}, J::UniformScaling) = Array(B) - J
(-)(J::UniformScaling, B::BitArray{2}) = J - Array(B)

for t in (:Symmetric, :Hermitian), op in (:+, :-)
@eval ($op)(X::$t, J::UniformScaling) = ($t)(($op)(X.data, J), Symbol(X.uplo))
Copy link
Contributor

Choose a reason for hiding this comment

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

should test that Hermitian plus a complex UniformScaling with nonzero imag component currently should error

Copy link
Member Author

Choose a reason for hiding this comment

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

Done. Thanks!

@ararslan
Copy link
Member Author

ararslan commented Nov 5, 2016

While I'm at it, would it make sense to implement other operators on Symmetric/Hermitian with UniformScaling, e.g. * and /? If so, which operators?

@stevengj
Copy link
Member

stevengj commented Nov 5, 2016

* and / are not nearly so useful, because you can just multiply or divide by a scalar instead.

@ararslan
Copy link
Member Author

ararslan commented Nov 5, 2016

It appears that operations on Symmetric and Hermitian with scalars similarly fail. I could define those as well...

@stevengj
Copy link
Member

stevengj commented Nov 5, 2016

That sounds useful.

@ararslan
Copy link
Member Author

ararslan commented Nov 5, 2016

Okay, can do.

@@ -133,6 +133,8 @@ convert{T}(::Type{AbstractMatrix{T}}, A::Hermitian) = Hermitian(convert(Abstract

copy{T,S}(A::Symmetric{T,S}) = (B = copy(A.data); Symmetric{T,typeof(B)}(B,A.uplo))
copy{T,S}(A::Hermitian{T,S}) = (B = copy(A.data); Hermitian{T,typeof(B)}(B,A.uplo))
copy!(dest::Symmetric, src::Symmetric) = copy!(dest.data, src.data)
Copy link
Member

Choose a reason for hiding this comment

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

The copy method should return the wrapped version, i.e. something like (copy!(dest.data, src.data);dest)

@@ -56,6 +56,10 @@ ishermitian(J::UniformScaling) = isreal(J.λ)
(-)(B::BitArray{2}, J::UniformScaling) = Array(B) - J
(-)(J::UniformScaling, B::BitArray{2}) = J - Array(B)

for t in (:Symmetric, :Hermitian), op in (:+, :-)
@eval ($op)(X::$t, J::UniformScaling) = ($t)(($op)(X.data, J), Symbol(X.uplo))
Copy link
Member

Choose a reason for hiding this comment

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

Did you try to define a setindex! method for Symmetric first? I think it might be sufficient.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep, that seems to have done the trick. I was able to remove all of the special methods for Symmetric and Hermitian with UniformScaling. Thanks!

@tkelman
Copy link
Contributor

tkelman commented Nov 6, 2016

Symmetric is a view wrapper type though, the inactive triangle should never be read from so its contents don't need to be symmetric

@@ -107,6 +107,24 @@ end
r
end

function setindex!(A::Symmetric, v, i::Integer, j::Integer)
if (A.uplo == 'U' && i >= j) || (A.uplo == 'L' && i <= j)
Copy link
Member

Choose a reason for hiding this comment

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

I don't think this is right. It should only be allowed to modify the diagonal. Modifying off-diagonals will effectively change two values at a time so I think it should be an error.

Copy link
Member Author

Choose a reason for hiding this comment

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

That makes sense. Then I'm back to defining specific methods for things, since only being able to set the diagonal doesn't allow more general operations, but maybe that's fine.

@ararslan
Copy link
Member Author

ararslan commented Nov 7, 2016

Okay, I've improved my initial implementation of copy!, restricted setindex! to the diagonals only, defined +, -, *, and / for Symmetric and Hermitian with scalars, and added more tests. The tests pass locally, so hopefully CI likes it too.

Thanks so much for your help, everyone!

@@ -218,6 +248,10 @@ A_mul_B!{T<:BlasComplex,S<:StridedMatrix}(C::StridedMatrix{T}, A::StridedMatrix{
*(A::HermOrSym, B::HermOrSym) = full(A)*full(B)
*(A::StridedMatrix, B::HermOrSym) = A*full(B)

for T in (:Symmetric, :Hermitian), op in (:+, :-, :*, :/)
@eval ($op)(A::$T, x::Number) = ($T)(($op)(A.data, x), Symbol(A.uplo))
Copy link
Member

Choose a reason for hiding this comment

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

The Hermitian operations should probably be restricted to operations with x::Real.

Copy link
Member Author

Choose a reason for hiding this comment

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

I had thought about that, but there are cases where operating with a complex value could still produce a real diagonal. But restricting to Real does seem the safer option.


function setindex!(A::Hermitian, v, i::Integer, j::Integer)
i == j || throw(ArgumentError("Cannot set a non-diagonal index in a Hermitian matrix"))
isreal(v) || throw(ArgumentError("Cannot set a nonreal value to the diagonal in a Hermitian matrix"))
Copy link
Contributor

Choose a reason for hiding this comment

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

little backwards? cannot set a diagonal entry to a non-real value?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good call. Fixed. Thanks!

@ararslan
Copy link
Member Author

ararslan commented Nov 7, 2016

The CI failure appears to be unrelated. (One of the tests in the ambiguity test set is returning a non-boolean value.)

@tkelman
Copy link
Contributor

tkelman commented Nov 7, 2016

That's a messy hack so when that test fails it tells you what the ambiguities were. But it's related.

@ararslan
Copy link
Member Author

ararslan commented Nov 7, 2016

Ohhhh okay, so the Tuple{Method,Method} thing in the log is the list of ambiguities. I guess it's angry about my + and - definitions being ambiguous with AbstractArray{Bool,N<:Any}. Hmm.

@ararslan
Copy link
Member Author

ararslan commented Nov 7, 2016

Any idea what I could do to ameliorate that? As long as Bool <: Number the solution is not clear to me.

@tkelman
Copy link
Contributor

tkelman commented Nov 7, 2016

Probably have to define some ambiguity-resolving methods for the Bool case where the existing method and yours intersect?

@ararslan
Copy link
Member Author

ararslan commented Nov 7, 2016

That seems to have done the trick. Thanks so much, @tkelman!

@ararslan
Copy link
Member Author

ararslan commented Nov 7, 2016

Looking better now, @andreasnoack?

@ararslan ararslan changed the title Define copy! for Symmetric and Hermitian Define copy!, setindex!, and arithmetic for Symmetric and Hermitian Nov 7, 2016
@@ -250,3 +250,36 @@ let a = randn(2,2)
cc = copy(c)
@test conj!(c) == conj(Array(c))
end

# 19225
let X = sparse([1 -1; -1 1])
Copy link
Member

Choose a reason for hiding this comment

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

I think these should to into the sparse tests for modularity. However, you might want to test the functionality with dense arrays as well.

Copy link
Member Author

Choose a reason for hiding this comment

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

I removed the sparse wrapper for these tests, then copied a simplified version of what's currently here to the sparse tests. Let me know if the format looks okay.

Copy link
Member

@andreasnoack andreasnoack left a comment

Choose a reason for hiding this comment

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

See the comment at line 255

@ararslan
Copy link
Member Author

ararslan commented Nov 8, 2016

All good now?

@ararslan
Copy link
Member Author

Bump. @andreasnoack

@andreasnoack andreasnoack merged commit 1c12e9e into JuliaLang:master Nov 10, 2016
@ararslan ararslan deleted the aa/copy-symmetric branch November 10, 2016 18:47
fcard pushed a commit to fcard/julia that referenced this pull request Feb 28, 2017
…uliaLang#19228)

* Define copy! and setindex! for Symmetric and Hermitian

* Define arithmetic operations on Symmetric/Hermitian with Number

* Add a test for the ambiguous case

* Separate sparse and dense tests
@@ -107,6 +107,21 @@ end
r
end

function setindex!(A::Symmetric, v, i::Integer, j::Integer)
i == j || throw(ArgumentError("Cannot set a non-diagonal index in a symmetric matrix"))
Copy link
Sponsor Member

Choose a reason for hiding this comment

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

This change doesn't make sense to me. Why shouldn't you be able to set an off diagonal element?

Copy link
Member

Choose a reason for hiding this comment

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

Then it wouldn't be symmetric anymore.

Copy link
Sponsor Member

@KristofferC KristofferC Apr 7, 2017

Choose a reason for hiding this comment

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

This is a symmetric wrapper so it is implicitly symmetric no matter what you do with it. Setting A[i,j] = v is the same as A[i,j] = A[j,i] = v.

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 problem then is that the behavior of setindex! differs from everywhere else in that it sets multiple values at once.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah. We discussed that and it was considered too surprising. Are you considering a generic case where modifying the underlying buffer would be inconvenient?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure what you mean by the right and wrong sides of a diagonal. Doesn't that only apply to triangular matrices?

Copy link
Sponsor Member

Choose a reason for hiding this comment

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

I mean the .uplo field.

Copy link
Member Author

Choose a reason for hiding this comment

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

Only being able to assign to the upper or lower triangle depending on the value of uplo also breaks the abstraction; the user shouldn't have to care about which triangle is stored since all they want to know about it is that it's symmetric.

Copy link
Contributor

Choose a reason for hiding this comment

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

we could introduce a symset! function, or something similar, that generically changes values in a way that maintains symmetry.

Copy link
Member

Choose a reason for hiding this comment

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

The usefulness of a symset! not withstanding, we could also think about permitting setindex! operations that maintain symmetry. E.g. A[:,:] = 0 might be expected to work. Also A[[CartesianIndex(1,2), CartesianIndex(2,1)]] = 1, although symset! would of course be nicer for this case.

@fredrikekre
Copy link
Member

Just leaving a note here that this broke the following:

julia> A = rand(2, 2); S = Symmetric(A); D = Diagonal(rand(2));

julia> S*D
ERROR: ArgumentError: Cannot set a non-diagonal index in a symmetric matrix
Stacktrace:
 [1] setindex!(::Symmetric{Float64,Array{Float64,2}}, ::Float64, ::Int64, ::Int64) at ./linalg/symmetric.jl:126
 [2] scale!(::Symmetric{Float64,Array{Float64,2}}, ::Symmetric{Float64,Array{Float64,2}}, ::Array{Float64,1}) at ./linalg/matmul.jl:19
 [3] *(::Symmetric{Float64,Array{Float64,2}}, ::Diagonal{Float64}) at ./linalg/diagonal.jl:152

julia> D*S
ERROR: ArgumentError: Cannot set a non-diagonal index in a symmetric matrix
Stacktrace:
 [1] setindex!(::Symmetric{Float64,Array{Float64,2}}, ::Float64, ::Int64, ::Int64) at ./linalg/symmetric.jl:126
 [2] scale!(::Symmetric{Float64,Array{Float64,2}}, ::Array{Float64,1}, ::Symmetric{Float64,Array{Float64,2}}) at ./linalg/matmul.jl:34
 [3] *(::Diagonal{Float64}, ::Symmetric{Float64,Array{Float64,2}}) at ./linalg/diagonal.jl:154

which I stumbled upon trying to add tests for #22396

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants