@@ -268,7 +268,121 @@ def initialise_random_directions(self, number_of_samples, num_directions, params
268
268
269
269
return None
270
270
271
- def trust_region_step (self ):
271
+ def initialise_general_constraints (self , number_of_samples , num_directions , params ):
272
+ # Initialisation when we have general projection constraints, as well as bound constraints
273
+ # Basic idea: start with bound-respecting coordinate directions, just like initialise_coordinate_directions
274
+ # Then, use geometry-improving procedures to make all these points feasible wrt projection constraints
275
+ # Only after this is all done, then evaluate the initial points
276
+ # (after the initial coordinate directions given, add points to model with nan objective value)
277
+ if self .do_logging :
278
+ module_logger .debug ("Initialising to satisfy general projection constraints" )
279
+ # self.model already has x0 evaluated (and x0 feasible), so only need to initialise the other points
280
+ # num_directions = params("growing.ndirs_initial")
281
+ assert self .model .num_pts <= (self .n () + 1 ) * (self .n () + 2 ) // 2 , "prelim: must have npt <= (n+1)(n+2)/2"
282
+ assert 1 <= num_directions < self .model .num_pts , "Initialisation: must have 1 <= ndirs_initial < npt"
283
+
284
+ at_lower_boundary = (self .model .sl > - 0.01 * self .delta ) # sl = xl - x0, should be -ve, actually < -rhobeg
285
+ at_upper_boundary = (self .model .su < 0.01 * self .delta ) # su = xu - x0, should be +ve, actually > rhobeg
286
+
287
+ # **** STEP 1 ****
288
+ # Build initial coordinate-based directions
289
+ xpts_added = np .zeros ((num_directions + 1 , self .n ()))
290
+ for k in range (1 , num_directions + 1 ):
291
+ # k = 0 --> base point (xpt = 0) [ not here]
292
+ # k = 1, ..., 2n --> coordinate directions [1,...,n and n+1,...,2n]
293
+ # k = 2n+1, ..., (n+1)(n+2)/2 --> off-diagonal directions
294
+ if 1 <= k < self .n () + 1 : # first step along coord directions
295
+ dirn = k - 1 # direction to move in (0,...,n-1)
296
+ stepa = self .delta if not at_upper_boundary [dirn ] else - self .delta
297
+ stepb = None
298
+ xpts_added [k , dirn ] = stepa
299
+
300
+ elif self .n () + 1 <= k < 2 * self .n () + 1 : # second step along coord directions
301
+ dirn = k - self .n () - 1 # direction to move in (0,...,n-1)
302
+ stepa = xpts_added [k - self .n (), dirn ]
303
+ stepb = - self .delta
304
+ if at_lower_boundary [dirn ]:
305
+ stepb = min (2.0 * self .delta , self .model .su [dirn ]) # su = xu - x0, should be +ve
306
+ if at_upper_boundary [dirn ]:
307
+ stepb = max (- 2.0 * self .delta , self .model .sl [dirn ]) # sl = xl - x0, should be -ve
308
+ xpts_added [k , dirn ] = stepb
309
+
310
+ else : # k = 2n+1, ..., (n+1)(n+2)/2
311
+ # p = (k - 1) % n + 1 # cycles through (1,...,n), starting at 2n+1 --> 1
312
+ # l = (k - 2 * n - 1) / n + 1 # (1,...,1, 2, ..., 2, etc.) where each number appears n times
313
+ # q = (p + l if p + l <= n else p + l - n)
314
+ stepa = None
315
+ stepb = None
316
+ itemp = (k - self .n () - 1 ) // self .n ()
317
+ q = k - itemp * self .n () - self .n ()
318
+ p = q + itemp
319
+ if p > self .n ():
320
+ p , q = q , p - self .n () # does swap correctly in Python
321
+
322
+ xpts_added [k , p - 1 ] = xpts_added [p , p - 1 ]
323
+ xpts_added [k , q - 1 ] = xpts_added [q , q - 1 ]
324
+
325
+ # Add this new point to the model with a nan function value for now, since it is likely to be moved
326
+ # to ensure the projection constraints are respected
327
+ x = self .model .as_absolute_coordinates (xpts_added [k , :])
328
+ self .model .change_point (k , x - self .model .xbase , np .nan ) # expect step, not absolute x
329
+
330
+ # (no need to switch the k-th and (k-N)-th points, since we can't compare function values at this stage)
331
+
332
+ # **** STEP 2 ****
333
+ # Now go through all interpolation points and check if they are feasible wrt projection constraints
334
+ # If not, do a geometry-improving step (again with nan objective value)
335
+ for k in range (1 , num_directions + 1 ):
336
+ x = self .model .as_absolute_coordinates (xpts_added [k , :])
337
+ this_x_feasible = True
338
+ for i , proj in enumerate (self .model .projections ):
339
+ if np .linalg .norm (x - proj (x )) > params ("projections.feasible_tol" ):
340
+ this_x_feasible = False
341
+ break # quit loop, only need to find one bad constraint
342
+
343
+ # Calculate Lagrange polynomial and update this point
344
+ if not this_x_feasible :
345
+ try :
346
+ c , g , H = self .model .lagrange_polynomial (k ) # based at xopt
347
+ if np .any (np .isinf (g )) or np .any (np .isnan (g )) or np .any (np .isinf (H )) or np .any (np .isnan (H )):
348
+ raise ValueError
349
+ xnew = ctrsbox_geometry (self .model .xbase , self .model .xopt (), c , g , H , self .model .sl , self .model .su ,
350
+ self .model .projections , self .delta ,
351
+ dykstra_max_iters = params ("projections.dykstra.max_iters" ),
352
+ dykstra_tol = params ("projections.feasible_tol" ),
353
+ gtol = params ("projections.pgd_tol" ))
354
+ except LA .LinAlgError :
355
+ exit_info = ExitInformation (EXIT_LINALG_ERROR , "Singular matrix encountered in initialization geometry step" )
356
+ return exit_info # didn't fix geometry - return & quit
357
+ except ValueError :
358
+ # A ValueError may be raised if gopt or H have nan/inf values (issue #23)
359
+ # Ideally this should be picked up earlier in self.model.lagrange_polynomial(...)
360
+ exit_info = ExitInformation (EXIT_LINALG_ERROR , "Error when calculating initialization geometry-improving step" )
361
+ return exit_info # didn't fix geometry - return & quit
362
+
363
+ # Update point and fill with np.nan again, just for now
364
+ self .model .change_point (k , xnew , np .nan ) # expect step, not absolute x
365
+
366
+ # **** STEP 3 ****
367
+ # Now we are ready to evaluate our initial points
368
+ for k in range (1 , num_directions + 1 ):
369
+ x = self .model .xpt (k , abs_coordinates = True )
370
+ f_list , num_samples_run , exit_info = self .evaluate_objective (x , number_of_samples , params )
371
+
372
+ # Handle exit conditions (f < min obj value or maxfun reached)
373
+ if exit_info is not None :
374
+ if num_samples_run > 0 :
375
+ self .model .save_point (x , np .mean (f_list [:num_samples_run ]), num_samples_run , x_in_abs_coords = True )
376
+ return exit_info # didn't fix geometry - return & quit
377
+
378
+ # Otherwise, add new results
379
+ self .model .change_point (k , x - self .model .xbase , f_list [0 ]) # expect step, not absolute x
380
+ for i in range (1 , num_samples_run ):
381
+ self .model .add_new_sample (k , f_extra = f_list [i ])
382
+
383
+ return None # return & continue
384
+
385
+ def trust_region_step (self , params ):
272
386
# Build model for full least squares objectives
273
387
gopt , H = self .model .build_full_model ()
274
388
if np .any (np .isinf (gopt )) or np .any (np .isnan (gopt )) or np .any (np .isinf (H )) or np .any (np .isnan (H )):
@@ -281,7 +395,11 @@ def trust_region_step(self):
281
395
if self .model .projections is None :
282
396
d , gnew , crvmin = trsbox (self .model .xopt (), gopt , H , self .model .sl , self .model .su , self .delta )
283
397
else :
284
- d , gnew , crvmin = ctrsbox (self .model .xbase , self .model .xopt (), gopt , H , self .model .sl , self .model .su , self .model .projections , self .delta )
398
+ d , gnew , crvmin = ctrsbox (self .model .xbase , self .model .xopt (), gopt , H , self .model .sl , self .model .su ,
399
+ self .model .projections , self .delta ,
400
+ dykstra_max_iters = params ("projections.dykstra.max_iters" ),
401
+ dykstra_tol = params ("projections.feasible_tol" ),
402
+ gtol = params ("projections.pgd_tol" ))
285
403
except ValueError :
286
404
# A ValueError may be raised if gopt or H have nan/inf values (issue #14)
287
405
# Although this should be picked up earlier, in this situation just return a zero
@@ -307,7 +425,11 @@ def geometry_step(self, knew, adelt, number_of_samples, params):
307
425
if self .model .projections is None :
308
426
xnew = trsbox_geometry (self .model .xopt (), c , g , H , self .model .sl , self .model .su , adelt )
309
427
else :
310
- xnew = ctrsbox_geometry (self .model .xbase , self .model .xopt (), c , g , H , self .model .sl , self .model .su , self .model .projections , adelt )
428
+ xnew = ctrsbox_geometry (self .model .xbase , self .model .xopt (), c , g , H , self .model .sl , self .model .su ,
429
+ self .model .projections , adelt ,
430
+ dykstra_max_iters = params ("projections.dykstra.max_iters" ),
431
+ dykstra_tol = params ("projections.feasible_tol" ),
432
+ gtol = params ("projections.pgd_tol" ))
311
433
except ValueError :
312
434
# A ValueError may be raised if gopt or H have nan/inf values (issue #23)
313
435
# Ideally this should be picked up earlier in self.model.lagrange_polynomial(...)
0 commit comments