@@ -44,11 +44,33 @@ function reformulate_model(model::JuMP.AbstractModel, method::LOA)
4444 best_result = nothing
4545 master_bound = nothing
4646
47- # Seed the master with an OA cut per set-covering combination, each
48- # NLP warm-started from the last feasible primal.
47+ # Set-covering seed. This mimics Pyomo GDPopt's set-covering
48+ # initialization: borrow the master as a covering MILP (only its
49+ # objective changes), let it pick a combination that activates the
50+ # nonlinear disjuncts still lacking a linearization, solve the NLP
51+ # there, and seed the resulting OA and no-good cuts. Each NLP
52+ # warm-starts from the last feasible primal.
4953 previous_result = nothing
50- for combination in problem. covering_combinations
54+ cover_disjuncts = _cover_disjuncts (problem)
55+ needs_cover = trues (length (cover_disjuncts))
56+ num_covered = 0
57+ for iteration in 1 : method. set_cover_max_iter
58+ (iteration == 1 || any (needs_cover)) || break
5159 time () < loop_deadline || break
60+ # Swap in the covering objective, solve, read off the combination,
61+ # then restore the OA objective (with its accumulated slack
62+ # penalties) before emitting cuts against it.
63+ oa_objective = JuMP. objective_function (master. model)
64+ JuMP. @objective (master. model, Max,
65+ _cover_objective (master, cover_disjuncts, needs_cover,
66+ num_covered))
67+ _cap_remaining_time (master. model, loop_deadline)
68+ JuMP. optimize! (master. model)
69+ solved = JuMP. is_solved_and_feasible (master. model)
70+ combination = solved ? _extract_combination (problem, master) : nothing
71+ JuMP. set_objective_sense (master. model, master. objective_sense)
72+ JuMP. set_objective_function (master. model, oa_objective)
73+ solved || break
5274 _set_nlp_warm_start (previous_result)
5375 result = _solve_nlp (problem, combination, method;
5476 deadline = loop_deadline)
@@ -60,6 +82,17 @@ function reformulate_model(model::JuMP.AbstractModel, method::LOA)
6082 best_result = result
6183 end
6284 result. feasible && (previous_result = result)
85+ # A disjunct counts as covered only once it is active in a
86+ # feasible NLP. An infeasible combination still contributes its
87+ # no-good cut above but leaves the coverage targets untouched.
88+ if result. feasible
89+ for i in eachindex (cover_disjuncts)
90+ needs_cover[i] || continue
91+ _disjunct_active (result. combination, cover_disjuncts[i]) &&
92+ (needs_cover[i] = false )
93+ end
94+ num_covered = count (! , needs_cover)
95+ end
6396 end
6497
6598 # Master/NLP loop: `alpha_oa` is the bound, the NLP refines the
@@ -143,25 +176,50 @@ function reformulate_model(::M, ::LOA) where {M}
143176end
144177
145178# ###############################################################################
146- # SET-COVERING INITIALIZATION (simple version from pyomo)
179+ # SET-COVERING INITIALIZATION
147180# ###############################################################################
148- # `K = max disjunction size` combinations that activate every indicator
149- # at least once: combination `k` activates the `k`-th indicator of each
150- # disjunction, cycling via `mod1`. Inconsistent nested combinations are
151- # caught by the no-good cut from the infeasible NLP.
152- function _set_covering_combinations (model:: JuMP.AbstractModel )
153- LogicalRef = LogicalVariableRef{typeof (model)}
154- indicator_lists = [collect (d. constraint. indicators)
155- for (_, d) in _disjunctions (model)]
156- isempty (indicator_lists) && return Dict{LogicalRef, Bool}[]
157- K = maximum (length, indicator_lists)
158- return [
159- Dict {LogicalRef, Bool} (
160- indicator => (indicator == indicators[mod1 (k, length (indicators))])
161- for indicators in indicator_lists
162- for indicator in indicators)
163- for k in 1 : K
164- ]
181+ # The nonlinear disjuncts to cover: one entry per distinct indicator that
182+ # owns a nonlinear disjunct constraint, carrying that indicator's
183+ # `binary_ref`. Keyed by `(underlying binary, active value)` so the two
184+ # disjuncts of a single-binary disjunction (`y` and its `1 - y`
185+ # complement) stay distinct. Purely linear disjuncts need no cover: the
186+ # inner reformulation already places them in the master exactly.
187+ function _cover_disjuncts (problem:: _LOAProblem )
188+ V = eltype (problem. binaries)
189+ seen = Set {Tuple{V, Bool}} ()
190+ disjuncts = Any[]
191+ for (binary_ref, _, _) in problem. disjunct_constraints
192+ key = (_underlying_binary (binary_ref),
193+ _underlying_value (binary_ref, true ))
194+ key in seen && continue
195+ push! (seen, key)
196+ push! (disjuncts, binary_ref)
197+ end
198+ return disjuncts
199+ end
200+
201+ # The set-covering objective (master space), mimicking Pyomo GDPopt:
202+ # maximize active disjuncts weighted `num_covered + 1` if still uncovered
203+ # else `1`, so one uncovered disjunct outweighs every covered one and each
204+ # solve must activate a new disjunct when the logic allows. Empty
205+ # `disjuncts` gives the zero expression: a constant objective that still
206+ # seeds one feasible combination (needed to bound `alpha_oa`).
207+ function _cover_objective (
208+ master:: _LOAMaster ,
209+ disjuncts,
210+ needs_cover,
211+ num_covered:: Int
212+ )
213+ T = JuMP. value_type (typeof (master. model))
214+ V = JuMP. variable_ref_type (typeof (master. model))
215+ expr = JuMP. GenericAffExpr {T, V} (zero (T))
216+ for i in eachindex (disjuncts)
217+ weight = needs_cover[i] ? num_covered + 1 : 1
218+ activation = _remap_indicator_to_binary (disjuncts[i],
219+ master. variable_map)
220+ JuMP. add_to_expression! (expr, T (weight), activation)
221+ end
222+ return expr
165223end
166224
167225# ###############################################################################
@@ -176,20 +234,6 @@ _underlying_binary(binary_ref::JuMP.GenericAffExpr) =
176234_underlying_value (:: JuMP.AbstractVariableRef , active:: Bool ) = active
177235_underlying_value (:: JuMP.GenericAffExpr , active:: Bool ) = ! active
178236
179- # Translate an indicator-level combination onto the underlying binary
180- # variables, inverting the value for complement-form indicators.
181- function _binary_combination (model:: JuMP.AbstractModel , combination)
182- V = JuMP. variable_ref_type (typeof (model))
183- binary_map = _indicator_to_binary (model)
184- result = Dict {V, Bool} ()
185- for (indicator, active) in combination
186- binary_ref = binary_map[indicator]
187- result[_underlying_binary (binary_ref)] =
188- _underlying_value (binary_ref, active)
189- end
190- return result
191- end
192-
193237"""
194238 build_loa_problem(
195239 model::JuMP.AbstractModel,
@@ -199,11 +243,11 @@ end
199243
200244Build the problem the LOA loop operates on from `model` (already
201245reformulated by the LOA inner method): the NLP subproblem, the binary
202- variables backing the indicators, the set-covering seed combinations,
203- the nonlinear disjunct `(binary_ref, function, set)` triples, the
204- nonlinear global `(function, set)` pairs, and the Hull disaggregation
205- map. The NLP is `model` itself; the InfiniteOpt extension overloads
206- this to build the problem from the transcribed backend instead.
246+ variables backing the indicators, the nonlinear disjunct
247+ `(binary_ref, function, set)` triples, the nonlinear global
248+ `(function, set)` pairs, and the Hull disaggregation map. The NLP is
249+ `model` itself; the InfiniteOpt extension overloads this to build the
250+ problem from the transcribed backend instead.
207251
208252## Returns
209253- `_LOAProblem`: the problem.
@@ -219,8 +263,6 @@ function build_loa_problem(
219263 binaries = V[_underlying_binary (binary_ref)
220264 for (_, binary_ref) in binary_map]
221265 unique! (binaries)
222- combinations = [_binary_combination (model, combination)
223- for combination in _set_covering_combinations (model)]
224266
225267 disjunct_constraints = Tuple{Any, Any, Any}[]
226268 for (_, disjunction) in _disjunctions (model)
@@ -255,7 +297,7 @@ function build_loa_problem(
255297 binary_disaggregations = disaggregation_map === nothing ? nothing :
256298 Dict ((variable, binary_map[indicator]) => disaggregated
257299 for ((variable, indicator), disaggregated) in disaggregation_map)
258- return _LOAProblem (model, binaries, combinations, disjunct_constraints,
300+ return _LOAProblem (model, binaries, disjunct_constraints,
259301 global_constraints, binary_disaggregations)
260302end
261303
0 commit comments