-
Notifications
You must be signed in to change notification settings - Fork 94
Closed
Labels
Description
MathOptInterface.jl/src/FileFormats/NL/NL.jl
Lines 243 to 430 in 6970f3e
function MOI.copy_to( | |
dest::Model, | |
model::MOI.ModelLike; | |
copy_names::Bool = false, | |
) | |
mapping = MOI.Utilities.IndexMap() | |
# Initialize the NLP block. | |
nlp_block = try | |
MOI.get(model, MOI.NLPBlock()) | |
catch | |
MOI.NLPBlockData(MOI.NLPBoundsPair[], _LinearNLPEvaluator(), false) | |
end | |
if !(:ExprGraph in MOI.features_available(nlp_block.evaluator)) | |
error( | |
"Unable to use AmplNLWriter because the nonlinear evaluator " * | |
"does not supply expression graphs.", | |
) | |
end | |
MOI.initialize(nlp_block.evaluator, [:ExprGraph]) | |
# Objective function. | |
if nlp_block.has_objective | |
dest.f = _NLExpr(MOI.objective_expr(nlp_block.evaluator)) | |
else | |
F = MOI.get(model, MOI.ObjectiveFunctionType()) | |
obj = MOI.get(model, MOI.ObjectiveFunction{F}()) | |
dest.f = _NLExpr(obj) | |
end | |
# Nonlinear constraints | |
for (i, bound) in enumerate(nlp_block.constraint_bounds) | |
push!( | |
dest.g, | |
_NLConstraint(MOI.constraint_expr(nlp_block.evaluator, i), bound), | |
) | |
end | |
dest.nlpblock_dim = length(dest.g) | |
starts = MOI.supports(model, MOI.VariablePrimalStart(), MOI.VariableIndex) | |
for x in MOI.get(model, MOI.ListOfVariableIndices()) | |
dest.x[x] = _VariableInfo() | |
if starts | |
start = MOI.get(model, MOI.VariablePrimalStart(), x) | |
MOI.set(dest, MOI.VariablePrimalStart(), x, start) | |
end | |
mapping[x] = x | |
end | |
dest.sense = MOI.get(model, MOI.ObjectiveSense()) | |
resize!(dest.order, length(dest.x)) | |
# Now deal with the normal MOI constraints. | |
for (F, S) in MOI.get(model, MOI.ListOfConstraintTypesPresent()) | |
_process_constraint(dest, model, F, S, mapping) | |
end | |
# Correct bounds of binary variables. Mainly because AMPL doesn't have the | |
# concept of binary nonlinear variables, but it does have binary linear | |
# variables! How annoying. | |
for (x, v) in dest.x | |
if v.type == _BINARY | |
v.lower = max(0.0, v.lower) | |
v.upper = min(1.0, v.upper) | |
end | |
end | |
# Jacobian counts. The zero terms for nonlinear constraints should have | |
# been added when the expression was constructed. | |
for g in dest.g, v in keys(g.expr.linear_terms) | |
dest.x[v].jacobian_count += 1 | |
end | |
for h in dest.h, v in keys(h.expr.linear_terms) | |
dest.x[v].jacobian_count += 1 | |
end | |
# Now comes the confusing part. | |
# | |
# AMPL, in all its wisdom, orders variables in a _very_ specific way. | |
# The only hint in "Writing NL files" is the line "Variables are ordered as | |
# described in Tables 3 and 4 of [5]". | |
# | |
# Reading these | |
# | |
# https://cfwebprod.sandia.gov/cfdocs/CompResearch/docs/nlwrite20051130.pdf | |
# https://ampl.com/REFS/hooking2.pdf | |
# | |
# leads us to the following order | |
# | |
# 1) Continuous variables that appear in a | |
# nonlinear objective AND a nonlinear constraint | |
# 2) Discrete variables that appear in a | |
# nonlinear objective AND a nonlinear constraint | |
# 3) Continuous variables that appear in a | |
# nonlinear constraint, but NOT a nonlinear objective | |
# 4) Discrete variables that appear in a | |
# nonlinear constraint, but NOT a nonlinear objective | |
# 5) Continuous variables that appear in a | |
# nonlinear objective, but NOT a nonlinear constraint | |
# 6) Discrete variables that appear in a | |
# nonlinear objective, but NOT a nonlinear constraint | |
# 7) Continuous variables that DO NOT appear in a | |
# nonlinear objective or a nonlinear constraint | |
# 8) Binary variables that DO NOT appear in a | |
# nonlinear objective or a nonlinear constraint | |
# 9) Integer variables that DO NOT appear in a | |
# nonlinear objective or a nonlinear constraint | |
# | |
# Yes, nonlinear variables are broken into continuous/discrete, but linear | |
# variables are partitioned into continuous, binary, and integer. (See also, | |
# the need to modify bounds for binary variables.) | |
if !dest.f.is_linear | |
for x in keys(dest.f.linear_terms) | |
dest.x[x].in_nonlinear_objective = true | |
end | |
for x in dest.f.nonlinear_terms | |
if x isa MOI.VariableIndex | |
dest.x[x].in_nonlinear_objective = true | |
end | |
end | |
end | |
for con in dest.g | |
for x in keys(con.expr.linear_terms) | |
dest.x[x].in_nonlinear_constraint = true | |
end | |
for x in con.expr.nonlinear_terms | |
if x isa MOI.VariableIndex | |
dest.x[x].in_nonlinear_constraint = true | |
end | |
end | |
end | |
types = dest.types | |
for (x, v) in dest.x | |
if v.in_nonlinear_constraint && v.in_nonlinear_objective | |
push!(v.type == _CONTINUOUS ? types[1] : types[2], x) | |
elseif v.in_nonlinear_constraint | |
push!(v.type == _CONTINUOUS ? types[3] : types[4], x) | |
elseif v.in_nonlinear_objective | |
push!(v.type == _CONTINUOUS ? types[5] : types[6], x) | |
elseif v.type == _CONTINUOUS | |
push!(types[7], x) | |
elseif v.type == _BINARY | |
push!(types[8], x) | |
else | |
@assert v.type == _INTEGER | |
push!(types[9], x) | |
end | |
end | |
# However! Don't let Tables 3 and 4 fool you, because the ordering actually | |
# depends on whether the number of nonlinear variables in the objective only | |
# is _strictly_ greater than the number of nonlinear variables in the | |
# constraints only. Quoting: | |
# | |
# For all versions, the first nlvc variables appear nonlinearly in at | |
# least one constraint. If nlvo > nlvc, the first nlvc variables may or | |
# may not appear nonlinearly in an objective, but the next nlvo – nlvc | |
# variables do appear nonlinearly in at least one objective. Otherwise | |
# all of the first nlvo variables appear nonlinearly in an objective. | |
# | |
# However, even this is slightly incorrect, because I think it should read | |
# "For all versions, the first nlvb variables appear nonlinearly." The "nlvo | |
# - nlvc" part is also clearly incorrect, and should probably read "nlvo - | |
# nlvb." | |
# | |
# It's a bit confusing, so here is the relevant code from Couenne: | |
# https://github.com/coin-or/Couenne/blob/683c5b305d78a009d59268a4bca01e0ad75ebf02/src/readnl/readnl.cpp#L76-L87 | |
# | |
# They interpret this paragraph to mean the switch on nlvo > nlvc determines | |
# whether the next block of variables are the ones that appear in the | |
# objective only, or the constraints only. | |
# | |
# That makes sense as a design choice, because you can read them in two | |
# contiguous blocks. | |
# | |
# Essentially, what all this means is if !(nlvo > nlvc), then swap 3-4 for | |
# 5-6 in the variable order. | |
nlvc = length(types[3]) + length(types[4]) | |
nlvo = length(types[5]) + length(types[6]) | |
order_i = if nlvo > nlvc | |
[1, 2, 3, 4, 5, 6, 7, 8, 9] | |
else | |
[1, 2, 5, 6, 3, 4, 7, 8, 9] | |
end | |
# Now we can order the variables. | |
n = 0 | |
for i in order_i | |
# Since variables come from a dictionary, there may be differences in | |
# the order depending on platform and Julia version. Sort by creation | |
# time for consistency. | |
for x in sort!(types[i]; by = y -> y.value) | |
dest.x[x].order = n | |
dest.order[n+1] = x | |
n += 1 | |
end | |
end | |
return mapping | |
end |
Causes AmplNLWriter to fail tests:
test_model_copy_to_UnsupportedConstraint: Test Failed at /Users/oscar/.julia/packages/MathOptInterface/2NqE9/src/Test/test_model.jl:605
Expression: MOI.copy_to(model, BadConstraintModel())
Expected: MathOptInterface.UnsupportedConstraint
No exception thrown
Stacktrace:
[1] test_model_copy_to_UnsupportedConstraint(model::MathOptInterface.Utilities.CachingOptimizer{MathOptInterface.Bridges.LazyBridgeOptimizer{MathOptInterface.Utilities.CachingOptimizer{AmplNLWriter.Optimizer, MathOptInterface.Utilities.UniversalFallback{MathOptInterface.Utilities.GenericModel{Float64, MathOptInterface.Utilities.ModelFunctionConstraints{Float64}}}}}, MathOptInterface.Utilities.UniversalFallback{MathOptInterface.Utilities.GenericModel{Float64, MathOptInterface.Utilities.ModelFunctionConstraints{Float64}}}}, #unused#::MathOptInterface.Test.Config{Float64})
@ MathOptInterface.Test ~/.julia/packages/MathOptInterface/2NqE9/src/Test/test_model.jl:605
[2] macro expansion
@ ~/.julia/packages/MathOptInterface/2NqE9/src/Test/Test.jl:187 [inlined]
[3] macro expansion
@ /Users/julia/buildbot/worker/package_macos64/build/usr/share/julia/stdlib/v1.6/Test/src/Test.jl:1151 [inlined]
[4] runtests(model::MathOptInterface.Utilities.CachingOptimizer{MathOptInterface.Bridges.LazyBridgeOptimizer{MathOptInterface.Utilities.CachingOptimizer{AmplNLWriter.Optimizer, MathOptInterface.Utilities.UniversalFallback{MathOptInterface.Utilities.GenericModel{Float64, MathOptInterface.Utilities.ModelFunctionConstraints{Float64}}}}}, MathOptInterface.Utilities.UniversalFallback{MathOptInterface.Utilities.GenericModel{Float64, MathOptInterface.Utilities.ModelFunctionConstraints{Float64}}}}, config::MathOptInterface.Test.Config{Float64}; include::Vector{String}, exclude::Vector{String}, warn_unsupported::Bool)
@ MathOptInterface.Test ~/.julia/packages/MathOptInterface/2NqE9/src/Test/Test.jl:181