|
| 1 | +mutable struct ConicForm{T, AT, VT, C} <: MOI.ModelLike |
| 2 | + num_rows::Vector{Int} |
| 3 | + dimension::Dict{Int, Int} |
| 4 | + sense::MOI.OptimizationSense |
| 5 | + objective_constant::T # The objective |
| 6 | + A::Union{Nothing, AT} # The constraints |
| 7 | + b::VT # `b - Ax in cones` |
| 8 | + c::VT # `sense c'x + objective_constant` |
| 9 | + cone_types::C |
| 10 | + |
| 11 | + function ConicForm{T, AT, VT}(cone_types) where {T, AT, VT} |
| 12 | + model = new{T, AT, VT, typeof(cone_types)}() |
| 13 | + model.cone_types = cone_types |
| 14 | + model.num_rows = zeros(Int, length(cone_types)) |
| 15 | + model.dimension = Dict{Int, Int}() |
| 16 | + model.A = nothing |
| 17 | + return model |
| 18 | + end |
| 19 | +end |
| 20 | + |
| 21 | +# The ordering of CONES matches SCS |
| 22 | +const CONES = Dict( |
| 23 | + MOI.Zeros => 1, |
| 24 | + MOI.Nonnegatives => 2, |
| 25 | + MOI.SecondOrderCone => 3, |
| 26 | + MOI.PositiveSemidefiniteConeTriangle => 4, |
| 27 | + MOI.ExponentialCone => 5, |
| 28 | + MOI.DualExponentialCone => 6 |
| 29 | +) |
| 30 | + |
| 31 | +_set_type(::MOI.ConstraintIndex{F,S}) where {F,S} = S |
| 32 | +cons_offset(conic::MOI.Zeros) = conic.dimension |
| 33 | +cons_offset(conic::MOI.Nonnegatives) = conic.dimension |
| 34 | +cons_offset(conic::MOI.SecondOrderCone) = conic.dimension |
| 35 | +cons_offset(conic::MOI.PositiveSemidefiniteConeTriangle) = Int64((conic.side_dimension*(conic.side_dimension+1))/2) |
| 36 | + |
| 37 | +function get_conic_form(::Type{T}, model::M, con_idx) where {T, M <: MOI.AbstractOptimizer} |
| 38 | + # reorder constraints |
| 39 | + cis = sort( |
| 40 | + con_idx, |
| 41 | + by = x->CONES[_set_type(x)] |
| 42 | + ) |
| 43 | + |
| 44 | + # extract cones |
| 45 | + cones = _set_type.(cis) |
| 46 | + cones = unique(cones) |
| 47 | + |
| 48 | + conic = ConicForm{T, SparseMatrixCSRtoCSC{Int64}, Array{T, 1}}(Tuple(cones)) |
| 49 | + |
| 50 | + idxmap = MOI.copy_to(conic, model) |
| 51 | + |
| 52 | + # fix optimization sense |
| 53 | + if MOI.get(model, MOI.ObjectiveSense()) == MOI.MAX_SENSE |
| 54 | + conic.c = -conic.c |
| 55 | + end |
| 56 | + |
| 57 | + return conic |
| 58 | +end |
| 59 | + |
| 60 | +MOI.is_empty(model::ConicForm) = model.A === nothing |
| 61 | +function MOI.empty!(model::ConicForm{T}) where T |
| 62 | + empty!(model.dimension) |
| 63 | + fill!(model.num_rows, 0) |
| 64 | + model.A = nothing |
| 65 | + model.sense = MOI.FEASIBILITY_SENSE |
| 66 | + model.objective_constant = zero(T) |
| 67 | +end |
| 68 | + |
| 69 | +function _first(::Type{S}, idx::Int, ::Type{S}, args::Vararg{Type, N}) where {S, N} |
| 70 | + return idx |
| 71 | +end |
| 72 | +function _first(::Type{S}, idx::Int, ::Type, args::Vararg{Type, N}) where {S, N} |
| 73 | + return _first(S, idx + 1, args...) |
| 74 | +end |
| 75 | +_first(::Type, idx::Int) = nothing |
| 76 | + |
| 77 | +_findfirst(::Type{S}, sets::Tuple) where {S} = _first(S, 1, sets...) |
| 78 | + |
| 79 | +function MOI.supports_constraint( |
| 80 | + model::ConicForm, |
| 81 | + ::Type{MOI.VectorAffineFunction{Float64}}, |
| 82 | + ::Type{S}) where S <: MOI.AbstractVectorSet |
| 83 | + return _findfirst(S, model.cone_types) !== nothing |
| 84 | +end |
| 85 | + |
| 86 | +function _allocate_variables(model::ConicForm{T, AT, VT}, vis_src, idxmap) where {T, AT, VT} |
| 87 | + model.A = AT(length(vis_src)) |
| 88 | + for (i, vi) in enumerate(vis_src) |
| 89 | + idxmap[vi] = MOI.VariableIndex(i) |
| 90 | + end |
| 91 | + return |
| 92 | +end |
| 93 | + |
| 94 | +function rows(model::ConicForm, ci::CI{MOI.VectorAffineFunction{Float64}}) |
| 95 | + return ci.value .+ (1:model.dimension[ci.value]) |
| 96 | +end |
| 97 | + |
| 98 | +function MOI.set(::ConicForm, ::MOI.VariablePrimalStart, |
| 99 | + ::MOI.VariableIndex, ::Nothing) |
| 100 | +end |
| 101 | +function MOI.set(model::ConicForm, ::MOI.VariablePrimalStart, |
| 102 | + vi::MOI.VariableIndex, value::Float64) |
| 103 | + model.primal[vi.value] = value |
| 104 | +end |
| 105 | +function MOI.set(::ConicForm, ::MOI.ConstraintPrimalStart, |
| 106 | + ::MOI.ConstraintIndex, ::Nothing) |
| 107 | +end |
| 108 | +function MOI.set(model::ConicForm, ::MOI.ConstraintPrimalStart, |
| 109 | + ci::MOI.ConstraintIndex, value) |
| 110 | + offset = constroffset(model, ci) |
| 111 | + model.slack[rows(model, ci)] .= value |
| 112 | +end |
| 113 | +function MOI.set(::ConicForm, ::MOI.ConstraintDualStart, |
| 114 | + ::MOI.ConstraintIndex, ::Nothing) |
| 115 | +end |
| 116 | +function MOI.set(model::ConicForm, ::MOI.ConstraintDualStart, |
| 117 | + ci::MOI.ConstraintIndex, value) |
| 118 | + offset = constroffset(model, ci) |
| 119 | + model.dual[rows(model, ci)] .= value |
| 120 | +end |
| 121 | +function MOI.set(model::ConicForm, ::MOI.ObjectiveSense, sense::MOI.OptimizationSense) |
| 122 | + model.sense = sense |
| 123 | +end |
| 124 | +variable_index_value(t::MOI.ScalarAffineTerm) = t.variable_index.value |
| 125 | +variable_index_value(t::MOI.VectorAffineTerm) = variable_index_value(t.scalar_term) |
| 126 | +function MOI.set(model::ConicForm, ::MOI.ObjectiveFunction, |
| 127 | + f::MOI.ScalarAffineFunction{Float64}) |
| 128 | + c = Vector(sparsevec(variable_index_value.(f.terms), MOI.coefficient.(f.terms), |
| 129 | + model.A.n)) |
| 130 | + model.objective_constant = f.constant |
| 131 | + model.c = c |
| 132 | + return nothing |
| 133 | +end |
| 134 | + |
| 135 | +function _allocate_constraint(model::ConicForm, src, indexmap, cone_id, ci) |
| 136 | + # TODO use `CanonicalConstraintFunction` |
| 137 | + func = MOI.get(src, MOI.ConstraintFunction(), ci) |
| 138 | + func = MOIU.is_canonical(func) ? func : MOI.Utilities.canonical(func) |
| 139 | + allocate_terms(model.A, indexmap, func) |
| 140 | + offset = model.num_rows[cone_id] |
| 141 | + model.num_rows[cone_id] = offset + MOI.output_dimension(func) |
| 142 | + return ci, offset, func |
| 143 | +end |
| 144 | + |
| 145 | +function _allocate_constraints(model::ConicForm, src, indexmap, cone_id, ::Type{S}) where S |
| 146 | + cis = MOI.get(src, MOI.ListOfConstraintIndices{MOI.VectorAffineFunction{Float64}, S}()) |
| 147 | + return map(cis) do ci |
| 148 | + _allocate_constraint(model, src, indexmap, cone_id, ci) |
| 149 | + end |
| 150 | +end |
| 151 | + |
| 152 | +function _load_variables(model::ConicForm, nvars::Integer) |
| 153 | + m = sum(model.num_rows) |
| 154 | + model.A.m = m |
| 155 | + model.b = zeros(m) |
| 156 | + model.c = zeros(model.A.n) |
| 157 | + allocate_nonzeros(model.A) |
| 158 | +end |
| 159 | + |
| 160 | +function _load_constraints(model::ConicForm, src, indexmap, cone_offset, i, cache) |
| 161 | + for (ci_src, offset_in_cone, func) in cache |
| 162 | + offset = cone_offset + offset_in_cone |
| 163 | + set = MOI.get(src, MOI.ConstraintSet(), ci_src) |
| 164 | + load_terms(model.A, indexmap, func, offset) |
| 165 | + copyto!(model.b, offset + 1, func.constants) |
| 166 | + model.dimension[offset] = MOI.output_dimension(func) |
| 167 | + indexmap[ci_src] = typeof(ci_src)(offset) |
| 168 | + end |
| 169 | +end |
| 170 | + |
| 171 | +function MOI.copy_to(dest::ConicForm, src::MOI.ModelLike; copy_names::Bool=true) |
| 172 | + MOI.empty!(dest) |
| 173 | + |
| 174 | + vis_src = MOI.get(src, MOI.ListOfVariableIndices()) |
| 175 | + idxmap = MOIU.IndexMap() |
| 176 | + |
| 177 | + has_constraints = BitSet() |
| 178 | + for (F, S) in MOI.get(src, MOI.ListOfConstraints()) |
| 179 | + i = _findfirst(S, dest.cone_types) |
| 180 | + if i === nothing || F != MOI.VectorAffineFunction{Float64} |
| 181 | + throw(MOI.UnsupportedConstraint{F, S}()) |
| 182 | + end |
| 183 | + push!(has_constraints, i) |
| 184 | + end |
| 185 | + |
| 186 | + _allocate_variables(dest, vis_src, idxmap) |
| 187 | + |
| 188 | + # Allocate constraints |
| 189 | + caches = map(collect(has_constraints)) do i |
| 190 | + _allocate_constraints(dest, src, idxmap, i, dest.cone_types[i]) |
| 191 | + end |
| 192 | + |
| 193 | + # Load variables |
| 194 | + _load_variables(dest, length(vis_src)) |
| 195 | + |
| 196 | + # Set variable attributes |
| 197 | + MOIU.pass_attributes(dest, src, copy_names, idxmap, vis_src) |
| 198 | + |
| 199 | + # Set model attributes |
| 200 | + MOIU.pass_attributes(dest, src, copy_names, idxmap) |
| 201 | + |
| 202 | + # Load constraints |
| 203 | + offset = 0 |
| 204 | + for (i, cache) in zip(has_constraints, caches) |
| 205 | + _load_constraints(dest, src, idxmap, offset, i, cache) |
| 206 | + offset += dest.num_rows[i] |
| 207 | + end |
| 208 | + |
| 209 | + return idxmap |
| 210 | +end |
0 commit comments