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

Multi-objective optimization #294

Closed
heileman opened this issue Feb 20, 2020 · 11 comments · Fixed by #295
Closed

Multi-objective optimization #294

heileman opened this issue Feb 20, 2020 · 11 comments · Fixed by #295

Comments

@heileman
Copy link

I believe I'm close to having a working example of multi-objective optimization using JuMP/Gurobi, but I'm getting error when using JuMP.optimize!(), and am wondering if this is a JuMP bug? It seems related to issue #291.

Here's the code, based on suggestions in issue #264:

# Make Gurobi-specific features related to multi-objective optimization accessible through JuMP.
using JuMP, Gurobi
const MOI = JuMP.MathOptInterface

struct NumberOfObjective <: MOI.AbstractModelAttribute
end

function MOI.set(model::Gurobi.Optimizer, ::NumberOfObjective, n::Integer)
    Gurobi.set_intattr!(model, "NumObj", n)
end

function MOI.get(model::Gurobi.Optimizer, ::NumberOfObjective)
    Gurobi.get_intattr!(model, "NumObj")
end

struct MultiObjectiveFunction <: MOI.AbstractModelAttribute
    id::Int
end

function MOI.set(model::Gurobi.Optimizer, attr::MultiObjectiveFunction, f::MOI.ScalarAffineFunction)
    num_vars = length(model.variable_info)
    obj = zeros(Float64, num_vars)
    for term in f.terms
        column = Gurobi._info(model, term.variable_index).column
        obj[column] += term.coefficient
    end
    # This update is needed because we might have added some variables.
    Gurobi._update_if_necessary(model)
    i0 = Gurobi.get_int_param(model.inner, "ObjNumber")
    Gurobi.set_int_param!(model.inner, "ObjNumber", attr.id - 1)
    Gurobi.set_dblattrarray!(model.inner, "ObjN", 1, num_vars, obj)
    Gurobi.set_dblattr!(model.inner, "ObjNCon", f.constant)
    Gurobi.set_int_param!(model.inner, "ObjNumber", i0)
    Gurobi._require_update(model)
end

struct MultiObjectivePriority <: MOI.AbstractModelAttribute
    id::Int
end

function MOI.set(model::Gurobi.Optimizer, attr::MultiObjectivePriority, priority::Int)
    i0 = Gurobi.get_int_param(model.inner, "ObjNumber")
    Gurobi.set_int_param!(model.inner, "ObjNumber", attr.id - 1)
    Gurobi.set_intattr!(model.inner, "ObjNPriority", priority)
    Gurobi.set_int_param!(model.inner, "ObjNumber", i0)
end

struct MultiObjectiveWeight <: MOI.AbstractModelAttribute
    id::Int
end

function MOI.set(model::Gurobi.Optimizer, attr::MultiObjectiveWeight, weight::Float64)
    i0 = Gurobi.get_int_param(model.inner, "ObjNumber")
    Gurobi.set_int_param!(model.inner, "ObjNumber", attr.id - 1)
    Gurobi.set_dblattr!(model.inner, "ObjNWeight", weight)
    Gurobi.set_int_param!(model.inner, "ObjNumber", i0)
end

Example using above code w/ JuMP:

using JuMP
model = Model(with_optimizer(Gurobi.Optimizer, OutputFlag=0))
@variable(model, x)
@variable(model, y)
MOI.set(model, NumberOfObjective(), 2)
MOI.set(model, MultiObjectiveWeight(0), 3.0)
MOI.set(model, MultiObjectiveWeight(1), 2.0)
MOI.set(model, MultiObjectiveFunction(0), moi_function(x + y))
MOI.set(model, MultiObjectiveFunction(1), moi_function(2x - y))
JuMP.optimize!(model)

Error output:

julia> JuMP.optimize!(model)
ERROR: MethodError: no method matching Gurobi.Env(::Ptr{Nothing})
Closest candidates are:
  Gurobi.Env() at /Users/heileman/.julia/packages/Gurobi/CI8ht/src/grb_env.jl:8
Stacktrace:
 [1] set_int_param!(::Gurobi.Model, ::String, ::Int64) at /Users/heileman/.julia/packages/Gurobi/CI8ht/src/grb_params.jl:275
 [2] setparam!(::Gurobi.Model, ::String, ::Int64) at /Users/heileman/.julia/packages/Gurobi/CI8ht/src/grb_params.jl:322
 [3] set(::Gurobi.Optimizer, ::MathOptInterface.RawParameter, ::Int64) at /Users/heileman/.julia/packages/Gurobi/CI8ht/src/MOI_wrapper.jl:296
 [4] _instantiate_and_check(::MathOptInterface.OptimizerWithAttributes) at /Users/heileman/.julia/packages/MathOptInterface/DmQBj/src/instantiate.jl:77
 [5] #instantiate#35(::Type{Float64}, ::Bool, ::typeof(MathOptInterface.instantiate), ::MathOptInterface.OptimizerWithAttributes) at /Users/heileman/.julia/packages/MathOptInterface/DmQBj/src/instantiate.jl:107
 [6] (::MathOptInterface.var"#kw##instantiate")(::NamedTuple{(:with_bridge_type, :with_names),Tuple{DataType,Bool}}, ::typeof(MathOptInterface.instantiate), ::MathOptInterface.OptimizerWithAttributes) at ./none:0
 [7] #set_optimizer#96(::Bool, ::typeof(set_optimizer), ::Model, ::MathOptInterface.OptimizerWithAttributes) at /Users/heileman/.julia/packages/JuMP/CZ8vV/src/optimizer_interface.jl:66
 [8] #Model#16 at ./none:0 [inlined]
 [9] Model(::MathOptInterface.OptimizerWithAttributes) at /Users/heileman/.julia/packages/JuMP/CZ8vV/src/JuMP.jl:233
 [10] top-level scope at REPL[24]:1
(v1.3) pkg> st
    Status `~/.julia/environments/v1.3/Project.toml`
  [2e9cd046] Gurobi v0.7.5
  [4076af6c] JuMP v0.21.1
$ gurobi_cl --version
Gurobi Optimizer version 9.0.1 build v9.0.1rc0 (mac64)
Copyright (c) 2020, Gurobi Optimization, LLC
@blegat
Copy link
Member

blegat commented Feb 20, 2020

Try the master version of Gurobi.jl with

(v1.3) pkg> add Gurobi#master

to have this fix: #291

@heileman
Copy link
Author

heileman commented Mar 2, 2020

Thanks, this got me a little closer, but I'm still not able to make the JuMP optimize!() call work. Below is my latest code iteration, and the error I'm now receiving. Any help is appreciated.

Using JuMP, Gurobi
const MOI = JuMP.MathOptInterface

struct PrintLevel <: MOI.AbstractOptimizerAttribute end
function MOI.set(model::Gurobi.Optimizer, ::PrintLevel, level::Int)
    # ... set the print level ...
    Gurobi.set_intattr!(model, "PrintLevel", level)
end

struct NumberOfObjective <: MOI.AbstractModelAttribute
end

function MOI.set(model::Gurobi.Optimizer, ::NumberOfObjective, n::Integer)
    Gurobi.set_intattr!(model, "NumObj", n)
end

function MOI.get(model::Gurobi.Optimizer, ::NumberOfObjective)
    Gurobi.get_intattr(model, "NumObj")
end

struct MultiObjectiveFunction <: MOI.AbstractModelAttribute
    id::Int
end

function MOI.set(model::Gurobi.Optimizer, attr::MultiObjectiveFunction, f::MOI.ScalarAffineFunction)
    num_vars = length(model.variable_info)
    obj = zeros(Float64, num_vars)
    for term in f.terms
        column = Gurobi._info(model, term.variable_index).column
        obj[column] += term.coefficient
    end
    # This update is needed because we might have added some variables.
    Gurobi._update_if_necessary(model)
    i0 = Gurobi.get_int_param(model.inner, "ObjNumber")
    Gurobi.set_int_param!(model.inner, "ObjNumber", attr.id)
    Gurobi.set_dblattrarray!(model.inner, "ObjN", 1, num_vars, obj)
    Gurobi.set_dblattr!(model.inner, "ObjNCon", f.constant)
    Gurobi.set_int_param!(model.inner, "ObjNumber", i0)
    Gurobi._require_update(model)
end

struct MultiObjectivePriority <: MOI.AbstractModelAttribute
    id::Int
end

function MOI.set(model::Gurobi.Optimizer, attr::MultiObjectivePriority, priority::Int)
    i0 = Gurobi.get_int_param(model.inner, "ObjNumber")
    temp = Gurobi.get_int_param(model.inner, "ObjNPriority")
    Gurobi.set_int_param!(model.inner, "ObjNumber", attr.id)
    Gurobi.set_intattr!(model.inner, "ObjNPriority", priority)
    temp1 = Gurobi.get_int_param(model.inner, "ObjNumber")
    Gurobi.set_int_param!(model.inner, "ObjNumber", i0)
end

struct MultiObjectiveWeight <: MOI.AbstractModelAttribute
    id::Int
end

function MOI.set(model::Gurobi.Optimizer, attr::MultiObjectiveWeight, weight::Float64)
    i0 = Gurobi.get_int_param(model.inner, "ObjNumber")
    Gurobi.set_int_param!(model.inner, "ObjNumber", attr.id)
    Gurobi.set_dblattr!(model.inner, "ObjNWeight", weight)
    Gurobi.set_int_param!(model.inner, "ObjNumber", i0)
end

Example using above code w/ JuMP:

using JuMP
model = Model(with_optimizer(Gurobi.Optimizer))
@variable(model, x)
@variable(model, y)
MOI.set(model, NumberOfObjective(), 2)
MOI.set(model, MultiObjectivePriority(1), 1)
MOI.set(model, MultiObjectivePriority(2), 2)
#MOI.set(model, MultiObjectiveWeight(0), 1.0)
#MOI.set(model, MultiObjectiveWeight(1), 1.0)
MOI.set(model, MultiObjectiveFunction(1), moi_function(x + y))
MOI.set(model, MultiObjectiveFunction(2), moi_function(2x - y))
JuMP.optimize!(model)

Error output:

JuMP.optimize!(model)
ERROR: MethodError: no method matching set_intattr!(::Gurobi.Optimizer, ::String, ::Int64)
Closest candidates are:
  set_intattr!(::Gurobi.Model, ::String, ::Integer) at /Users/heileman/.julia/packages/Gurobi/EhH9J/src/grb_attrs.jl:222
Stacktrace:
 [1] set(::Gurobi.Optimizer, ::NumberOfObjective, ::Int64) at ./REPL[35]:2
 [2] set(::MathOptInterface.Bridges.LazyBridgeOptimizer{Gurobi.Optimizer}, ::NumberOfObjective, ::Int64) at /Users/heileman/.julia/packages/MathOptInterface/XiH8D/src/Bridges/bridge_optimizer.jl:593
 [3] _pass_attributes(::MathOptInterface.Bridges.LazyBridgeOptimizer{Gurobi.Optimizer}, ::MathOptInterface.Utilities.UniversalFallback{MathOptInterface.Utilities.Model{Float64}}, ::Bool, ::MathOptInterface.Utilities.IndexMap, ::Array{MathOptInterface.AbstractModelAttribute,1}, ::Tuple{}, ::Tuple{}, ::Tuple{}, ::typeof(MathOptInterface.set)) at /Users/heileman/.julia/packages/MathOptInterface/XiH8D/src/Utilities/copy.jl:148
 [4] pass_attributes at /Users/heileman/.julia/packages/MathOptInterface/XiH8D/src/Utilities/copy.jl:112 [inlined]
 [5] pass_attributes at /Users/heileman/.julia/packages/MathOptInterface/XiH8D/src/Utilities/copy.jl:111 [inlined]
 [6] default_copy_to(::MathOptInterface.Bridges.LazyBridgeOptimizer{Gurobi.Optimizer}, ::MathOptInterface.Utilities.UniversalFallback{MathOptInterface.Utilities.Model{Float64}}, ::Bool) at /Users/heileman/.julia/packages/MathOptInterface/XiH8D/src/Utilities/copy.jl:337
 [7] #automatic_copy_to#109 at /Users/heileman/.julia/packages/MathOptInterface/XiH8D/src/Utilities/copy.jl:15 [inlined]
 [8] #automatic_copy_to at ./none:0 [inlined]
 [9] #copy_to#3 at /Users/heileman/.julia/packages/MathOptInterface/XiH8D/src/Bridges/bridge_optimizer.jl:268 [inlined]
 [10] (::MathOptInterface.var"#kw##copy_to")(::NamedTuple{(:copy_names,),Tuple{Bool}}, ::typeof(MathOptInterface.copy_to), ::MathOptInterface.Bridges.LazyBridgeOptimizer{Gurobi.Optimizer}, ::MathOptInterface.Utilities.UniversalFallback{MathOptInterface.Utilities.Model{Float64}}) at ./none:0
 [11] attach_optimizer(::MathOptInterface.Utilities.CachingOptimizer{MathOptInterface.AbstractOptimizer,MathOptInterface.Utilities.UniversalFallback{MathOptInterface.Utilities.Model{Float64}}}) at /Users/heileman/.julia/packages/MathOptInterface/XiH8D/src/Utilities/cachingoptimizer.jl:149
 [12] optimize!(::MathOptInterface.Utilities.CachingOptimizer{MathOptInterface.AbstractOptimizer,MathOptInterface.Utilities.UniversalFallback{MathOptInterface.Utilities.Model{Float64}}}) at /Users/heileman/.julia/packages/MathOptInterface/XiH8D/src/Utilities/cachingoptimizer.jl:185
 [13] #optimize!#78(::Bool, ::Bool, ::Base.Iterators.Pairs{Union{},Union{},Tuple{},NamedTuple{(),Tuple{}}}, ::typeof(optimize!), ::Model, ::Nothing) at /Users/heileman/.julia/packages/JuMP/MsUSY/src/optimizer_interface.jl:141
 [14] optimize! at /Users/heileman/.julia/packages/JuMP/MsUSY/src/optimizer_interface.jl:111 [inlined] (repeats 2 times)
 [15] top-level scope at REPL[131]:1
(v1.3) pkg> st
    Status `~/.julia/environments/v1.3/Project.toml`
  [2e9cd046] Gurobi v0.7.5 #master (https://github.com/JuliaOpt/Gurobi.jl.git)
  [4076af6c] JuMP v0.20.1

@odow
Copy link
Member

odow commented Mar 2, 2020

function MOI.set(model::Gurobi.Optimizer, ::NumberOfObjective, n::Integer)
    Gurobi.set_intattr!(model, "NumObj", n)
end

# needs to be

function MOI.set(model::Gurobi.Optimizer, ::NumberOfObjective, n::Integer)
    Gurobi.set_intattr!(model.inner, "NumObj", n)
end

There are a few other occurrences as well.

@heileman
Copy link
Author

heileman commented Mar 3, 2020

Fixed the "model.inner" issue, and the optimizer now does not recognize that this is a multi-objective problem (error below). I checked the Gurobi NumObj attribute, and it is set properly (i.e., to 2), and according to the Gurobi documentation, this is supposed to enough to make the model multi-objective.

julia> JuMP.optimize!(model)
ERROR: Gurobi.GurobiError(10008, "It isn't a multi-objective model")
Stacktrace:
 [1] get_intattr(::Gurobi.Model, ::String) at /Users/heileman/.julia/packages/Gurobi/EhH9J/src/grb_attrs.jl:16
 [2] set(::Gurobi.Optimizer, ::MultiObjectivePriority, ::Int64) at ./REPL[99]:3
 [3] set(::MathOptInterface.Bridges.LazyBridgeOptimizer{Gurobi.Optimizer}, ::MultiObjectivePriority, ::Int64) at /Users/heileman/.julia/packages/MathOptInterface/C1XBe/src/Bridges/bridge_optimizer.jl:593
 [4] _pass_attributes(::MathOptInterface.Bridges.LazyBridgeOptimizer{Gurobi.Optimizer}, ::MathOptInterface.Utilities.UniversalFallback{MathOptInterface.Utilities.Model{Float64}}, ::Bool, ::MathOptInterface.Utilities.IndexMap, ::Array{MathOptInterface.AbstractModelAttribute,1}, ::Tuple{}, ::Tuple{}, ::Tuple{}, ::typeof(MathOptInterface.set)) at /Users/heileman/.julia/packages/MathOptInterface/C1XBe/src/Utilities/copy.jl:148
 [5] pass_attributes at /Users/heileman/.julia/packages/MathOptInterface/C1XBe/src/Utilities/copy.jl:112 [inlined]
 [6] pass_attributes at /Users/heileman/.julia/packages/MathOptInterface/C1XBe/src/Utilities/copy.jl:111 [inlined]
 [7] default_copy_to(::MathOptInterface.Bridges.LazyBridgeOptimizer{Gurobi.Optimizer}, ::MathOptInterface.Utilities.UniversalFallback{MathOptInterface.Utilities.Model{Float64}}, ::Bool) at /Users/heileman/.julia/packages/MathOptInterface/C1XBe/src/Utilities/copy.jl:337
 [8] #automatic_copy_to#109 at /Users/heileman/.julia/packages/MathOptInterface/C1XBe/src/Utilities/copy.jl:15 [inlined]
 [9] #automatic_copy_to at ./none:0 [inlined]
 [10] #copy_to#3 at /Users/heileman/.julia/packages/MathOptInterface/C1XBe/src/Bridges/bridge_optimizer.jl:268 [inlined]
 [11] (::MathOptInterface.var"#kw##copy_to")(::NamedTuple{(:copy_names,),Tuple{Bool}}, ::typeof(MathOptInterface.copy_to), ::MathOptInterface.Bridges.LazyBridgeOptimizer{Gurobi.Optimizer}, ::MathOptInterface.Utilities.UniversalFallback{MathOptInterface.Utilities.Model{Float64}}) at ./none:0
 [12] attach_optimizer(::MathOptInterface.Utilities.CachingOptimizer{MathOptInterface.AbstractOptimizer,MathOptInterface.Utilities.UniversalFallback{MathOptInterface.Utilities.Model{Float64}}}) at /Users/heileman/.julia/packages/MathOptInterface/C1XBe/src/Utilities/cachingoptimizer.jl:149
 [13] optimize!(::MathOptInterface.Utilities.CachingOptimizer{MathOptInterface.AbstractOptimizer,MathOptInterface.Utilities.UniversalFallback{MathOptInterface.Utilities.Model{Float64}}}) at /Users/heileman/.julia/packages/MathOptInterface/C1XBe/src/Utilities/cachingoptimizer.jl:185
 [14] #optimize!#78(::Bool, ::Bool, ::Base.Iterators.Pairs{Union{},Union{},Tuple{},NamedTuple{(),Tuple{}}}, ::typeof(optimize!), ::Model, ::Nothing) at /Users/heileman/.julia/packages/JuMP/MsUSY/src/optimizer_interface.jl:141
 [15] optimize! at /Users/heileman/.julia/packages/JuMP/MsUSY/src/optimizer_interface.jl:111 [inlined] (repeats 2 times)
 [16] top-level scope at REPL[157]:1

@odow
Copy link
Member

odow commented Mar 3, 2020

Okay so the issue is that by default, JuMP stores a cache of the model before passing it to Gurobi (this is the CachingOptimizer you can see in the error log). So it goes

JuMP -> CachingOptimizer -> Gurobi.Optimizer -> Gurobi.Optimizer.inner

The issue is that the CachingOptimizer doesn't remember the order of operations.

You have two options:

  1. Use model = JuMP.direct_model(Gurobi.Optimizer()). This will skip the caching optimizer
  2. Call optimize! before adding multiple objectives. This will "attach" the caching optimizer, so that future calls are done immediately.
model = Model(Gurobi.Optimizer)
@variable(model, x)
optimize!(model)
# set multi-objectives
optimize!(model)

cc @blegat. Thoughts on CachingOptimizer remembering an order of operations?

@blegat
Copy link
Member

blegat commented Mar 3, 2020

@odow Thoughts on CachingOptimizer remembering an order of operations?

We could make the UniversalFallback use OrderedDicts but we probably don't want to inforce the order on ListOfModelAttributes for any implementation of MOI model

@heileman
Copy link
Author

heileman commented Mar 3, 2020

Neither option appears to fix the issue.
The first option, using JuMP direct_model(), produces the following:

julia> m = JuMP.direct_model(Gurobi.Optimizer())
Academic license - for non-commercial use only
A JuMP Model
Feasibility problem with:
Variables: 0
Model mode: DIRECT
Solver name: Gurobi

julia> MOI.set(m, NumberOfObjective(), 2)
julia> print("Number of objectives = $(MOI.get(m, NumberOfObjective()))")
Number of objectives = 1
julia> MOI.set(m, MultiObjectivePriority(1), 1)
ERROR: Gurobi.GurobiError(10008, "It isn't a multi-objective model")
Stacktrace:
 [1] get_intattr(::Gurobi.Model, ::String) at /Users/heileman/.julia/packages/Gurobi/EhH9J/src/grb_attrs.jl:16
 [2] set(::Gurobi.Optimizer, ::MultiObjectivePriority, ::Int64) at ./REPL[15]:3
 [3] set(::Model, ::MultiObjectivePriority, ::Int64) at /Users/heileman/.julia/packages/JuMP/MsUSY/src/JuMP.jl:679
 [4] top-level scope at REPL[30]:1

The second option, i.e., calling optimize! before setting the multiple objectives, produces the following error using the previously provided code:

julia> JuMP.optimize!(model)
Academic license - for non-commercial use only
Gurobi Optimizer version 9.0.1 build v9.0.1rc0 (mac64)
Optimize a model with 0 rows, 2 columns and 0 nonzeros
Model fingerprint: 0x8dfcd1e3
Coefficient statistics:
  Matrix range     [0e+00, 0e+00]
  Objective range  [0e+00, 0e+00]
  Bounds range     [0e+00, 0e+00]
  RHS range        [0e+00, 0e+00]
Presolve removed 0 rows and 2 columns
Presolve time: 0.00s
Presolve: All rows and columns removed
Iteration    Objective       Primal Inf.    Dual Inf.      Time
       0    0.0000000e+00   0.000000e+00   3.000000e-06      0s

Solved in 0 iterations and 0.00 seconds
Optimal objective  0.000000000e+00

julia> MOI.set(model, NumberOfObjective(), 2)
julia> MOI.set(model, MultiObjectivePriority(1), 1)
ERROR: Gurobi.GurobiError(10008, "It isn't a multi-objective model")
Stacktrace:
 [1] get_intattr(::Gurobi.Model, ::String) at /Users/heileman/.julia/packages/Gurobi/EhH9J/src/grb_attrs.jl:16
 [2] set(::Gurobi.Optimizer, ::MultiObjectivePriority, ::Int64) at ./REPL[15]:3
 [3] set(::MathOptInterface.Bridges.LazyBridgeOptimizer{Gurobi.Optimizer}, ::MultiObjectivePriority, ::Int64) at /Users/heileman/.julia/packages/MathOptInterface/C1XBe/src/Bridges/bridge_optimizer.jl:593
 [4] set(::MathOptInterface.Utilities.CachingOptimizer{MathOptInterface.AbstractOptimizer,MathOptInterface.Utilities.UniversalFallback{MathOptInterface.Utilities.Model{Float64}}}, ::MultiObjectivePriority, ::Int64) at /Users/heileman/.julia/packages/MathOptInterface/C1XBe/src/Utilities/cachingoptimizer.jl:425
 [5] set(::Model, ::MultiObjectivePriority, ::Int64) at /Users/heileman/.julia/packages/JuMP/MsUSY/src/JuMP.jl:679
 [6] top-level scope at REPL[26]:1

in both cases, Gurobi does not recognize that the model is multi-objective

@heileman
Copy link
Author

heileman commented Mar 3, 2020

Greg's Challenge

I will award a prize of $2^8 (in the form of an Amazon gift certificate) to the first person who can demonstrate a working example of the use of mulit-objective optimization using Gurobi.jl through JuMP.jl.

To claim this prize, one must provide working code (using the latest versions of Gurobi.jl and JuMP.jl), and associated output for:

  • a multiple objective optimization problem involving at least one linear objective and one quadratic objective, and multiple variables.
  • use of both the hierarchical and weighted approaches to solving multi-objective problems using Gurobi.

Additional constraints:

  • Using MultiJuMP does not count, unless you can make it work with the latest versions of JuMP.jl and Gurobi.jl.
  • I must be able to validate the solution by executing the code locally on my machine, and produce the same output (documenting your work would probably be helpful in this regard).

Good luck!

@odow
Copy link
Member

odow commented Mar 4, 2020

I have it working. Pull request incoming.

@odow
Copy link
Member

odow commented Mar 4, 2020

@heileman, no need for the $2^8, but feel free to donate to JuMP!

You can do so here: https://numfocus.org/project/jump

We're part of NumFOCUS, a US non-profit that supports scientific computing.

@heileman
Copy link
Author

heileman commented Mar 5, 2020

Winner, winner, chicken dinner! I will make a $256 donation this evening.

@odow odow closed this as completed in #295 Mar 24, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging a pull request may close this issue.

3 participants