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

Add a few missing methods for AbstractJuMPScalar to support e.g. Distances.jl #3585

Merged
merged 4 commits into from
Nov 27, 2023

Conversation

LebedevRI
Copy link
Contributor

@LebedevRI LebedevRI commented Nov 26, 2023

Currently, AbstractJuMPScalar is somewhat partial, and e.g. Distances.jl does not want to play along:

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
  ...

That is solved by providing Base.length(::AbstractJuMPScalar) = 1

But fixing that we hit yet another issue,
with a call to oneunit() with bogus Type{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:

julia> euclidean(a, b)
sqrt(a² - 2 b*a + b²)

Thanks!

Copy link

codecov bot commented Nov 26, 2023

Codecov Report

All modified and coverable lines are covered by tests ✅

Comparison is base (94ba2dd) 98.39% compared to head (0405c3c) 98.39%.
Report is 1 commits behind head on master.

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.
📢 Have feedback on the report? Share it here.

src/aff_expr.jl Show resolved Hide resolved
src/variables.jl Show resolved Hide resolved
Copy link
Member

@odow odow left a comment

Choose a reason for hiding this comment

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

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.

@LebedevRI
Copy link
Contributor Author

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...

@odow
Copy link
Member

odow commented Nov 26, 2023

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.

@LebedevRI
Copy link
Contributor Author

FWIW, i'm a bit surprised AbstractJuMPScalar does not derive from Real.
I will be really surprised if i manage to convince Measurements.jl to support this :(
(JuliaPhysics/Measurements.jl#142 / JuliaPhysics/Measurements.jl#144)

```
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

```
@odow
Copy link
Member

odow commented Nov 26, 2023

FWIW, i'm a bit surprised AbstractJuMPScalar does not derive from Real.

Because they can be things other than Real, like complex-valued expressions.

In general, there is some need in Julia for struct Foo{T} <: T, but this isn't supported, so we cannot make a lot of things work (like your Measurements stuff).

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.

```
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

```
test/test_variable.jl Outdated Show resolved Hide resolved
test/test_variable.jl Outdated Show resolved Hide resolved
test/test_variable.jl Outdated Show resolved Hide resolved
test/test_variable.jl Outdated Show resolved Hide resolved
@odow odow merged commit f7fb42b into jump-dev:master Nov 27, 2023
10 checks passed
@LebedevRI LebedevRI deleted the distances.jl branch November 27, 2023 15:22
@LebedevRI
Copy link
Contributor Author

@odow thank you!

@LebedevRI
Copy link
Contributor Author

FWIW, i'm a bit surprised AbstractJuMPScalar does not derive from Real.

Because they can be things other than Real, like complex-valued expressions.

Right, but so what, so can the Symbolic.Num-based symbolic variables.
It's not like the code that receives symbolic variable
can detect/decide if it should handle it as Complex or not.

In general, there is some need in Julia for struct Foo{T} <: T, but this isn't supported, so we cannot make a lot of things work (like your Measurements stuff).

Ah, i guess i see what the bigger picture/problem is now:
AbstractJuMPScalar is derived from MutableArithmetics.AbstractMutable,
which would need said struct Foo{T} <: T.

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.

But i'm rather disappointed that JuMP has to invent it's own Symbolics.jl instead using the existing one.
Just 2c.

@odow
Copy link
Member

odow commented Dec 11, 2023

Ah, i guess i see what the bigger picture/problem is now

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.

But i'm rather disappointed that JuMP has to invent its own Symbolics.jl instead using the existing one.

Development of JuMP started in 2013, long before Symbolics existed. When we added GennericNonlinearExpr, we looked into Symbolics. Ultimately, we decided that Symbolics was not the right choice for JuMP. See #3106 for a slightly long conversation...

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

Successfully merging this pull request may close these issues.

2 participants