/
parse_nlp.jl
295 lines (273 loc) · 12.5 KB
/
parse_nlp.jl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# Copyright 2017, Iain Dunning, Joey Huchette, Miles Lubin, and contributors
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# Returns the block expression inside a :let that holds the code to be run.
# The other block (not returned) is for declaring variables in the scope of the
# let.
function _let_code_block(ex::Expr)
@assert isexpr(ex, :let)
return ex.args[2]
end
# generates code which converts an expression into a NodeData array (tape)
# parent is the index of the parent expression
# values is the name of the list of constants which appear in the expression
function _parse_NL_expr(m, x, tapevar, parent, values)
if isexpr(x,:call) && length(x.args) >= 2 && (isexpr(x.args[2],:generator) || isexpr(x.args[2],:flatten))
header = x.args[1]
if _is_sum(header)
operatorid = operator_to_id[:+]
elseif _is_prod(header)
operatorid = operator_to_id[:*]
else
error("Unrecognized expression $header(...)")
end
codeblock = :(let; end)
block = _let_code_block(codeblock)
push!(block.args, :(push!($tapevar, NodeData(CALL, $operatorid, $parent))))
parentvar = gensym()
push!(block.args, :($parentvar = length($tapevar)))
code = _parse_gen(x.args[2], t -> _parse_NL_expr(m, t, tapevar, parentvar, values))
push!(block.args, code)
return codeblock
end
if isexpr(x, :call)
if _is_sum(x.args[1]) || _is_prod(x.args[1])
opname = x.args[1]
errorstring = "$opname() can appear in nonlinear expressions " *
" only if the argument is a generator statement, for example, " *
"$opname(x[i] for i in 1:N)."
return :(error($errorstring))
end
if length(x.args) == 2 && !isexpr(x.args[2], :...) # univariate
code = :(let; end)
block = _let_code_block(code)
@assert isexpr(block, :block)
if haskey(univariate_operator_to_id,x.args[1])
operatorid = univariate_operator_to_id[x.args[1]]
push!(block.args, :(push!($tapevar, NodeData(CALLUNIVAR, $operatorid, $parent))))
else
opname = quot(x.args[1])
errorstring = "Unrecognized function \"$(x.args[1])\" used in nonlinear expression."
errorstring2 = "Incorrect number of arguments for \"$(x.args[1])\" in nonlinear expression."
lookupcode = quote
if $(esc(m)).nlp_data === nothing
error($errorstring)
end
if !haskey($(esc(m)).nlp_data.user_operators.univariate_operator_to_id,$opname)
if haskey($(esc(m)).nlp_data.user_operators.multivariate_operator_to_id,$opname)
error($errorstring2)
else
error($errorstring)
end
end
operatorid = $(esc(m)).nlp_data.user_operators.univariate_operator_to_id[$opname] + _Derivatives.USER_UNIVAR_OPERATOR_ID_START - 1
end
push!(block.args, :($lookupcode; push!($tapevar, NodeData(CALLUNIVAR, operatorid, $parent))))
end
parentvar = gensym()
push!(block.args, :($parentvar = length($tapevar)))
push!(block.args, _parse_NL_expr(m, x.args[2], tapevar, parentvar, values))
return code
else
code = :(let; end)
block = _let_code_block(code)
@assert isexpr(block, :block)
if haskey(operator_to_id,x.args[1]) # fast compile-time lookup
operatorid = operator_to_id[x.args[1]]
push!(block.args, :(push!($tapevar, NodeData(CALL, $operatorid, $parent))))
elseif haskey(comparison_operator_to_id,x.args[1])
operatorid = comparison_operator_to_id[x.args[1]]
push!(block.args, :(push!($tapevar, NodeData(COMPARISON, $operatorid, $parent))))
else # could be user defined
opname = quot(x.args[1])
errorstring = "Unrecognized function \"$(x.args[1])\" used in nonlinear expression."
errorstring2 = "Incorrect number of arguments for \"$(x.args[1])\" in nonlinear expression."
lookupcode = quote
if $(esc(m)).nlp_data === nothing
error($errorstring)
end
if !haskey($(esc(m)).nlp_data.user_operators.multivariate_operator_to_id,$opname)
if haskey($(esc(m)).nlp_data.user_operators.univariate_operator_to_id,$opname)
error($errorstring2)
else
error($errorstring)
end
end
operatorid = $(esc(m)).nlp_data.user_operators.multivariate_operator_to_id[$opname] + _Derivatives.USER_OPERATOR_ID_START - 1
end
push!(block.args, :($lookupcode; push!($tapevar, NodeData(CALL, operatorid, $parent))))
end
parentvar = gensym()
push!(block.args, :($parentvar = length($tapevar)))
for i in 1:length(x.args)-1
arg = x.args[i + 1]
if isexpr(arg, :...)
if !isa(arg.args[1], Symbol)
error("Unexpected expression in $x. JuMP supports " *
"splatting only symbols. For example, x... is " *
"ok, but (x + 1)..., [x; y]... and g(f(y)...) " *
"are not.")
end
push!(block.args, quote
for val in $(esc(arg.args[1]))
_parse_NL_expr_runtime($(esc(m)), val,
$tapevar, $parentvar, $values)
end
end)
else
push!(block.args, _parse_NL_expr(m, arg, tapevar, parentvar,
values))
end
end
return code
end
end
if isexpr(x, :comparison)
code = :(let; end)
block = _let_code_block(code)
op = x.args[2]
operatorid = comparison_operator_to_id[op]
for k in 2:2:length(x.args)-1
@assert x.args[k] == op # don't handle a <= b >= c
end
parentvar = gensym()
push!(block.args, :(push!($tapevar, NodeData(COMPARISON, $operatorid, $parent))))
push!(block.args, :($parentvar = length($tapevar)))
for k in 1:2:length(x.args)
push!(block.args, _parse_NL_expr(m, x.args[k], tapevar, parentvar, values))
end
return code
end
if isexpr(x, :&&) || isexpr(x, :||)
code = :(let; end)
block = _let_code_block(code)
op = x.head
operatorid = logic_operator_to_id[op]
parentvar = gensym()
push!(block.args, :(push!($tapevar, NodeData(LOGIC, $operatorid, $parent))))
push!(block.args, :($parentvar = length($tapevar)))
push!(block.args, _parse_NL_expr(m, x.args[1], tapevar, parentvar, values))
push!(block.args, _parse_NL_expr(m, x.args[2], tapevar, parentvar, values))
return code
end
if isexpr(x, :curly)
_error_curly(x)
end
if isexpr(x, :...)
error("Unexpected splatting expression $x.")
end
# at the lowest level?
return :( _parse_NL_expr_runtime($(esc(m)),$(esc(x)), $tapevar, $parent, $values) )
end
function _parse_NL_expr_runtime(m::Model, x::Real, tape, parent, values)
push!(values, x)
push!(tape, NodeData(VALUE, length(values), parent))
nothing
end
function _parse_NL_expr_runtime(m::Model, x::VariableRef, tape, parent, values)
if owner_model(x) !== m
error("Variable in nonlinear expression does not belong to the " *
"corresponding model")
end
push!(tape, NodeData(MOIVARIABLE, x.index.value, parent))
nothing
end
function _parse_NL_expr_runtime(m::Model, x::NonlinearExpression, tape, parent, values)
push!(tape, NodeData(SUBEXPRESSION, x.index, parent))
nothing
end
function _parse_NL_expr_runtime(m::Model, x::NonlinearParameter, tape, parent, values)
push!(tape, NodeData(PARAMETER, x.index, parent))
nothing
end
function _parse_NL_expr_runtime(m::Model, x::AbstractArray, tape, parent, values)
error("Unexpected array $x in nonlinear expression. Nonlinear expressions may contain only scalar expressions.")
end
function _parse_NL_expr_runtime(m::Model, x::GenericQuadExpr, tape, parent, values)
error("Unexpected quadratic expression $x in nonlinear expression. " *
"Quadratic expressions (e.g., created using @expression) and " *
"nonlinear expressions cannot be mixed.")
end
function _parse_NL_expr_runtime(m::Model, x::GenericAffExpr, tape, parent, values)
error("Unexpected affine expression $x in nonlinear expression. " *
"Affine expressions (e.g., created using @expression) and " *
"nonlinear expressions cannot be mixed.")
end
function _parse_NL_expr_runtime(m::Model, x, tape, parent, values)
error("Unexpected object $x (of type $(typeof(x)) in nonlinear expression.")
end
function _expression_complexity(ex::Expr)
return isempty(ex.args) ? 1 : sum(_expression_complexity, ex.args)
end
_expression_complexity(other) = 1
# This is separated from the macro version to make it available for other @NL*
# macros.
function _process_NL_expr(model, ex)
# This is an arbitrary cutoff. See issue #1355.
if _expression_complexity(ex) > 5000
@warn "Processing a very large nonlinear expression with " *
"@NLexpression/@NLconstraint/@NLobjective. This may be very " *
"slow. Consider using setNLobjective() and addNLconstraint() " *
"instead of the macros or reformulating the expressions using " *
"sum() and prod() to make them more compact. The macros are " *
"designed to process smaller, human-readable expressions."
end
parsed = _parse_NL_expr(model, ex, :tape, -1, :values)
return quote
tape = NodeData[]
values = Float64[]
$parsed
_NonlinearExprData(tape, values)
end
end
macro _process_NL_expr(model, ex)
return _process_NL_expr(model, ex)
end
function _Derivatives.expr_to_nodedata(ex::VariableRef,
nd::Vector{NodeData},
values::Vector{Float64}, parentid,
r::_Derivatives.UserOperatorRegistry)
push!(nd, NodeData(MOIVARIABLE, ex.index.value, parentid))
nothing
end
function _Derivatives.expr_to_nodedata(ex::NonlinearExpression,
nd::Vector{NodeData},
values::Vector{Float64}, parentid,
r::_Derivatives.UserOperatorRegistry)
push!(nd, NodeData(SUBEXPRESSION, ex.index, parentid))
nothing
end
function _Derivatives.expr_to_nodedata(ex::NonlinearParameter,
nd::Vector{NodeData},
values::Vector{Float64}, parentid,
r::_Derivatives.UserOperatorRegistry)
push!(nd, NodeData(PARAMETER, ex.index, parentid))
nothing
end
# Construct a _NonlinearExprData from a Julia expression.
# VariableRef objects should be spliced into the expression.
function _NonlinearExprData(m::Model, ex::Expr)
_init_NLP(m)
_check_expr(m,ex)
nd, values = _Derivatives.expr_to_nodedata(ex,m.nlp_data.user_operators)
return _NonlinearExprData(nd, values)
end
_NonlinearExprData(m::Model, ex) = _NonlinearExprData(m, :($ex + 0))
# Error if:
# 1) Unexpected expression
# 2) VariableRef doesn't match the model
function _check_expr(m::Model, ex::Expr)
if ex.head == :ref # if we have x[1] already in there, something is wrong
error("Unrecognized expression $ex. JuMP variable objects and input coefficients should be spliced directly into expressions.")
end
for e in ex.args
_check_expr(m, e)
end
return
end
function _check_expr(m::Model, v::VariableRef)
owner_model(v) === m || error("Variable $v does not belong to this model.")
return
end
_check_expr(m::Model, ex) = nothing