Skip to content

Commit

Permalink
json options field is required; add tests and simplify some code; fre…
Browse files Browse the repository at this point in the history
…e model memory
  • Loading branch information
chriscoey committed Sep 22, 2023
1 parent 50fc35d commit 525744f
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 59 deletions.
94 changes: 45 additions & 49 deletions src/SolverAPI.jl
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,12 @@ function response(

res["termination_status"] = string(MOI.get(model, MOI.TerminationStatus()))

options = get(() -> Dict{String,Any}(), json, :options)
format = get(options, :print_format, nothing)
format = get(json.options, :print_format, nothing)
if !isnothing(format)
res["model_string"] = print_model(model, format)
end

if Bool(get(options, :print_only, false))
if Bool(get(json.options, :print_only, false))
return res
end

Expand Down Expand Up @@ -178,12 +177,23 @@ function solve(fn, json::Request, solver::MOI.AbstractOptimizer)
errors = validate(json)
isempty(errors) || return response(; errors)

# TODO (dba) `SolverAPI.jl` should be decoupled from any solver specific code.
solver_info = Dict{Symbol,Any}()
if lowercase(get(json.options, :solver, "highs")) == "minizinc"
T = Int
solver_info[:use_indicator] = false
else
T = Float64
solver_info[:use_indicator] = true
end

model = MOI.instantiate(() -> solver; with_cache_type = T, with_bridge_type = T)

try
T, solver_info, model = initialize(json, solver)
load!(json, T, solver_info, model)
set_options!(model, json.options)
load!(model, json, T, solver_info)
fn(model)
options = get(() -> Dict{String,Any}(), json, :options)
if !Bool(get(options, :print_only, false))
if !Bool(get(json.options, :print_only, false))
MOI.optimize!(model)
end
return response(json, model, solver)
Expand All @@ -208,6 +218,8 @@ function solve(fn, json::Request, solver::MOI.AbstractOptimizer)
else
rethrow()
end
finally
MOI.empty!(model)
end
end
solve(request::Request, solver::MOI.AbstractOptimizer) =
Expand All @@ -232,30 +244,30 @@ function print_model(model::MOI.ModelLike, format::String)
format_lower = lowercase(format)
if format_lower == "moi" || format_lower == "latex"
mime = MIME(format_lower == "latex" ? "text/latex" : "text/plain")
options = MOI.Utilities._PrintOptions(
print_options = MOI.Utilities._PrintOptions(
mime;
simplify_coefficients = true,
print_types = false,
)
return sprint() do io
return MOI.Utilities._print_model(io, options, model)
return MOI.Utilities._print_model(io, print_options, model)
end
elseif format_lower == "latex"
# NOTE: there are options for latex_formulation, e.g. don't print MOI function/set types
# https://jump.dev/MathOptInterface.jl/dev/submodules/Utilities/reference/#MathOptInterface.Utilities.latex_formulation
return sprint(print, MOI.Utilities.latex_formulation(model))
elseif format_lower == "mof"
options = (; format = MOI.FileFormats.FORMAT_MOF, print_compact = true)
print_options = (; format = MOI.FileFormats.FORMAT_MOF, print_compact = true)
elseif format_lower == "lp"
options = (; format = MOI.FileFormats.FORMAT_LP)
print_options = (; format = MOI.FileFormats.FORMAT_LP)
elseif format_lower == "mps"
options = (; format = MOI.FileFormats.FORMAT_MPS)
print_options = (; format = MOI.FileFormats.FORMAT_MPS)
elseif format_lower == "nl"
options = (; format = MOI.FileFormats.FORMAT_NL)
print_options = (; format = MOI.FileFormats.FORMAT_NL)
else
throw(Error(Unsupported, "File type \"$format\" not supported."))
end
dest = MOI.FileFormats.Model(; options...)
dest = MOI.FileFormats.Model(; print_options...)
MOI.copy_to(dest, model)
return sprint(write, dest)
end
Expand All @@ -265,16 +277,15 @@ function print_model(request::Request; T = Float64)
throw(CompositeException(errors))
end

options = get(() -> Dict{String,Any}(), request, :options)
# Default to MOI format.
format = get(options, :print_format, "MOI")
format = get(request.options, :print_format, "MOI")

# TODO cleanup/refactor solver_info logic.
use_indicator = T == Float64
solver_info = Dict{Symbol,Any}(:use_indicator => use_indicator)
model = MOI.Utilities.Model{T}()

load!(request, T, solver_info, model)
load!(model, request, T, solver_info)
return print_model(model, format)
end
print_model(request::Dict) = print_model(JSON3.read(JSON3.write(request)))
Expand All @@ -289,7 +300,7 @@ function validate(json::Request)#::Vector{Error}
valid_shape = true

# Syntax.
for k in [:version, :sense, :variables, :constraints, :objectives]
for k in [:version, :sense, :variables, :constraints, :objectives, :options]
if !haskey(json, k)
valid_shape = false
_err("Missing required field `$(k)`.")
Expand All @@ -307,7 +318,7 @@ function validate(json::Request)#::Vector{Error}
_err("Invalid version `$(repr(json.version))`. Only `\"0.1\"` is supported.")
end

if haskey(json, :options) && !isa(json.options, JSON3.Object)
if !isa(json.options, JSON3.Object)
_err("Invalid `options` field. Must be an object.")
end

Expand All @@ -326,20 +337,18 @@ function validate(json::Request)#::Vector{Error}
_err("Invalid `sense` field. Must be one of `feas`, `min`, or `max`.")
end

if haskey(json, :options)
for (T, k) in [(String, :print_format), (Number, :time_limit_sec)]
if haskey(json.options, k) && !isa(json.options[k], T)
_err("Invalid `options.$(k)` field. Must be of type `$(T)`.")
end
for (T, k) in [(String, :print_format), (Number, :time_limit_sec)]
if haskey(json.options, k) && !isa(json.options[k], T)
_err("Invalid `options.$(k)` field. Must be of type `$(T)`.")
end
end

for k in [:silent, :print_only]
if haskey(json.options, k)
val = json.options[k]
# We allow `0` and `1` for convenience.
if !isa(val, Bool) && val isa Number && val != 0 && val != 1
_err("Invalid `options.$(k)` field. Must be a boolean.")
end
for k in [:silent, :print_only]
if haskey(json.options, k)
val = json.options[k]
# We allow `0` and `1` for convenience.
if !isa(val, Bool) && val isa Number && val != 0 && val != 1
_err("Invalid `options.$(k)` field. Must be a boolean.")
end
end
end
Expand All @@ -357,29 +366,16 @@ function validate(json::Request)#::Vector{Error}
return out
end

function initialize(json::Request, solver::MOI.AbstractOptimizer)#::Tuple{Type, Dict{Symbol, Any}, MOI.ModelLike}
solver_info = Dict{Symbol,Any}()

# TODO (dba) `SolverAPI.jl` should be decoupled from any solver specific code.
options = get(() -> Dict{String,Any}(), json, :options)
if lowercase(get(options, :solver, "highs")) == "minizinc"
T = Int
solver_info[:use_indicator] = false
else
T = Float64
solver_info[:use_indicator] = true
end

model = MOI.instantiate(() -> solver; with_cache_type = T, with_bridge_type = T)

# Set solver options.
function set_options!(model::MOI.ModelLike, options::JSON3.Object)#::Nothing
if MOI.supports(model, MOI.TimeLimitSec())
# Set time limit, defaulting to 5min.
MOI.set(model, MOI.TimeLimitSec(), Float64(get(options, :time_limit_sec, 300.0)))
end

# Set other solver options.
for (key, val) in options
if key in [:solver, :print_format, :print_only, :time_limit_sec]
# Skip - these are handled elsewhere.
continue
elseif key == :silent
MOI.set(model, MOI.Silent(), Bool(val))
Expand All @@ -392,10 +388,10 @@ function initialize(json::Request, solver::MOI.AbstractOptimizer)#::Tuple{Type,
end
end

return (T, solver_info, model)
return nothing
end

function load!(json::Request, T::Type, solver_info::Dict{Symbol,Any}, model::MOI.ModelLike)#::Nothing
function load!(model::MOI.ModelLike, json::Request, T::Type, solver_info::Dict{Symbol,Any})#::Nothing
# handle variables
vars_map = Dict{String,MOI.VariableIndex}()
vars = MOI.add_variables(model, length(json.variables))
Expand Down
32 changes: 22 additions & 10 deletions test/all_tests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ end

function run_solve(input::String)
json = deserialize(input)
solver = get_solver(json.options.solver)
solver_name = try
json.options.solver
catch
"highs"
end
solver = get_solver(solver_name)
output = solve(json, solver)
return String(serialize(output))
end
Expand All @@ -34,18 +39,19 @@ end # end of setup module.
@testitem "print" setup = [SolverSetup] begin
using SolverAPI: print_model

model = Dict(
json = Dict(
:version => "0.1",
:sense => "min",
:variables => ["x"],
:constraints => [["==", "x", 1], ["Int", "x"]],
:objectives => ["x"],
:options => Dict(:print_format => "none"),
)

# check MOI model printing for each format
@testset "$f" for f in ["moi", "latex", "mof", "lp", "mps", "nl"]
request = Dict(model..., :options => Dict(:print_format => f))
@test print_model(request) isa String
json[:options][:print_format] = f
@test print_model(json) isa String
end
end

Expand Down Expand Up @@ -91,6 +97,8 @@ end
("missing_sense", "InvalidFormat"),
# missing field version
("missing_version", "InvalidFormat"),
# missing field options
("missing_options", "InvalidFormat"),
# field variables is not a string
("vars_is_not_str", "InvalidFormat"),
# field variables is not an array
Expand Down Expand Up @@ -125,6 +133,7 @@ end
("unsupported_print_format", "Unsupported"),
]

# check that expected error types are returned for each json input
@testset "$j" for (j, es...) in json_names_and_errors
result = JSON3.read(run_solve(read_json("inputs", j)))
@test haskey(result, :errors) && length(result.errors) >= 1
Expand All @@ -138,18 +147,21 @@ end
# setup linear objective model with n variables
n = 1000
vars = ["x$i" for i in 1:n]
model = Dict(
json = Dict(
:version => "0.1",
:sense => "max",
:variables => vars,
:constraints => [["Bin", v] for v in vars],
:objectives => [vcat("+", 1, [["*", i, v] for (i, v) in enumerate(vars)])],
:options => Dict(:solver => "HiGHS"),
)

# check that model is solved correctly without errors
output = solve(model, get_solver("HiGHS"))
@test output isa Dict{String,Any}
@test !haskey(output, "errors")
@test output["termination_status"] == "OPTIMAL"
@test output["results"][1]["objective_value"] 1 + div(n * (n + 1), 2)
@testset "solve n=$n" begin
output = solve(json, get_solver("HiGHS"))
@test output isa Dict{String,Any}
@test !haskey(output, "errors")
@test output["termination_status"] == "OPTIMAL"
@test output["results"][1]["objective_value"] 1 + div(n * (n + 1), 2)
end
end
1 change: 1 addition & 0 deletions test/inputs/missing_options.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"version":"0.1","sense":"feas","variables":["x"],"constraints":[["==","x",1],["Int","x"]],"objectives":[]}

0 comments on commit 525744f

Please sign in to comment.