-
Notifications
You must be signed in to change notification settings - Fork 11
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
Use 3-arg mul! for Companion and improve test coverage #69
Conversation
Pull Request Test Coverage Report for Build 3377679255Warning: This coverage report may be inaccurate.This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.
Details
💛 - Coveralls |
Codecov ReportBase: 93.48% // Head: 94.02% // Increases project coverage by
Additional details and impacted files@@ Coverage Diff @@
## master #69 +/- ##
==========================================
+ Coverage 93.48% 94.02% +0.54%
==========================================
Files 8 8
Lines 215 201 -14
==========================================
- Hits 201 189 -12
+ Misses 14 12 -2
Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. ☔ View full report at Codecov. |
@ivanslapnicar this applied the 3-arg @mkitti you were the last to edit the companion.jl files. Any comments here? |
# getindex(C::Companion, i, j) = getindex(Matrix(C), i, j) | ||
# isassigned(C::Companion, i, j) = isassigned(Matrix(C), i, j) | ||
getindex(C::Companion{T}, i::Int, j::Int) where T = (j==length(C.c)) ? -C.c[i] : (i==j+1 ? one(T) : zero(T) ) | ||
isassigned(C::Companion, i::Int, j::Int) = (j==length(C.c)) ? isassigned(C.c,i) : true |
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.
Why is isassigned
being removed 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.
HI @mkitti, thank for the very thorough review. I will address (or commit) your comments one-by-one, which might take a while, and I will ping you again when it is ready for another review.
This repo goes back to early versions of Julia and many things in the original version are no longer needed in current Julia versions. Now Base
has default fallbacks for isassigned
for any AbstractArray
types so there is no need for this function here. To double check that that fallback is working correctly, I have added tests of this form:
@test isassigned(Z, 1)
@test isassigned(Z, n^2)
@test !isassigned(Z, 0)
@test !isassigned(Z, n^2+1)
Now, these are all standard uses of isassigned
with a single index argument per the manual. I have never seen a 2-index use of isassigned
before, and it was never tested in this package. If there is a use case for it, then it's fine to restore it, but then it should have a docstring and jldoctests that explains what it does.
Note that SpecialMatrices.jl
does an import
of Base.isassigned
and this is the only one of the special matrices that extends this method, so it really looked like a bug (or historical baggage) to be importing the 1-index version and then defining a 2-index version. Again, happy to restore it if there is a reason to have this non-standard method...
Update: I accidentally pushed those tests to master, which is embarrassing. I thought I had set up the settings here to prevent direct pushes to master without a PR. I will have to look into that. I have reverted that in master and will push them here now. Sigh.
Actually the settings do require a PR before merging so now I am baffled:
I know it is off topic but if you are a Julia wizard who knows how to make sure all merges come from a PR please let me know, so that no one including myself can make that mistake...
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.
Here is the fallback function. While technically this works, I think we may be able to do this more efficiently.
julia> @which isassigned(Companion(1:3),1,1)
isassigned(a::AbstractArray, i::Integer...) in Base at abstractarray.jl:563
function isassigned(a::AbstractArray, i::Integer...)
try
a[i...]
true
catch e
if isa(e, BoundsError) || isa(e, UndefRefError)
return false
else
rethrow()
end
end
end
That said that previous definition does seem problematic.
julia> using SpecialMatrices
julia> C = Companion(1:3)
3×3 Companion{Int64}:
0 0 -1
1 0 -2
0 1 -3
julia> isassigned(C, 1, 4)
false
julia> Base.isassigned(C::Companion, i::Int, j::Int) = (j==length(C.c)) ? isassigned(C.c,i) : true
julia> isassigned(C, 1, 4)
true
My question is does anything depend on this unusual behavior? Otherwise, I suggest we consider writing an optimized version of this.
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.
Similarly, using the old definition of getindex
changes the situation as well.
julia> Base.getindex(C::Companion{T}, i::Int, j::Int) where T = (j==length(C.c)) ? -C.c[i] : (i==j+1 ? one(T) : zero(T) )
julia> C[1,4]
0
This is also weird. Just to be clear what we are doing here is materially changing the functionality of Companion
. At the moment this seems odd, but perhaps there is a reason it is like this.
julia> [C[ci] for ci in CartesianIndices((3,10))]
3×10 Matrix{Int64}:
0 0 -1 0 0 0 0 0 0 0
1 0 -2 0 0 0 0 0 0 0
0 1 -3 0 0 0 0 0 0 0
This seems amenable adding additional columns.
src/companion.jl
Outdated
c :: Vector{T} | ||
end | ||
|
||
# Generate companion matrix from a polynomial | ||
Companion(v::AbstractVector) = Companion(collect(v)) |
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.
Why is this new constructor necessary? Julia will automatically perform convert(Vector{T}, v)
for assignment to the field.
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.
Hmm, what version of Julia are you using that does that automatic conversion? I am using 1.8.2, and if I remove this constructor then it throws an error:
julia> Companion(1:3)
ERROR: MethodError: no method matching Companion(::UnitRange{Int64})
Closest candidates are:
Companion(::Polynomial) at ~/.julia/dev/SpecialMatrices/src/companion.jl:37
Companion(::Vector{T}) where T at ~/.julia/dev/SpecialMatrices/src/companion.jl:30
I'm pretty sure we need this.
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 see, it's due to the parameter.
julia> Companion{Int}(1:3)
3×3 Companion{Int64}:
0 0 -1
1 0 -2
0 1 -3
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.
There is a lot going on here and usually my preference would be to separate the stylistic changes from the functional changes. However, let's try to proceed with this.
The biggest issue here is the introduction of AbstractVector
and AbstractMatrix
. For these abstract types, there are much looser guarantees on the interface. For example, they could be an OffsetArray
from OffsetArrays.jl and thus may not use 1-based indexing. You may need to use Base.require_one_based_indexing
. We should also test for the OffsetArray
case.
The function signatures have changed. It may be good to do some analysis if this has any unintended consequences. Otherwise, could you please elucidate the expected call chain or why the removed function signatures are redundant?
size(C::Companion, r::Int) = (r==1 || r==2) ? length(C.c) : | ||
throw(ArgumentError("Companion is of rank 2")) |
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 you prove this is redundant? Have you considered constant propagation and invalidations?
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 have added a test of the form size(C,3) == 1
to verify that the defaults in Base for an AbstractArray do what is needed here. I already had a test of the form size(C,1) == n
.
The "proof" is that the manual for the AA interface specifies only that a single size
function that returns a tuple is needed.
I have no idea about "constant propagation and invalidations." (These are static structs.)
b[1] = y | ||
b | ||
# 3-argument mul! mutates first argument: y <= C * x | ||
function mul!(y::Vector, C::Companion, x::AbstractVector) |
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.
You've dropped the element type parameter here. What are the implications?
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 benefit is now one can multiply C
and x
with different element types, as long as the result vector y
has a suitable eltype. If it does not, then the user will get an error, just like they would with a regular matrix:
using LinearAlgebra: mul!
mul!(ones(Float64, 2,3), ones(Int32, 2, 5), rand(Float16, 5, 3)) # works fine and is convenient for user
mul!(ones(Int64, 2,3), ones(Int32, 2, 5), rand(Float16, 5, 3)) # fails with InexactError, understandably
(Note that base Julia does not fail with a Method error here.)
Co-authored-by: Mark Kittisopikul <[email protected]>
Co-authored-by: Mark Kittisopikul <[email protected]>
Co-authored-by: Mark Kittisopikul <[email protected]>
Co-authored-by: Mark Kittisopikul <[email protected]>
…es.jl into jf/companion
@mkitti this is ready for your re-review now. Thanks! |
@mkitti ok, I added the OffsetArray test. Any remaining concerns? |
Could you test multiplication with an |
Ok, I added a |
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.
Rather than forcing a collect
pass to the parameterized constructor.
src/companion.jl
Outdated
c :: Vector{T} | ||
end | ||
|
||
# Generate companion matrix from a polynomial | ||
Companion(v::AbstractVector) = Companion(collect(v)) |
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 see, it's due to the parameter.
julia> Companion{Int}(1:3)
3×3 Companion{Int64}:
0 0 -1
1 0 -2
0 1 -3
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.
getindex
and isassigned
change in function here.
Let's review the dependencies to see if anyone is depending on the anomalous prior functionality.
This is also technically a breaking change, so we need to make sure to bump the version number accordingly. Could you add a version number bump to 3.0.0 in Project.toml?
# getindex(C::Companion, i, j) = getindex(Matrix(C), i, j) | ||
# isassigned(C::Companion, i, j) = isassigned(Matrix(C), i, j) | ||
getindex(C::Companion{T}, i::Int, j::Int) where T = (j==length(C.c)) ? -C.c[i] : (i==j+1 ? one(T) : zero(T) ) | ||
isassigned(C::Companion, i::Int, j::Int) = (j==length(C.c)) ? isassigned(C.c,i) : true |
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.
Here is the fallback function. While technically this works, I think we may be able to do this more efficiently.
julia> @which isassigned(Companion(1:3),1,1)
isassigned(a::AbstractArray, i::Integer...) in Base at abstractarray.jl:563
function isassigned(a::AbstractArray, i::Integer...)
try
a[i...]
true
catch e
if isa(e, BoundsError) || isa(e, UndefRefError)
return false
else
rethrow()
end
end
end
That said that previous definition does seem problematic.
julia> using SpecialMatrices
julia> C = Companion(1:3)
3×3 Companion{Int64}:
0 0 -1
1 0 -2
0 1 -3
julia> isassigned(C, 1, 4)
false
julia> Base.isassigned(C::Companion, i::Int, j::Int) = (j==length(C.c)) ? isassigned(C.c,i) : true
julia> isassigned(C, 1, 4)
true
My question is does anything depend on this unusual behavior? Otherwise, I suggest we consider writing an optimized version of this.
# getindex(C::Companion, i, j) = getindex(Matrix(C), i, j) | ||
# isassigned(C::Companion, i, j) = isassigned(Matrix(C), i, j) | ||
getindex(C::Companion{T}, i::Int, j::Int) where T = (j==length(C.c)) ? -C.c[i] : (i==j+1 ? one(T) : zero(T) ) | ||
isassigned(C::Companion, i::Int, j::Int) = (j==length(C.c)) ? isassigned(C.c,i) : true |
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.
Similarly, using the old definition of getindex
changes the situation as well.
julia> Base.getindex(C::Companion{T}, i::Int, j::Int) where T = (j==length(C.c)) ? -C.c[i] : (i==j+1 ? one(T) : zero(T) )
julia> C[1,4]
0
This is also weird. Just to be clear what we are doing here is materially changing the functionality of Companion
. At the moment this seems odd, but perhaps there is a reason it is like this.
julia> [C[ci] for ci in CartesianIndices((3,10))]
3×10 Matrix{Int64}:
0 0 -1 0 0 0 0 0 0 0
1 0 -2 0 0 0 0 0 0 0
0 1 -3 0 0 0 0 0 0 0
This seems amenable adding additional columns.
According to https://juliahub.com/ui/Packages/SpecialMatrices/F3uIk/2.0.1?page=2 there are only two dependent packages at the moment: Neither seems to use |
Co-authored-by: Mark Kittisopikul <[email protected]>
I committed your suggestion to use built-in |
In generally I would agree with this approach and if you want to write and run benchmarks for this then please do. My sense is that companion matrices are mainly used academically for quite small sizes to illustrate points about characteristic polynomials, so I'd prefer to put my own energy into writing and testing benchmarks in other places that are more performance critical. |
I added the version bump as suggested. I prefer to wait to release that version until the similarly breaking changes in #17 are reviewed and merged. If you would be willing to look at that one it would be great :) |
I would prefer to keep the |
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.
Keep the convert based mechanism, change the test.
The reason for this is that the |
Co-authored-by: Mark Kittisopikul <[email protected]>
Co-authored-by: Mark Kittisopikul <[email protected]>
The changes are fine with me. I don't see what makes |
When is convert called?
|
I see now - thanks! So is this PR fine now, or do you plan to explore the efficiency issue further first or propose more changes? |
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.
Approved
Thanks so much - I really appreciate the constructive dialog and improvements to the code. |
This is a breaking change because
mul!
now is the 3-arg version, just like #68.It also adds tests to get complete coverage.