Add a few missing methods for AbstractJuMPScalar to support e.g. Distances.jl#3585
Conversation
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## master #3585 +/- ##
=======================================
Coverage 98.39% 98.39%
=======================================
Files 38 38
Lines 5655 5664 +9
=======================================
+ Hits 5564 5573 +9
Misses 91 91 ☔ View full report in Codecov by Sentry. |
odow
left a comment
There was a problem hiding this comment.
I would just add tests for the new methods that you've added. I don't think we should add Distances.jl as a test dependency just for this.
Aha, doing that now... |
|
This is another example where Julia's lack of a formal interface specification hurts us. It's really hard to know what third-party packages expect from the implementer of a set of methods. |
|
FWIW, i'm a bit surprised |
10812cc to
5fcaeee
Compare
``` using JuMP, Ipopt, Distances model = Model(Ipopt.Optimizer) @variable(model, a) @variable(model, b) euclidean(a, b) ``` Results in: ``` ERROR: MethodError: no method matching length(::VariableRef) Closest candidates are: length(::Union{Base.KeySet, Base.ValueIterator}) @ Base abstractdict.jl:58 length(::Union{LinearAlgebra.Adjoint{T, S}, LinearAlgebra.Transpose{T, S}} where {T, S}) @ LinearAlgebra /builddirs/julia/usr/share/julia/stdlib/v1.9/LinearAlgebra/src/adjtrans.jl:295 length(::Union{SparseArrays.FixedSparseVector{Tv, Ti}, SparseArrays.SparseVector{Tv, Ti}} where {Tv, Ti}) @ SparseArrays /builddirs/julia/usr/share/julia/stdlib/v1.9/SparseArrays/src/sparsevector.jl:95 ... ```
```
julia> euclidean(a, b)
ERROR: MethodError: no method matching oneunit(::Type{Any})
Closest candidates are:
oneunit(::Type{Union{Missing, T}}) where T
@ Base missing.jl:105
oneunit(::Type{T}) where T
@ Base number.jl:370
oneunit(::T) where T
@ Base number.jl:369
...
Stacktrace:
[1] oneunit(#unused#::Type{Any})
@ Base ./missing.jl:106
[2] _eval_start(d::Euclidean, #unused#::Type{Any}, #unused#::Type{Any}, #unused#::Nothing)
@ Distances ~/.julia/packages/Distances/PvoXa/src/metrics.jl:320
[3] _eval_start(d::Euclidean, #unused#::Type{Any}, #unused#::Type{Any})
@ Distances ~/.julia/packages/Distances/PvoXa/src/metrics.jl:318
[4] eval_start(d::Euclidean, a::VariableRef, b::VariableRef)
@ Distances ~/.julia/packages/Distances/PvoXa/src/metrics.jl:317
[5] _evaluate(d::Euclidean, a::VariableRef, b::VariableRef, #unused#::Nothing)
@ Distances ~/.julia/packages/Distances/PvoXa/src/metrics.jl:236
[6] (::Euclidean)(a::VariableRef, b::VariableRef)
@ Distances ~/.julia/packages/Distances/PvoXa/src/metrics.jl:328
[7] top-level scope
@ REPL[10]:1
```
5fcaeee to
53f3e38
Compare
Because they can be things other than In general, there is some need in Julia for As a general comment, the composability of Julia is beneficial, but it often leads to more problems that it is worth. JuMP has been very conservative in how we interact with other Julia packages. When we find things that are broken, we'll generally improve the error message rather than enable new features. |
53f3e38 to
837549c
Compare
```
julia> euclidean(a, b)
ERROR: MethodError: no method matching VariableRef(::AffExpr)
Closest candidates are:
(::Type{GenericVariableRef{T}} where T)(::Any, ::Any)
@ JuMP ~/.julia/packages/JuMP/h0lrf/src/variables.jl:251
GenericVariableRef{T}(::ConstraintRef{GenericModel{T}, <:MathOptInterface.ConstraintIndex{MathOptInterface.VariableIndex}}) where T
@ JuMP ~/.julia/packages/JuMP/h0lrf/src/variables.jl:520
GenericVariableRef{T}(::GenericModel{T}) where T
@ JuMP ~/.julia/packages/JuMP/h0lrf/src/variables.jl:494
Stacktrace:
[1] oneunit(#unused#::Type{VariableRef})
@ Base ./number.jl:370
[2] _eval_start(d::Euclidean, #unused#::Type{VariableRef}, #unused#::Type{VariableRef}, #unused#::Nothing)
@ Distances ~/.julia/packages/Distances/PvoXa/src/metrics.jl:320
[3] _eval_start(d::Euclidean, #unused#::Type{VariableRef}, #unused#::Type{VariableRef})
@ Distances ~/.julia/packages/Distances/PvoXa/src/metrics.jl:318
[4] eval_start(d::Euclidean, a::VariableRef, b::VariableRef)
@ Distances ~/.julia/packages/Distances/PvoXa/src/metrics.jl:317
[5] _evaluate(d::Euclidean, a::VariableRef, b::VariableRef, #unused#::Nothing)
@ Distances ~/.julia/packages/Distances/PvoXa/src/metrics.jl:236
[6] (::Euclidean)(a::VariableRef, b::VariableRef)
@ Distances ~/.julia/packages/Distances/PvoXa/src/metrics.jl:328
[7] top-level scope
@ REPL[8]:1
```
837549c to
61ef5f5
Compare
|
@odow thank you! |
Right, but so what, so can the
Ah, i guess i see what the bigger picture/problem is now:
But i'm rather disappointed that JuMP has to invent it's own |
Yes 😄. As a general comment, we've put a lot of thought into the design of JuMP. If something seems odd, it's probably that way for a reason.
Development of JuMP started in 2013, long before Symbolics existed. When we added |
Currently,
AbstractJuMPScalaris somewhat partial, and e.g.Distances.jldoes not want to play along:Results in:
That is solved by providing
Base.length(::AbstractJuMPScalar) = 1But fixing that we hit yet another issue,
with a call to
oneunit()with bogusType{Any}.There, the fix appears to be providing
Base.IteratorEltype()/Base.eltype(),this looks a bit weird to me, but seems symmetrical with e.g.
Int32?And then we finally hit the missing
Base.oneunit(a::GenericAffExpr),and since i don't believe there are any types here,
it's identical to the
one().Afterwards, we finally get the right answer:
Thanks!