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

Fix StackOverflowError coming from MOI.Utilities.operate #6

Merged
merged 20 commits into from
Sep 22, 2023
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
230 changes: 2 additions & 228 deletions src/SolverAPI.jl
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import JSON3

export serialize, deserialize, solve, print_model, response

include("json_to_moi.jl") # Utilities for building MOI constraints/objectives from JSON.

# SolverAPI
# ========================================================================================

Expand Down Expand Up @@ -413,232 +415,4 @@ function load!(model::MOI.ModelLike, json::Request, T::Type, solver_info::Dict{S
return nothing
end

# convert JSON array to MOI ScalarNonlinearFunction
function json_to_snf(a::JSON3.Array, vars_map::Dict)
length(a) > 0 || throw(Error(InvalidModel, "The given JSON array `$a` is empty."))

head = a[1]
args = Any[json_to_snf(a[i], vars_map) for i in eachindex(a) if i != 1]

head isa String || return args
if head == "range"
# TODO handle variables in different positions, etc
# TODO handle as interval constraint?
lb, ub, step, x = args
step == 1 || throw(Error(NotAllowed, "Step size $step is not supported."))
return MOI.ScalarNonlinearFunction(
:∧,
Any[
MOI.ScalarNonlinearFunction(:<=, Any[lb, x]),
MOI.ScalarNonlinearFunction(:<=, Any[x, ub]),
],
)
elseif head == "and"
head = "forall"
args = Any[args]
elseif head == "or"
head = "exists"
args = Any[args]
elseif head == "not"
head = "!"
elseif head == "implies"
head = "=>"
elseif head == "natural_exp"
head = "exp"
elseif head == "natural_log"
head = "log"
elseif head == "alldifferent"
args = Any[args]
elseif head == "count"
args = Any[args]
elseif head == "max"
head = "maximum"
args = Any[args]
end
return MOI.ScalarNonlinearFunction(Symbol(head), args)
end

json_to_snf(a::String, vars_map::Dict) = vars_map[a]
json_to_snf(a::Real, ::Dict) = a

# convert SNF to SAF/SQF{T}
function nl_to_aff_or_quad(::Type{T}, f::MOI.ScalarNonlinearFunction) where {T<:Real}
args = nl_to_aff_or_quad.(T, f.args)
if !any(Base.Fix2(isa, MOI.ScalarNonlinearFunction), args)
if f.head == :^
if length(args) == 2 && args[2] == 2
return MOI.Utilities.operate(*, T, args[1], args[1])
end
else
h = get(_quad_ops, f.head, nothing)
isnothing(h) || return MOI.Utilities.operate(h, T, args...)
end
end
return error() # Gets caught by canonicalize_SNF.
end

nl_to_aff_or_quad(::Type{<:Real}, f::MOI.VariableIndex) = f
nl_to_aff_or_quad(::Type{T}, f::T) where {T<:Real} = f
nl_to_aff_or_quad(::Type{T}, f::Real) where {T<:Real} = convert(T, f)

_quad_ops = Dict(:+ => +, :- => -, :* => *, :/ => /)

function canonicalize_SNF(::Type{T}, f) where {T<:Real}
try
f = nl_to_aff_or_quad(T, f)
catch
end
return f
end

function add_obj!(
::Type{T},
model::MOI.ModelLike,
sense::String,
a::Union{String,JSON3.Array},
vars_map::Dict,
::Dict,
) where {T<:Real}
if sense == "min"
moi_sense = MOI.MIN_SENSE
elseif sense == "max"
moi_sense = MOI.MAX_SENSE
end
MOI.set(model, MOI.ObjectiveSense(), moi_sense)

g = canonicalize_SNF(T, json_to_snf(a, vars_map))
g_type = MOI.ObjectiveFunction{typeof(g)}()
if !MOI.supports(model, g_type)
msg = "Objective function $(trunc_str(g)) isn't supported by this solver."
throw(Error(Unsupported, msg))
end
MOI.set(model, g_type, g)
return nothing
end

function add_cons!(
::Type{T},
model::MOI.ModelLike,
a::JSON3.Array,
vars_map::Dict,
solver_info::Dict,
) where {T<:Real}
head = a[1]
if head == "and"
for i in eachindex(a)
i == 1 && continue
if a[i] isa Bool
if !a[i]
throw(Error(InvalidModel, "Model is infeasible."))
end
else
add_cons!(T, model, a[i], vars_map, solver_info)
end
end
elseif head == "Int"
v = json_to_snf(a[2], vars_map)
_check_v_type(v)
MOI.add_constraint(model, v, MOI.Integer())
elseif head == "Bin"
v = json_to_snf(a[2], vars_map)
_check_v_type(v)
MOI.add_constraint(model, v, MOI.ZeroOne())
elseif head == "Float"
elseif head == "Nonneg"
v = json_to_snf(a[2], vars_map)
_check_v_type(v)
MOI.add_constraint(model, v, MOI.GreaterThan(zero(T)))
elseif head == "PosNegOne"
v = json_to_snf(a[2], vars_map)
_check_v_type(v)
# TODO only for MiniZinc
MOI.add_constraint(model, v, MOI.Integer())
f = MOI.ScalarNonlinearFunction(:abs, Any[v])
MOI.add_constraint(model, f, MOI.EqualTo(1))
elseif head == "range"
if length(a) != 5
throw(Error(InvalidModel, "The `range` constraint expects 4 arguments."))
end
v = json_to_snf(a[5], vars_map)
_check_v_type(v)
if !(a[2] isa Int && a[3] isa Int)
throw(Error(InvalidModel, "The `range` constraint expects integer bounds."))
end
if a[4] != 1
throw(Error(InvalidModel, "The `range` constraint expects a step size of 1."))
end
MOI.add_constraint(model, v, MOI.Integer())
MOI.add_constraint(model, v, MOI.Interval{T}(a[2], a[3]))
elseif head == "implies" && solver_info[:use_indicator]
# TODO maybe just check if model supports indicator constraints
# use an MOI indicator constraint
if length(a) != 3
throw(Error(InvalidModel, "The `implies` constraint expects 2 arguments."))
end
f = json_to_snf(a[2], vars_map)
g = json_to_snf(a[3], vars_map)
if !(f.head == :(==) && length(f.args) == 2)
msg = "The first argument of the `implies` constraint expects to be converted to an equality SNF with 2 arguments."
throw(Error(InvalidModel, msg))
end

v, b = f.args
_check_v_type(v)
if b != 1 && b != 0
msg = "The second argument of the derived equality SNF from the `implies` constraint expects a binary variable."
throw(Error(InvalidModel, msg))
end

A = (b == 1) ? MOI.ACTIVATE_ON_ONE : MOI.ACTIVATE_ON_ZERO
S1 = get(ineq_to_moi, g.head, nothing)
if isnothing(S1) || length(g.args) != 2
msg = "The second argument of the `implies` constraint expects to be converted to an (in)equality SNF with 2 arguments."
throw(Error(InvalidModel, msg))
end

h = shift_terms(T, g.args)
vaf = MOI.Utilities.operate(vcat, T, v, h)
MOI.add_constraint(model, vaf, MOI.Indicator{A}(S1(zero(T))))
else
f = json_to_snf(a, vars_map)
S = get(ineq_to_moi, f.head, nothing)
if isnothing(S)
# CSP constraint
ci = MOI.add_constraint(model, f, MOI.EqualTo(1))
else
# (in)equality constraint
g = shift_terms(T, f.args)
s = S(zero(T))
if !MOI.supports_constraint(model, typeof(g), typeof(s))
msg = "Constraint $(trunc_str(g)) in $(trunc_str(s)) isn't supported by this solver."
throw(Error(Unsupported, msg))
end
ci = MOI.Utilities.normalize_and_add_constraint(model, g, s)
end
end
return nothing
end

_check_v_type(::MOI.VariableIndex) = nothing
_check_v_type(_) =
throw(Error(InvalidModel, "$v must be a `MOI.VariableIndex`, not $(typeof(v))."))

ineq_to_moi = Dict(:<= => MOI.LessThan, :>= => MOI.GreaterThan, :(==) => MOI.EqualTo)

function shift_terms(::Type{T}, args::Vector) where {T<:Real}
@assert length(args) == 2 # This should never happen.
g1 = canonicalize_SNF(T, args[1])
g2 = canonicalize_SNF(T, args[2])
return MOI.Utilities.operate(-, T, g1, g2)
end

# Convert object to string and truncate string length if too long.
function trunc_str(f::Union{MOI.AbstractScalarFunction,MOI.AbstractScalarSet})
f_str = string(f)
if length(f_str) > 256
f_str = f_str[1:256] * " ... (truncated)"
end
return f_str
end

end # module SolverAPI
Loading
Loading