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

feat: add flexibility for solvers besides Gurobi #99

Merged
merged 3 commits into from
Jan 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,14 @@ REISE.run_scenario(;
interval=24, n_interval=3, start_index=1, outputfolder="output",
inputfolder=pwd(), num_segments=3)
```
If another solver is desired, it can be passed via the `optimizer_factory` argument, e.g.:
```julia
import GLPK
REISE.run_scenario(;
interval=24, n_interval=3, start_index=1, outputfolder="output",
inputfolder=pwd(), optimizer_factory=GLPK.Optimizer)
```
Be sure to pass the factory itself (e.g. `GLPK.Optimizer`) rather than an instance (e.g. `GLPK.Optimizer()`). See the [JuMP.Model documentation] for more information.

## Usage (Python)

Expand Down Expand Up @@ -665,3 +673,4 @@ Penalty for ending the interval with less stored energy than the start, or rewar

[Gurobi.jl]: https://github.com/JuliaOpt/Gurobi.jl#installation
[Julia Package Manager]: https://julialang.github.io/Pkg.jl/v1/managing-packages/
[JuMP.Model documentation]: https://jump.dev/JuMP.jl/stable/solvers/#JuMP.Model-Tuple{Any}
27 changes: 18 additions & 9 deletions src/REISE.jl
Original file line number Diff line number Diff line change
Expand Up @@ -29,23 +29,31 @@ Run a scenario consisting of several intervals.
'n_interval' specifies the number of intervals in a scenario.
'start_index' specifies the starting hour of the first interval, to determine
which time-series data should be loaded into each intervals.
'outputfolder' specifies where to store the results. This folder will be
created if it does not exist at runtime.
'inputfolder' specifies where to load the relevant data from. Required files
are 'case.mat', 'demand.csv', 'hydro.csv', 'solar.csv', and 'wind.csv'.
'outputfolder' specifies where to store the results. Defaults to an `output`
subdirectory of inputfolder. This folder will be created if it does not exist at
runtime.
'optimizer_factory' is the solver used for optimization. If not specified, Gurobi is
used by default.
"""
function run_scenario(;
num_segments::Int=1, interval::Int, n_interval::Int, start_index::Int,
inputfolder::String, outputfolder::Union{String, Nothing}=nothing,
threads::Union{Int, Nothing}=nothing)
threads::Union{Int, Nothing}=nothing, optimizer_factory=nothing)
# Setup things that build once
# If outputfolder not given, by default assign it inside inputfolder
isnothing(outputfolder) && (outputfolder = joinpath(inputfolder, "output"))
# If outputfolder doesn't exist (isdir evaluates false) create it (mkdir)
isdir(outputfolder) || mkdir(outputfolder)
stdout_filepath = joinpath(outputfolder, "stdout.log")
stderr_filepath = joinpath(outputfolder, "stderr.err")
env = Gurobi.Env()
if isnothing(optimizer_factory)
optimizer_factory = Gurobi.Env()
solver_kwargs = Dict("Method" => 2, "Crossover" => 0)
else
solver_kwargs = Dict()
end
case = read_case(inputfolder)
storage = read_storage(inputfolder)
println("All scenario files loaded!")
Expand All @@ -56,19 +64,20 @@ function run_scenario(;
"storage" => storage,
"interval_length" => interval,
)
solver_kwargs = Dict("Method" => 2, "Crossover" => 0)
# If a number of threads is specified, add to solver settings dict
isnothing(threads) || (solver_kwargs["Threads"] = threads)
println("All preparation complete!")
# While redirecting stdout and stderr...
println("Redirecting outputs, see stdout.log & stderr.err in outputfolder")
redirect_stdout_stderr(stdout_filepath, stderr_filepath) do
# Loop through intervals
interval_loop(env, model_kwargs, solver_kwargs, interval, n_interval,
start_index, inputfolder, outputfolder)
interval_loop(optimizer_factory, model_kwargs, solver_kwargs, interval,
n_interval, start_index, inputfolder, outputfolder)
GC.gc()
Gurobi.finalize(env)
println("Connection closed successfully!")
if isa(optimizer_factory, Gurobi.Env)
Gurobi.finalize(optimizer_factory)
println("Connection closed successfully!")
Copy link
Collaborator

Choose a reason for hiding this comment

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

We don't need to do similar thing for other solvers or this is something to explore?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't know that other solvers have a similar 'environment' feature that needs to be managed.

end
end
end

Expand Down
55 changes: 34 additions & 21 deletions src/loop.jl
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
"""Convert a dict with string keys to a NamedTuple, for python-eqsue kwargs splatting"""
function symbolize(d::Dict{String,Any})::NamedTuple
return (; (Symbol(k) => v for (k,v) in d)...)
end


function new_model(factory_like)::Union{JuMP.Model, JuMP.MOI.AbstractOptimizer}
if isa(factory_like, Gurobi.Env)
return JuMP.direct_model(Gurobi.Optimizer(factory_like))
else
return JuMP.Model(factory_like)
Comment on lines +7 to +11
Copy link
Collaborator

Choose a reason for hiding this comment

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

Again, this implies JuMP treats Gurobi differently but all the other solvers in the same way?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We need to use the direct_model feature to be able to pass the Gurobi environment to the Gurobi.Optimizer.

end
end


"""
interval_loop(env, model_kwargs, solver_kwargs, interval, n_interval,
interval_loop(factory_like, model_kwargs, solver_kwargs, interval, n_interval,
start_index, inputfolder, outputfolder)

Given:
- a Gurobi environment `env`
- optimizer instantiation object `factory_like`:
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm wondering about the term factory_like...I'm not sure exactly what it communicates. Maybe something like factory_spec or factory_env or optimization_factory_env, etc?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's either an optimization factory or a Gurobi environment.

either a Gurobi environment or something that can be passed to JuMP.Model
- a dictionary of model keyword arguments `model_kwargs`
- a dictionary of solver keyword arguments `solver_kwargs`
- an interval length `interval` (hours)
Expand All @@ -15,7 +31,7 @@ Given:
Build a model, and run through the intervals, re-building the model and/or
re-setting constraint right-hand-side values as necessary.
"""
function interval_loop(env::Gurobi.Env, model_kwargs::Dict,
function interval_loop(factory_like, model_kwargs::Dict,
solver_kwargs::Dict, interval::Int,
n_interval::Int, start_index::Int,
inputfolder::String, outputfolder::String)
Expand Down Expand Up @@ -45,21 +61,19 @@ function interval_loop(env::Gurobi.Env, model_kwargs::Dict,
if storage_enabled
model_kwargs["storage_e0"] = storage.sd_table.InitialStorage
end
m_kwargs = (; (Symbol(k) => v for (k,v) in model_kwargs)...)
m = JuMP.direct_model(Gurobi.Optimizer(env))
m = new_model(factory_like)
JuMP.set_optimizer_attributes(m, pairs(solver_kwargs)...)
m, voi = _build_model(m; m_kwargs...)
m, voi = _build_model(m; symbolize(model_kwargs)...)
elseif i == 2
# Build a model with an initial ramp constraint
model_kwargs["initial_ramp_enabled"] = true
model_kwargs["initial_ramp_g0"] = pg0
if storage_enabled
model_kwargs["storage_e0"] = storage_e0
end
m_kwargs = (; (Symbol(k) => v for (k,v) in model_kwargs)...)
m = JuMP.direct_model(Gurobi.Optimizer(env))
m = new_model(factory_like)
JuMP.set_optimizer_attributes(m, pairs(solver_kwargs)...)
m, voi = _build_model(m; m_kwargs...)
m, voi = _build_model(m; symbolize(model_kwargs)...)
else
# Reassign right-hand-side of constraints to match profiles
bus_demand = _make_bus_demand(case, interval_start, interval_end)
Expand Down Expand Up @@ -125,33 +139,33 @@ function interval_loop(env::Gurobi.Env, model_kwargs::Dict,
results = get_results(f, voi, model_kwargs["case"])
break
elseif ((status in numeric_statuses)
& isa(factory_like, Gurobi.Env)
& !("BarHomogeneous" in keys(solver_kwargs)))
# if BarHomogeneous is not enabled, enable it and re-build
# if Gurobi, and BarHomogeneous is not enabled, enable it and re-solve
solver_kwargs["BarHomogeneous"] = 1
println("enable BarHomogeneous")
JuMP.set_optimizer_attribute(m, "BarHomogeneous", 1)
elseif ((status in infeasible_statuses)
& !("load_shed_enabled" in keys(model_kwargs)))
# if load shed not enabled, enable it and re-build the model
model_kwargs["load_shed_enabled"] = true
m_kwargs = (; (Symbol(k) => v for (k,v) in model_kwargs)...)
println("rebuild with load shed")
m = JuMP.direct_model(Gurobi.Optimizer(env))
m = new_model(factory_like)
JuMP.set_optimizer_attributes(m, pairs(solver_kwargs)...)
m, voi = _build_model(m; m_kwargs...)
m, voi = _build_model(m; symbolize(model_kwargs)...)
intervals_without_loadshed = 0
elseif !("BarHomogeneous" in keys(solver_kwargs))
# if BarHomogeneous is not enabled, enable it and re-build
elseif (isa(factory_like, Gurobi.Env)
& !("BarHomogeneous" in keys(solver_kwargs)))
# if Gurobi, and BarHomogeneous is not enabled, enable it and re-solve
solver_kwargs["BarHomogeneous"] = 1
println("enable BarHomogeneous")
JuMP.set_optimizer_attribute(m, "BarHomogeneous", 1)
elseif !("load_shed_enabled" in keys(model_kwargs))
model_kwargs["load_shed_enabled"] = true
m_kwargs = (; (Symbol(k) => v for (k,v) in model_kwargs)...)
println("rebuild with load shed")
m = JuMP.direct_model(Gurobi.Optimizer(env))
m = new_model(factory_like)
JuMP.set_optimizer_attributes(m, pairs(solver_kwargs)...)
m, voi = _build_model(m; m_kwargs...)
m, voi = _build_model(m; symbolize(model_kwargs)...)
intervals_without_loadshed = 0
else
# Something has gone very wrong
Expand Down Expand Up @@ -199,10 +213,9 @@ function interval_loop(env::Gurobi.Env, model_kwargs::Dict,
# delete! will work here even if the key is not present
delete!(solver_kwargs, "BarHomogeneous")
delete!(model_kwargs, "load_shed_enabled")
m_kwargs = (; (Symbol(k) => v for (k,v) in model_kwargs)...)
m = JuMP.direct_model(Gurobi.Optimizer(env))
m = new_model(factory_like)
JuMP.set_optimizer_attributes(m, pairs(solver_kwargs)...)
m, voi = _build_model(m; m_kwargs...)
m, voi = _build_model(m; symbolize(model_kwargs)...)
end
end
end
Expand Down