Skip to content

NL.copy_to does not check for attributes #1514

@odow

Description

@odow

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions