Skip to content

Commit b08c2f1

Browse files
authored
Add support for MOI.ScalarNonlinearFunction (#329)
1 parent 16f95f0 commit b08c2f1

File tree

6 files changed

+308
-6
lines changed

6 files changed

+308
-6
lines changed

.github/workflows/MINLP.yml

+9-4
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,12 @@ jobs:
2626
arch: ${{ matrix.arch }}
2727
- uses: julia-actions/cache@v2
2828
- uses: julia-actions/julia-buildpkg@v1
29-
- shell: bash
30-
run: julia -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.activate("test/MINLPTests"); Pkg.instantiate()'
31-
- shell: bash
32-
run: julia --project=test/MINLPTests test/MINLPTests/run_minlptests.jl
29+
- shell: julia --color=yes {0}
30+
run: |
31+
path = joinpath(ENV["GITHUB_WORKSPACE"], "test", "MINLPTests")
32+
cd(path)
33+
using Pkg
34+
Pkg.activate(".")
35+
Pkg.instantiate()
36+
Pkg.add(Pkg.PackageSpec(; path = ENV["GITHUB_WORKSPACE"]))
37+
include(joinpath(path, "run_minlptests.jl"))

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ List of supported constraint types:
101101
* [`MOI.ScalarAffineFunction{Float64}`](@ref) in [`MOI.GreaterThan{Float64}`](@ref)
102102
* [`MOI.ScalarAffineFunction{Float64}`](@ref) in [`MOI.Interval{Float64}`](@ref)
103103
* [`MOI.ScalarAffineFunction{Float64}`](@ref) in [`MOI.LessThan{Float64}`](@ref)
104+
* [`MOI.ScalarNonlinearFunction`](@ref) in [`MOI.EqualTo{Float64}`](@ref)
105+
* [`MOI.ScalarNonlinearFunction`](@ref) in [`MOI.GreaterThan{Float64}`](@ref)
106+
* [`MOI.ScalarNonlinearFunction`](@ref) in [`MOI.Interval{Float64}`](@ref)
107+
* [`MOI.ScalarNonlinearFunction`](@ref) in [`MOI.LessThan{Float64}`](@ref)
104108
* [`MOI.ScalarQuadraticFunction{Float64}`](@ref) in [`MOI.EqualTo{Float64}`](@ref)
105109
* [`MOI.ScalarQuadraticFunction{Float64}`](@ref) in [`MOI.GreaterThan{Float64}`](@ref)
106110
* [`MOI.ScalarQuadraticFunction{Float64}`](@ref) in [`MOI.Interval{Float64}`](@ref)

src/MOI_wrapper/constraints.jl

+3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ function MOI.supports(
1111
F<:Union{
1212
MOI.ScalarAffineFunction{Float64},
1313
MOI.ScalarQuadraticFunction{Float64},
14+
MOI.ScalarNonlinearFunction,
1415
MOI.VectorAffineFunction{Float64},
1516
MOI.VectorOfVariables,
1617
},
@@ -27,6 +28,7 @@ function MOI.get(
2728
F<:Union{
2829
MOI.ScalarAffineFunction{Float64},
2930
MOI.ScalarQuadraticFunction{Float64},
31+
MOI.ScalarNonlinearFunction,
3032
MOI.VectorAffineFunction{Float64},
3133
MOI.VectorOfVariables,
3234
},
@@ -43,6 +45,7 @@ function MOI.set(
4345
F<:Union{
4446
MOI.ScalarAffineFunction{Float64},
4547
MOI.ScalarQuadraticFunction{Float64},
48+
MOI.ScalarNonlinearFunction,
4649
MOI.VectorAffineFunction{Float64},
4750
MOI.VectorOfVariables,
4851
},

src/MOI_wrapper/nonlinear_constraints.jl

+218
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,221 @@ function MOI.set(o::Optimizer, attr::MOI.NLPBlock, data::MOI.NLPBlockData)
2323
end
2424
return nothing
2525
end
26+
27+
# MOI.ScalarNonlinearFunction
28+
29+
function MOI.get(::Optimizer, ::MOI.ListOfSupportedNonlinearOperators)
30+
return Symbol[:+, :-, :*, :/, :^, :abs, :exp, :log, :sin, :cos, :sqrt]
31+
end
32+
33+
function MOI.supports_constraint(
34+
::Optimizer,
35+
::Type{MOI.ScalarNonlinearFunction},
36+
::Type{S},
37+
) where {S<:BOUNDS}
38+
return true
39+
end
40+
41+
function MOI.add_constraint(
42+
model::Optimizer,
43+
f::MOI.ScalarNonlinearFunction,
44+
s::BOUNDS,
45+
)
46+
allow_modification(model)
47+
expr = NonlinExpr()
48+
root = _SCIPcreateExpr(model, f, expr)
49+
l, u = bounds(model, s)
50+
cons__ = Ref{Ptr{SCIP_CONS}}(C_NULL)
51+
@SCIP_CALL SCIPcreateConsBasicNonlinear(model, cons__, "", root[], l, u)
52+
@SCIP_CALL SCIPaddCons(model, cons__[])
53+
push!(model.inner.nonlinear_storage, expr)
54+
cr = store_cons!(model.inner, cons__)
55+
ci = MOI.ConstraintIndex{MOI.ScalarNonlinearFunction,typeof(s)}(cr.val)
56+
register!(model, ci)
57+
register!(model, cons(model, ci), cr)
58+
return ci
59+
end
60+
61+
function _SCIPcreateExpr(model::Optimizer, f::Real, expr::NonlinExpr)
62+
ret = Ref{Ptr{SCIP_EXPR}}(C_NULL)
63+
@SCIP_CALL SCIPcreateExprValue(model, ret, f, C_NULL, C_NULL)
64+
push!(expr.exprs, ret)
65+
return ret
66+
end
67+
68+
function _SCIPcreateExpr(
69+
model::Optimizer,
70+
f::MOI.VariableIndex,
71+
expr::NonlinExpr,
72+
)
73+
ret = Ref{Ptr{SCIP_EXPR}}(C_NULL)
74+
v = var(model, f)
75+
@SCIP_CALL SCIPcreateExprVar(model, ret, v, C_NULL, C_NULL)
76+
push!(expr.exprs, ret)
77+
return ret
78+
end
79+
80+
function _SCIPcreateExpr(
81+
model::Optimizer,
82+
f::MOI.ScalarNonlinearFunction,
83+
expr::NonlinExpr,
84+
)
85+
ret = Ref{Ptr{SCIP_EXPR}}(C_NULL)
86+
if f.head == :+
87+
children = map(arg -> _SCIPcreateExpr(model, arg, expr)[], f.args)
88+
@SCIP_CALL SCIPcreateExprSum(
89+
model,
90+
ret,
91+
length(f.args), # int nchildren
92+
children, # SCIP_EXPR **children,
93+
ones(Float64, length(f.args)), # SCIP_REAL *coefficients
94+
0.0, # SCIP_Real constant
95+
C_NULL,
96+
C_NULL,
97+
)
98+
elseif f.head == :*
99+
x = map(arg -> _SCIPcreateExpr(model, arg, expr)[], f.args)
100+
n = length(f.args)
101+
@SCIP_CALL SCIPcreateExprProduct(model, ret, n, x, 1.0, C_NULL, C_NULL)
102+
elseif f.head == :/
103+
# Convert x / y --> x * y^-1
104+
@assert length(f.args) == 2
105+
x = _SCIPcreateExpr(model, f.args[1], expr)[]
106+
y = _SCIPcreateExpr(model, f.args[2], expr)[]
107+
ret_y = Ref{Ptr{SCIP_EXPR}}(C_NULL)
108+
@SCIP_CALL SCIPcreateExprPow(model, ret_y, y, -1.0, C_NULL, C_NULL)
109+
push!(expr.exprs, ret_y)
110+
xy = [x, ret_y[]]
111+
@SCIP_CALL SCIPcreateExprProduct(model, ret, 2, xy, 1.0, C_NULL, C_NULL)
112+
elseif f.head == :-
113+
@assert 1 <= length(f.args) <= 2
114+
children = map(arg -> _SCIPcreateExpr(model, arg, expr)[], f.args)
115+
n = length(f.args)
116+
coefficients = ones(Float64, n)
117+
coefficients[end] = -1.0
118+
@SCIP_CALL SCIPcreateExprSum(
119+
model,
120+
ret,
121+
n,
122+
children,
123+
coefficients,
124+
0.0,
125+
C_NULL,
126+
C_NULL,
127+
)
128+
elseif f.head == :^
129+
@assert length(f.args) == 2
130+
if !(f.args[2] isa Real)
131+
throw(MOI.UnsupportedNonlinearOperator(f.head))
132+
end
133+
child = _SCIPcreateExpr(model, first(f.args), expr)
134+
expon = convert(Float64, f.args[2])
135+
@SCIP_CALL SCIPcreateExprPow(model, ret, child[], expon, C_NULL, C_NULL)
136+
elseif f.head == :abs
137+
child = _SCIPcreateExpr(model, only(f.args), expr)
138+
@SCIP_CALL SCIPcreateExprAbs(model, ret, child[], C_NULL, C_NULL)
139+
elseif f.head == :exp
140+
child = _SCIPcreateExpr(model, only(f.args), expr)
141+
@SCIP_CALL SCIPcreateExprExp(model, ret, child[], C_NULL, C_NULL)
142+
elseif f.head == :log
143+
child = _SCIPcreateExpr(model, only(f.args), expr)
144+
@SCIP_CALL SCIPcreateExprLog(model, ret, child[], C_NULL, C_NULL)
145+
elseif f.head == :sin
146+
child = _SCIPcreateExpr(model, only(f.args), expr)
147+
@SCIP_CALL SCIPcreateExprSin(model, ret, child[], C_NULL, C_NULL)
148+
elseif f.head == :cos
149+
child = _SCIPcreateExpr(model, only(f.args), expr)
150+
@SCIP_CALL SCIPcreateExprCos(model, ret, child[], C_NULL, C_NULL)
151+
elseif f.head == :sqrt
152+
child = _SCIPcreateExpr(model, only(f.args), expr)
153+
@SCIP_CALL SCIPcreateExprPow(model, ret, child[], 0.5, C_NULL, C_NULL)
154+
else
155+
throw(MOI.UnsupportedNonlinearOperator(f.head))
156+
end
157+
push!(expr.exprs, ret)
158+
return ret
159+
end
160+
161+
function _SCIPcreateExpr(
162+
model::Optimizer,
163+
f::MOI.ScalarAffineFunction,
164+
expr::NonlinExpr,
165+
)
166+
ret = Ref{Ptr{SCIP_EXPR}}(C_NULL)
167+
n = length(f.terms)
168+
children = map(f.terms) do term
169+
return _SCIPcreateExpr(model, term.variable, expr)[]
170+
end
171+
coefficients = map(f.terms) do term
172+
return convert(Float64, term.coefficient)
173+
end
174+
@SCIP_CALL SCIPcreateExprSum(
175+
model,
176+
ret,
177+
n,
178+
children,
179+
coefficients,
180+
f.constant,
181+
C_NULL,
182+
C_NULL,
183+
)
184+
push!(expr.exprs, ret)
185+
return ret
186+
end
187+
188+
function _SCIPcreateExpr(
189+
model::Optimizer,
190+
f::MOI.ScalarQuadraticFunction,
191+
expr::NonlinExpr,
192+
)
193+
ret = Ref{Ptr{SCIP_EXPR}}(C_NULL)
194+
children = map(f.affine_terms) do term
195+
return _SCIPcreateExpr(model, term.variable, expr)[]
196+
end
197+
coefficients = map(f.affine_terms) do term
198+
return convert(Float64, term.coefficient)
199+
end
200+
for term in f.quadratic_terms
201+
ret_xy = Ref{Ptr{SCIP_EXPR}}(C_NULL)
202+
x = _SCIPcreateExpr(model, term.variable_1, expr)
203+
y = _SCIPcreateExpr(model, term.variable_2, expr)
204+
scale = ifelse(term.variable_1 == term.variable_2, 0.5, 1.0)
205+
@SCIP_CALL SCIPcreateExprProduct(
206+
model,
207+
ret_xy,
208+
2,
209+
[x[], y[]],
210+
1.0,
211+
C_NULL,
212+
C_NULL,
213+
)
214+
push!(children, ret_xy[])
215+
push!(coefficients, scale * term.coefficient)
216+
push!(expr.exprs, ret_xy)
217+
end
218+
@SCIP_CALL SCIPcreateExprSum(
219+
model,
220+
ret,
221+
length(children),
222+
children,
223+
coefficients,
224+
f.constant,
225+
C_NULL,
226+
C_NULL,
227+
)
228+
push!(expr.exprs, ret)
229+
return ret
230+
end
231+
232+
function MOI.get(
233+
o::Optimizer,
234+
::MOI.ConstraintPrimal,
235+
ci::MOI.ConstraintIndex{MOI.ScalarNonlinearFunction},
236+
)
237+
_throw_if_invalid(o, ci)
238+
c = cons(o, ci)
239+
expr_ref = SCIPgetExprNonlinear(c)
240+
sol = SCIPgetBestSol(o)
241+
@SCIP_CALL SCIPevalExpr(o, expr_ref, sol, Clonglong(0))
242+
return SCIPexprGetEvalValue(expr_ref)
243+
end

test/MINLPTests/run_minlptests.jl

+56
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,29 @@ MINLPTests.test_directory(
3333
],
3434
)
3535

36+
MINLPTests.test_directory(
37+
"nlp-expr",
38+
JuMP.optimizer_with_attributes(SCIP.Optimizer, "display/verblevel" => 0);
39+
objective_tol=1e-3,
40+
primal_tol=1e-3,
41+
dual_tol=NaN,
42+
termination_target=MINLPTests.TERMINATION_TARGET_GLOBAL,
43+
primal_target=MINLPTests.PRIMAL_TARGET_GLOBAL,
44+
exclude=[
45+
# MOI.UnsupportedNonlinearOperator(:tan)
46+
"004_010",
47+
"004_011",
48+
# MOI.UnsupportedNonlinearOperator(:inv)
49+
"005_010",
50+
# User-defined function
51+
"006_010",
52+
# MOI.UnsupportedNonlinearOperator(:min)
53+
"009_010",
54+
# MOI.UnsupportedNonlinearOperator(:max)
55+
"009_011",
56+
],
57+
)
58+
3659
MINLPTests.test_directory(
3760
"nlp-cvx",
3861
JuMP.optimizer_with_attributes(SCIP.Optimizer, "display/verblevel" => 0);
@@ -52,6 +75,16 @@ MINLPTests.test_directory(
5275
],
5376
)
5477

78+
MINLPTests.test_directory(
79+
"nlp-cvx-expr",
80+
JuMP.optimizer_with_attributes(SCIP.Optimizer, "display/verblevel" => 0);
81+
objective_tol=1e-4,
82+
primal_tol=1e-3,
83+
dual_tol=NaN,
84+
termination_target=MINLPTests.TERMINATION_TARGET_GLOBAL,
85+
primal_target=MINLPTests.PRIMAL_TARGET_GLOBAL,
86+
)
87+
5588
MINLPTests.test_directory(
5689
"nlp-mi",
5790
JuMP.optimizer_with_attributes(SCIP.Optimizer, "display/verblevel" => 0);
@@ -73,3 +106,26 @@ MINLPTests.test_directory(
73106
"006_010",
74107
],
75108
)
109+
110+
MINLPTests.test_directory(
111+
"nlp-mi-expr",
112+
JuMP.optimizer_with_attributes(SCIP.Optimizer, "display/verblevel" => 0);
113+
objective_tol=1e-4,
114+
primal_tol=1e-3,
115+
dual_tol=NaN,
116+
termination_target=MINLPTests.TERMINATION_TARGET_GLOBAL,
117+
primal_target=MINLPTests.PRIMAL_TARGET_GLOBAL,
118+
exclude=[
119+
# Fails because of the periodicity in cos(y). We chould fix this in
120+
# MINLPTests by finitely bounding the `y` variable. Currently it is just
121+
# y >= 0.1
122+
"001_010",
123+
# MOI.UnsupportedNonlinearOperator(:tan)
124+
"004_010",
125+
"004_011",
126+
# MOI.UnsupportedNonlinearOperator(:inv)
127+
"005_010",
128+
# User-defined function
129+
"006_010",
130+
],
131+
)

test/MOI_tests.jl

+18-2
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,30 @@ function test_runtests_cached()
4242
with_cache_type=Float64,
4343
)
4444
MOI.set(model, MOI.Silent(), true)
45-
MOI.Test.runtests(model, CONFIG)
45+
MOI.Test.runtests(
46+
model,
47+
CONFIG;
48+
# TODO(odow): these doesn't converge. Can we fix upstream?
49+
exclude=[
50+
r"^test_nonlinear_expression_hs110$",
51+
r"^test_nonlinear_expression_hs109$",
52+
],
53+
)
4654
return
4755
end
4856

4957
function test_runtests_bridged()
5058
model = MOI.instantiate(SCIP.Optimizer; with_bridge_type=Float64)
5159
MOI.set(model, MOI.Silent(), true)
52-
MOI.Test.runtests(model, CONFIG)
60+
MOI.Test.runtests(
61+
model,
62+
CONFIG;
63+
# TODO(odow): these doesn't converge. Can we fix upstream?
64+
exclude=[
65+
r"^test_nonlinear_expression_hs110$",
66+
r"^test_nonlinear_expression_hs109$",
67+
],
68+
)
5369
return
5470
end
5571

0 commit comments

Comments
 (0)