Skip to content

Commit ad45651

Browse files
authored
[Utilities] maintain order of variables in default_copy_to (#2495)
1 parent 193aec6 commit ad45651

File tree

3 files changed

+220
-123
lines changed

3 files changed

+220
-123
lines changed

docs/src/submodules/Utilities/overview.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -503,24 +503,24 @@ julia> A.m
503503
julia> A.colptr
504504
4-element Vector{Int32}:
505505
0
506-
1
507-
3
506+
2
507+
4
508508
5
509509
510510
julia> A.rowval
511511
5-element Vector{Int32}:
512512
0
513513
1
514+
1
514515
2
515516
0
516-
1
517517
518518
julia> A.nzval
519519
5-element Vector{Float64}:
520+
-4.0
520521
1.0
521522
1.0
522523
2.0
523-
-4.0
524524
1.0
525525
```
526526
The lower and upper row bounds:
@@ -543,15 +543,15 @@ The lower and upper variable bounds:
543543
```jldoctest matrixofconstraints
544544
julia> dest.variables.lower
545545
3-element Vector{Float64}:
546-
5.0
547-
-Inf
548546
0.0
547+
-Inf
548+
5.0
549549
550550
julia> dest.variables.upper
551551
3-element Vector{Float64}:
552-
5.0
553-
10.0
554552
1.0
553+
10.0
554+
5.0
555555
```
556556
Because of larger variations between solvers, the objective can be queried using
557557
the standard MOI methods:
@@ -563,7 +563,7 @@ julia> F = MOI.get(dest, MOI.ObjectiveFunctionType())
563563
MathOptInterface.ScalarAffineFunction{Float64}
564564
565565
julia> F = MOI.get(dest, MOI.ObjectiveFunction{F}())
566-
0.0 + 1.0 MOI.VariableIndex(3) + 2.0 MOI.VariableIndex(2) - 3.1 MOI.VariableIndex(1)
566+
0.0 + 1.0 MOI.VariableIndex(1) + 2.0 MOI.VariableIndex(2) - 3.1 MOI.VariableIndex(3)
567567
```
568568

569569
Thus, Clp.jl implements [`copy_to`](@ref) methods similar to the following:

src/Utilities/copy.jl

Lines changed: 165 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -154,85 +154,6 @@ function _pass_attribute(
154154
return
155155
end
156156

157-
"""
158-
_try_constrain_variables_on_creation(
159-
dest::MOI.ModelLike,
160-
src::MOI.ModelLike,
161-
index_map::IndexMap,
162-
::Type{S},
163-
) where {S<:MOI.AbstractVectorSet}
164-
165-
Copy the constraints of type `MOI.VectorOfVariables`-in-`S` from the model `src`
166-
to the model `dest` and fill `index_map` accordingly. The copy is only done when
167-
the variables to be copied are not already keys of `index_map`.
168-
169-
It returns a list of the constraints that were not added.
170-
"""
171-
function _try_constrain_variables_on_creation(
172-
dest::MOI.ModelLike,
173-
src::MOI.ModelLike,
174-
index_map::IndexMap,
175-
::Type{S},
176-
) where {S<:MOI.AbstractVectorSet}
177-
not_added = MOI.ConstraintIndex{MOI.VectorOfVariables,S}[]
178-
for ci_src in
179-
MOI.get(src, MOI.ListOfConstraintIndices{MOI.VectorOfVariables,S}())
180-
f_src = MOI.get(src, MOI.ConstraintFunction(), ci_src)
181-
if !allunique(f_src.variables)
182-
# Can't add it because there are duplicate variables
183-
push!(not_added, ci_src)
184-
elseif any(vi -> haskey(index_map, vi), f_src.variables)
185-
# Can't add it because it contains a variable previously added
186-
push!(not_added, ci_src)
187-
else
188-
set = MOI.get(src, MOI.ConstraintSet(), ci_src)::S
189-
vis_dest, ci_dest = MOI.add_constrained_variables(dest, set)
190-
index_map[ci_src] = ci_dest
191-
for (vi_src, vi_dest) in zip(f_src.variables, vis_dest)
192-
index_map[vi_src] = vi_dest
193-
end
194-
end
195-
end
196-
return not_added
197-
end
198-
199-
"""
200-
_try_constrain_variables_on_creation(
201-
dest::MOI.ModelLike,
202-
src::MOI.ModelLike,
203-
index_map::IndexMap,
204-
::Type{S},
205-
) where {S<:MOI.AbstractScalarSet}
206-
207-
Copy the constraints of type `MOI.VariableIndex`-in-`S` from the model `src` to
208-
the model `dest` and fill `index_map` accordingly. The copy is only done when the
209-
variables to be copied are not already keys of `index_map`.
210-
211-
It returns a list of the constraints that were not added.
212-
"""
213-
function _try_constrain_variables_on_creation(
214-
dest::MOI.ModelLike,
215-
src::MOI.ModelLike,
216-
index_map::IndexMap,
217-
::Type{S},
218-
) where {S<:MOI.AbstractScalarSet}
219-
not_added = MOI.ConstraintIndex{MOI.VariableIndex,S}[]
220-
for ci_src in
221-
MOI.get(src, MOI.ListOfConstraintIndices{MOI.VariableIndex,S}())
222-
f_src = MOI.get(src, MOI.ConstraintFunction(), ci_src)
223-
if haskey(index_map, f_src)
224-
# Can't add it because it contains a variable previously added
225-
push!(not_added, ci_src)
226-
else
227-
set = MOI.get(src, MOI.ConstraintSet(), ci_src)::S
228-
vi_dest, ci_dest = MOI.add_constrained_variable(dest, set)
229-
index_map[ci_src] = ci_dest
230-
index_map[f_src] = vi_dest
231-
end
232-
end
233-
return not_added
234-
end
235-
236157
"""
237158
_copy_constraints(
238159
dest::MOI.ModelLike,
@@ -344,22 +265,6 @@ function _pass_constraints(
344265
return
345266
end
346267

347-
function _copy_free_variables(dest::MOI.ModelLike, index_map::IndexMap, vis_src)
348-
if length(vis_src) == length(index_map.var_map)
349-
return # All variables already added
350-
end
351-
x = MOI.add_variables(dest, length(vis_src) - length(index_map.var_map))
352-
i = 1
353-
for vi in vis_src
354-
if !haskey(index_map, vi)
355-
index_map[vi] = x[i]
356-
i += 1
357-
end
358-
end
359-
@assert i == length(x) + 1
360-
return
361-
end
362-
363268
_is_variable_function(::Type{MOI.VariableIndex}) = true
364269
_is_variable_function(::Type{MOI.VectorOfVariables}) = true
365270
_is_variable_function(::Any) = false
@@ -478,25 +383,8 @@ function default_copy_to(dest::MOI.ModelLike, src::MOI.ModelLike)
478383
error("Model $(typeof(dest)) does not support copy_to.")
479384
end
480385
MOI.empty!(dest)
481-
vis_src = MOI.get(src, MOI.ListOfVariableIndices())
482-
index_map = IndexMap()
483-
# The `NLPBlock` assumes that the order of variables does not change (#849)
484-
# Therefore, all VariableIndex and VectorOfVariable constraints are added
485-
# seprately, and no variables constrained-on-creation are added.
486-
has_nlp = MOI.NLPBlock() in MOI.get(src, MOI.ListOfModelAttributesSet())
487-
constraints_not_added = if has_nlp
488-
Any[
489-
MOI.get(src, MOI.ListOfConstraintIndices{F,S}()) for
490-
(F, S) in MOI.get(src, MOI.ListOfConstraintTypesPresent()) if
491-
_is_variable_function(F)
492-
]
493-
else
494-
Any[
495-
_try_constrain_variables_on_creation(dest, src, index_map, S)
496-
for S in sorted_variable_sets_by_cost(dest, src)
497-
]
498-
end
499-
_copy_free_variables(dest, index_map, vis_src)
386+
index_map, vis_src, constraints_not_added =
387+
_copy_variables_with_set(dest, src)
500388
# Copy variable attributes
501389
pass_attributes(dest, src, index_map, vis_src)
502390
# Copy model attributes
@@ -507,6 +395,169 @@ function default_copy_to(dest::MOI.ModelLike, src::MOI.ModelLike)
507395
return index_map
508396
end
509397

398+
struct _CopyVariablesWithSetCache
399+
variable_to_column::Dict{MOI.VariableIndex,Int}
400+
constraints_not_added::Vector{Any}
401+
variables_with_domain::Set{MOI.VariableIndex}
402+
variable_cones::Vector{Tuple{Vector{MOI.VariableIndex},Any}}
403+
function _CopyVariablesWithSetCache()
404+
return new(
405+
Dict{MOI.VariableIndex,Int}(),
406+
Any[],
407+
Set{MOI.VariableIndex}(),
408+
Tuple{Vector{MOI.VariableIndex},Any}[],
409+
)
410+
end
411+
end
412+
413+
function _build_copy_variables_with_set_cache(
414+
src::MOI.ModelLike,
415+
cache::_CopyVariablesWithSetCache,
416+
::Type{S},
417+
) where {S<:MOI.AbstractScalarSet}
418+
F = MOI.VariableIndex
419+
indices = MOI.ConstraintIndex{F,S}[]
420+
for ci in MOI.get(src, MOI.ListOfConstraintIndices{F,S}())
421+
x = MOI.get(src, MOI.ConstraintFunction(), ci)
422+
if x in cache.variables_with_domain
423+
# `x` is already assigned to a domain. Add this constraint via
424+
# `add_constraint`.
425+
push!(indices, ci)
426+
else
427+
# `x` is not assigned to a domain. Choose to add this constraint via
428+
# `x, ci = add_constraint_variable(model, set)`
429+
push!(cache.variables_with_domain, x)
430+
push!(cache.variable_cones, ([x], ci))
431+
end
432+
end
433+
if !isempty(indices)
434+
# If indices is not empty, then we have some constraints to add.
435+
push!(cache.constraints_not_added, indices)
436+
end
437+
return
438+
end
439+
440+
# This function is a heuristic that checks whether `f` should be added via
441+
# `MOI.add_constrained_variables`.
442+
function _is_variable_cone(
443+
cache::_CopyVariablesWithSetCache,
444+
f::MOI.VectorOfVariables,
445+
)
446+
if isempty(f.variables)
447+
# If the dimension is `0`, `f` cannot be added via
448+
# `add_constrained_variables`
449+
return false
450+
end
451+
offset = cache.variable_to_column[f.variables[1]] - 1
452+
for (i, xi) in enumerate(f.variables)
453+
if xi in cache.variables_with_domain
454+
# The function contains at least one element that is already
455+
# assigned to a domain. We can't add `f` via
456+
# `add_constrained_variables`
457+
return false
458+
elseif cache.variable_to_column[xi] != offset + i
459+
# The variables in the function are not contiguous in their column
460+
# ordering. In theory, we could add `f` via `add_constrained_variables`,
461+
# but this would introduce a permutation so we choose not to.
462+
return false
463+
end
464+
end
465+
return true
466+
end
467+
468+
function _build_copy_variables_with_set_cache(
469+
src::MOI.ModelLike,
470+
cache::_CopyVariablesWithSetCache,
471+
::Type{S},
472+
) where {S<:MOI.AbstractVectorSet}
473+
F = MOI.VectorOfVariables
474+
indices = MOI.ConstraintIndex{F,S}[]
475+
for ci in MOI.get(src, MOI.ListOfConstraintIndices{F,S}())
476+
f = MOI.get(src, MOI.ConstraintFunction(), ci)
477+
if _is_variable_cone(cache, f)
478+
for fi in f.variables
479+
# We need to assign each variable in `f` to a domain
480+
push!(cache.variables_with_domain, fi)
481+
end
482+
# And we need to add the variables via `add_constrained_variables`.
483+
push!(cache.variable_cones, (f.variables, ci))
484+
else
485+
# Not a variable cone, so add via `add_constraint`.
486+
push!(indices, ci)
487+
end
488+
end
489+
if !isempty(indices)
490+
# If indices is not empty, then we have some constraints to add.
491+
push!(cache.constraints_not_added, indices)
492+
end
493+
return
494+
end
495+
496+
function _add_variable_with_domain(
497+
dest,
498+
src,
499+
index_map,
500+
f,
501+
ci::MOI.ConstraintIndex{MOI.VariableIndex,<:MOI.AbstractScalarSet},
502+
)
503+
set = MOI.get(src, MOI.ConstraintSet(), ci)
504+
dest_x, dest_ci = MOI.add_constrained_variable(dest, set)
505+
index_map[only(f)] = dest_x
506+
index_map[ci] = dest_ci
507+
return
508+
end
509+
510+
function _add_variable_with_domain(
511+
dest,
512+
src,
513+
index_map,
514+
f,
515+
ci::MOI.ConstraintIndex{MOI.VectorOfVariables,<:MOI.AbstractVectorSet},
516+
)
517+
set = MOI.get(src, MOI.ConstraintSet(), ci)
518+
dest_x, dest_ci = MOI.add_constrained_variables(dest, set)
519+
for (fi, xi) in zip(f, dest_x)
520+
index_map[fi] = xi
521+
end
522+
index_map[ci] = dest_ci
523+
return
524+
end
525+
526+
function _copy_variables_with_set(dest, src)
527+
index_map = IndexMap()
528+
vis_src = MOI.get(src, MOI.ListOfVariableIndices())
529+
cache = _CopyVariablesWithSetCache()
530+
for (i, v) in enumerate(vis_src)
531+
cache.variable_to_column[v] = i
532+
end
533+
for S in sorted_variable_sets_by_cost(dest, src)
534+
_build_copy_variables_with_set_cache(src, cache, S)
535+
end
536+
column(x::MOI.VariableIndex) = cache.variable_to_column[x]
537+
start_column(x) = column(first(x[1]))
538+
current_column = 0
539+
sort!(cache.variable_cones; by = start_column)
540+
for (f, ci) in cache.variable_cones
541+
offset = column(first(f)) - current_column - 1
542+
if offset > 0
543+
dest_x = MOI.add_variables(dest, offset)
544+
for i in 1:offset
545+
index_map[vis_src[current_column+i]] = dest_x[i]
546+
end
547+
end
548+
_add_variable_with_domain(dest, src, index_map, f, ci)
549+
current_column = column(last(f))
550+
end
551+
offset = length(cache.variable_to_column) - current_column
552+
if offset > 0
553+
dest_x = MOI.add_variables(dest, offset)
554+
for i in 1:offset
555+
index_map[vis_src[current_column+i]] = dest_x[i]
556+
end
557+
end
558+
return index_map, vis_src, cache.constraints_not_added
559+
end
560+
510561
"""
511562
ModelFilter(filter::Function, model::MOI.ModelLike)
512563

0 commit comments

Comments
 (0)