4343 'EXIT_INPUT_ERROR' , 'EXIT_TR_INCREASE_ERROR' , 'EXIT_LINALG_ERROR' , 'EXIT_FALSE_SUCCESS_WARNING' ,
4444 'EXIT_AUTO_DETECT_RESTART_WARNING' ]
4545
46+ EXIT_TR_INCREASE_WARNING = 5 # warning, TR increase in proj constrained case - likely due to multiple active constraints
4647EXIT_AUTO_DETECT_RESTART_WARNING = 4 # warning, auto-detected restart criteria
4748EXIT_FALSE_SUCCESS_WARNING = 3 # warning, maximum fake successful steps reached
4849EXIT_SLOW_WARNING = 2 # warning, maximum number of slow (successful) iterations reached
@@ -70,6 +71,8 @@ def message(self, with_stem=True):
7071 return "Warning (slow progress): " + self .msg
7172 elif self .flag == EXIT_MAXFUN_WARNING :
7273 return "Warning (max evals): " + self .msg
74+ elif self .flag == EXIT_TR_INCREASE_WARNING :
75+ return "Warning (trust region increase): " + self .msg
7376 elif self .flag == EXIT_INPUT_ERROR :
7477 return "Error (bad input): " + self .msg
7578 elif self .flag == EXIT_TR_INCREASE_ERROR :
@@ -82,7 +85,7 @@ def message(self, with_stem=True):
8285 return "Unknown exit flag: " + self .msg
8386
8487 def able_to_do_restart (self ):
85- if self .flag in [EXIT_TR_INCREASE_ERROR , EXIT_LINALG_ERROR , EXIT_SLOW_WARNING , EXIT_AUTO_DETECT_RESTART_WARNING ]:
88+ if self .flag in [EXIT_TR_INCREASE_ERROR , EXIT_TR_INCREASE_WARNING , EXIT_LINALG_ERROR , EXIT_SLOW_WARNING , EXIT_AUTO_DETECT_RESTART_WARNING ]:
8689 return True
8790 elif self .flag in [EXIT_MAXFUN_WARNING , EXIT_INPUT_ERROR ]:
8891 return False
@@ -92,13 +95,13 @@ def able_to_do_restart(self):
9295
9396
9497class Controller (object ):
95- def __init__ (self , objfun , args , x0 , r0 , r0_nsamples , xl , xu , npt , rhobeg , rhoend , nf , nx , maxfun , params ,
98+ def __init__ (self , objfun , args , x0 , r0 , r0_nsamples , xl , xu , projections , npt , rhobeg , rhoend , nf , nx , maxfun , params ,
9699 scaling_changes , do_logging ):
97100 self .do_logging = do_logging
98101 self .objfun = objfun
99102 self .args = args
100103 self .maxfun = maxfun
101- self .model = Model (npt , x0 , r0 , xl , xu , r0_nsamples , precondition = params ("interpolation.precondition" ),
104+ self .model = Model (npt , x0 , r0 , xl , xu , projections , r0_nsamples , precondition = params ("interpolation.precondition" ),
102105 abs_tol = params ("model.abs_tol" ), rel_tol = params ("model.rel_tol" ), do_logging = do_logging )
103106 self .nf = nf
104107 self .nx = nx
@@ -137,6 +140,107 @@ def initialise_coordinate_directions(self, number_of_samples, num_directions, pa
137140 assert self .model .num_pts <= (self .n () + 1 ) * (self .n () + 2 ) // 2 , "prelim: must have npt <= (n+1)(n+2)/2"
138141 assert 1 <= num_directions < self .model .num_pts , "Initialisation: must have 1 <= ndirs_initial < npt"
139142
143+
144+ if self .model .projections :
145+ D = np .zeros ((self .n (),self .n ()))
146+ k = 0
147+ while k < self .n ():
148+ ek = np .zeros (self .n ())
149+ ek [k ] = 1
150+ p = np .dot (ek ,min (1 ,self .delta ))
151+ yk = dykstra (self .model .projections , self .model .xbase + p , max_iter = params ("dykstra.max_iters" ), tol = params ("dykstra.d_tol" ))
152+ D [k ,:] = yk - self .model .xbase
153+
154+ k += 1 # move on to next point
155+
156+ # Have at least one L.D. vector, try negative direction on bad one first
157+ k = 0
158+ mr_tol = params ("matrix_rank.r_tol" )
159+ D_rank , diag = qr_rank (D ,tol = mr_tol )
160+ while D_rank != num_directions and k < self .n ():
161+ if diag [k ] < mr_tol :
162+ ek = np .zeros (self .n ())
163+ ek [k ] = 1
164+ p = - np .dot (ek ,min (1 ,self .delta ))
165+ yk = dykstra (self .model .projections , self .model .xbase + p , max_iter = params ("dykstra.max_iters" ), tol = params ("dykstra.d_tol" ))
166+ dk = D [k ,:].copy ()
167+ D [k ,:] = yk - self .model .xbase
168+ D_rank2 , _diag2 = qr_rank (D ,tol = params ("matrix_rank.r_tol" ))
169+ if D_rank2 <= D_rank :
170+ # Did not improve rank, revert change
171+ D [k ,:] = dk
172+ # rank was improved, update D_rank for next comparison
173+ D_rank = D_rank2
174+ k += 1
175+
176+ # Try random combination of negatives...
177+ k = 0
178+ slctr = np .random .randint (0 , 1 + 1 , self .n ()) # generate rand binary "selector" array
179+ D_rank , diag = qr_rank (D ,tol = params ("matrix_rank.r_tol" ))
180+ while D_rank != num_directions and k < 100 * self .n ():
181+ if slctr [k % self .n ()] == 1 : # if selector says make -ve, make -ve
182+ ek = np .zeros (self .n ())
183+ ek [k % self .n ()] = 1
184+ p = - np .dot (ek ,min (1 ,self .delta ))
185+ yk = dykstra (self .model .projections , self .model .xbase + p , max_iter = params ("dykstra.max_iters" ), tol = params ("dykstra.d_tol" ))
186+ dk = D [k % self .n (),:].copy ()
187+ D [k % self .n (),:] = yk - self .model .xbase
188+ D_rank2 , _diag2 = qr_rank (D ,tol = params ("matrix_rank.r_tol" ))
189+ if D_rank2 <= D_rank :
190+ # Did not improve rank, revert change
191+ D [k % self .n (),:] = dk
192+ # rank was improved, update D_rank for next comparison
193+ D_rank = D_rank2
194+
195+ # Go again
196+ slctr = np .random .randint (0 , 1 + 1 , self .n ())
197+ k += 1
198+
199+ # Set still not L.I? Try random directions
200+ i = 0
201+ D_rank , diag = qr_rank (D ,tol = params ("matrix_rank.r_tol" ))
202+ while D_rank != num_directions and i <= 100 * num_directions :
203+ k = 0
204+ while k < self .n ():
205+ if diag [k ] < mr_tol :
206+ p = np .random .normal (size = self .n ())
207+ p = p / np .linalg .norm (p )
208+ p = np .dot (p ,min (1 ,self .delta ))
209+ yk = dykstra (self .model .projections , self .model .xbase + p , max_iter = params ("dykstra.max_iters" ), tol = params ("dykstra.d_tol" ))
210+ dk = D [k ,:].copy ()
211+ D [k ,:] = yk - self .model .xbase
212+ D_rank2 , _diag2 = qr_rank (D ,tol = params ("matrix_rank.r_tol" ))
213+ if D_rank2 <= D_rank :
214+ # Did not improve rank, revert change
215+ D [k ,:] = dk
216+ # rank was improved, update D_rank for next comparison
217+ D_rank = D_rank2
218+ k += 1
219+ i += 1
220+
221+ if D_rank != num_directions :
222+ raise RuntimeError ("Unable to generate suitable initial directions" )
223+
224+ # we have a L.I set of interpolation points
225+ for k in range (0 ,self .n ()):
226+ # Evaluate objective at this new point
227+ x = self .model .as_absolute_coordinates (D [k , :])
228+ rvec_list , f_list , num_samples_run , exit_info = self .evaluate_objective (x , number_of_samples , params )
229+
230+ # Handle exit conditions (f < min obj value or maxfun reached)
231+ if exit_info is not None :
232+ if num_samples_run > 0 :
233+ self .model .save_point (x , np .mean (rvec_list [:num_samples_run , :], axis = 0 ), num_samples_run ,
234+ x_in_abs_coords = True )
235+ return exit_info # return & quit
236+
237+ # Otherwise, add new results (increments model.npt_so_far)
238+ self .model .change_point (k + 1 , x - self .model .xbase , rvec_list [0 , :]) # expect step, not absolute x
239+ for i in range (1 , num_samples_run ):
240+ self .model .add_new_sample (k + 1 , rvec_extra = rvec_list [i , :])
241+
242+ return None # return & continue
243+
140244 at_lower_boundary = (self .model .sl > - 0.01 * self .delta ) # sl = xl - x0, should be -ve, actually < -rhobeg
141245 at_upper_boundary = (self .model .su < 0.01 * self .delta ) # su = xu - x0, should be +ve, actually > rhobeg
142246
@@ -147,17 +251,19 @@ def initialise_coordinate_directions(self, number_of_samples, num_directions, pa
147251 # k = 2n+1, ..., (n+1)(n+2)/2 --> off-diagonal directions
148252 if 1 <= k < self .n () + 1 : # first step along coord directions
149253 dirn = k - 1 # direction to move in (0,...,n-1)
150- stepa = self .delta if not at_upper_boundary [dirn ] else - self .delta
254+ stepa = self .delta if not at_upper_boundary [dirn ] else - self .delta # take a +delta step if at lower, -delta if at upper
151255 stepb = None
152- xpts_added [k , dirn ] = stepa
256+ xpts_added [k , dirn ] = stepa # set new (relative) point to the step since we haven't done any moving, so relative point is all zeros.
153257
154258 elif self .n () + 1 <= k < 2 * self .n () + 1 : # second step along coord directions
155259 dirn = k - self .n () - 1 # direction to move in (0,...,n-1)
156- stepa = xpts_added [k - self .n (), dirn ]
157- stepb = - self .delta
260+ stepa = xpts_added [k - self .n (), dirn ] # previous step
261+ stepb = - self .delta # new step
158262 if at_lower_boundary [dirn ]:
263+ # if at lower boundary, set the second step to be +ve
159264 stepb = min (2.0 * self .delta , self .model .su [dirn ]) # su = xu - x0, should be +ve
160265 if at_upper_boundary [dirn ]:
266+ # if at upper boundary, set the second step to be -ve
161267 stepb = max (- 2.0 * self .delta , self .model .sl [dirn ]) # sl = xl - x0, should be -ve
162268 xpts_added [k , dirn ] = stepb
163269
@@ -325,10 +431,13 @@ def get_new_direction_for_growing(self, step_length):
325431
326432 return dirn * (step_length / LA .norm (dirn ))
327433
328- def trust_region_step (self ):
434+ def trust_region_step (self , params ):
329435 # Build model for full least squares objectives
330436 gopt , H = self .model .build_full_model ()
331- d , gnew , crvmin = trsbox (self .model .xopt (), gopt , H , self .model .sl , self .model .su , self .delta )
437+ if self .model .projections :
438+ d , gnew , crvmin = ctrsbox (self .model .xopt (abs_coordinates = True ), gopt , H , self .model .projections , self .delta , d_max_iters = params ("dykstra.max_iters" ), d_tol = params ("dykstra.d_tol" ))
439+ else :
440+ d , gnew , crvmin = trsbox (self .model .xopt (), gopt , H , self .model .sl , self .model .su , self .delta )
332441 return d , gopt , H , gnew , crvmin
333442
334443 def geometry_step (self , knew , adelt , number_of_samples , params ):
@@ -337,8 +446,13 @@ def geometry_step(self, knew, adelt, number_of_samples, params):
337446 try :
338447 c , g = self .model .lagrange_gradient (knew )
339448 # c = 1.0 if knew == self.model.kopt else 0.0 # based at xopt, just like d
340- # Solve problem: bounds are sl <= xnew <= su, and ||xnew-xopt|| <= adelt
341- xnew = trsbox_geometry (self .model .xopt (), c , g , np .minimum (self .model .sl , 0.0 ), np .maximum (self .model .su , 0.0 ), adelt )
449+ if self .model .projections :
450+ # Solve problem: use projection onto arbitrary constraints, and ||xnew-xopt|| <= adelt
451+ step = ctrsbox_geometry (self .model .xopt (abs_coordinates = True ), c , g , self .model .projections , adelt , d_max_iters = params ("dykstra.max_iters" ), d_tol = params ("dykstra.d_tol" ))
452+ xnew = self .model .xopt () + step
453+ else :
454+ # Solve problem: bounds are sl <= xnew <= su, and ||xnew-xopt|| <= adelt
455+ xnew = trsbox_geometry (self .model .xopt (), c , g , np .minimum (self .model .sl , 0.0 ), np .maximum (self .model .su , 0.0 ), adelt )
342456 except LA .LinAlgError :
343457 exit_info = ExitInformation (EXIT_LINALG_ERROR , "Singular matrix encountered in geometry step" )
344458 return exit_info # didn't fix geometry - return & quit
@@ -499,13 +613,16 @@ def reduce_rho(self, current_iter, params):
499613 def calculate_ratio (self , current_iter , rvec_list , d , gopt , H ):
500614 exit_info = None
501615 f = sumsq (np .mean (rvec_list , axis = 0 )) # estimate actual objective value
502- pred_reduction = - model_value (gopt , H , d )
616+ pred_reduction = - model_value (gopt , H , d ) # negative of m since m(0) = 0
503617 actual_reduction = self .model .fopt () - f
504618 self .diffs = [abs (actual_reduction - pred_reduction ), self .diffs [0 ], self .diffs [1 ]]
505619 if min (sqrt (sumsq (d )), self .delta ) > self .rho : # if ||d|| >= rho, successful!
506620 self .last_successful_iter = current_iter
507621 if pred_reduction < 0.0 :
508- exit_info = ExitInformation (EXIT_TR_INCREASE_ERROR , "Trust region step gave model increase" )
622+ if len (self .model .projections ) > 1 : # if we are using multiple projections, only warn since likely due to constraint intersection
623+ exit_info = ExitInformation (EXIT_TR_INCREASE_WARNING , "Either multiple constraints are active or trust region step gave model increase" )
624+ else :
625+ exit_info = ExitInformation (EXIT_TR_INCREASE_ERROR , "Either rust region step gave model increase" )
509626
510627 ratio = actual_reduction / pred_reduction
511628 return ratio , exit_info
0 commit comments