6
6
# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md for
7
7
# full copyright and license information.
8
8
###############################################################################
9
+ import math
9
10
import pyomo .environ as pyo
10
11
import numpy as np
11
12
from mpisppy .cylinders .lagrangian_bounder import LagrangianOuterBound
15
16
16
17
class ReducedCostsSpoke (LagrangianOuterBound ):
17
18
18
- send_fields = (* LagrangianOuterBound .send_fields , Field .EXPECTED_REDUCED_COST , Field .SCENARIO_REDUCED_COST ,)
19
+ send_fields = (* LagrangianOuterBound .send_fields , Field .EXPECTED_REDUCED_COST , Field .SCENARIO_REDUCED_COST ,
20
+ Field .NONANT_LOWER_BOUNDS , Field .NONANT_UPPER_BOUNDS ,)
19
21
receive_fields = (* LagrangianOuterBound .receive_fields ,)
20
22
21
23
converger_spoke_char = 'R'
@@ -57,8 +59,40 @@ def register_send_fields(self) -> None:
57
59
scenario_buffer_len += len (s ._mpisppy_data .nonant_indices )
58
60
self ._scenario_rc_buffer = np .zeros (scenario_buffer_len )
59
61
62
+ self .initialize_bound_fields ()
63
+ self .create_integer_variable_where ()
64
+
60
65
return
61
66
67
+ def initialize_bound_fields (self ):
68
+ self ._nonant_lower_bounds = self .send_buffers [Field .NONANT_LOWER_BOUNDS ].value_array ()
69
+ self ._nonant_upper_bounds = self .send_buffers [Field .NONANT_UPPER_BOUNDS ].value_array ()
70
+
71
+ self ._nonant_lower_bounds [:] = - np .inf
72
+ self ._nonant_upper_bounds [:] = np .inf
73
+
74
+ for s in self .opt .local_scenarios .values ():
75
+ scenario_lower_bounds = np .fromiter (
76
+ _lb_generator (s ._mpisppy_data .nonant_indices .values ()),
77
+ dtype = float ,
78
+ count = len (s ._mpisppy_data .nonant_indices ),
79
+ )
80
+ self ._nonant_lower_bounds = np .maximum (self ._nonant_lower_bounds , scenario_lower_bounds )
81
+
82
+ scenario_upper_bounds = np .fromiter (
83
+ _ub_generator (s ._mpisppy_data .nonant_indices .values ()),
84
+ dtype = float ,
85
+ count = len (s ._mpisppy_data .nonant_indices ),
86
+ )
87
+ self ._nonant_upper_bounds = np .minimum (self ._nonant_upper_bounds , scenario_upper_bounds )
88
+
89
+ def create_integer_variable_where (self ):
90
+ self ._integer_variable_where = np .full (len (self ._nonant_lower_bounds ), False )
91
+ for s in self .opt .local_scenarios .values ():
92
+ for idx , xvar in enumerate (s ._mpisppy_data .nonant_indices .values ()):
93
+ if xvar .is_integer ():
94
+ self ._integer_variable_where [idx ] = True
95
+
62
96
@property
63
97
def rc_global (self ):
64
98
return self .send_buffers [Field .EXPECTED_REDUCED_COST ].value_array ()
@@ -84,24 +118,19 @@ def lagrangian_prep(self):
84
118
same as base class, but relax the integer variables and
85
119
attach the reduced cost suffix
86
120
"""
87
- # Split up PH_Prep? Prox option is important for APH.
88
- # Seems like we shouldn't need the Lagrangian stuff, so attach_prox=False
89
- # Scenarios are created here
90
- self .opt .PH_Prep (attach_prox = False )
91
- self .opt ._reenable_W ()
92
-
93
121
relax_integer_vars = pyo .TransformationFactory ("core.relax_integer_vars" )
94
122
for s in self .opt .local_subproblems .values ():
95
123
relax_integer_vars .apply_to (s )
96
124
s .rc = pyo .Suffix (direction = pyo .Suffix .IMPORT )
97
- self . opt . _create_solvers ( presolve = False )
125
+ super (). lagrangian_prep ( )
98
126
99
127
def lagrangian (self , need_solution = True ):
100
128
if not need_solution :
101
129
raise RuntimeError ("ReducedCostsSpoke always needs a solution to work" )
102
130
bound = super ().lagrangian (need_solution = need_solution )
103
131
if bound is not None :
104
132
self .extract_and_store_reduced_costs ()
133
+ self .extract_and_store_updated_nonant_bounds (bound )
105
134
return bound
106
135
107
136
def extract_and_store_reduced_costs (self ):
@@ -176,6 +205,56 @@ def extract_and_store_reduced_costs(self):
176
205
Field .SCENARIO_REDUCED_COST ,
177
206
)
178
207
208
+ def extract_and_store_updated_nonant_bounds (self , lr_outer_bound ):
209
+ # update the best bound from the hub
210
+ # as a side effect, calls update_receive_buffers
211
+ # TODO: fix this side effect
212
+ if self .got_kill_signal ():
213
+ return
214
+
215
+ self .update_innerbounds ()
216
+
217
+ if math .isinf (self .BestInnerBound ):
218
+ # can do anything with no bound
219
+ return
220
+
221
+ nonzero_rc = np .where (self .rc_global == 0 , np .nan , self .rc_global )
222
+ bound_tightening = np .divide (self .BestInnerBound - lr_outer_bound , nonzero_rc )
223
+
224
+ tighten_upper = np .where (bound_tightening > 0 , np .nan , bound_tightening )
225
+ tighten_lower = np .where (bound_tightening < 0 , np .nan , bound_tightening )
226
+
227
+ tighten_upper += self ._nonant_lower_bounds
228
+ tighten_lower += self ._nonant_upper_bounds
229
+
230
+ # max of existing lower and new lower
231
+ np .fmax (tighten_lower , self ._nonant_lower_bounds , out = self ._nonant_lower_bounds )
232
+
233
+ # min of existing upper and new upper
234
+ np .fmin (tighten_upper , self ._nonant_upper_bounds , out = self ._nonant_upper_bounds )
235
+
236
+ # ceiling of lower bounds for integer variables
237
+ np .ceil (self ._nonant_lower_bounds , out = self ._nonant_lower_bounds , where = self ._integer_variable_where )
238
+ # floor of upper bounds for integer variables
239
+ np .floor (self ._nonant_upper_bounds , out = self ._nonant_upper_bounds , where = self ._integer_variable_where )
240
+
241
+
179
242
def main (self ):
180
243
# need the solution for ReducedCostsSpoke
181
244
super ().main (need_solution = True )
245
+
246
+
247
+ def _lb_generator (var_iterable ):
248
+ for v in var_iterable :
249
+ lb = v .lb
250
+ if lb is None :
251
+ yield - np .inf
252
+ yield lb
253
+
254
+
255
+ def _ub_generator (var_iterable ):
256
+ for v in var_iterable :
257
+ ub = v .ub
258
+ if ub is None :
259
+ yield np .inf
260
+ yield ub
0 commit comments