diff --git a/docs/src/manual.md b/docs/src/manual.md index 38f942e..127059d 100644 --- a/docs/src/manual.md +++ b/docs/src/manual.md @@ -202,3 +202,22 @@ Dualize the model dual_model = dualize(model) print(dual_model) ``` + +## Advanced: add support for new attributes + +You might feel that the Dualization layer is getting in your way to get information from an inner layer. +If you ever see yourself typing `model.dual_problem.dual_model`, it is a sign that you need to define new attributes, +especially if these information are related to specific variables and constraints. +The mapping between variables and constraints before and after dualization is quite complex. +It depends on whether constraints corresponded to variables constrained at creation or not and +whether the corresponding set is an equality (i.e., `MOI.EqualTo` or `MOI.Zeros`). + +When define MOI attributes to communicate information in MOI, layers like Dualization can transfer +these attributes to the right variables or constraints before or after their transformation. +So the best practice is to define such variable or constraint attributes and attempt to `MOI.get` +or `MOI.set` them. They is an API that you need to define for these attributes that contains the +functions are `dual_attribute`, `dual_attribute_value`, +`constrained_variable_dual_attribute`, `fixed_variable_value`, `fixed_constrained_variables_get`, +`equality_constraint_get`. That's a lot of functions, some of which may never be useful for +your specific attributes (e.g., if it is never used on an equality constraint) so no need +to implement the whole API for all attributes. diff --git a/docs/src/reference.md b/docs/src/reference.md index 9938cb8..30bdf97 100644 --- a/docs/src/reference.md +++ b/docs/src/reference.md @@ -70,3 +70,15 @@ Dualization.PrimalConstraintData ```@docs Dualization.PrimalDualMap ``` + +## Attributes + +```@docs +Dualization.dual_attribute +Dualization.dual_attribute_value_get +Dualization.dual_attribute_value_set +Dualization.constrained_variable_dual_attribute +Dualization.fixed_variable_value +Dualization.fixed_constrained_variables_get +Dualization.equality_constraint_get +``` diff --git a/src/Dualization.jl b/src/Dualization.jl index de27364..5f3e0e0 100644 --- a/src/Dualization.jl +++ b/src/Dualization.jl @@ -20,6 +20,8 @@ include("dual_model_variables.jl") include("dual_equality_constraints.jl") include("dualize.jl") include("MOI_wrapper.jl") +include("vectorize_emulator.jl") +include("attributes.jl") export dualize export dual_optimizer diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index b90cbe7..b1e0bdb 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -207,7 +207,7 @@ end function MOI.supports_add_constrained_variables( optimizer::DualOptimizer{T}, - S::Type{MOI.Reals}, + ::Type{MOI.Reals}, ) where {T} return MOI.supports_constraint( optimizer.dual_problem.dual_model, @@ -234,21 +234,29 @@ function MOI.supports_add_constrained_variables( end function MOI.copy_to(dest::DualOptimizer, src::MOI.ModelLike) + MOI.empty!(dest) dualize( src, dest.dual_problem, assume_min_if_feasibility = dest.assume_min_if_feasibility, ) - idx_map = MOI.Utilities.IndexMap() - for vi in MOI.get(src, MOI.ListOfVariableIndices()) - setindex!(idx_map, vi, vi) + # Copy attributes except names which have already been passed in `dualize` + primal_without_names = MOI.Utilities.ModelFilter(src) do attr + return !(attr isa Union{MOI.VariableName,MOI.ConstraintName}) end + index_map = MOI.Utilities.identity_index_map(src) + vis = MOI.get(src, MOI.ListOfVariableIndices()) + MOI.Utilities.pass_attributes(dest, primal_without_names, index_map, vis) for (F, S) in MOI.get(src, MOI.ListOfConstraintTypesPresent()) - for con in MOI.get(src, MOI.ListOfConstraintIndices{F,S}()) - setindex!(idx_map, con, con) - end + cis = MOI.get(src, MOI.ListOfConstraintIndices{F,S}()) + MOI.Utilities.pass_attributes( + dest, + primal_without_names, + index_map, + cis, + ) end - return idx_map + return index_map end function MOI.optimize!(optimizer::DualOptimizer) @@ -270,226 +278,3 @@ function MOI.get(optimizer::DualOptimizer, ::MOI.SolverName) name = MOI.get(optimizer.dual_problem.dual_model, MOI.SolverName()) return "Dual model with $name attached" end - -function MOI.get( - optimizer::DualOptimizer{T}, - ::MOI.VariablePrimal, - vi::MOI.VariableIndex, -)::T where {T} - primal_dual_map = optimizer.dual_problem.primal_dual_map - data = get(primal_dual_map.primal_variable_data, vi, nothing) - if data === nothing - # error - elseif data.dual_constraint === nothing - return zero(T) - elseif data.primal_constrained_variable_constraint === nothing - return -MOI.get( - optimizer.dual_problem.dual_model, - MOI.ConstraintDual(), - data.dual_constraint, - ) - elseif data.dual_constraint isa - MOI.ConstraintIndex{<:MOI.AbstractVectorFunction} - return MOI.get( - optimizer.dual_problem.dual_model, - MOI.ConstraintDual(), - data.dual_constraint, - )[data.primal_constrained_variable_index] - else - return MOI.get( - optimizer.dual_problem.dual_model, - MOI.ConstraintDual(), - data.dual_constraint, - ) - end -end - -function MOI.get( - optimizer::DualOptimizer, - ::MOI.ConstraintDual, - ci::MOI.ConstraintIndex{F,S}, -) where {F<:MOI.AbstractScalarFunction,S<:MOI.AbstractScalarSet} - primal_dual_map = optimizer.dual_problem.primal_dual_map - if haskey(primal_dual_map.primal_constrained_variables, ci) - vi = primal_dual_map.primal_constrained_variables[ci][] - ci_dual = primal_dual_map.primal_variable_data[vi].dual_constraint - if ci_dual === nothing - return MOI.Utilities.eval_variables( - primal_dual_map.primal_variable_data[vi].dual_function, - ) do inner_vi - return MOI.get( - optimizer.dual_problem.dual_model, - MOI.VariablePrimal(), - inner_vi, - ) - end - end - set = MOI.get( - optimizer.dual_problem.dual_model, - MOI.ConstraintSet(), - ci_dual, - ) - return MOI.get( - optimizer.dual_problem.dual_model, - MOI.ConstraintPrimal(), - ci_dual, - ) - MOI.constant(set) - else - return MOI.get( - optimizer.dual_problem.dual_model, - MOI.VariablePrimal(), - primal_dual_map.primal_constraint_data[ci].dual_variables[], - ) - end -end - -function MOI.get( - optimizer::DualOptimizer, - ::MOI.ConstraintDual, - ci::MOI.ConstraintIndex{F,S}, -) where {F<:MOI.AbstractVectorFunction,S<:MOI.AbstractVectorSet} - primal_dual_map = optimizer.dual_problem.primal_dual_map - if !haskey(primal_dual_map.primal_constraint_data, ci) - vis = primal_dual_map.primal_constrained_variables[ci] - ci_dual = primal_dual_map.primal_variable_data[vis[1]].dual_constraint - if ci_dual === nothing - return [ - MOI.Utilities.eval_variables( - primal_dual_map.primal_variable_data[vi].dual_function, - ) do inner_vi - return MOI.get( - optimizer.dual_problem.dual_model, - MOI.VariablePrimal(), - inner_vi, - ) - end for vi in vis - ] - end - return MOI.get( - optimizer.dual_problem.dual_model, - MOI.ConstraintPrimal(), - ci_dual, - ) - else - return MOI.get.( - optimizer.dual_problem.dual_model, - MOI.VariablePrimal(), - primal_dual_map.primal_constraint_data[ci].dual_variables, - ) - end -end - -function MOI.get( - optimizer::DualOptimizer{T}, - ::MOI.ConstraintPrimal, - ci::MOI.ConstraintIndex{F,S}, -) where {T,F<:MOI.AbstractScalarFunction,S<:MOI.AbstractScalarSet} - primal_dual_map = optimizer.dual_problem.primal_dual_map - data = get(primal_dual_map.primal_constraint_data, ci, nothing) - if data === nothing - first_vi = primal_dual_map.primal_constrained_variables[ci][1] - ci_dual = primal_dual_map.primal_variable_data[first_vi].dual_constraint - if ci_dual === nothing - return zero(T) - else - return MOI.get( - optimizer.dual_problem.dual_model, - MOI.ConstraintDual(), - ci_dual, - ) - end - else - primal_ci_constant = data.primal_set_constants[1] - # If it has no key then there is no dual constraint - ci_dual = data.dual_constrained_variable_constraint - if ci_dual === nothing - return -primal_ci_constant - end - return MOI.get( - optimizer.dual_problem.dual_model, - MOI.ConstraintDual(), - ci_dual, - ) - primal_ci_constant - end -end - -function MOI.get( - optimizer::DualOptimizer{T}, - ::MOI.ConstraintPrimal, - ci::MOI.ConstraintIndex{F,S}, -) where {T,F<:MOI.AbstractVectorFunction,S<:MOI.AbstractVectorSet} - primal_dual_map = optimizer.dual_problem.primal_dual_map - data = get(primal_dual_map.primal_constraint_data, ci, nothing) - if data === nothing - vis = primal_dual_map.primal_constrained_variables[ci] - ci_dual = primal_dual_map.primal_variable_data[vis[1]].dual_constraint - if ci_dual === nothing - return zeros(T, length(vis)) - else - return MOI.get( - optimizer.dual_problem.dual_model, - MOI.ConstraintDual(), - ci_dual, - ) - end - else - ci_dual = data.dual_constrained_variable_constraint - # If it has no key then there is no dual constraint - if ci_dual === nothing - # The number of dual variable associated with the primal constraint is the ci dimension - ci_dimension = length(data.dual_variables) - return zeros(T, ci_dimension) - end - return MOI.get( - optimizer.dual_problem.dual_model, - MOI.ConstraintDual(), - ci_dual, - ) - end -end - -function MOI.get(optimizer::DualOptimizer, ::MOI.TerminationStatus) - return _dual_status( - MOI.get(optimizer.dual_problem.dual_model, MOI.TerminationStatus()), - ) -end - -function _dual_status(term::MOI.TerminationStatusCode) - if term == MOI.INFEASIBLE - return MOI.DUAL_INFEASIBLE - elseif term == MOI.DUAL_INFEASIBLE - return MOI.INFEASIBLE - elseif term == MOI.ALMOST_INFEASIBLE - return MOI.ALMOST_DUAL_INFEASIBLE - elseif term == MOI.ALMOST_DUAL_INFEASIBLE - return MOI.ALMOST_INFEASIBLE - end - return term -end - -function MOI.supports( - optimizer::DualOptimizer, - attr::MOI.AbstractOptimizerAttribute, -) - return MOI.supports(optimizer.dual_problem.dual_model, attr) -end - -function MOI.set( - optimizer::DualOptimizer, - attr::MOI.AbstractOptimizerAttribute, - value, -) - return MOI.set(optimizer.dual_problem.dual_model, attr, value) -end - -function MOI.get(optimizer::DualOptimizer, attr::MOI.AbstractOptimizerAttribute) - return MOI.get(optimizer.dual_problem.dual_model, attr) -end - -# For now we don't support setting arbitrary AbstractModelAttribute because -# we don't know if they need to be modified via the dualization. One example -# would be `MOI.set(model, MOI.ObjectiveFunction{F}(), f)`. We currently -# don't support the incremental interface. -function MOI.get(optimizer::DualOptimizer, attr::MOI.AbstractModelAttribute) - return MOI.get(optimizer.dual_problem.dual_model, attr) -end diff --git a/src/attributes.jl b/src/attributes.jl new file mode 100644 index 0000000..3d63b74 --- /dev/null +++ b/src/attributes.jl @@ -0,0 +1,516 @@ +_minus(::Nothing) = nothing +_minus(x) = -x + +function _variable_attribute(attr::MOI.ConstraintPrimal) + return MOI.VariablePrimal(attr.result_index) +end + +function _variable_attribute(::MOI.ConstraintPrimalStart) + return MOI.VariablePrimalStart() +end + +function _constraint_attribute(attr::MOI.VariablePrimal) + return MOI.ConstraintPrimal(attr.result_index) +end + +function _constraint_attribute(::MOI.VariablePrimalStart) + return MOI.ConstraintPrimalStart() +end + +""" + dual_attribute(attr::MOI.AbstractModelAttribute) + dual_attribute(attr::MOI.AbstractVariableAttribute) + dual_attribute(attr::MOI.AbstractConstraintAttribute) + +Corresponding attribute to get `MOI.set` or `MOI.get` `attr` from the primal +model with the dual model. +""" +function dual_attribute end + +dual_attribute(attr::MOI.ResultCount) = attr + +dual_attribute(::MOI.VariableName) = MOI.ConstraintName() + +dual_attribute(::MOI.ConstraintName) = MOI.VariableName() + +function dual_attribute(attr::Union{MOI.VariablePrimal,MOI.ConstraintPrimal}) + return MOI.ConstraintDual(attr.result_index) +end + +function dual_attribute( + ::Union{MOI.VariablePrimalStart,MOI.ConstraintPrimalStart}, +) + return MOI.ConstraintDualStart() +end + +function dual_attribute(attr::MOI.ConstraintDual) + return MOI.VariablePrimal(attr.result_index) +end + +function dual_attribute(::MOI.ConstraintDualStart) + return MOI.VariablePrimalStart() +end + +""" + dual_attribute_value_set(attr::MOI.AbstractVariableAttribute, value) + +Used as pre-processing for `MOI.set`ting `value` for a variable. +""" +function dual_attribute_value_set end + +""" + dual_attribute_value_get(attr::MOI.AbstractVariableAttribute, value) + +Used as pre-processing for `MOI.get`ting `value` for a variable. +""" +function dual_attribute_value_get end + +function dual_attribute_value_set( + ::Union{MOI.VariablePrimal,MOI.VariablePrimalStart}, + value, +) + return _minus(value) +end + +function dual_attribute_value_get( + ::Union{MOI.VariablePrimal,MOI.VariablePrimalStart}, + value, +) + return _minus(value) +end + +""" + constrained_variable_dual_attribute(attr::MOI.AbstractConstraintAttribute) + +Same as [`dual_attribute`](@ref) but used in case a constraint was added as +part of constrained variables. +""" +function constrained_variable_dual_attribute end + +function constrained_variable_dual_attribute( + attr::Union{MOI.ConstraintDual,MOI.ConstraintDualStart}, +) + return _constraint_attribute(dual_attribute(attr)) +end + +function constrained_variable_dual_attribute( + attr::MOI.AbstractConstraintAttribute, +) + return dual_attribute(attr) +end + +""" + fixed_variable_value(attr::MOI.AbstractVariableAttribute, ::Type{T}) where {T} + +Value of `attr` for a value constrained to be equal to zero. This should be the +same as how `MOI.get` is implemented for the `MOI.Bridges.Variable.ZerosBridge`. +""" +function fixed_variable_value end + +fixed_variable_value(::MOI.VariablePrimal, ::Type{T}) where {T} = zero(T) + +# The inner optimizer may not support equality constraints (e.g. MOI.FileFormats.SDPA.Model) +# In this case, if all variables are created using constrained variables then dualization won't +# have to create any equality constraints so it will work. +# In that case, we have two choices: +# 1) say we don't support `MOI.VariablePrimalStart` and ignore them, rely on the value set to +# `MOI.ConstraintPrimalStart` to the constraint associated to the constrained variables +# 2) use the value in `MOI.VariablePrimalStart` as fallback in case `MOI.ConstraintPrimalStart` +# is not set +# The issue with option 2) is that it is difficult to know what type of constraints we should use +# in `MOI.supports` here so we should basically just return `true`. +# But if we `return true` and the solver don't support starting values then it will error, and we +# don't benefit from the silent ignoring of starting values relying on +# https://github.com/jump-dev/MathOptInterface.jl/blob/9884cfacb044724427a7d6c7a21f4bd6ff5a8c15/src/Utilities/copy.jl#L73-L74 +# So let's go for option 1) for now +function MOI.supports( + optimizer::DualOptimizer{T}, + attr::MOI.AbstractVariableAttribute, + ::Type{MOI.VariableIndex}, +) where {T} + return MOI.supports( + optimizer.dual_problem.dual_model, + dual_attribute(attr), + MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}, + ) +end + +function MOI.set( + optimizer::DualOptimizer, + attr::MOI.AbstractVariableAttribute, + vi::MOI.VariableIndex, + value, +) + primal_dual_map = optimizer.dual_problem.primal_dual_map + data = primal_dual_map.primal_variable_data[vi] + if !isnothing(data.primal_constrained_variable_constraint) + msg = "Setting $attr for variables constrained at creation is not supported yet" + throw(MOI.SetAttributeNotAllowed(attr, msg)) + end + MOI.set( + optimizer.dual_problem.dual_model, + dual_attribute(attr), + data.dual_constraint, + dual_attribute_value_set(attr, value), + ) + return +end + +function MOI.get( + optimizer::DualOptimizer{T}, + attr::MOI.AbstractVariableAttribute, + vi::MOI.VariableIndex, +) where {T} + primal_dual_map = optimizer.dual_problem.primal_dual_map + data = primal_dual_map.primal_variable_data[vi] + if isnothing(data.primal_constrained_variable_constraint) + # Classical free variable + return dual_attribute_value_get( + attr, + MOI.get( + optimizer.dual_problem.dual_model, + dual_attribute(attr), + data.dual_constraint, + ), + ) + end + if isnothing(data.dual_constraint) + # Fixed variable: variable constrained to `MOI.EqualTo` or `MOI.Zeros` + return fixed_variable_value(attr, T) + end + # Added as constrained variable + value = MOI.get( + optimizer.dual_problem.dual_model, + dual_attribute(attr), + data.dual_constraint, + ) + if !isnothing(value) && + data.dual_constraint isa + MOI.ConstraintIndex{<:MOI.AbstractVectorFunction} + # Added as part of a vector of constrained variable + return value[data.primal_constrained_variable_index] + else + # Added as a scalar constrained variable + return value + end +end + +function MOI.supports( + optimizer::DualOptimizer, + attr::MOI.AbstractConstraintAttribute, + ::Type{<:MOI.ConstraintIndex}, +) + return MOI.supports( + optimizer.dual_problem.dual_model, + dual_attribute(attr), + MOI.VariableIndex, + ) +end + +function MOI.supports( + optimizer::DualOptimizer, + attr::MOI.ConstraintPrimalStart, + C::Type{<:MOI.ConstraintIndex}, +) + return MOI.supports( + optimizer.dual_problem.dual_model, + dual_attribute(attr), + C, + ) +end + +function MOI.set( + av::_AfterVectorize, + attr::MOI.AbstractConstraintAttribute, + ci::MOI.ConstraintIndex, + value, +) + optimizer = av.inner + if !isnothing(av.ci) + ci = av.ci + # Needed because the Vectorize bridge has vectorized it + value = _scalarize(ci, value) + end + primal_dual_map = optimizer.dual_problem.primal_dual_map + data = get(primal_dual_map.primal_constraint_data, ci, nothing) + if isnothing(data) + msg = "Setting $attr for variables constrained at creation is not supported yet" + throw(MOI.SetAttributeNotAllowed(attr, msg)) + else + dual_attr = dual_attribute(attr) + if dual_attr isa MOI.AbstractVariableAttribute + index = _scalarize( + ci, + primal_dual_map.primal_constraint_data[ci].dual_variables, + ) + else + @assert dual_attr isa MOI.AbstractConstraintAttribute + index = data.dual_constrained_variable_constraint + end + # `index` is `nothing` for affine equality constraints + if !isnothing(index) + MOI.set(optimizer.dual_problem.dual_model, dual_attr, index, value) + end + end + return +end + +""" + fixed_constrained_variables_get( + optimizer::DualOptimizer, + attr::MOI.AbstractConstraintAttribute, + primal_vi::MOI.VariableIndex, + dual_function::MOI.ScalarAffineFunction, + ) + +Given a fixed variable `primal_vi`, so part of a +`MOI.VariableIndex`-in-`MOI.EqualTo` constraint or a +`MOI.VectorOfVariables`-in-`MOI.Zeros` constraint, return the value of the +attribute `attr` at the entry corresponding to `primal_vi`. +The terms of `dual_function` are the product of the coefficient of `primal_vi` +in each constraint multiplied by the corresponding dual variable. +The constant is the coefficient of `primal_vi` in the objective function. +""" +function fixed_constrained_variables_get end + +function fixed_constrained_variables_get( + optimizer, + attr::Union{MOI.ConstraintDual,MOI.ConstraintDualStart}, + ::MOI.VariableIndex, + dual_function::MOI.ScalarAffineFunction, +) + function eval(inner_vi) + return MOI.get( + optimizer.dual_problem.dual_model, + dual_attribute(attr), + inner_vi, + ) + end + return MOI.Utilities.eval_variables(eval, dual_function) +end + +function fixed_constrained_variables_get( + optimizer::DualOptimizer{T}, + attr::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, + primal_vi::MOI.VariableIndex, + ::MOI.ScalarAffineFunction, +) where {T} + return MOI.get(optimizer, _variable_attribute(attr), primal_vi) +end + +# Not sure how to rely on a bridge for this one. +# What we did is equivalent to applying `MOI.Bridges.Variable.VectorizeBridge` +# so we substituted the variable, hence it's a bit trickier. +# Anyway I think we should just drop support for scalar constraints. +# We can revisit if we have a custom attribute that needs to extend this. +function _maybe_shift_for_vectorize( + optimizer, + attr, + dual_ci::MOI.ConstraintIndex{ + <:MOI.AbstractScalarFunction, + <:MOI.Utilities.ScalarLinearSet, + }, + value, +) + return _shift_for_vectorize(optimizer, attr, dual_ci, value) +end + +function _maybe_shift_for_vectorize( + optimizer, + attr, + dual_ci::MOI.ConstraintIndex, + value, +) + return value +end + +function _shift_for_vectorize( + optimizer, + ::Union{MOI.ConstraintDual,MOI.ConstraintDualStart}, + dual_ci, + value, +) + set = + MOI.get(optimizer.dual_problem.dual_model, MOI.ConstraintSet(), dual_ci) + return value - MOI.constant(set) +end + +function _shift_for_vectorize( + optimizer, + ::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, + dual_ci, + value, +) + return value +end + +""" + equality_constraint_get( + optimizer::DualOptimizer, + attr::MOI.AbstractConstraintAttribute, + dual_variable::MOI.ScalarAffineFunction, + ) + +Return the value of `attr` for an equality constraint whose dual variable +is `dual_variable`. +""" +function equality_constraint_get end + +function equality_constraint_get( + optimizer, + attr::Union{MOI.ConstraintDual,MOI.ConstraintDualStart}, + dual_variable::MOI.VariableIndex, +) + return MOI.get( + optimizer.dual_problem.dual_model, + dual_attribute(attr), + dual_variable, + ) +end + +function equality_constraint_get( + ::DualOptimizer{T}, + ::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, + ::MOI.VariableIndex, +) where {T} + return zero(T) +end + +function _scalarize(::MOI.ConstraintIndex{<:MOI.AbstractVectorFunction}, v) + return v +end + +function _scalarize(::MOI.ConstraintIndex{<:MOI.AbstractScalarFunction}, v) + return only(v) +end + +function MOI.get( + av::_AfterVectorize{T}, + attr::MOI.AbstractConstraintAttribute, + ci::MOI.ConstraintIndex, +) where {T} + optimizer = av.inner + if !isnothing(av.ci) + ci = av.ci + end + primal_dual_map = optimizer.dual_problem.primal_dual_map + data = get(primal_dual_map.primal_constraint_data, ci, nothing) + if isnothing(data) + # Constraint associated to variables constrained at creation + vis = primal_dual_map.primal_constrained_variables[ci] + data = primal_dual_map.primal_variable_data[first(vis)] + if isnothing(data.dual_constraint) + # Fixed variables (constrained in `MOI.Zeros` or `MOI.EqualTo`) + dual_functions = MOI.ScalarAffineFunction{T}[ + primal_dual_map.primal_variable_data[vi].dual_function for + vi in vis + ] + return fixed_constrained_variables_get.( + optimizer, + attr, + _scalarize(ci, vis), + _scalarize(ci, dual_functions), + ) + else + return _maybe_shift_for_vectorize( + optimizer, + attr, + data.dual_constraint, + MOI.get( + optimizer.dual_problem.dual_model, + constrained_variable_dual_attribute(attr), + data.dual_constraint, + ), + ) + end + else + @assert !haskey(primal_dual_map.primal_constrained_variables, ci) + dual_ci = data.dual_constrained_variable_constraint + if isnothing(dual_ci) + # Primal equality constraint, so no dual constraint + return equality_constraint_get.( + optimizer, + attr, + _scalarize(ci, data.dual_variables), + ) + else + dual_attr = dual_attribute(attr) + if dual_attr isa MOI.AbstractVariableAttribute + index = _scalarize( + ci, + primal_dual_map.primal_constraint_data[ci].dual_variables, + ) + return _scalarize( + ci, + MOI.get.( + optimizer.dual_problem.dual_model, + dual_attr, + data.dual_variables, + ), + ) + else + @assert dual_attr isa MOI.AbstractConstraintAttribute + return MOI.get( + optimizer.dual_problem.dual_model, + dual_attr, + dual_ci, + ) + end + end + end +end + +function MOI.get(optimizer::DualOptimizer, ::MOI.TerminationStatus) + return _dual_status( + MOI.get(optimizer.dual_problem.dual_model, MOI.TerminationStatus()), + ) +end + +function _dual_status(term::MOI.TerminationStatusCode) + if term == MOI.INFEASIBLE + return MOI.DUAL_INFEASIBLE + elseif term == MOI.DUAL_INFEASIBLE + return MOI.INFEASIBLE + elseif term == MOI.ALMOST_INFEASIBLE + return MOI.ALMOST_DUAL_INFEASIBLE + elseif term == MOI.ALMOST_DUAL_INFEASIBLE + return MOI.ALMOST_INFEASIBLE + end + return term +end + +function MOI.supports( + optimizer::DualOptimizer, + attr::MOI.AbstractOptimizerAttribute, +) + return MOI.supports(optimizer.dual_problem.dual_model, attr) +end + +function MOI.set( + optimizer::DualOptimizer, + attr::MOI.AbstractOptimizerAttribute, + value, +) + return MOI.set(optimizer.dual_problem.dual_model, attr, value) +end + +function MOI.get(optimizer::DualOptimizer, attr::MOI.AbstractOptimizerAttribute) + return MOI.get(optimizer.dual_problem.dual_model, attr) +end + +dual_attribute(attr::MOI.PrimalStatus) = MOI.DualStatus(attr.result_index) +dual_attribute(attr::MOI.DualStatus) = MOI.PrimalStatus(attr.result_index) +function dual_attribute(attr::MOI.ObjectiveValue) + return MOI.DualObjectiveValue(attr.result_index) +end +function dual_attribute(attr::MOI.DualObjectiveValue) + return MOI.ObjectiveValue(attr.result_index) +end + +# For now we don't support setting arbitrary AbstractModelAttribute because +# we don't know if they need to be modified via the dualization. One example +# would be `MOI.set(model, MOI.ObjectiveFunction{F}(), f)`. We currently +# don't support the incremental interface. +function MOI.get(optimizer::DualOptimizer, attr::MOI.AbstractModelAttribute) + return MOI.get(optimizer.dual_problem.dual_model, dual_attribute(attr)) +end diff --git a/src/dualize.jl b/src/dualize.jl index d3dcdad..f57f6c6 100644 --- a/src/dualize.jl +++ b/src/dualize.jl @@ -216,5 +216,6 @@ function dualize( # Add dual objective to the model _set_dual_objective(dual_problem.dual_model, dual_objective) end + return dual_problem end diff --git a/src/structures.jl b/src/structures.jl index c9a9943..15dbea1 100644 --- a/src/structures.jl +++ b/src/structures.jl @@ -19,18 +19,23 @@ variables and their dual counterparts. this is the position in that vector. * `dual_constraint::Union{Nothing,MOI.ConstraintIndex}`: dual constraint - associated with the variable. If the variable is not constrained then the - set is EqualTo{T}(zero(T)). If the variable is a constrained variable then - the set is the dual set of the constrained variable set. If the dual set is - `Reals` then the field is kept as `nothing` as teh constraint is not added. - - * `dual_function::Union{Nothing,MOI.ScalarAffineFunction{T}}`: if the + associated with the variable. If this is a variable constrained in + `MOI.Zeros` or `MOI.EqualTo`, then this is `nothing`. Otherwise, this is + a constraint with set corresponding to the dual set this variable is + constrained to. If the variable is not constrained then this an equality + constrained, so the set is `MOI.EqualTo` even if strictly speaking, the dual + set of `MOI.Reals` is `MOI.Zeros`. If the variable is a constrained variable + in a given set `S`, this is a constraint on the dual set `MOI.dual_set(S)`. + + * `dual_function::Union{Nothing,MOI.ScalarAffineFunction{T}}`: if it is a constrained variable is `VectorOfVariables`-in-`Zeros` or `VariableIndex`-in-`EqualTo(zero(T))` then the dual is `func`-in-`Reals`, - which is "irrelevant" to the model. But this information is cached for - completeness of the `DualOptimizer` for `get`ting `ConstraintDuals`. + which is "irrelevant" to the model. So then no constrained is added (hence + `dual_constraint` is `nothing` but the function is cached in this field for + completeness of the `DualOptimizer` for `get`ting `ConstraintDual`s. + Otherwise, `dual_function` is `nothing`. -To got from the constrained variable constraint to the primal variable, use the +To go from the constrained variable constraint to the primal variable, use the `primal_constrained_variables` field of `PrimalDualMap`. See also `PrimalDualMap` and `PrimalConstraintData`. @@ -52,9 +57,9 @@ constraints and their dual counterparts. Constraint indices for constrained variables are not in this structure. They are added in the `primal_constrained_variables` field of `PrimalDualMap`. - * `primal_set_constants::Vector{T}`: a vector of primal set constants that are - used in MOI getters. This is used to get the primal constants of the primal - constraints. + * `primal_set_constants::Vector{T}`: for vector constraints, it is `T[]`. + For scalar constraints, it is equal to the constant of the set minus + the constant of the function. * `dual_variables::Vector{MOI.VariableIndex}`: vector of dual variables. If primal constraint is scalar then, the vector has length = 1. diff --git a/src/vectorize_emulator.jl b/src/vectorize_emulator.jl new file mode 100644 index 0000000..12156a8 --- /dev/null +++ b/src/vectorize_emulator.jl @@ -0,0 +1,75 @@ +# Dualization handles scalar constraints like `f(x) >= lb` in a way that's equivalent +# to applying a `MOI.Bridges.Constraint.VectorizeBridge`. That is, it is equivalent to +# transforming it into `[f(x) - lb] in MOI.Nonnegatives(1)`. +# For packages that define custom attributes, to avoid having them to deal with both +# defining how it should go through the vectorize bridge and for a scalar constraint +# in a dualization layer, we just use the vectorize bridge implementation here: +# This also help us get the mechanism that detect if it is a ray or not. +# It's getting quite hacky, maybe we should just drop support for scalar constraint +# in Dualization and rely on a bridge layer. + +struct _AfterVectorize{T,OT,F,S} <: MOI.ModelLike + inner::DualOptimizer{T,OT} + # If `ci` is `nothing`, the bridge isn't used + ci::Union{Nothing,MOI.ConstraintIndex{F,S}} +end + +# Vectorize bridge uses this to check if it is a ray or not +function MOI.get(av::_AfterVectorize, attr::MOI.AbstractModelAttribute) + return MOI.get(av.inner, attr) +end + +function _vectorize_bridge( + ::Type{MOI.Bridges.Constraint.VectorizeBridge{T,F,S,G}}, + constant, +) where {T,F,S,G} + dummy_ci = MOI.ConstraintIndex{F,S}(1) + return MOI.Bridges.Constraint.VectorizeBridge{T,F,S,G}(dummy_ci, constant) +end + +function _wrap( + optimizer::DualOptimizer{T,OT}, + ci::MOI.ConstraintIndex{F,S}, +) where {T,OT,F,S} + return _AfterVectorize{T,OT,F,S}(optimizer, nothing), ci +end + +function _wrap( + optimizer::DualOptimizer{T,OT}, + ci::MOI.ConstraintIndex{F,S}, +) where {T,OT,F<:MOI.AbstractScalarFunction,S<:MOI.Utilities.ScalarLinearSet} + primal_dual_map = optimizer.dual_problem.primal_dual_map + data = get(primal_dual_map.primal_constraint_data, ci, nothing) + if isnothing(data) + model = _AfterVectorize{T,OT,F,S}(optimizer, nothing) + else + model = _AfterVectorize{T,OT,F,S}(optimizer, ci) + constant = data.primal_set_constants[] + BT = MOI.Bridges.Constraint.concrete_bridge_type( + MOI.Bridges.Constraint.VectorizeBridge{T}, + F, + S, + ) + ci = _vectorize_bridge(BT, -constant) + end + return model, ci +end + +function MOI.set( + optimizer::DualOptimizer, + attr::MOI.AbstractConstraintAttribute, + ci::MOI.ConstraintIndex, + value, +) + model, new_ci = _wrap(optimizer, ci) + return MOI.set(model, attr, new_ci, value) +end + +function MOI.get( + optimizer::DualOptimizer, + attr::MOI.AbstractConstraintAttribute, + ci::MOI.ConstraintIndex, +) + model, new_ci = _wrap(optimizer, ci) + return MOI.get(model, attr, new_ci) +end diff --git a/test/Project.toml b/test/Project.toml index 419f0f6..24f77f2 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,5 +1,6 @@ [deps] CSDP = "0a46da34-8e4b-519e-b418-48813639ff34" +Dualization = "191a621a-6537-11e9-281d-650236a99e60" HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" Hypatia = "b99e6be6-89ff-11e8-14f8-45c827f4f8f2" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" @@ -10,5 +11,8 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [compat] CSDP = "1.0.0" HiGHS = "1.1.0" -Hypatia = "0.8.1" -SCS = "1.0.1" +Hypatia = "0.9" +SCS = "2" + +[sources] +Dualization = {path = ".."} diff --git a/test/Tests/test_MOI_wrapper.jl b/test/Tests/test_MOI_wrapper.jl index 5b926d8..cdcad6d 100644 --- a/test/Tests/test_MOI_wrapper.jl +++ b/test/Tests/test_MOI_wrapper.jl @@ -19,16 +19,16 @@ linear_config, include = ["test_linear_"], exclude = [ - "test_linear_FEASIBILITY_SENSE", - "test_linear_INFEASIBLE_2", - "test_linear_Interval_inactive", - "test_linear_add_constraints", - "test_linear_inactive_bounds", - "test_linear_integration_2", - "test_linear_integration_Interval", - "test_linear_integration_delete_variables", - "test_linear_complex_Zeros", - "test_linear_complex_Zeros_duplicate", + r"^test_linear_FEASIBILITY_SENSE$", + r"^test_linear_INFEASIBLE_2$", + r"^test_linear_Interval_inactive$", + r"^test_linear_add_constraints$", + r"^test_linear_inactive_bounds$", + r"^test_linear_integration_2$", + r"^test_linear_integration_Interval$", + r"^test_linear_integration_delete_variables$", + r"^test_linear_complex_Zeros$", + r"^test_linear_complex_Zeros_duplicate$", ], ) end @@ -38,6 +38,7 @@ conic_config = MOI.Test.Config(atol = 1e-4, rtol = 1e-4) conic_cache = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()) + MOI.empty!(opt) conic_cached = MOI.Utilities.CachingOptimizer(conic_cache, opt) conic_bridged = MOI.Bridges.full_bridge_optimizer(conic_cached, Float64) @@ -48,18 +49,18 @@ include = ["test_conic_"], exclude = [ # uses FEASIBILITY_SENSE - "test_conic_NormInfinityCone_INFEASIBLE", - "test_conic_NormOneCone_INFEASIBLE", - "test_conic_PositiveSemidefiniteConeSquare_3", - "test_conic_PositiveSemidefiniteConeTriangle_3", - "test_conic_SecondOrderCone_INFEASIBLE", - "test_conic_SecondOrderCone_negative_post_bound_2", - "test_conic_SecondOrderCone_negative_post_bound_3", - "test_conic_SecondOrderCone_no_initial_bound", - "test_conic_RotatedSecondOrderCone_out_of_order", - "test_conic_linear_INFEASIBLE", - "test_conic_empty_matrix", - "test_conic_HermitianPositiveSemidefiniteConeTriangle_2", + r"test_conic_NormInfinityCone_INFEASIBLE$", + r"test_conic_NormOneCone_INFEASIBLE$", + r"test_conic_PositiveSemidefiniteConeSquare_3$", + r"test_conic_PositiveSemidefiniteConeTriangle_3$", + r"test_conic_SecondOrderCone_INFEASIBLE$", + r"test_conic_SecondOrderCone_negative_post_bound_2$", + r"test_conic_SecondOrderCone_negative_post_bound_3$", + r"test_conic_SecondOrderCone_no_initial_bound$", + r"test_conic_RotatedSecondOrderCone_out_of_order$", + r"test_conic_linear_INFEASIBLE", + r"test_conic_empty_matrix$", + r"test_conic_HermitianPositiveSemidefiniteConeTriangle_2$", ], ) end @@ -144,4 +145,23 @@ ) @test model.assume_min_if_feasibility end + + @testset "Copy twice" begin + T = Float64 + model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()) + x = MOI.add_variable(model) + c = MOI.add_constraint(model, T(2) * x, MOI.GreaterThan(T(0))) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.set(model, MOI.VariablePrimalStart(), x, T(1)) + MOI.set(model, MOI.ConstraintPrimalStart(), c, T(3)) + MOI.set(model, MOI.ConstraintDualStart(), c, T(4)) + dual_model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()) + dual_problem = Dualization.DualProblem{T}(dual_model) + OptimizerType = typeof(dual_problem.dual_model) + dual = DualOptimizer{T,OptimizerType}(dual_problem) + MOI.copy_to(dual, model) + # Test that it is emptied + MOI.copy_to(dual, model) + @test MOI.get(dual_model, MOI.NumberOfVariables()) == 1 + end end diff --git a/test/Tests/test_attributes.jl b/test/Tests/test_attributes.jl new file mode 100644 index 0000000..bca7e6c --- /dev/null +++ b/test/Tests/test_attributes.jl @@ -0,0 +1,353 @@ +# Copyright (c) 2017: Guilherme Bodin, 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. + +module TestAttributes + +using Test + +import MathOptInterface as MOI +import MathOptInterface.Utilities as MOIU +import Dualization +import SCS + +function runtests() + for name in names(@__MODULE__; all = true) + if startswith("$(name)", "test_") + @testset "$(name)" begin + getfield(@__MODULE__, name)() + end + end + end + return +end + +### +### Helper structs +### + +struct DummyModelAttribute <: MOI.AbstractModelAttribute end + +struct DummyVariableAttribute <: MOI.AbstractVariableAttribute end + +struct DummyConstraintAttribute <: MOI.AbstractConstraintAttribute end + +### +### The tests +### + +function _test_constraint_attribute(; constrained_variable::Bool, vector::Bool) + T = Float64 + function rand_value() + value = rand(T) + if vector + return [value] + end + return value + end + mock = MOI.Utilities.MockOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()); + eval_variable_constraint_dual = false, + ) + cached = MOI.Utilities.CachingOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()), + MOI.Utilities.MANUAL, # easier to debug with less try-catch hidding stuff + ) + dual = Dualization.DualOptimizer(mock) + MOI.Utilities.reset_optimizer(cached, dual) + set_constant = T(-4) + if vector + set = MOI.Nonnegatives(1) + else + set = MOI.GreaterThan(set_constant) + end + if constrained_variable + if vector + X, ci = MOI.add_constrained_variables(cached, set) + x = X[] + else + x, ci = MOI.add_constrained_variable(cached, set) + end + else + x = MOI.add_variable(cached) + func = T(1) * x + if vector + func = MOI.Utilities.operate(vcat, T, func - set_constant) + end + ci = MOI.add_constraint(cached, func, set) + end + MOI.set(cached, MOI.ObjectiveSense(), MOI.MIN_SENSE) + obj = T(2) * x + MOI.set(cached, MOI.ObjectiveFunction{typeof(obj)}(), obj) + MOI.Utilities.attach_optimizer(cached) + MOI.optimize!(cached) + for attr in [MOI.ConstraintDualStart(), MOI.ConstraintPrimalStart()] + attr = MOI.ConstraintDualStart() + @test MOI.supports(dual, attr, typeof(ci)) + value = rand_value() + if constrained_variable && vector + # FIXME not supported yet + @test_throws MOI.SetAttributeNotAllowed{MOI.ConstraintDualStart} MOI.set( + dual, + attr, + ci, + value, + ) + @test isnothing(MOI.get(dual, attr, ci)) + else + MOI.set(dual, attr, ci, value) + @test MOI.get(dual, attr, ci) == value + end + end + + if vector && constrained_variable + value = 2ones(T, 1) + else + value = rand(T) + mock_vi = MOI.get(mock, MOI.ListOfVariableIndices())[] + MOI.set(mock, MOI.VariablePrimal(), mock_vi, value) + if vector + value = [value] + end + end + #MOI.set(mock, MOI.ConstraintPrimal(), mock_ci, value) + @test MOI.get(dual, MOI.ConstraintDual(), ci) ≈ value + + value = rand_value() + if vector && constrained_variable + F = MOI.VectorAffineFunction{T} + mock_ci = MOI.get(mock, MOI.ListOfConstraintIndices{F,typeof(set)}())[] + else + F = vector ? MOI.VectorOfVariables : MOI.VariableIndex + mock_ci = MOI.get(mock, MOI.ListOfConstraintIndices{F,typeof(set)}())[] + end + MOI.set(mock, MOI.ConstraintDual(), mock_ci, value) + if !vector + value += set_constant + end + @test MOI.get(dual, MOI.ConstraintPrimal(), ci) ≈ value + return +end + +function test_constraint_attribute_VariableIndex() + return _test_constraint_attribute(; + constrained_variable = true, + vector = false, + ) +end + +function test_constraint_attribute_ScalarAffineFunction() + return _test_constraint_attribute(; + constrained_variable = false, + vector = false, + ) +end + +function test_constraint_attribute_VectorOfVariables() + return _test_constraint_attribute(; + constrained_variable = true, + vector = true, + ) +end + +function test_constraint_attribute_VectorAffineFunction() + return _test_constraint_attribute(; + constrained_variable = false, + vector = true, + ) +end + +function _test_fixed(T, dual_model) + model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()) + x, cx = MOI.add_constrained_variable(model, MOI.EqualTo(T(1))) + c = MOI.add_constraint(model, T(2) * x, MOI.LessThan(T(3))) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + + MOI.set(model, MOI.VariablePrimalStart(), x, T(4)) + MOI.set(model, MOI.ConstraintPrimalStart(), c, T(5)) + MOI.set(model, MOI.ConstraintDualStart(), c, T(6)) + + dual_problem = Dualization.DualProblem{T}(dual_model) + OptimizerType = typeof(dual_problem.dual_model) + dual = Dualization.DualOptimizer{T,OptimizerType}(dual_problem) + @test MOI.supports(dual, MOI.VariablePrimalStart(), typeof(x)) + @test MOI.supports(dual, MOI.ConstraintDualStart(), typeof(c)) + @test MOI.supports(dual, MOI.ConstraintPrimalStart(), typeof(c)) + + MOI.copy_to(dual, model) + @test dual_model === dual.dual_problem.dual_model + + @test MOI.get(dual, MOI.VariablePrimalStart(), x) == 4 + @test MOI.get(dual, MOI.ConstraintPrimalStart(), cx) == 1 + @test isnothing(MOI.get(dual, MOI.ConstraintDualStart(), cx)) + @test MOI.get(dual, MOI.ConstraintPrimalStart(), c) == 5 + @test MOI.get(dual, MOI.ConstraintDualStart(), c) == 6 + return +end + +function test_fixed() + for T in [Int, Float64] + dual_model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()) + _test_fixed(T, dual_model) + end + dual_model = MOI.instantiate( + SCS.Optimizer, + with_bridge_type = nothing, + with_cache_type = Float64, + ) + _test_fixed(Float64, dual_model) + return +end + +function _test_simple(T, dual_model) + model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()) + x = MOI.add_variable(model) + c = MOI.add_constraint(model, T(2) * x, MOI.GreaterThan(T(0))) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.set(model, MOI.VariablePrimalStart(), x, T(1)) + MOI.set(model, MOI.ConstraintPrimalStart(), c, T(3)) + MOI.set(model, MOI.ConstraintDualStart(), c, T(4)) + + dual_problem = Dualization.DualProblem{T}(dual_model) + OptimizerType = typeof(dual_problem.dual_model) + dual = Dualization.DualOptimizer{T,OptimizerType}(dual_problem) + + @test MOI.supports(dual, MOI.VariablePrimalStart(), typeof(x)) + @test MOI.supports(dual, MOI.ConstraintDualStart(), typeof(c)) + @test MOI.supports(dual, MOI.ConstraintPrimalStart(), typeof(c)) + @test MOI.supports(dual, MOI.VariableName(), typeof(x)) + @test MOI.supports(dual, MOI.ConstraintName(), typeof(c)) + + index_map = MOI.copy_to(dual, model) + @test dual_model === dual.dual_problem.dual_model + + vars = MOI.get(dual_model, MOI.ListOfVariableIndices()) + @test MOI.get(dual_model, MOI.VariablePrimalStart(), vars[]) == 4 + + dual_eq = MOI.get( + dual_model, + MOI.ListOfConstraintIndices{ + MOI.ScalarAffineFunction{T}, + MOI.EqualTo{T}, + }(), + )[] + @test MOI.get(dual, MOI.VariablePrimalStart(), x) == 1 + @test MOI.get(dual_model, MOI.ConstraintDualStart(), dual_eq) == -1 + # We could set it to zero, but `nothing` should be fine for the solver, + # let's only revisit if we have a convincing use case + @test isnothing(MOI.get(dual_model, MOI.ConstraintPrimalStart(), dual_eq)) + + dual_bound = MOI.get( + dual_model, + MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.GreaterThan{T}}(), + )[] + @test MOI.get(dual_model, MOI.ConstraintDualStart(), dual_bound) == 3 + # We could set it to the value of the variable, but `nothing` should be fine for the solver. + # Let's revisit only if we have a solver needing `ConstraintPrimalStart` for `VariableIndex`-in-`S` + # constraints. + @test isnothing( + MOI.get(dual_model, MOI.ConstraintPrimalStart(), dual_bound), + ) + + MOI.set(dual, MOI.VariablePrimalStart(), vars[], nothing) + @test isnothing(MOI.get(dual, MOI.VariablePrimalStart(), vars[])) + return +end + +function test_simple() + for T in [Int, Float64] + dual_model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()) + _test_simple(T, dual_model) + end + dual_model = MOI.instantiate( + SCS.Optimizer, + with_bridge_type = nothing, + with_cache_type = Float64, + ) + _test_simple(Float64, dual_model) + return +end + +function _test_conic(T, dual_model, cone1, cone2) + model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()) + x, cx = MOI.add_constrained_variables(model, cone1) + c = MOI.add_constraint(model, MOI.Utilities.vectorize(x .+ T(1)), cone2) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.set.(model, MOI.VariablePrimalStart(), x, T.(1:2)) + MOI.set(model, MOI.ConstraintPrimalStart(), cx, T.(3:4)) + MOI.set(model, MOI.ConstraintDualStart(), cx, T.(4:5)) + MOI.set(model, MOI.ConstraintPrimalStart(), c, T.(6:7)) + MOI.set(model, MOI.ConstraintDualStart(), c, T.(8:9)) + + dual_problem = Dualization.DualProblem{T}(dual_model) + OptimizerType = typeof(dual_problem.dual_model) + dual = Dualization.DualOptimizer{T,OptimizerType}(dual_problem) + @test MOI.supports(dual, MOI.VariablePrimalStart(), eltype(x)) + @test MOI.supports(dual, MOI.ConstraintDualStart(), typeof(cx)) + @test MOI.supports(dual, MOI.ConstraintPrimalStart(), typeof(cx)) + @test MOI.supports(dual, MOI.ConstraintDualStart(), typeof(c)) + @test MOI.supports(dual, MOI.ConstraintPrimalStart(), typeof(c)) + + attr = MOI.VariablePrimalStart() + msg = "Setting $attr for variables constrained at creation is not supported yet" + err = MOI.SetAttributeNotAllowed(attr, msg) + @test_throws err MOI.copy_to(dual, model) + + MOI.set.(model, MOI.VariablePrimalStart(), x, [nothing, nothing]) + attr1 = MOI.ConstraintPrimalStart() + attr2 = MOI.ConstraintDualStart() + msg1 = "Setting $attr1 for variables constrained at creation is not supported yet" + msg2 = "Setting $attr2 for variables constrained at creation is not supported yet" + err1 = MOI.SetAttributeNotAllowed(attr1, msg1) + err2 = MOI.SetAttributeNotAllowed(attr2, msg2) + @test_throws Union{typeof(err1),typeof(err2)} MOI.copy_to(dual, model) + + MOI.set(model, MOI.ConstraintPrimalStart(), cx, nothing) + MOI.set(model, MOI.ConstraintDualStart(), cx, nothing) + MOI.copy_to(dual, model) + + @test dual_model === dual.dual_problem.dual_model + + vars = MOI.get(dual_model, MOI.ListOfVariableIndices()) + @test MOI.get(dual_model, MOI.VariablePrimalStart(), vars) == T[8, 9] + + if !(cone2 isa MOI.Zeros) + dual_c = MOI.get( + dual_model, + MOI.ListOfConstraintIndices{MOI.VectorOfVariables,typeof(cone2)}(), + )[] + @test isnothing( + MOI.get(dual_model, MOI.ConstraintPrimalStart(), dual_c), + ) + @test MOI.get(dual_model, MOI.ConstraintDualStart(), dual_c) == T[6, 7] + end + + @test MOI.get(model, MOI.ConstraintPrimalStart(), c) == T.(6:7) + @test MOI.get(model, MOI.ConstraintDualStart(), c) == T.(8:9) + return +end + +function test_conic() + cones = [MOI.SecondOrderCone(2), MOI.Zeros(2)] + for cone1 in cones + for cone2 in cones + for T in [Float32, Float64] + dual_model = + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()) + _test_conic(T, dual_model, cone1, cone2) + end + dual_model = MOI.instantiate( + SCS.Optimizer, + with_bridge_type = nothing, + with_cache_type = Float64, + ) + _test_conic(Float64, dual_model, cone1, cone2) + end + end + return +end + +end # module + +TestAttributes.runtests() diff --git a/test/runtests.jl b/test/runtests.jl index e8e8b08..efb6f59 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -57,6 +57,7 @@ include("Problems/Power/power_cone_problems.jl") include("Problems/Feasibility/feasibility_problems.jl") # Run tests to travis ci +include("Tests/test_attributes.jl") include("Tests/test_structures.jl") include("Tests/test_supported.jl") include("Tests/test_objective_coefficients.jl")