From 6e44bd451343f405bf825ee7bca4975ede27dced Mon Sep 17 00:00:00 2001 From: fpacaud Date: Mon, 19 Sep 2022 09:42:06 -0500 Subject: [PATCH] update MOI wrapper - upgrade MOI wrapper to incorporate latest development in Ipopt.jl - test integration of `QPBlockData` in MadNLP --- src/Interfaces/MOI_interface.jl | 1611 +++++++++++++------------------ src/Interfaces/utils.jl | 533 ++++++++++ test/MOI_interface_test.jl | 55 +- 3 files changed, 1217 insertions(+), 982 deletions(-) create mode 100644 src/Interfaces/utils.jl diff --git a/src/Interfaces/MOI_interface.jl b/src/Interfaces/MOI_interface.jl index 47565460..1137a89c 100644 --- a/src/Interfaces/MOI_interface.jl +++ b/src/Interfaces/MOI_interface.jl @@ -1,857 +1,699 @@ # MadNLP.jl # Modified from Ipopt.jl (https://github.com/jump-dev/Ipopt.jl) -@kwdef mutable struct VariableInfo - lower_bound::Float64 = -Inf - has_lower_bound::Bool = false - lower_bound_dual_start::Union{Nothing, Float64} = nothing - upper_bound::Float64 = Inf - has_upper_bound::Bool = false - upper_bound_dual_start::Union{Nothing, Float64} = nothing - is_fixed::Bool = false - start::Union{Nothing,Float64} = nothing -end +include("utils.jl") -mutable struct ConstraintInfo{F, S} - func::F - set::S - dual_start::Union{Nothing, Float64} -end -ConstraintInfo(func, set) = ConstraintInfo(func, set, nothing) +""" + Optimizer() +Create a new MadNLP optimizer. +""" mutable struct Optimizer <: MOI.AbstractOptimizer solver::Union{Nothing,MadNLPSolver} nlp::Union{Nothing,AbstractNLPModel} result::Union{Nothing,MadNLPExecutionStats{Float64}} - # Problem data. - variable_info::Vector{VariableInfo} - nlp_data::MOI.NLPBlockData + name::String + invalid_model::Bool + silent::Bool + options::Dict{Symbol,Any} + solve_time::Float64 sense::MOI.OptimizationSense - objective::Union{ - MOI.VariableIndex,MOI.ScalarAffineFunction{Float64},MOI.ScalarQuadraticFunction{Float64},Nothing} - - linear_le_constraints::Vector{ConstraintInfo{MOI.ScalarAffineFunction{Float64},MOI.LessThan{Float64}}} - linear_ge_constraints::Vector{ConstraintInfo{MOI.ScalarAffineFunction{Float64},MOI.GreaterThan{Float64}}} - linear_eq_constraints::Vector{ConstraintInfo{MOI.ScalarAffineFunction{Float64},MOI.EqualTo{Float64}}} - quadratic_le_constraints::Vector{ConstraintInfo{MOI.ScalarQuadraticFunction{Float64},MOI.LessThan{Float64}}} - quadratic_ge_constraints::Vector{ConstraintInfo{MOI.ScalarQuadraticFunction{Float64},MOI.GreaterThan{Float64}}} - quadratic_eq_constraints::Vector{ConstraintInfo{MOI.ScalarQuadraticFunction{Float64},MOI.EqualTo{Float64}}} - nlp_dual_start::Union{Nothing,Vector{Float64}} - # Parameters. - option_dict::Dict{Symbol, Any} + variables::MOI.Utilities.VariablesContainer{Float64} + variable_primal_start::Vector{Union{Nothing,Float64}} + mult_x_L::Vector{Union{Nothing,Float64}} + mult_x_U::Vector{Union{Nothing,Float64}} - # Solution attributes. - solve_time::Float64 + nlp_data::MOI.NLPBlockData + nlp_dual_start::Union{Nothing,Vector{Float64}} + + qp_data::QPBlockData{Float64} end -function Optimizer(;kwargs...) +function Optimizer(; kwargs...) option_dict = Dict{Symbol, Any}() for (name, value) in kwargs option_dict[name] = value end - return Optimizer(nothing,nothing,nothing,[],empty_nlp_data(),MOI.FEASIBILITY_SENSE, - nothing,[],[],[],[],[],[],nothing,option_dict,NaN) + return Optimizer( + nothing, + nothing, + nothing, + "", + false, + false, + option_dict, + NaN, + MOI.FEASIBILITY_SENSE, + MOI.Utilities.VariablesContainer{Float64}(), + Union{Nothing,Float64}[], + Union{Nothing,Float64}[], + Union{Nothing,Float64}[], + MOI.NLPBlockData([], _EmptyNLPEvaluator(), false), + nothing, + QPBlockData{Float64}(), + ) +end + +const _SETS = + Union{MOI.GreaterThan{Float64},MOI.LessThan{Float64},MOI.EqualTo{Float64}} + +const _FUNCTIONS = Union{ + MOI.ScalarAffineFunction{Float64}, + MOI.ScalarQuadraticFunction{Float64}, +} + +MOI.get(::Optimizer, ::MOI.SolverVersion) = "3.14.4" + +### _EmptyNLPEvaluator + +struct _EmptyNLPEvaluator <: MOI.AbstractNLPEvaluator end + +MOI.features_available(::_EmptyNLPEvaluator) = [:Grad, :Jac, :Hess] +MOI.initialize(::_EmptyNLPEvaluator, ::Any) = nothing +MOI.eval_constraint(::_EmptyNLPEvaluator, g, x) = nothing +MOI.jacobian_structure(::_EmptyNLPEvaluator) = Tuple{Int64,Int64}[] +MOI.hessian_lagrangian_structure(::_EmptyNLPEvaluator) = Tuple{Int64,Int64}[] +MOI.eval_constraint_jacobian(::_EmptyNLPEvaluator, J, x) = nothing +MOI.eval_hessian_lagrangian(::_EmptyNLPEvaluator, H, x, σ, μ) = nothing + +function MOI.empty!(model::Optimizer) + model.solver = nothing + model.invalid_model = false + model.sense = MOI.FEASIBILITY_SENSE + MOI.empty!(model.variables) + empty!(model.variable_primal_start) + empty!(model.mult_x_L) + empty!(model.mult_x_U) + model.nlp_data = MOI.NLPBlockData([], _EmptyNLPEvaluator(), false) + model.nlp_dual_start = nothing + model.qp_data = QPBlockData{Float64}() + return +end + +function MOI.is_empty(model::Optimizer) + return MOI.is_empty(model.variables) && + isempty(model.variable_primal_start) && + isempty(model.mult_x_L) && + isempty(model.mult_x_U) && + model.nlp_data.evaluator isa _EmptyNLPEvaluator && + model.sense == MOI.FEASIBILITY_SENSE end -# define empty nlp evaluator -struct EmptyNLPEvaluator <: MOI.AbstractNLPEvaluator end -MOI.features_available(::EmptyNLPEvaluator) = [:Grad, :Jac, :Hess] -MOI.initialize(::EmptyNLPEvaluator, features) = nothing -MOI.eval_objective(::EmptyNLPEvaluator, x) = NaN -MOI.eval_constraint(::EmptyNLPEvaluator, g, x) = @assert length(g) == 0 -MOI.eval_objective_gradient(::EmptyNLPEvaluator, g, x) = fill!(g, 0.0) -MOI.jacobian_structure(::EmptyNLPEvaluator) = Tuple{Int,Int}[] -MOI.hessian_lagrangian_structure(::EmptyNLPEvaluator) = Tuple{Int,Int}[] -MOI.eval_constraint_jacobian(::EmptyNLPEvaluator, J, x) = nothing -MOI.eval_hessian_lagrangian(::EmptyNLPEvaluator, H, x, σ, μ) = nothing -empty_nlp_data() = MOI.NLPBlockData([], EmptyNLPEvaluator(), false) +MOI.supports_incremental_interface(::Optimizer) = true -# for throw_if_valid -MOI.is_valid(model::Optimizer, vi::MOI.VariableIndex) = vi.value in eachindex(model.variable_info) -function MOI.is_valid(model::Optimizer, ci::MOI.ConstraintIndex{MOI.VariableIndex, MOI.LessThan{Float64}}) - vi = MOI.VariableIndex(ci.value) - return MOI.is_valid(model, vi) && has_upper_bound(model, vi) +function MOI.copy_to(model::Optimizer, src::MOI.ModelLike) + return MOI.Utilities.default_copy_to(model, src) end -function MOI.is_valid(model::Optimizer, ci::MOI.ConstraintIndex{MOI.VariableIndex, MOI.GreaterThan{Float64}}) - vi = MOI.VariableIndex(ci.value) - return MOI.is_valid(model, vi) && has_lower_bound(model, vi) + +MOI.get(::Optimizer, ::MOI.SolverName) = "MadNLP" + +function MOI.supports_constraint( + ::Optimizer, + ::Type{<:Union{MOI.VariableIndex,_FUNCTIONS}}, + ::Type{<:_SETS}, +) + return true end -function MOI.is_valid(model::Optimizer, ci::MOI.ConstraintIndex{MOI.VariableIndex, MOI.EqualTo{Float64}}) - vi = MOI.VariableIndex(ci.value) - return MOI.is_valid(model, vi) && is_fixed(model, vi) + +### MOI.ListOfConstraintTypesPresent + +function MOI.get(model::Optimizer, attr::MOI.ListOfConstraintTypesPresent) + ret = MOI.get(model.variables, attr) + append!(ret, MOI.get(model.qp_data, attr)) + return ret end +### MOI.Name -# supported obj/var/cons -MOI.supports(::Optimizer, ::MOI.NLPBlock) = true -MOI.supports(::Optimizer,::MOI.ObjectiveFunction{MOI.VariableIndex}) = true -MOI.supports(::Optimizer,::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}) = true -MOI.supports(::Optimizer,::MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}) = true -MOI.supports(::Optimizer, ::MOI.VariablePrimalStart,::Type{MOI.VariableIndex}) = true -MOI.supports_constraint(::Optimizer,::Type{MOI.VariableIndex},::Type{MOI.LessThan{Float64}})=true -MOI.supports_constraint(::Optimizer,::Type{MOI.VariableIndex},::Type{MOI.GreaterThan{Float64}})=true -MOI.supports_constraint(::Optimizer,::Type{MOI.VariableIndex},::Type{MOI.EqualTo{Float64}})=true -MOI.supports_constraint(::Optimizer,::Type{MOI.ScalarAffineFunction{Float64}},::Type{MOI.LessThan{Float64}})=true -MOI.supports_constraint(::Optimizer,::Type{MOI.ScalarAffineFunction{Float64}},::Type{MOI.GreaterThan{Float64}})=true -MOI.supports_constraint(::Optimizer,::Type{MOI.ScalarAffineFunction{Float64}},::Type{MOI.EqualTo{Float64}})=true -MOI.supports_constraint(::Optimizer,::Type{MOI.ScalarQuadraticFunction{Float64}},::Type{MOI.LessThan{Float64}})=true -MOI.supports_constraint(::Optimizer,::Type{MOI.ScalarQuadraticFunction{Float64}},::Type{MOI.GreaterThan{Float64}})=true -MOI.supports_constraint(::Optimizer,::Type{MOI.ScalarQuadraticFunction{Float64}},::Type{MOI.EqualTo{Float64}})=true - -## TODO -MOI.supports_constraint(::Optimizer,::Type{MOI.ScalarAffineFunction{Float64}},::Type{MOI.Interval{Float64}})=false -MOI.supports_constraint(::Optimizer,::Type{MOI.ScalarQuadraticFunction{Float64}},::Type{MOI.Interval{Float64}})=false +MOI.supports(::Optimizer, ::MOI.Name) = true -MOI.get(::Optimizer, ::MOI.SolverName) = "MadNLP" -MOI.get(::Optimizer, ::MOI.SolverVersion) = version() -MOI.get(model::Optimizer,::MOI.ObjectiveFunctionType)=typeof(model.objective) -MOI.get(model::Optimizer,::MOI.NumberOfVariables)=length(model.variable_info) -MOI.get(model::Optimizer,::MOI.NumberOfConstraints{MOI.ScalarAffineFunction{Float64},MOI.LessThan{Float64}})=length(model.linear_le_constraints) -MOI.get(model::Optimizer,::MOI.NumberOfConstraints{MOI.ScalarAffineFunction{Float64},MOI.EqualTo{Float64}})=length(model.linear_eq_constraints) -MOI.get(model::Optimizer,::MOI.NumberOfConstraints{MOI.ScalarAffineFunction{Float64},MOI.GreaterThan{Float64}})=length(model.linear_ge_constraints) -MOI.get(model::Optimizer,::MOI.NumberOfConstraints{MOI.VariableIndex,MOI.LessThan{Float64}})=count(e->e.has_upper_bound,model.variable_info) -MOI.get(model::Optimizer,::MOI.NumberOfConstraints{MOI.VariableIndex,MOI.EqualTo{Float64}})=count(e->e.is_fixed,model.variable_info) -MOI.get(model::Optimizer,::MOI.NumberOfConstraints{MOI.VariableIndex,MOI.GreaterThan{Float64}})=count(e->e.has_lower_bound,model.variable_info) -MOI.get(model::Optimizer,::MOI.ObjectiveFunction) = model.objective -MOI.get(model::Optimizer,::MOI.ListOfVariableIndices) = [MOI.VariableIndex(i) for i in 1:length(model.variable_info)] -MOI.get(model::Optimizer,::MOI.BarrierIterations) = model.solver.cnt.k - -macro define_get_con_attr(attr,function_type, set_type, prefix, attrfield) - array_name = Symbol(string(prefix) * "_constraints") - quote - function MOI.get(model::Optimizer, ::$attr, - c::MOI.ConstraintIndex{$function_type,$set_type}) - return model.$(array_name)[c.value].$attrfield - end - end +function MOI.set(model::Optimizer, ::MOI.Name, value::String) + model.name = value + return end -@define_get_con_attr(MOI.ConstraintSet,MOI.ScalarAffineFunction{Float64},MOI.LessThan{Float64},linear_le,set) -@define_get_con_attr(MOI.ConstraintSet,MOI.ScalarAffineFunction{Float64},MOI.GreaterThan{Float64},linear_ge,set) -@define_get_con_attr(MOI.ConstraintSet,MOI.ScalarAffineFunction{Float64},MOI.EqualTo{Float64},linear_eq,set) -@define_get_con_attr(MOI.ConstraintSet,MOI.ScalarQuadraticFunction{Float64},MOI.LessThan{Float64},quadratic_le,set) -@define_get_con_attr(MOI.ConstraintSet,MOI.ScalarQuadraticFunction{Float64},MOI.GreaterThan{Float64},quadratic_ge,set) -@define_get_con_attr(MOI.ConstraintSet,MOI.ScalarQuadraticFunction{Float64},MOI.EqualTo{Float64},quadratic_eq,set) +MOI.get(model::Optimizer, ::MOI.Name) = model.name -@define_get_con_attr(MOI.ConstraintFunction,MOI.ScalarAffineFunction{Float64},MOI.LessThan{Float64},linear_le,func) -@define_get_con_attr(MOI.ConstraintFunction,MOI.ScalarAffineFunction{Float64},MOI.GreaterThan{Float64},linear_ge,func) -@define_get_con_attr(MOI.ConstraintFunction,MOI.ScalarAffineFunction{Float64},MOI.EqualTo{Float64},linear_eq,func) +### MOI.Silent +MOI.supports(::Optimizer, ::MOI.Silent) = true -function MOI.get(model::Optimizer,::MOI.ConstraintSet, - c::MOI.ConstraintIndex{MOI.VariableIndex, MOI.LessThan{Float64}}) - return MOI.LessThan{Float64}(model.variable_info[c.value].upper_bound) +function MOI.set(model::Optimizer, ::MOI.Silent, value) + model.silent = value + return end -function MOI.get(model::Optimizer,::MOI.ConstraintSet, - c::MOI.ConstraintIndex{MOI.VariableIndex, MOI.EqualTo{Float64}}) - return MOI.EqualTo{Float64}(model.variable_info[c.value].lower_bound) +MOI.get(model::Optimizer, ::MOI.Silent) = model.silent + +### MOI.TimeLimitSec + +MOI.supports(::Optimizer, ::MOI.TimeLimitSec) = true + +function MOI.set(model::Optimizer, ::MOI.TimeLimitSec, value::Real) + MOI.set(model, MOI.RawOptimizerAttribute("max_cpu_time"), Float64(value)) + return end -function MOI.get(model::Optimizer,::MOI.ConstraintSet, - c::MOI.ConstraintIndex{MOI.VariableIndex, MOI.GreaterThan{Float64}}) - return MOI.GreaterThan{Float64}(model.variable_info[c.value].lower_bound) +function MOI.set(model::Optimizer, ::MOI.TimeLimitSec, ::Nothing) + delete!(model.options, "max_cpu_time") + return end -function MOI.get(model::Optimizer,::MOI.ConstraintFunction, - c::MOI.ConstraintIndex{MOI.VariableIndex, MOI.LessThan{Float64}}) - return MOI.VariableIndex(c.value) + +function MOI.get(model::Optimizer, ::MOI.TimeLimitSec) + return get(model.options, "max_cpu_time", nothing) end -function MOI.get(model::Optimizer,::MOI.ConstraintFunction, - c::MOI.ConstraintIndex{MOI.VariableIndex, MOI.EqualTo{Float64}}) - return MOI.VariableIndex(c.value) + +### MOI.RawOptimizerAttribute + +MOI.supports(::Optimizer, ::MOI.RawOptimizerAttribute) = true + +function MOI.set(model::Optimizer, p::MOI.RawOptimizerAttribute, value) + model.options[p.name] = value + # No need to reset model.solver because this gets handled in optimize!. + return end -function MOI.get( - model::Optimizer,::MOI.ConstraintFunction, - c::MOI.ConstraintIndex{MOI.VariableIndex, MOI.GreaterThan{Float64}}) - return MOI.VariableIndex(c.value) + +function MOI.get(model::Optimizer, p::MOI.RawOptimizerAttribute) + if !haskey(model.options, p.name) + error("RawParameter with name $(p.name) is not set.") + end + return model.options[p.name] end +### Variables +""" + column(x::MOI.VariableIndex) -function MOI.get(model::Optimizer, ::MOI.ListOfConstraintTypesPresent) - constraints = Set{Tuple{DataType, DataType}}() +Return the column associated with a variable. +""" +column(x::MOI.VariableIndex) = x.value - for info in model.variable_info - info.has_lower_bound && push!(constraints, (MOI.VariableIndex, MOI.LessThan{Float64})) - info.has_upper_bound && push!(constraints, (MOI.VariableIndex, MOI.GreaterThan{Float64})) - info.is_fixed && push!(constraints, (MOI.VariableIndex, MOI.EqualTo{Float64})) - end +function MOI.add_variable(model::Optimizer) + push!(model.variable_primal_start, nothing) + push!(model.mult_x_L, nothing) + push!(model.mult_x_U, nothing) + model.solver = nothing + return MOI.add_variable(model.variables) +end - isempty(model.linear_le_constraints) || - push!(constraints,(MOI.ScalarAffineFunction{Float64},MOI.LessThan{Float64})) - isempty(model.linear_ge_constraints) || - push!(constraints, (MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64})) - isempty(model.linear_eq_constraints) || - push!(constraints, (MOI.ScalarAffineFunction{Float64}, MOI.EqualTo{Float64})) - isempty(model.quadratic_le_constraints) || - push!(constraints, (MOI.ScalarQuadraticFunction{Float64}, MOI.LessThan{Float64})) - isempty(model.quadratic_ge_constraints) || - push!(constraints, (MOI.ScalarQuadraticFunction{Float64}, MOI.GreaterThan{Float64})) - isempty(model.quadratic_eq_constraints) || - push!(constraints, (MOI.ScalarQuadraticFunction{Float64}, MOI.EqualTo{Float64})) - - return collect(constraints) +function MOI.is_valid(model::Optimizer, x::MOI.VariableIndex) + return MOI.is_valid(model.variables, x) end + function MOI.get( model::Optimizer, - ::MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{Float64}, MOI.LessThan{Float64}} + attr::Union{MOI.NumberOfVariables,MOI.ListOfVariableIndices}, ) - return MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}, MOI.LessThan{Float64}}.(eachindex(model.linear_le_constraints)) + return MOI.get(model.variables, attr) end -function MOI.get( + +function MOI.is_valid( model::Optimizer, - ::MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{Float64}, MOI.EqualTo{Float64}} + ci::MOI.ConstraintIndex{MOI.VariableIndex,<:_SETS}, ) - return MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}, MOI.EqualTo{Float64}}.(eachindex(model.linear_eq_constraints)) + return MOI.is_valid(model.variables, ci) end + function MOI.get( model::Optimizer, - ::MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64}} + attr::Union{ + MOI.NumberOfConstraints{MOI.VariableIndex,<:_SETS}, + MOI.ListOfConstraintIndices{MOI.VariableIndex,<:_SETS}, + }, ) - return MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64}}.(eachindex(model.linear_ge_constraints)) -end -function MOI.get(model::Optimizer, ::MOI.ListOfConstraintIndices{MOI.VariableIndex, MOI.LessThan{Float64}}) - dict = Dict(model.variable_info[i] => i for i in 1:length(model.variable_info)) - filter!(info -> info.first.has_upper_bound, dict) - return MOI.ConstraintIndex{MOI.VariableIndex, MOI.LessThan{Float64}}.(values(dict)) -end -function MOI.get(model::Optimizer, ::MOI.ListOfConstraintIndices{MOI.VariableIndex, MOI.EqualTo{Float64}}) - dict = Dict(model.variable_info[i] => i for i in 1:length(model.variable_info)) - filter!(info -> info.first.is_fixed, dict) - return MOI.ConstraintIndex{MOI.VariableIndex, MOI.EqualTo{Float64}}.(values(dict)) -end -function MOI.get(model::Optimizer, ::MOI.ListOfConstraintIndices{MOI.VariableIndex, MOI.GreaterThan{Float64}}) - dict = Dict(model.variable_info[i] => i for i in 1:length(model.variable_info)) - filter!(info -> info.first.has_lower_bound, dict) - return MOI.ConstraintIndex{MOI.VariableIndex, MOI.GreaterThan{Float64}}.(values(dict)) + return MOI.get(model.variables, attr) end +function MOI.get( + model::Optimizer, + attr::Union{MOI.ConstraintFunction,MOI.ConstraintSet}, + c::MOI.ConstraintIndex{MOI.VariableIndex,<:_SETS}, +) + return MOI.get(model.variables, attr, c) +end -function MOI.set(model::Optimizer, ::MOI.ConstraintSet, - ci::MOI.ConstraintIndex{MOI.VariableIndex, MOI.LessThan{Float64}}, - set::MOI.LessThan{Float64}) - MOI.throw_if_not_valid(model, ci) - model.variable_info[ci.value].upper_bound = set.upper - return +function MOI.add_constraint(model::Optimizer, x::MOI.VariableIndex, set::_SETS) + index = MOI.add_constraint(model.variables, x, set) + model.solver = nothing + return index end -function MOI.set(model::Optimizer, ::MOI.ConstraintSet, - ci::MOI.ConstraintIndex{MOI.VariableIndex, MOI.GreaterThan{Float64}}, - set::MOI.GreaterThan{Float64}) - MOI.throw_if_not_valid(model, ci) - model.variable_info[ci.value].lower_bound = set.lower +function MOI.set( + model::Optimizer, + ::MOI.ConstraintSet, + ci::MOI.ConstraintIndex{MOI.VariableIndex,S}, + set::S, +) where {S<:_SETS} + MOI.set(model.variables, MOI.ConstraintSet(), ci, set) + model.solver = nothing return end -function MOI.set(model::Optimizer, ::MOI.ConstraintSet, - ci::MOI.ConstraintIndex{MOI.VariableIndex, MOI.EqualTo{Float64}}, - set::MOI.EqualTo{Float64}) - MOI.throw_if_not_valid(model, ci) - model.variable_info[ci.value].lower_bound = set.value - model.variable_info[ci.value].upper_bound = set.value +function MOI.delete( + model::Optimizer, + ci::MOI.ConstraintIndex{MOI.VariableIndex,<:_SETS}, +) + MOI.delete(model.variables, ci) + model.solver = nothing return end -# default_copy_to -MOI.supports_incremental_interface(model::Optimizer) = true -MOI.copy_to(model::Optimizer, src::MOI.ModelLike) = MOIU.default_copy_to(model, src) +### ScalarAffineFunction and ScalarQuadraticFunction constraints -# objective sense -MOI.supports(::Optimizer,::MOI.ObjectiveSense) = true -MOI.set(model::Optimizer, ::MOI.ObjectiveSense,sense::MOI.OptimizationSense) = (model.sense = sense) -MOI.get(model::Optimizer, ::MOI.ObjectiveSense) = model.sense - -# silence -const SILENT_KEY = :print_level -const SILENT_VAL = ERROR -MOI.supports(::Optimizer,::MOI.Silent) = true -MOI.set(model::Optimizer, ::MOI.Silent, value::Bool)= value ? - MOI.set(model,MOI.RawOptimizerAttribute(String(SILENT_KEY)),SILENT_VAL) : delete!(model.option_dict,SILENT_KEY) -MOI.get(model::Optimizer, ::MOI.Silent) = get(model.option_dict,SILENT_KEY,String) == SILENT_VAL - -# time limit -const TIME_LIMIT = :max_wall_time -MOI.supports(::Optimizer,::MOI.TimeLimitSec) = true -MOI.set(model::Optimizer,::MOI.TimeLimitSec,value)= value isa Real ? - MOI.set(model,MOI.RawOptimizerAttribute(String(TIME_LIMIT)),Float64(value)) : error("Invalid time limit: $value") -MOI.set(model::Optimizer,::MOI.TimeLimitSec,::Nothing)=delete!(model.option_dict,TIME_LIMIT) -MOI.get(model::Optimizer,::MOI.TimeLimitSec)=get(model.option_dict,TIME_LIMIT,Float64) - -# set/get options -MOI.supports(::Optimizer,::MOI.RawOptimizerAttribute) = true -MOI.set(model::Optimizer, p::MOI.RawOptimizerAttribute, value) = (model.option_dict[Symbol(p.name)] = value) -MOI.get(model::Optimizer, p::MOI.RawOptimizerAttribute) = haskey(model.option_dict, Symbol(p.name)) ? - (return model.option_dict[Symbol(p.name)]) : error("RawOptimizerAttribute with name $(p.name) is not set.") - -# solve time -MOI.get(model::Optimizer, ::MOI.SolveTimeSec) = model.solver.cnt.total_time - -function MOI.empty!(model::Optimizer) - # empty!(model.option_dict) - empty!(model.variable_info) - model.nlp_data = empty_nlp_data() - model.sense = MOI.FEASIBILITY_SENSE - model.objective = nothing - empty!(model.linear_le_constraints) - empty!(model.linear_ge_constraints) - empty!(model.linear_eq_constraints) - empty!(model.quadratic_le_constraints) - empty!(model.quadratic_ge_constraints) - empty!(model.quadratic_eq_constraints) - model.nlp_dual_start = nothing - model.solver = nothing - model.nlp = nothing - model.result = nothing +function MOI.is_valid( + model::Optimizer, + ci::MOI.ConstraintIndex{<:_FUNCTIONS,<:_SETS}, +) + return MOI.is_valid(model.qp_data, ci) end -function MOI.is_empty(model::Optimizer) - return isempty(model.variable_info) && - model.nlp_data.evaluator isa EmptyNLPEvaluator && - model.sense == MOI.FEASIBILITY_SENSE && - isempty(model.linear_le_constraints) && - isempty(model.linear_ge_constraints) && - isempty(model.linear_eq_constraints) && - isempty(model.quadratic_le_constraints) && - isempty(model.quadratic_ge_constraints) && - isempty(model.quadratic_eq_constraints) +function MOI.add_constraint(model::Optimizer, func::_FUNCTIONS, set::_SETS) + index = MOI.add_constraint(model.qp_data, func, set) + model.solver = nothing + return index end -function MOI.add_variable(model::Optimizer) - push!(model.variable_info, VariableInfo()) - return MOI.VariableIndex(length(model.variable_info)) +function MOI.get( + model::Optimizer, + attr::Union{MOI.NumberOfConstraints{F,S},MOI.ListOfConstraintIndices{F,S}}, +) where {F<:_FUNCTIONS,S<:_SETS} + return MOI.get(model.qp_data, attr) end -MOI.add_variables(model::Optimizer, n::Int) = [MOI.add_variable(model) for i in 1:n] -# checking inbounds -function check_inbounds(model::Optimizer, vi::MOI.VariableIndex) - num_variables = length(model.variable_info) - if !(1 <= vi.value <= num_variables) - error("Invalid variable index $vi. ($num_variables variables in the model.)") - end -end -function check_inbounds(model::Optimizer, aff::MOI.ScalarAffineFunction) - for term in aff.terms - check_inbounds(model, term.variable) - end -end -function check_inbounds(model::Optimizer, quad::MOI.ScalarQuadraticFunction) - for term in quad.affine_terms - check_inbounds(model, term.variable) - end - for term in quad.quadratic_terms - check_inbounds(model, term.variable_1) - check_inbounds(model, term.variable_2) - end +function MOI.get( + model::Optimizer, + attr::Union{ + MOI.ConstraintFunction, + MOI.ConstraintSet, + MOI.ConstraintDualStart, + }, + c::MOI.ConstraintIndex{F,S}, +) where {F<:_FUNCTIONS,S<:_SETS} + return MOI.get(model.qp_data, attr, c) end -has_upper_bound(model::Optimizer, vi::MOI.VariableIndex) = model.variable_info[vi.value].has_upper_bound -has_lower_bound(model::Optimizer, vi::MOI.VariableIndex) = model.variable_info[vi.value].has_lower_bound -is_fixed(model::Optimizer, vi::MOI.VariableIndex) = model.variable_info[vi.value].is_fixed - -function MOI.delete(model::Optimizer, ci::MOI.ConstraintIndex{MOI.VariableIndex, MOI.LessThan{Float64}}) - MOI.throw_if_not_valid(model, ci) - model.variable_info[ci.value].upper_bound = Inf - model.variable_info[ci.value].has_upper_bound = false +function MOI.set( + model::Optimizer, + ::MOI.ConstraintSet, + ci::MOI.ConstraintIndex{F,S}, + set::S, +) where {F<:_FUNCTIONS,S<:_SETS} + MOI.set(model.qp_data, MOI.ConstraintSet(), ci, set) + model.solver = nothing return end -function MOI.delete(model::Optimizer, ci::MOI.ConstraintIndex{MOI.VariableIndex, MOI.GreaterThan{Float64}}) - MOI.throw_if_not_valid(model, ci) - model.variable_info[ci.value].lower_bound = -Inf - model.variable_info[ci.value].has_lower_bound = false - return +function MOI.supports( + ::Optimizer, + ::MOI.ConstraintDualStart, + ::Type{MOI.ConstraintIndex{F,S}}, +) where {F<:_FUNCTIONS,S<:_SETS} + return true end -function MOI.delete(model::Optimizer, ci::MOI.ConstraintIndex{MOI.VariableIndex, MOI.EqualTo{Float64}}) +function MOI.set( + model::Optimizer, + attr::MOI.ConstraintDualStart, + ci::MOI.ConstraintIndex{F,S}, + value::Union{Real,Nothing}, +) where {F<:_FUNCTIONS,S<:_SETS} MOI.throw_if_not_valid(model, ci) - model.variable_info[ci.value].lower_bound = -Inf - model.variable_info[ci.value].upper_bound = Inf - model.variable_info[ci.value].is_fixed = false + MOI.set(model.qp_data, attr, ci, value) + # No need to reset model.solver, because this gets handled in optimize!. return end +### MOI.VariablePrimalStart -function MOI.add_constraint(model::Optimizer, vi::MOI.VariableIndex, lt::MOI.LessThan{Float64}) - check_inbounds(model, vi) - if isnan(lt.upper) - error("Invalid upper bound value $(lt.upper).") - end - if has_upper_bound(model, vi) - throw(MOI.UpperBoundAlreadySet{MOI.LessThan{Float64}, typeof(lt)}(vi)) - end - if is_fixed(model, vi) - throw(MOI.UpperBoundAlreadySet{MOI.EqualTo{Float64}, typeof(lt)}(vi)) - end - model.variable_info[vi.value].upper_bound = lt.upper - model.variable_info[vi.value].has_upper_bound = true - return MOI.ConstraintIndex{MOI.VariableIndex, MOI.LessThan{Float64}}(vi.value) +function MOI.supports( + ::Optimizer, + ::MOI.VariablePrimalStart, + ::Type{MOI.VariableIndex}, +) + return true end -function MOI.add_constraint(model::Optimizer, vi::MOI.VariableIndex, gt::MOI.GreaterThan{Float64}) - check_inbounds(model, vi) - if isnan(gt.lower) - error("Invalid lower bound value $(gt.lower).") - end - if has_lower_bound(model, vi) - throw(MOI.LowerBoundAlreadySet{MOI.GreaterThan{Float64}, typeof(gt)}(vi)) - end - if is_fixed(model, vi) - throw(MOI.LowerBoundAlreadySet{MOI.EqualTo{Float64}, typeof(gt)}(vi)) - end - model.variable_info[vi.value].lower_bound = gt.lower - model.variable_info[vi.value].has_lower_bound = true - return MOI.ConstraintIndex{MOI.VariableIndex, MOI.GreaterThan{Float64}}(vi.value) +function MOI.set( + model::Optimizer, + ::MOI.VariablePrimalStart, + vi::MOI.VariableIndex, + value::Union{Real,Nothing}, +) + MOI.throw_if_not_valid(model, vi) + model.variable_primal_start[column(vi)] = value + # No need to reset model.solver, because this gets handled in optimize!. + return end -function MOI.add_constraint(model::Optimizer, vi::MOI.VariableIndex, eq::MOI.EqualTo{Float64}) - check_inbounds(model, vi) - if isnan(eq.value) - error("Invalid fixed value $(gt.lower).") - end - if has_lower_bound(model, vi) - throw(MOI.LowerBoundAlreadySet{MOI.GreaterThan{Float64}, typeof(eq)}(vi)) - end - if has_upper_bound(model, vi) - throw(MOI.UpperBoundAlreadySet{MOI.LessThan{Float64}, typeof(eq)}(vi)) - end - if is_fixed(model, vi) - throw(MOI.LowerBoundAlreadySet{MOI.EqualTo{Float64}, typeof(eq)}(vi)) - end - model.variable_info[vi.value].lower_bound = eq.value - model.variable_info[vi.value].upper_bound = eq.value - model.variable_info[vi.value].is_fixed = true - return MOI.ConstraintIndex{MOI.VariableIndex, MOI.EqualTo{Float64}}(vi.value) -end - -macro define_add_constraint(function_type, set_type, prefix) - array_name = Symbol(string(prefix) * "_constraints") - quote - function MOI.add_constraint(model::Optimizer, func::$function_type, set::$set_type) - check_inbounds(model, func) - push!(model.$(array_name), ConstraintInfo(func, set)) - return MOI.ConstraintIndex{$function_type, $set_type}(length(model.$(array_name))) - end - end -end +### MOI.ConstraintDualStart -@define_add_constraint(MOI.ScalarAffineFunction{Float64}, MOI.LessThan{Float64},linear_le) -@define_add_constraint(MOI.ScalarAffineFunction{Float64},MOI.GreaterThan{Float64}, linear_ge) -@define_add_constraint(MOI.ScalarAffineFunction{Float64}, MOI.EqualTo{Float64},linear_eq) -@define_add_constraint(MOI.ScalarQuadraticFunction{Float64},MOI.LessThan{Float64}, quadratic_le) -@define_add_constraint(MOI.ScalarQuadraticFunction{Float64},MOI.GreaterThan{Float64}, quadratic_ge) -@define_add_constraint(MOI.ScalarQuadraticFunction{Float64},MOI.EqualTo{Float64}, quadratic_eq) +_dual_start(::Optimizer, ::Nothing, ::Int = 1) = 0.0 -function MOI.set(model::Optimizer, ::MOI.VariablePrimalStart, - vi::MOI.VariableIndex, value::Union{Real, Nothing}) - check_inbounds(model, vi) - model.variable_info[vi.value].start = value - return +function _dual_start(model::Optimizer, value::Real, scale::Int = 1) + return value * scale end -function MOI.supports(model::Optimizer, ::MOI.ConstraintDualStart, - ci::MOI.ConstraintIndex{MOI.VariableIndex, MOI.GreaterThan{Float64}}, - value::Union{Real, Nothing}) +function MOI.supports( + ::Optimizer, + ::MOI.ConstraintDualStart, + ::Type{MOI.ConstraintIndex{MOI.VariableIndex,S}}, +) where {S<:_SETS} return true end -function MOI.set(model::Optimizer, ::MOI.ConstraintDualStart, - ci::MOI.ConstraintIndex{MOI.VariableIndex, MOI.GreaterThan{Float64}}, - value::Union{Real, Nothing}) - vi = MOI.VariableIndex(ci.value) - check_inbounds(model, vi) - model.variable_info[vi.value].lower_bound_dual_start = value + +function MOI.set( + model::Optimizer, + ::MOI.ConstraintDualStart, + ci::MOI.ConstraintIndex{MOI.VariableIndex,MOI.GreaterThan{Float64}}, + value::Union{Real,Nothing}, +) + MOI.throw_if_not_valid(model, ci) + model.mult_x_L[ci.value] = value + # No need to reset model.solver, because this gets handled in optimize!. return end -function MOI.supports(model::Optimizer, ::MOI.ConstraintDualStart, - ci::MOI.ConstraintIndex{MOI.VariableIndex, MOI.LessThan{Float64}}, - value::Union{Real, Nothing}) - return true + +function MOI.get( + model::Optimizer, + ::MOI.ConstraintDualStart, + ci::MOI.ConstraintIndex{MOI.VariableIndex,MOI.GreaterThan{Float64}}, +) + MOI.throw_if_not_valid(model, ci) + return model.mult_x_L[ci.value] end -function MOI.set(model::Optimizer, ::MOI.ConstraintDualStart, - ci::MOI.ConstraintIndex{MOI.VariableIndex, MOI.LessThan{Float64}}, - value::Union{Real, Nothing}) - vi = MOI.VariableIndex(ci.value) - check_inbounds(model, vi) - model.variable_info[vi.value].upper_bound_dual_start = value + +function MOI.set( + model::Optimizer, + ::MOI.ConstraintDualStart, + ci::MOI.ConstraintIndex{MOI.VariableIndex,MOI.LessThan{Float64}}, + value::Union{Real,Nothing}, +) + MOI.throw_if_not_valid(model, ci) + model.mult_x_U[ci.value] = value + # No need to reset model.solver, because this gets handled in optimize!. return end -function MOI.supports(model::Optimizer, ::MOI.ConstraintDualStart, - ci::MOI.ConstraintIndex{MOI.VariableIndex, MOI.EqualTo{Float64}}, - value::Union{Real, Nothing}) - return true + +function MOI.get( + model::Optimizer, + ::MOI.ConstraintDualStart, + ci::MOI.ConstraintIndex{MOI.VariableIndex,MOI.LessThan{Float64}}, +) + MOI.throw_if_not_valid(model, ci) + return model.mult_x_U[ci.value] end -function MOI.set(model::Optimizer, ::MOI.ConstraintDualStart, - ci::MOI.ConstraintIndex{MOI.VariableIndex, MOI.EqualTo{Float64}}, - value::Union{Real, Nothing}) - vi = MOI.VariableIndex(ci.value) - check_inbounds(model, vi) - model.variable_info[vi.value].upper_bound_dual_start = value - model.variable_info[vi.value].lower_bound_dual_start = value + +function MOI.set( + model::Optimizer, + ::MOI.ConstraintDualStart, + ci::MOI.ConstraintIndex{MOI.VariableIndex,MOI.EqualTo{Float64}}, + value::Union{Real,Nothing}, +) + MOI.throw_if_not_valid(model, ci) + if value === nothing + model.mult_x_L[ci.value] = nothing + model.mult_x_U[ci.value] = nothing + elseif value >= 0.0 + model.mult_x_L[ci.value] = value + model.mult_x_U[ci.value] = 0.0 + else + model.mult_x_L[ci.value] = 0.0 + model.mult_x_U[ci.value] = value + end + # No need to reset model.solver, because this gets handled in optimize!. return end -# NLPBlock Dual start +function MOI.get( + model::Optimizer, + ::MOI.ConstraintDualStart, + ci::MOI.ConstraintIndex{MOI.VariableIndex,MOI.EqualTo{Float64}}, +) + MOI.throw_if_not_valid(model, ci) + l = model.mult_x_L[ci.value] + u = model.mult_x_U[ci.value] + return (l === u === nothing) ? nothing : (l + u) +end + +### MOI.NLPBlockDualStart + MOI.supports(::Optimizer, ::MOI.NLPBlockDualStart) = true -MOI.set(model::Optimizer, ::MOI.NLPBlockDualStart, values) = (model.nlp_dual_start = -values) -MOI.set(model::Optimizer, ::MOI.NLPBlock, nlp_data::MOI.NLPBlockData) = (model.nlp_data = nlp_data) -function MOI.set(model::Optimizer, ::MOI.ObjectiveFunction, - func::Union{MOI.VariableIndex, MOI.ScalarAffineFunction,MOI.ScalarQuadraticFunction}) - check_inbounds(model, func) - model.objective = func +function MOI.set( + model::Optimizer, + ::MOI.NLPBlockDualStart, + values::Union{Nothing,Vector}, +) + model.nlp_dual_start = values + # No need to reset model.solver, because this gets handled in optimize!. return end -linear_le_offset(model::Optimizer) = 0 -linear_ge_offset(model::Optimizer) = length(model.linear_le_constraints) -linear_eq_offset(model::Optimizer) = linear_ge_offset(model) + length(model.linear_ge_constraints) -quadratic_le_offset(model::Optimizer) = linear_eq_offset(model) + length(model.linear_eq_constraints) -quadratic_ge_offset(model::Optimizer) = quadratic_le_offset(model) + length(model.quadratic_le_constraints) -quadratic_eq_offset(model::Optimizer) = quadratic_ge_offset(model) + length(model.quadratic_ge_constraints) -nlp_constraint_offset(model::Optimizer) = quadratic_eq_offset(model) + length(model.quadratic_eq_constraints) - -# Convenience functions used only in optimize! -function append_to_jacobian_sparsity!(I,J, - aff::MOI.ScalarAffineFunction, row, offset) - cnt = 0 - for term in aff.terms - I[offset+cnt]= row - J[offset+cnt]= term.variable.value - cnt += 1 - end - return cnt -end +MOI.get(model::Optimizer, ::MOI.NLPBlockDualStart) = model.nlp_dual_start -function append_to_jacobian_sparsity!(I,J, - quad::MOI.ScalarQuadraticFunction, row, offset) - cnt = 0 - for term in quad.affine_terms - I[offset+cnt]= row - J[offset+cnt]= term.variable.value - cnt += 1 - end - for term in quad.quadratic_terms - row_idx = term.variable_1 - col_idx = term.variable_2 - if row_idx == col_idx - I[offset+cnt]= row - J[offset+cnt]= row_idx.value - cnt += 1 - else - I[offset+cnt]= row - J[offset+cnt]= row_idx.value - I[offset+cnt+1]= row - J[offset+cnt+1]= col_idx.value - cnt += 2 - end - end - return cnt -end +### MOI.NLPBlock -# Refers to local variables in jacobian_structure() below. -macro append_to_jacobian_sparsity(array_name) - escrow = esc(:row) - escoffset = esc(:offset) - quote - for info in $(esc(array_name)) - $escoffset += append_to_jacobian_sparsity!($(esc(:I)), $(esc(:J)),info.func, $escrow, $escoffset) - $escrow += 1 - end - end -end +MOI.supports(::Optimizer, ::MOI.NLPBlock) = true -function jacobian_structure(model::Optimizer,I,J) - offset = 1 - row = 1 +function MOI.set(model::Optimizer, ::MOI.NLPBlock, nlp_data::MOI.NLPBlockData) + model.nlp_data = nlp_data + model.solver = nothing + return +end - @append_to_jacobian_sparsity model.linear_le_constraints - @append_to_jacobian_sparsity model.linear_ge_constraints - @append_to_jacobian_sparsity model.linear_eq_constraints - @append_to_jacobian_sparsity model.quadratic_le_constraints - @append_to_jacobian_sparsity model.quadratic_ge_constraints - @append_to_jacobian_sparsity model.quadratic_eq_constraints +### ObjectiveSense - cnt = 0 - for (nlp_row, nlp_col) in MOI.jacobian_structure(model.nlp_data.evaluator) - I[offset+cnt] = nlp_row + row - 1 - J[offset+cnt] = nlp_col - cnt+=1 - end +MOI.supports(::Optimizer, ::MOI.ObjectiveSense) = true - return I,J +function MOI.set( + model::Optimizer, + ::MOI.ObjectiveSense, + sense::MOI.OptimizationSense, +) + model.sense = sense + model.solver = nothing + return end -append_to_hessian_sparsity!(I,J,::Union{MOI.VariableIndex,MOI.ScalarAffineFunction},offset) = 0 +MOI.get(model::Optimizer, ::MOI.ObjectiveSense) = model.sense -function append_to_hessian_sparsity!(I,J,quad::MOI.ScalarQuadraticFunction,offset) - cnt = 0 - for term in quad.quadratic_terms - I[offset+cnt]=term.variable_1.value - J[offset+cnt]=term.variable_2.value - cnt+=1 - end - return cnt +### ObjectiveFunction + +function MOI.get( + model::Optimizer, + attr::Union{MOI.ObjectiveFunctionType,MOI.ObjectiveFunction}, +) + return MOI.get(model.qp_data, attr) end -function hessian_lagrangian_structure(model::Optimizer,I,J) - offset = 1 - if !model.nlp_data.has_objective && model.objective !== nothing - offset+=append_to_hessian_sparsity!(I,J,model.objective,offset) - end - for info in model.quadratic_le_constraints - offset+=append_to_hessian_sparsity!(I,J,info.func,offset) - end - for info in model.quadratic_ge_constraints - offset+=append_to_hessian_sparsity!(I,J,info.func,offset) - end - for info in model.quadratic_eq_constraints - offset+=append_to_hessian_sparsity!(I,J,info.func,offset) - end - cnt = 0 - for (row,col) in MOI.hessian_lagrangian_structure(model.nlp_data.evaluator) - I[offset+cnt]=row - J[offset+cnt]=col - cnt+=1 - end - return I,J -end - -get_nnz_hess(model::Optimizer) = get_nnz_hess_nonlinear(model) + get_nnz_hess_quadratic(model) -get_nnz_hess_nonlinear(model::Optimizer) = - ((!model.nlp_data.has_objective && model.objective isa MOI.ScalarQuadraticFunction) ? length(model.objective.quadratic_terms) : 0) + length(MOI.hessian_lagrangian_structure(model.nlp_data.evaluator)) -get_nnz_hess_quadratic(model::Optimizer) = - (isempty(model.quadratic_eq_constraints) ? 0 : sum(length(term.func.quadratic_terms) for term in model.quadratic_eq_constraints)) + - (isempty(model.quadratic_le_constraints) ? 0 : sum(length(term.func.quadratic_terms) for term in model.quadratic_le_constraints)) + - (isempty(model.quadratic_ge_constraints) ? 0 : sum(length(term.func.quadratic_terms) for term in model.quadratic_ge_constraints)) - -get_nnz_jac(model::Optimizer) = get_nnz_jac_linear(model) + get_nnz_jac_quadratic(model) + get_nnz_jac_nonlinear(model) -get_nnz_jac_linear(model::Optimizer) = - (isempty(model.linear_eq_constraints) ? 0 : sum(length(info.func.terms) for info in model.linear_eq_constraints)) + - (isempty(model.linear_le_constraints) ? 0 : sum(length(info.func.terms) for info in model.linear_le_constraints)) + - (isempty(model.linear_ge_constraints) ? 0 : sum(length(info.func.terms) for info in model.linear_ge_constraints)) -get_nnz_jac_quadratic(model::Optimizer) = - (isempty(model.quadratic_eq_constraints) ? 0 : sum(length(info.func.affine_terms) for info in model.quadratic_eq_constraints)) + - (isempty(model.quadratic_eq_constraints) ? 0 : sum(isempty(info.func.quadratic_terms) ? 0 : sum(term.variable_1 == term.variable_2 ? 1 : 2 for term in info.func.quadratic_terms) for info in model.quadratic_eq_constraints)) + - (isempty(model.quadratic_le_constraints) ? 0 : sum(length(info.func.affine_terms) for info in model.quadratic_le_constraints)) + - (isempty(model.quadratic_le_constraints) ? 0 : sum(isempty(info.func.quadratic_terms) ? 0 : sum(term.variable_1 == term.variable_2 ? 1 : 2 for term in info.func.quadratic_terms) for info in model.quadratic_le_constraints)) + - (isempty(model.quadratic_ge_constraints) ? 0 : sum(length(info.func.affine_terms) for info in model.quadratic_ge_constraints)) + - (isempty(model.quadratic_ge_constraints) ? 0 : sum(isempty(info.func.quadratic_terms) ? 0 : sum(term.variable_1 == term.variable_2 ? 1 : 2 for term in info.func.quadratic_terms) for info in model.quadratic_ge_constraints)) -get_nnz_jac_nonlinear(model::Optimizer) = - (isempty(model.nlp_data.constraint_bounds) ? 0 : length(MOI.jacobian_structure(model.nlp_data.evaluator))) - - -function eval_function(var::MOI.VariableIndex, x) - return x[var.value] -end - -function eval_function(aff::MOI.ScalarAffineFunction, x) - function_value = aff.constant - for term in aff.terms - function_value += term.coefficient*x[term.variable.value] - end - return function_value + +function MOI.supports( + ::Optimizer, + ::MOI.ObjectiveFunction{<:Union{MOI.VariableIndex,<:_FUNCTIONS}}, +) + return true end -function eval_function(quad::MOI.ScalarQuadraticFunction, x) - function_value = quad.constant +function MOI.set( + model::Optimizer, + attr::MOI.ObjectiveFunction{F}, + func::F, +) where {F<:Union{MOI.VariableIndex,<:_FUNCTIONS}} + MOI.set(model.qp_data, attr, func) + model.solver = nothing + return +end - for term in quad.affine_terms - function_value += term.coefficient*x[term.variable.value] - end +### Eval_F_CB - for term in quad.quadratic_terms - row_idx = term.variable_1 - col_idx = term.variable_2 - coefficient = term.coefficient - if row_idx == col_idx - function_value += 0.5*coefficient*x[row_idx.value]*x[col_idx.value] - else - function_value += coefficient*x[row_idx.value]*x[col_idx.value] - end +function MOI.eval_objective(model::Optimizer, x) + if model.sense == MOI.FEASIBILITY_SENSE + return 0.0 + elseif model.nlp_data.has_objective + return MOI.eval_objective(model.nlp_data.evaluator, x) end - return function_value + return MOI.eval_objective(model.qp_data, x) end -function eval_objective(model::Optimizer, x) - # The order of the conditions is important. NLP objectives override regular - # objectives. - if model.nlp_data.has_objective - return MOI.eval_objective(model.nlp_data.evaluator, x) - elseif model.objective !== nothing - return eval_function(model.objective, x) +### Eval_Grad_F_CB + +function MOI.eval_objective_gradient(model::Optimizer, grad, x) + if model.sense == MOI.FEASIBILITY_SENSE + grad .= zero(eltype(grad)) + elseif model.nlp_data.has_objective + MOI.eval_objective_gradient(model.nlp_data.evaluator, grad, x) else - # No objective function set. This could happen with FEASIBILITY_SENSE. - return 0.0 + MOI.eval_objective_gradient(model.qp_data, grad, x) end + return end -function fill_gradient!(grad, x, var::MOI.VariableIndex) - fill!(grad, 0.0) - grad[var.value] = 1.0 -end +### Eval_G_CB -function fill_gradient!(grad, x, aff::MOI.ScalarAffineFunction{Float64}) - fill!(grad, 0.0) - for term in aff.terms - grad[term.variable.value] += term.coefficient - end +function MOI.eval_constraint(model::Optimizer, g, x) + MOI.eval_constraint(model.qp_data, g, x) + g_nlp = view(g, (length(model.qp_data)+1):length(g)) + MOI.eval_constraint(model.nlp_data.evaluator, g_nlp, x) + return end -function fill_gradient!(grad, x, quad::MOI.ScalarQuadraticFunction{Float64}) - fill!(grad, 0.0) - for term in quad.affine_terms - grad[term.variable.value] += term.coefficient - end - for term in quad.quadratic_terms - row_idx = term.variable_1 - col_idx = term.variable_2 - coefficient = term.coefficient - if row_idx == col_idx - grad[row_idx.value] += coefficient*x[row_idx.value] - else - grad[row_idx.value] += coefficient*x[col_idx.value] - grad[col_idx.value] += coefficient*x[row_idx.value] +### Eval_Jac_G_CB + +function MOI.jacobian_structure(model::Optimizer) + J = MOI.jacobian_structure(model.qp_data) + offset = length(model.qp_data) + if length(model.nlp_data.constraint_bounds) > 0 + for (row, col) in MOI.jacobian_structure(model.nlp_data.evaluator) + push!(J, (row + offset, col)) end end + return J end -function eval_objective_gradient(model::Optimizer, grad, x) - if model.nlp_data.has_objective - MOI.eval_objective_gradient(model.nlp_data.evaluator, grad, x) - elseif model.objective !== nothing - fill_gradient!(grad, x, model.objective) - else - fill!(grad, 0.0) - end +function MOI.eval_constraint_jacobian(model::Optimizer, values, x) + offset = MOI.eval_constraint_jacobian(model.qp_data, values, x) + nlp_values = view(values, (offset+1):length(values)) + MOI.eval_constraint_jacobian(model.nlp_data.evaluator, nlp_values, x) return end -# Refers to local variables in eval_constraint() below. -macro eval_function(array_name) - escrow = esc(:row) - quote - for info in $(esc(array_name)) - $(esc(:g))[$escrow] = eval_function(info.func, $(esc(:x))) - $escrow += 1 - end - end +### Eval_H_CB + +function MOI.hessian_lagrangian_structure(model::Optimizer) + H = MOI.hessian_lagrangian_structure(model.qp_data) + append!(H, MOI.hessian_lagrangian_structure(model.nlp_data.evaluator)) + return H end -function eval_constraint(model::Optimizer, g, x) - row = 1 - @eval_function model.linear_le_constraints - @eval_function model.linear_ge_constraints - @eval_function model.linear_eq_constraints - @eval_function model.quadratic_le_constraints - @eval_function model.quadratic_ge_constraints - @eval_function model.quadratic_eq_constraints - nlp_g = view(g, row:length(g)) - MOI.eval_constraint(model.nlp_data.evaluator, nlp_g, x) + +function MOI.eval_hessian_lagrangian(model::Optimizer, H, x, σ, μ) + offset = MOI.eval_hessian_lagrangian(model.qp_data, H, x, σ, μ) + H_nlp = view(H, (offset+1):length(H)) + μ_nlp = view(μ, (length(model.qp_data)+1):length(μ)) + MOI.eval_hessian_lagrangian(model.nlp_data.evaluator, H_nlp, x, σ, μ_nlp) return end -function fill_constraint_jacobian!(values, start_offset, x, aff::MOI.ScalarAffineFunction) - num_coefficients = length(aff.terms) - for i in 1:num_coefficients - values[start_offset+i] = aff.terms[i].coefficient - end - return num_coefficients +### NLPModels wrapper +struct MOIModel{T} <: AbstractNLPModel{T,Vector{T}} + meta::NLPModelMeta{T, Vector{T}} + model::Optimizer + counters::NLPModels.Counters end -function fill_constraint_jacobian!(values, start_offset, x, quad::MOI.ScalarQuadraticFunction) - num_affine_coefficients = length(quad.affine_terms) - for i in 1:num_affine_coefficients - values[start_offset+i] = quad.affine_terms[i].coefficient - end - num_quadratic_coefficients = 0 - for term in quad.quadratic_terms - row_idx = term.variable_1 - col_idx = term.variable_2 - coefficient = term.coefficient - if row_idx == col_idx - values[start_offset+num_affine_coefficients+num_quadratic_coefficients+1] = coefficient*x[col_idx.value] - num_quadratic_coefficients += 1 - else - # Note that the order matches the Jacobian sparsity pattern. - values[start_offset+num_affine_coefficients+num_quadratic_coefficients+1] = coefficient*x[col_idx.value] - values[start_offset+num_affine_coefficients+num_quadratic_coefficients+2] = coefficient*x[row_idx.value] - num_quadratic_coefficients += 2 - end - end - return num_affine_coefficients + num_quadratic_coefficients +obj(nlp::MOIModel,x::Vector{Float64}) = MOI.eval_objective(nlp.model,x) + +function grad!(nlp::MOIModel,x::Vector{Float64},f::Vector{Float64}) + MOI.eval_objective_gradient(nlp.model,f,x) end -# Refers to local variables in eval_constraint_jacobian() below. -macro fill_constraint_jacobian(array_name) - esc_offset = esc(:offset) - quote - for info in $(esc(array_name)) - $esc_offset += fill_constraint_jacobian!($(esc(:values)), - $esc_offset, $(esc(:x)), - info.func) - end - end +function cons!(nlp::MOIModel,x::Vector{Float64},c::Vector{Float64}) + MOI.eval_constraint(nlp.model,c,x) end -function eval_constraint_jacobian(model::Optimizer, values, x) - offset = 0 - @fill_constraint_jacobian model.linear_le_constraints - @fill_constraint_jacobian model.linear_ge_constraints - @fill_constraint_jacobian model.linear_eq_constraints - @fill_constraint_jacobian model.quadratic_le_constraints - @fill_constraint_jacobian model.quadratic_ge_constraints - @fill_constraint_jacobian model.quadratic_eq_constraints +function jac_coord!(nlp::MOIModel,x::Vector{Float64},jac::Vector{Float64}) + MOI.eval_constraint_jacobian(nlp.model,jac,x) +end - nlp_values = view(values, 1+offset:length(values)) - MOI.eval_constraint_jacobian(model.nlp_data.evaluator, nlp_values, x) - return +function hess_coord!(nlp::MOIModel,x::Vector{Float64},l::Vector{Float64},hess::Vector{Float64}; obj_weight::Float64=1.) + MOI.eval_hessian_lagrangian(nlp.model,hess,x,obj_weight,l) end -function fill_hessian_lagrangian!(values, start_offset, scale_factor, - ::Union{MOI.VariableIndex, - MOI.ScalarAffineFunction,Nothing}) - return 0 +function hess_structure!(nlp::MOIModel, I::AbstractVector{T}, J::AbstractVector{T}) where T + cnt = 1 + for (row, col) in MOI.hessian_lagrangian_structure(nlp.model) + I[cnt], J[cnt] = row, col + cnt += 1 + end end -function fill_hessian_lagrangian!(values, start_offset, scale_factor, - quad::MOI.ScalarQuadraticFunction) - for i in 1:length(quad.quadratic_terms) - values[start_offset + i] = scale_factor*quad.quadratic_terms[i].coefficient +function jac_structure!(nlp::MOIModel, I::AbstractVector{T}, J::AbstractVector{T}) where T + cnt = 1 + for (row, col) in MOI.jacobian_structure(nlp.model) + I[cnt], J[cnt] = row, col + cnt += 1 end - return length(quad.quadratic_terms) end -function eval_hessian_lagrangian(model::Optimizer, values, x, obj_factor, lambda) - offset = 0 - if !model.nlp_data.has_objective - offset += fill_hessian_lagrangian!(values, 0, obj_factor, - model.objective) +### MOI.optimize! +function MOIModel(model::Optimizer) + :Hess in MOI.features_available(model.nlp_data.evaluator) || error("Hessian information is needed.") + MOI.initialize(model.nlp_data.evaluator, [:Grad,:Hess,:Jac]) + + # Initial variable + nvar = length(model.variables.lower) + x0 = Vector{Float64}(undef,nvar) + for i in 1:length(model.variable_primal_start) + x0[i] = if model.variable_primal_start[i] !== nothing + model.variable_primal_start[i] + else + clamp(0.0, model.variables.lower[i], model.variables.upper[i]) + end end - for (i, info) in enumerate(model.quadratic_le_constraints) - offset += fill_hessian_lagrangian!(values, offset, lambda[i+quadratic_le_offset(model)], info.func) + + # Constraints bounds + g_L, g_U = copy(model.qp_data.g_L), copy(model.qp_data.g_U) + for bound in model.nlp_data.constraint_bounds + push!(g_L, bound.lower) + push!(g_U, bound.upper) end - for (i, info) in enumerate(model.quadratic_ge_constraints) - offset += fill_hessian_lagrangian!(values, offset, lambda[i+quadratic_ge_offset(model)], info.func) + ncon = length(g_L) + + # Sparsity + jacobian_sparsity = MOI.jacobian_structure(model) + hessian_sparsity = MOI.hessian_lagrangian_structure(model) + nnzh = length(hessian_sparsity) + nnzj = length(jacobian_sparsity) + + # Dual multipliers + y0 = Vector{Float64}(undef,ncon) + for (i, start) in enumerate(model.qp_data.mult_g) + y0[i] = _dual_start(model, start, -1) end - for (i, info) in enumerate(model.quadratic_eq_constraints) - offset += fill_hessian_lagrangian!(values, offset, lambda[i+quadratic_eq_offset(model)], info.func) + offset = length(model.qp_data.mult_g) + if model.nlp_dual_start === nothing + y0[(offset+1):end] .= 0.0 + else + for (i, start) in enumerate(model.nlp_dual_start::Vector{Float64}) + y0[offset+i] = _dual_start(model, start, -1) + end end - nlp_values = view(values, 1 + offset : length(values)) - nlp_lambda = view(lambda, 1 + nlp_constraint_offset(model) : length(lambda)) - MOI.eval_hessian_lagrangian(model.nlp_data.evaluator, nlp_values, x, obj_factor, nlp_lambda) -end + # TODO + model.options[:jacobian_constant], model.options[:hessian_constant] = false, false + model.options[:dual_initialized] = !iszero(y0) + return MOIModel( + NLPModelMeta( + nvar, + x0 = x0, + lvar = model.variables.lower, + uvar = model.variables.upper, + ncon = ncon, + y0 = y0, + lcon = g_L, + ucon = g_U, + nnzj = nnzj, + nnzh = nnzh, + minimize = model.sense == MOI.MIN_SENSE + ), + model,NLPModels.Counters()) +end -MOI.get(model::Optimizer, ::MOI.TerminationStatus) = model.result === nothing ? - MOI.OPTIMIZE_NOT_CALLED : termination_status(model.result) -MOI.get(model::Optimizer, ::MOI.RawStatusString) = string(model.result.status) -MOI.get(model::Optimizer, ::MOI.ResultCount) = (model.result !== nothing) ? 1 : 0 -MOI.get(model::Optimizer, attr::MOI.PrimalStatus) = !(1 <= attr.result_index <= MOI.get(model, MOI.ResultCount())) ? - MOI.NO_SOLUTION : primal_status(model.result) -MOI.get(model::Optimizer, attr::MOI.DualStatus) = !(1 <= attr.result_index <= MOI.get(model, MOI.ResultCount())) ? - MOI.NO_SOLUTION : dual_status(model.result) +function MOI.optimize!(model::Optimizer) + model.nlp = MOIModel(model) + if model.silent + model.options[:print_level] = MadNLP.ERROR + end + model.solver = MadNLPSolver(model.nlp; model.options...) + model.result = solve!(model.solver) + model.solve_time = model.solver.cnt.total_time + return +end -const status_moi_dict = Dict( +# From Ipopt/src/Interfaces/IpReturnCodes_inc.h +const _STATUS_CODES = Dict( SOLVE_SUCCEEDED => MOI.LOCALLY_SOLVED, SOLVED_TO_ACCEPTABLE_LEVEL => MOI.ALMOST_LOCALLY_SOLVED, SEARCH_DIRECTION_BECOMES_TOO_SMALL => MOI.SLOW_PROGRESS, @@ -865,308 +707,179 @@ const status_moi_dict = Dict( ERROR_IN_STEP_COMPUTATION => MOI.NUMERICAL_ERROR, NOT_ENOUGH_DEGREES_OF_FREEDOM => MOI.INVALID_MODEL, USER_REQUESTED_STOP => MOI.INTERRUPTED, - INTERNAL_ERROR => MOI.OTHER_ERROR) -const status_primal_dict = Dict( - SOLVE_SUCCEEDED => MOI.FEASIBLE_POINT, - SOLVED_TO_ACCEPTABLE_LEVEL => MOI.NEARLY_FEASIBLE_POINT, - INFEASIBLE_PROBLEM_DETECTED => MOI.INFEASIBLE_POINT) -const status_dual_dict = Dict( - SOLVE_SUCCEEDED => MOI.FEASIBLE_POINT, - SOLVED_TO_ACCEPTABLE_LEVEL => MOI.NEARLY_FEASIBLE_POINT, - INFEASIBLE_PROBLEM_DETECTED => MOI.INFEASIBLE_POINT) - -termination_status(result::MadNLPExecutionStats) = haskey(status_moi_dict,result.status) ? status_moi_dict[result.status] : MOI.UNKNOWN_RESULT_STATUS -termination_status(solver::MadNLPSolver) = haskey(status_moi_dict,solver.status) ? status_moi_dict[solver.status] : MOI.UNKNOWN_RESULT_STATUS -primal_status(result::MadNLPExecutionStats) = haskey(status_primal_dict,result.status) ? - status_primal_dict[result.status] : MOI.UNKNOWN_RESULT_STATUS -dual_status(result::MadNLPExecutionStats) = haskey(status_dual_dict,result.status) ? - status_dual_dict[result.status] : MOI.UNKNOWN_RESULT_STATUS + INTERNAL_ERROR => MOI.OTHER_ERROR, +) +### MOI.ResultCount -function MOI.get(model::Optimizer, attr::MOI.ObjectiveValue) - MOI.check_result_index_bounds(model, attr) - scale = (model.sense == MOI.MAX_SENSE) ? -1 : 1 - return scale * model.result.objective -end - -function MOI.get(model::Optimizer, attr::MOI.VariablePrimal, vi::MOI.VariableIndex) - MOI.check_result_index_bounds(model, attr) - check_inbounds(model, vi) - return model.result.solution[vi.value] +# Ipopt always has an iterate available. +function MOI.get(model::Optimizer, ::MOI.ResultCount) + return (model.solver !== nothing) ? 1 : 0 end -macro define_constraint_primal(function_type, set_type, prefix) - constraint_array = Symbol(string(prefix) * "_constraints") - offset_function = Symbol(string(prefix) * "_offset") - quote - function MOI.get(model::Optimizer, attr::MOI.ConstraintPrimal, - ci::MOI.ConstraintIndex{$function_type, $set_type}) - MOI.check_result_index_bounds(model, attr) - if !(1 <= ci.value <= length(model.$(constraint_array))) - error("Invalid constraint index ", ci.value) - end - return model.result.constraints[ci.value + $offset_function(model)] - end - end -end -@define_constraint_primal(MOI.ScalarAffineFunction{Float64},MOI.LessThan{Float64}, linear_le) -@define_constraint_primal(MOI.ScalarAffineFunction{Float64},MOI.GreaterThan{Float64}, linear_ge) -@define_constraint_primal(MOI.ScalarAffineFunction{Float64},MOI.EqualTo{Float64}, linear_eq) -@define_constraint_primal(MOI.ScalarQuadraticFunction{Float64},MOI.LessThan{Float64}, quadratic_le) -@define_constraint_primal(MOI.ScalarQuadraticFunction{Float64},MOI.GreaterThan{Float64}, quadratic_ge) -@define_constraint_primal(MOI.ScalarQuadraticFunction{Float64},MOI.EqualTo{Float64}, quadratic_eq) +### MOI.RawStatusString -function MOI.get(model::Optimizer, attr::MOI.ConstraintPrimal, - ci::MOI.ConstraintIndex{MOI.VariableIndex, - MOI.LessThan{Float64}}) - MOI.check_result_index_bounds(model, attr) - vi = MOI.VariableIndex(ci.value) - check_inbounds(model, vi) - if !has_upper_bound(model, vi) - error("Variable $vi has no upper bound -- ConstraintPrimal not defined.") +function MOI.get(model::Optimizer, ::MOI.RawStatusString) + if model.invalid_model + return "The model has no variable" + elseif model.solver === nothing + return "Optimize not called" + else + return string(_STATUS_CODES[model.result.status]) end - return model.result.solution[vi.value] end -function MOI.get(model::Optimizer, attr::MOI.ConstraintPrimal, - ci::MOI.ConstraintIndex{MOI.VariableIndex, - MOI.GreaterThan{Float64}}) - MOI.check_result_index_bounds(model, attr) - vi = MOI.VariableIndex(ci.value) - check_inbounds(model, vi) - if !has_lower_bound(model, vi) - error("Variable $vi has no lower bound -- ConstraintPrimal not defined.") +### MOI.TerminationStatus +# +function MOI.get(model::Optimizer, ::MOI.TerminationStatus) + if model.invalid_model + return MOI.INVALID_MODEL + elseif model.solver === nothing + return MOI.OPTIMIZE_NOT_CALLED + end + if haskey(_STATUS_CODES, model.result.status) + return _STATUS_CODES[model.result.status] + else + return MOI.UNKNOWN_RESULT_STATUS end - return model.result.solution[vi.value] end -function MOI.get(model::Optimizer, attr::MOI.ConstraintPrimal, - ci::MOI.ConstraintIndex{MOI.VariableIndex, - MOI.EqualTo{Float64}}) - MOI.check_result_index_bounds(model, attr) - vi = MOI.VariableIndex(ci.value) - check_inbounds(model, vi) - if !is_fixed(model, vi) - error("Variable $vi is not fixed -- ConstraintPrimal not defined.") +### MOI.PrimalStatus + +function MOI.get(model::Optimizer, attr::MOI.PrimalStatus) + if !(1 <= attr.result_index <= MOI.get(model, MOI.ResultCount())) + return MOI.NO_SOLUTION + end + status = model.result.status + if status == SOLVE_SUCCEEDED + return MOI.FEASIBLE_POINT + elseif status == SOLVED_TO_ACCEPTABLE_LEVEL + return MOI.NEARLY_FEASIBLE_POINT + elseif status == INFEASIBLE_PROBLEM_DETECTED + return MOI.INFEASIBLE_POINT + else + return MOI.UNKNOWN_RESULT_STATUS end - return model.result.solution[vi.value] end -macro define_constraint_dual(function_type, set_type, prefix) - constraint_array = Symbol(string(prefix) * "_constraints") - offset_function = Symbol(string(prefix) * "_offset") - quote - function MOI.get(model::Optimizer, attr::MOI.ConstraintDual, - ci::MOI.ConstraintIndex{$function_type, $set_type}) - MOI.check_result_index_bounds(model, attr) - if !(1 <= ci.value <= length(model.$(constraint_array))) - error("Invalid constraint index ", ci.value) - end - return -1 * model.result.multipliers[ci.value + $offset_function(model)] - end +### MOI.DualStatus + +function MOI.get(model::Optimizer, attr::MOI.DualStatus) + if !(1 <= attr.result_index <= MOI.get(model, MOI.ResultCount())) + return MOI.NO_SOLUTION + end + status = model.result.status + if status == SOLVE_SUCCEEDED + return MOI.FEASIBLE_POINT + elseif status == SOLVED_TO_ACCEPTABLE_LEVEL + return MOI.NEARLY_FEASIBLE_POINT + elseif status == INFEASIBLE_PROBLEM_DETECTED + return MOI.INFEASIBLE_POINT + else + return MOI.UNKNOWN_RESULT_STATUS end end -@define_constraint_dual(MOI.ScalarAffineFunction{Float64},MOI.LessThan{Float64}, linear_le) -@define_constraint_dual(MOI.ScalarAffineFunction{Float64},MOI.GreaterThan{Float64}, linear_ge) -@define_constraint_dual(MOI.ScalarAffineFunction{Float64},MOI.EqualTo{Float64}, linear_eq) -@define_constraint_dual(MOI.ScalarQuadraticFunction{Float64},MOI.LessThan{Float64}, quadratic_le) -@define_constraint_dual(MOI.ScalarQuadraticFunction{Float64},MOI.GreaterThan{Float64}, quadratic_ge) -@define_constraint_dual(MOI.ScalarQuadraticFunction{Float64},MOI.EqualTo{Float64}, quadratic_eq) -function MOI.get(model::Optimizer, attr::MOI.ConstraintDual, - ci::MOI.ConstraintIndex{MOI.VariableIndex,MOI.LessThan{Float64}}) - MOI.check_result_index_bounds(model, attr) - vi = MOI.VariableIndex(ci.value) - check_inbounds(model, vi) - has_upper_bound(model, vi) || error("Variable $vi has no upper bound -- ConstraintDual not defined.") - return -1 * model.result.multipliers_U[vi.value] # MOI convention is for feasible LessThan duals to be nonpositive. -end +### MOI.SolveTimeSec -function MOI.get(model::Optimizer, attr::MOI.ConstraintDual, - ci::MOI.ConstraintIndex{MOI.VariableIndex, - MOI.GreaterThan{Float64}}) - MOI.check_result_index_bounds(model, attr) - vi = MOI.VariableIndex(ci.value) - check_inbounds(model, vi) - has_lower_bound(model, vi) || error("Variable $vi has no lower bound -- ConstraintDual not defined.") - return model.result.multipliers_L[vi.value] -end +MOI.get(model::Optimizer, ::MOI.SolveTimeSec) = model.solve_time -function MOI.get(model::Optimizer, attr::MOI.ConstraintDual, - ci::MOI.ConstraintIndex{MOI.VariableIndex, - MOI.EqualTo{Float64}}) - MOI.check_result_index_bounds(model, attr) - vi = MOI.VariableIndex(ci.value) - check_inbounds(model, vi) - if !is_fixed(model, vi) - error("Variable $vi is not fixed -- ConstraintDual not defined.") - end - return model.result.multipliers_L[vi.value] - model.result.multipliers_U[vi.value] -end +### MOI.ObjectiveValue -function MOI.get(model::Optimizer, attr::MOI.NLPBlockDual) +function MOI.get(model::Optimizer, attr::MOI.ObjectiveValue) MOI.check_result_index_bounds(model, attr) - return -1 * model.result.multipliers[(1 + nlp_constraint_offset(model)):end] + scale = (model.sense == MOI.MAX_SENSE) ? -1 : 1 + return scale * model.result.objective end -function num_constraints(model::Optimizer) - m_linear_le = length(model.linear_le_constraints) - m_linear_ge = length(model.linear_ge_constraints) - m_linear_eq = length(model.linear_eq_constraints) - m_quadratic_le = length(model.quadratic_le_constraints) - m_quadratic_ge = length(model.quadratic_ge_constraints) - m_quadratic_eq = length(model.quadratic_eq_constraints) - m_nl = length(model.nlp_data.constraint_bounds) - return m_linear_le + m_linear_ge + m_linear_eq + m_quadratic_le + m_quadratic_ge + m_quadratic_eq + m_nl -end +### MOI.VariablePrimal -function is_jac_hess_constant(model::Optimizer) - isempty(model.nlp_data.constraint_bounds) || return (false,false) - isempty(model.quadratic_le_constraints) || return (false,false) - isempty(model.quadratic_ge_constraints) || return (false,false) - isempty(model.quadratic_eq_constraints) || return (false,false) - return (true,model.nlp_data.has_objective ? false : true) +function MOI.get( + model::Optimizer, + attr::MOI.VariablePrimal, + vi::MOI.VariableIndex, +) + MOI.check_result_index_bounds(model, attr) + MOI.throw_if_not_valid(model, vi) + return model.result.solution[vi.value] end -zero_if_nothing(x) = x == nothing ? 0. : x - -function set_x!(model,x,xl,xu) +### MOI.ConstraintPrimal - for i=1:length(model.variable_info) - info = model.variable_info[i] - x[i] = info.start == nothing ? 0. : info.start - xl[i] = info.lower_bound - xu[i] = info.upper_bound - i += 1 - end +function MOI.get( + model::Optimizer, + attr::MOI.ConstraintPrimal, + ci::MOI.ConstraintIndex{<:_FUNCTIONS,<:_SETS}, +) + MOI.check_result_index_bounds(model, attr) + MOI.throw_if_not_valid(model, ci) + return model.result.constraints[ci.value] end -function set_g!(model::Optimizer,l,gl,gu) - i = 1 - for info in model.linear_le_constraints - l[i] = zero_if_nothing(info.dual_start) - gl[i]= -Inf - gu[i]= info.set.upper - i+=1 - end - for info in model.linear_ge_constraints - l[i] = zero_if_nothing(info.dual_start) - gl[i]= info.set.lower - gu[i]= Inf - i+=1 - end - for info in model.linear_eq_constraints - l[i] = zero_if_nothing(info.dual_start) - gl[i]= info.set.value - gu[i]= info.set.value - i+=1 - end - for info in model.quadratic_le_constraints - l[i] = zero_if_nothing(info.dual_start) - gl[i]= -Inf - gu[i]= info.set.upper - i+=1 - end - for info in model.quadratic_ge_constraints - l[i] = zero_if_nothing(info.dual_start) - gl[i]= info.set.lower - gu[i]= Inf - i+=1 - end - for info in model.quadratic_eq_constraints - l[i] = zero_if_nothing(info.dual_start) - gl[i]= info.set.value - gu[i]= info.set.value - i+=1 - end - j=0 - for bound in model.nlp_data.constraint_bounds - gl[i+j]= bound.lower - gu[i+j]= bound.upper - j+=1 - end - k=0 - if model.nlp_dual_start != nothing - for v in model.nlp_dual_start - l[i+k] = v - k+=1 - end - else - l[i:end] .= 0. - end +function MOI.get( + model::Optimizer, + attr::MOI.ConstraintPrimal, + ci::MOI.ConstraintIndex{MOI.VariableIndex,<:_SETS}, +) + MOI.check_result_index_bounds(model, attr) + MOI.throw_if_not_valid(model, ci) + return model.result.solution[ci.value] end -num_variables(model::Optimizer) = length(model.variable_info) +### MOI.ConstraintDual +_dual_multiplier(model::Optimizer) = model.sense == MOI.MIN_SENSE ? 1.0 : -1.0 -struct MOIModel{T} <: AbstractNLPModel{T,Vector{T}} - meta::NLPModelMeta{T, Vector{T}} - model::Optimizer - counters::NLPModels.Counters +function MOI.get( + model::Optimizer, + attr::MOI.ConstraintDual, + ci::MOI.ConstraintIndex{<:_FUNCTIONS,<:_SETS}, +) + MOI.check_result_index_bounds(model, attr) + MOI.throw_if_not_valid(model, ci) + s = -1.0 + return s * model.result.multipliers[ci.value] end -obj(nlp::MOIModel,x::Vector{Float64}) = eval_objective(nlp.model,x) -grad!(nlp::MOIModel,x::Vector{Float64},f::Vector{Float64}) = - eval_objective_gradient(nlp.model,f,x) -cons!(nlp::MOIModel,x::Vector{Float64},c::Vector{Float64}) = - eval_constraint(nlp.model,c,x) -jac_coord!(nlp::MOIModel,x::Vector{Float64},jac::Vector{Float64})= - eval_constraint_jacobian(nlp.model,jac,x) -hess_coord!(nlp::MOIModel,x::Vector{Float64},l::Vector{Float64},hess::Vector{Float64}; - obj_weight::Float64=1.) = eval_hessian_lagrangian(nlp.model,hess,x,obj_weight,l) -function hess_structure!(nlp::MOIModel, I::AbstractVector{T}, J::AbstractVector{T}) where T - return hessian_lagrangian_structure(nlp.model,I,J) -end -function jac_structure!(nlp::MOIModel, I::AbstractVector{T}, J::AbstractVector{T}) where T - return jacobian_structure(nlp.model,I,J) +function MOI.get( + model::Optimizer, + attr::MOI.ConstraintDual, + ci::MOI.ConstraintIndex{MOI.VariableIndex,MOI.LessThan{Float64}}, +) + MOI.check_result_index_bounds(model, attr) + MOI.throw_if_not_valid(model, ci) + rc = model.result.multipliers_L[ci.value] - model.result.multipliers_U[ci.value] + return min(0.0, rc) end -function MOIModel(model::Optimizer) - :Hess in MOI.features_available(model.nlp_data.evaluator) || error("Hessian information is needed.") - MOI.initialize(model.nlp_data.evaluator, [:Grad,:Hess,:Jac]) - - nvar = num_variables(model) - ncon = num_constraints(model) - nnzh = get_nnz_hess(model) - nnzj = get_nnz_jac(model) - - x0 = Vector{Float64}(undef,nvar) - xl = Vector{Float64}(undef,nvar) - xu = Vector{Float64}(undef,nvar) - - y0 = Vector{Float64}(undef,ncon) - gl = Vector{Float64}(undef,ncon) - gu = Vector{Float64}(undef,ncon) - - set_x!(model,x0,xl,xu) - set_g!(model,y0,gl,gu) - - model.option_dict[:jacobian_constant], model.option_dict[:hessian_constant] = is_jac_hess_constant(model) - model.option_dict[:dual_initialized] = !iszero(y0) - - return MOIModel( - NLPModelMeta( - nvar, - x0 = x0, - lvar = xl, - uvar = xu, - ncon = ncon, - y0 = y0, - lcon = gl, - ucon = gu, - nnzj = nnzj, - nnzh = nnzh, - minimize = model.sense == MOI.MIN_SENSE - ), - model,NLPModels.Counters()) +function MOI.get( + model::Optimizer, + attr::MOI.ConstraintDual, + ci::MOI.ConstraintIndex{MOI.VariableIndex,MOI.GreaterThan{Float64}}, +) + MOI.check_result_index_bounds(model, attr) + MOI.throw_if_not_valid(model, ci) + rc = model.result.multipliers_L[ci.value] - model.result.multipliers_U[ci.value] + return max(0.0, rc) end -function MOI.optimize!(model::Optimizer) +function MOI.get( + model::Optimizer, + attr::MOI.ConstraintDual, + ci::MOI.ConstraintIndex{MOI.VariableIndex,MOI.EqualTo{Float64}}, +) + MOI.check_result_index_bounds(model, attr) + MOI.throw_if_not_valid(model, ci) + rc = model.result.multipliers_L[ci.value] - model.result.multipliers_U[ci.value] + return rc +end - model.nlp = MOIModel(model) - model.solver = MadNLPSolver(model.nlp; model.option_dict...) - model.result = solve!(model.solver) - model.solve_time = model.solver.cnt.total_time +### MOI.NLPBlockDual - return +function MOI.get(model::Optimizer, attr::MOI.NLPBlockDual) + MOI.check_result_index_bounds(model, attr) + s = -1.0 + offset = length(model.qp_data) + return s .* model.result.multipliers[(offset+1):end] end + diff --git a/src/Interfaces/utils.jl b/src/Interfaces/utils.jl new file mode 100644 index 00000000..70bcfa87 --- /dev/null +++ b/src/Interfaces/utils.jl @@ -0,0 +1,533 @@ +# Copyright (c) 2013: Iain Dunning, Miles Lubin, and contributors +# +# Use of this source code is governed by an MIT-style license that can be found +# in the LICENSE.md file or at https://opensource.org/licenses/MIT. + +# !!! warning +# +# The contents of this file are experimental. +# +# Until this message is removed, breaking changes to the functions and types, +# including their deletion, may be introduced in any minor or patch release of Ipopt. + +@enum( + _FunctionType, + _kFunctionTypeVariableIndex, + _kFunctionTypeScalarAffine, + _kFunctionTypeScalarQuadratic, +) + +@enum( + _BoundType, + _kBoundTypeLessThan, + _kBoundTypeGreaterThan, + _kBoundTypeEqualTo, +) + +mutable struct QPBlockData{T} + objective_type::_FunctionType + objective_constant::T + objective_linear_columns::Vector{Int} + objective_linear_coefficients::Vector{T} + objective_hessian_structure::Vector{Tuple{Int,Int}} + objective_hessian_coefficients::Vector{T} + + linear_row_ends::Vector{Int} + linear_jacobian_structure::Vector{Tuple{Int,Int}} + linear_coefficients::Vector{T} + + quadratic_row_ends::Vector{Int} + hessian_structure::Vector{Tuple{Int,Int}} + quadratic_coefficients::Vector{T} + + g_L::Vector{T} + g_U::Vector{T} + mult_g::Vector{Union{Nothing,T}} + function_type::Vector{_FunctionType} + bound_type::Vector{_BoundType} + + function QPBlockData{T}() where {T} + return new( + # Objective coefficients + _kFunctionTypeScalarAffine, + zero(T), + Int[], + T[], + Tuple{Int,Int}[], + T[], + # Linear constraints + Int[], + Tuple{Int,Int}[], + T[], + # Affine constraints + Int[], + Tuple{Int,Int}[], + T[], + # Bounds + T[], + T[], + Union{Nothing,T}[], + _FunctionType[], + _BoundType[], + ) + end +end + +Base.length(block::QPBlockData) = length(block.bound_type) + +function _set_objective(block::QPBlockData{T}, f::MOI.VariableIndex) where {T} + push!(block.objective_linear_columns, f.value) + push!(block.objective_linear_coefficients, one(T)) + return zero(T) +end + +function _set_objective( + block::QPBlockData{T}, + f::MOI.ScalarAffineFunction{T}, +) where {T} + _set_objective(block, f.terms) + return f.constant +end + +function _set_objective( + block::QPBlockData{T}, + f::MOI.ScalarQuadraticFunction{T}, +) where {T} + _set_objective(block, f.affine_terms) + for term in f.quadratic_terms + i, j = term.variable_1.value, term.variable_2.value + push!(block.objective_hessian_structure, (i, j)) + push!(block.objective_hessian_coefficients, term.coefficient) + end + return f.constant +end + +function _set_objective( + block::QPBlockData{T}, + terms::Vector{MOI.ScalarAffineTerm{T}}, +) where {T} + for term in terms + push!(block.objective_linear_columns, term.variable.value) + push!(block.objective_linear_coefficients, term.coefficient) + end + return +end + +function MOI.set( + block::QPBlockData{T}, + ::MOI.ObjectiveFunction{F}, + func::F, +) where { + T, + F<:Union{ + MOI.VariableIndex, + MOI.ScalarAffineFunction{T}, + MOI.ScalarQuadraticFunction{T}, + }, +} + empty!(block.objective_hessian_structure) + empty!(block.objective_hessian_coefficients) + empty!(block.objective_linear_columns) + empty!(block.objective_linear_coefficients) + block.objective_constant = _set_objective(block, func) + block.objective_type = _function_info(func) + return +end + +function MOI.get(block::QPBlockData{T}, ::MOI.ObjectiveFunctionType) where {T} + return _function_type_to_set(T, block.objective_type) +end + +function MOI.get(block::QPBlockData{T}, ::MOI.ObjectiveFunction{F}) where {T,F} + affine_terms = MOI.ScalarAffineTerm{T}[ + MOI.ScalarAffineTerm( + block.objective_linear_coefficients[i], + MOI.VariableIndex(x), + ) for (i, x) in enumerate(block.objective_linear_columns) + ] + quadratic_terms = MOI.ScalarQuadraticTerm{T}[] + for (i, coef) in enumerate(block.objective_hessian_coefficients) + r, c = block.objective_hessian_structure[i] + push!( + quadratic_terms, + MOI.ScalarQuadraticTerm( + coef, + MOI.VariableIndex(r), + MOI.VariableIndex(c), + ), + ) + end + obj = MOI.ScalarQuadraticFunction( + quadratic_terms, + affine_terms, + block.objective_constant, + ) + return convert(F, obj) +end + +function MOI.get( + block::QPBlockData{T}, + ::MOI.ListOfConstraintTypesPresent, +) where {T} + constraints = Set{Tuple{Type,Type}}() + for i in 1:length(block) + F = _function_type_to_set(T, block.function_type[i]) + S = _bound_type_to_set(T, block.bound_type[i]) + push!(constraints, (F, S)) + end + return collect(constraints) +end + +function MOI.is_valid( + block::QPBlockData{T}, + ci::MOI.ConstraintIndex{F,S}, +) where { + T, + F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}, + S<:Union{MOI.LessThan{T},MOI.GreaterThan{T},MOI.EqualTo{T}}, +} + return 1 <= ci.value <= length(block) +end + +function MOI.get( + block::QPBlockData{T}, + ::MOI.ListOfConstraintIndices{F,S}, +) where { + T, + F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}, + S<:Union{MOI.LessThan{T},MOI.GreaterThan{T},MOI.EqualTo{T}}, +} + ret = MOI.ConstraintIndex{F,S}[] + for i in 1:length(block) + if _bound_type_to_set(T, block.bound_type[i]) != S + continue + elseif _function_type_to_set(T, block.function_type[i]) != F + continue + end + push!(ret, MOI.ConstraintIndex{F,S}(i)) + end + return ret +end + +function MOI.get( + block::QPBlockData{T}, + ::MOI.NumberOfConstraints{F,S}, +) where { + T, + F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}, + S<:Union{MOI.LessThan{T},MOI.GreaterThan{T},MOI.EqualTo{T}}, +} + return length(MOI.get(block, MOI.ListOfConstraintIndices{F,S}())) +end + +function _bound_type_to_set(::Type{T}, k::_BoundType) where {T} + if k == _kBoundTypeEqualTo + return MOI.EqualTo{T} + elseif k == _kBoundTypeLessThan + return MOI.LessThan{T} + else + @assert k == _kBoundTypeGreaterThan + return MOI.GreaterThan{T} + end +end + +function _function_type_to_set(::Type{T}, k::_FunctionType) where {T} + if k == _kFunctionTypeVariableIndex + return MOI.VariableIndex + elseif k == _kFunctionTypeScalarAffine + return MOI.ScalarAffineFunction{T} + else + @assert k == _kFunctionTypeScalarQuadratic + return MOI.ScalarQuadraticFunction{T} + end +end + +_function_info(::MOI.VariableIndex) = _kFunctionTypeVariableIndex +_function_info(::MOI.ScalarAffineFunction) = _kFunctionTypeScalarAffine +_function_info(::MOI.ScalarQuadraticFunction) = _kFunctionTypeScalarQuadratic + +_set_info(s::MOI.LessThan) = _kBoundTypeLessThan, -Inf, s.upper +_set_info(s::MOI.GreaterThan) = _kBoundTypeGreaterThan, s.lower, Inf +_set_info(s::MOI.EqualTo) = _kBoundTypeEqualTo, s.value, s.value + +function _add_function( + block::QPBlockData{T}, + f::MOI.ScalarAffineFunction{T}, +) where {T} + _add_function(block, f.terms) + push!(block.quadratic_row_ends, length(block.quadratic_coefficients)) + return _kFunctionTypeScalarAffine, f.constant +end + +function _add_function( + block::QPBlockData{T}, + f::MOI.ScalarQuadraticFunction{T}, +) where {T} + _add_function(block, f.affine_terms) + for term in f.quadratic_terms + i, j = term.variable_1.value, term.variable_2.value + push!(block.hessian_structure, (i, j)) + push!(block.quadratic_coefficients, term.coefficient) + end + push!(block.quadratic_row_ends, length(block.quadratic_coefficients)) + return _kFunctionTypeScalarQuadratic, f.constant +end + +function _add_function( + block::QPBlockData{T}, + terms::Vector{MOI.ScalarAffineTerm{T}}, +) where {T} + row = length(block) + 1 + for term in terms + push!(block.linear_jacobian_structure, (row, term.variable.value)) + push!(block.linear_coefficients, term.coefficient) + end + push!(block.linear_row_ends, length(block.linear_jacobian_structure)) + return +end + +function MOI.add_constraint( + block::QPBlockData{T}, + f::Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}, + set::Union{MOI.LessThan{T},MOI.GreaterThan{T},MOI.EqualTo{T}}, +) where {T} + function_type, constant = _add_function(block, f) + bound_type, l, u = _set_info(set) + push!(block.g_L, l - constant) + push!(block.g_U, u - constant) + push!(block.mult_g, nothing) + push!(block.bound_type, bound_type) + push!(block.function_type, function_type) + return MOI.ConstraintIndex{typeof(f),typeof(set)}(length(block.bound_type)) +end + +function MOI.get( + block::QPBlockData{T}, + ::MOI.ConstraintFunction, + c::MOI.ConstraintIndex{F,S}, +) where {T,F,S} + row = c.value + offset = row == 1 ? 1 : (block.linear_row_ends[row-1] + 1) + affine_terms = MOI.ScalarAffineTerm{T}[ + MOI.ScalarAffineTerm( + block.linear_coefficients[i], + MOI.VariableIndex(block.linear_jacobian_structure[i][2]), + ) for i in offset:block.linear_row_ends[row] + ] + quadratic_terms = MOI.ScalarQuadraticTerm{T}[] + offset = row == 1 ? 1 : (block.quadratic_row_ends[row-1] + 1) + for i in offset:block.quadratic_row_ends[row] + r, c = block.hessian_structure[i] + push!( + quadratic_terms, + MOI.ScalarQuadraticTerm( + block.quadratic_coefficients[i], + MOI.VariableIndex(r), + MOI.VariableIndex(c), + ), + ) + end + if length(quadratic_terms) == 0 + return MOI.ScalarAffineFunction(affine_terms, zero(T)) + end + return MOI.ScalarQuadraticFunction(quadratic_terms, affine_terms, zero(T)) +end + +function MOI.get( + block::QPBlockData{T}, + ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{F,S}, +) where {T,F,S} + row = c.value + if block.bound_type[row] == _kBoundTypeEqualTo + return MOI.EqualTo(block.g_L[row]) + elseif block.bound_type[row] == _kBoundTypeLessThan + return MOI.LessThan(block.g_U[row]) + else + @assert block.bound_type[row] == _kBoundTypeGreaterThan + return MOI.GreaterThan(block.g_L[row]) + end +end + +function MOI.set( + block::QPBlockData{T}, + ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{F,MOI.LessThan{T}}, + set::MOI.LessThan{T}, +) where {T,F} + row = c.value + block.g_U[row] = set.upper + return +end + +function MOI.set( + block::QPBlockData{T}, + ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{F,MOI.GreaterThan{T}}, + set::MOI.GreaterThan{T}, +) where {T,F} + row = c.value + block.g_L[row] = set.lower + return +end + +function MOI.set( + block::QPBlockData{T}, + ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{F,MOI.EqualTo{T}}, + set::MOI.EqualTo{T}, +) where {T,F} + row = c.value + block.g_L[row] = set.value + block.g_U[row] = set.value + return +end + +function MOI.get( + block::QPBlockData{T}, + ::MOI.ConstraintDualStart, + c::MOI.ConstraintIndex{F,S}, +) where {T,F,S} + return block.mult_g[c.value] +end + +function MOI.set( + block::QPBlockData{T}, + ::MOI.ConstraintDualStart, + c::MOI.ConstraintIndex{F,S}, + value, +) where {T,F,S} + block.mult_g[c.value] = value + return +end + +function MOI.eval_objective( + block::QPBlockData{T}, + x::AbstractVector{T}, +) where {T} + y = block.objective_constant + for (i, c) in enumerate(block.objective_linear_columns) + y += block.objective_linear_coefficients[i] * x[c] + end + for (i, (r, c)) in enumerate(block.objective_hessian_structure) + if r == c + y += block.objective_hessian_coefficients[i] * x[r] * x[c] / 2 + else + y += block.objective_hessian_coefficients[i] * x[r] * x[c] + end + end + return y +end + +function MOI.eval_objective_gradient( + block::QPBlockData{T}, + g::AbstractVector{T}, + x::AbstractVector{T}, +) where {T} + g .= zero(T) + for (i, c) in enumerate(block.objective_linear_columns) + g[c] += block.objective_linear_coefficients[i] + end + for (i, (r, c)) in enumerate(block.objective_hessian_structure) + g[r] += block.objective_hessian_coefficients[i] * x[c] + if r != c + g[c] += block.objective_hessian_coefficients[i] * x[r] + end + end + return +end + +function MOI.eval_constraint( + block::QPBlockData{T}, + g::AbstractVector{T}, + x::AbstractVector{T}, +) where {T} + for i in 1:length(g) + g[i] = zero(T) + end + for (i, (r, c)) in enumerate(block.linear_jacobian_structure) + g[r] += block.linear_coefficients[i] * x[c] + end + i = 0 + for row in 1:length(block.quadratic_row_ends) + while i < block.quadratic_row_ends[row] + i += 1 + r, c = block.hessian_structure[i] + if r == c + g[row] += block.quadratic_coefficients[i] * x[r] * x[c] / 2 + else + g[row] += block.quadratic_coefficients[i] * x[r] * x[c] + end + end + end + return +end + +function MOI.jacobian_structure(block::QPBlockData) + J = copy(block.linear_jacobian_structure) + i = 0 + for row in 1:length(block.quadratic_row_ends) + while i < block.quadratic_row_ends[row] + i += 1 + r, c = block.hessian_structure[i] + push!(J, (row, r)) + if r != c + push!(J, (row, c)) + end + end + end + return J +end + +function MOI.eval_constraint_jacobian( + block::QPBlockData{T}, + J::AbstractVector{T}, + x::AbstractVector{T}, +) where {T} + nterms = 0 + for coef in block.linear_coefficients + nterms += 1 + J[nterms] = coef + end + i = 0 + for row in 1:length(block.quadratic_row_ends) + while i < block.quadratic_row_ends[row] + i += 1 + r, c = block.hessian_structure[i] + nterms += 1 + J[nterms] = block.quadratic_coefficients[i] * x[c] + if r != c + nterms += 1 + J[nterms] = block.quadratic_coefficients[i] * x[r] + end + end + end + return nterms +end + +function MOI.hessian_lagrangian_structure(block::QPBlockData) + return vcat(block.objective_hessian_structure, block.hessian_structure) +end + +function MOI.eval_hessian_lagrangian( + block::QPBlockData{T}, + H::AbstractVector{T}, + ::AbstractVector{T}, + σ::T, + μ::AbstractVector{T}, +) where {T} + nterms = 0 + for c in block.objective_hessian_coefficients + nterms += 1 + H[nterms] = σ * c + end + i = 0 + for row in 1:length(block.quadratic_row_ends) + while i < block.quadratic_row_ends[row] + i += 1 + nterms += 1 + H[nterms] = μ[row] * block.quadratic_coefficients[i] + end + end + return nterms +end diff --git a/test/MOI_interface_test.jl b/test/MOI_interface_test.jl index 6da0c261..cfccd2de 100644 --- a/test/MOI_interface_test.jl +++ b/test/MOI_interface_test.jl @@ -17,12 +17,9 @@ function runtests() end function test_MOI_Test() - model = MOI.Bridges.full_bridge_optimizer( - MOI.Utilities.CachingOptimizer( - MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), - MadNLP.Optimizer(), - ), - Float64, + model = MOI.Utilities.CachingOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + MadNLP.Optimizer(), ) MOI.set(model, MOI.Silent(), true) MOI.Test.runtests( @@ -30,10 +27,9 @@ function test_MOI_Test() MOI.Test.Config( atol = 1e-4, rtol = 1e-4, + infeasible_status = MOI.LOCALLY_INFEASIBLE, optimal_status = MOI.LOCALLY_SOLVED, exclude = Any[ - MOI.delete, - MOI.ConstraintDual, MOI.ConstraintBasisStatus, MOI.DualObjectiveValue, MOI.ObjectiveBound, @@ -41,32 +37,17 @@ function test_MOI_Test() ); exclude = String[ "test_modification", - # - Need to implement TimeLimitSec "test_attribute_TimeLimitSec", - # - Wrong return type - "test_model_UpperBoundAlreadySet", - # - Final objective value is not equal to 0.0 - "test_objective_FEASIBILITY_SENSE_clears_objective", - - # TODO: Need to investigate why these tests are breaking - # get(model, MOI.ConstraintPrimal(), c) returns the - # opposite value: if 1.0 is expected, -1.0 is returned - "test_constraint_ScalarAffineFunction_EqualTo", - "test_quadratic_nonconvex_constraint_basic", + # TODO: MadNLP does not return the correct multiplier + # when a variable is fixed with MOI.EqualTo. "test_linear_integration", - - # TODO: there might be an issue with VectorAffineFunction/VectorOfVariables - "test_conic_NormOneCone_VectorOfVariables", - "test_conic_NormOneCone_VectorAffineFunction", - "test_conic_NormInfinityCone_VectorOfVariables", - "test_conic_NormInfinityCone_VectorAffineFunction", - "test_conic_linear_VectorAffineFunction", - "test_conic_linear_VectorOfVariables", - + "test_quadratic_constraint_GreaterThan", + "test_quadratic_constraint_LessThan", + # MadNLP reaches maximum number of iterations instead + # of returning infeasibility certificate. + "test_linear_DUAL_INFEASIBLE", + "test_solve_TerminationStatus_DUAL_INFEASIBLE", # Tests excluded on purpose - # - Excluded as MadNLP returns LOCALLY_INFEASIBLE instead of INFEASIBLE - "INFEASIBLE", - "test_solve_DualStatus_INFEASIBILITY_CERTIFICATE_", # - Excluded because Hessian information is needed "test_nonlinear_hs071_hessian_vector_product", # - Excluded because Hessian information is needed @@ -76,10 +57,18 @@ function test_MOI_Test() # - Excluded because this test is optional "test_model_ScalarFunctionConstantNotZero", - # - Excluded because MadNLP returns INVALID_MODEL instead of LOCALLY_SOLVED - "test_linear_VectorAffineFunction_empty_row", ] ) + + return +end + +function test_Name() + model = MadNLP.Optimizer() + @test MOI.supports(model, MOI.Name()) + @test MOI.get(model, MOI.Name()) == "" + MOI.set(model, MOI.Name(), "Model") + @test MOI.get(model, MOI.Name()) == "Model" return end