DFO-LS 1.2.1__py3-none-any.whl → 1.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of DFO-LS might be problematic. Click here for more details.

dfols/params.py CHANGED
@@ -44,6 +44,7 @@ class ParameterList(object):
44
44
  self.params["init.random_directions_make_orthogonal"] = True # although random > orthogonal, avoid for init
45
45
  # Interpolation
46
46
  self.params["interpolation.precondition"] = True
47
+ self.params["interpolation.throw_error_on_nans"] = False # throw numpy.linalg.LinAlgError if interpolating to nan data?
47
48
  # Logging
48
49
  self.params["logging.n_to_print_whole_x_vector"] = 6
49
50
  self.params["logging.save_diagnostic_info"] = False
@@ -108,6 +109,11 @@ class ParameterList(object):
108
109
  self.params["growing.full_rank.min_sing_val"] = 1e-6 # absolute floor on singular values
109
110
  self.params["growing.full_rank.svd_max_jac_cond"] = 1e8 # maximum condition number of Jacobian
110
111
  self.params["growing.perturb_trust_region_step"] = False # add random direction onto TRS solution?
112
+ # Dykstra's algorithm
113
+ self.params["dykstra.d_tol"] = 1e-10
114
+ self.params["dykstra.max_iters"] = 100
115
+ # Matrix rank algorithm
116
+ self.params["matrix_rank.r_tol"] = 1e-18
111
117
 
112
118
  self.params_changed = {}
113
119
  for p in self.params:
@@ -142,6 +148,8 @@ class ParameterList(object):
142
148
  type_str, nonetype_ok, lower, upper = 'bool', False, None, None
143
149
  elif key == "interpolation.precondition":
144
150
  type_str, nonetype_ok, lower, upper = 'bool', False, None, None
151
+ elif key == "interpolation.throw_error_on_nans":
152
+ type_str, nonetype_ok, lower, upper = 'bool', False, None, None
145
153
  elif key == "logging.n_to_print_whole_x_vector":
146
154
  type_str, nonetype_ok, lower, upper = 'int', False, 0, None
147
155
  elif key == "logging.save_diagnostic_info":
@@ -254,6 +262,12 @@ class ParameterList(object):
254
262
  type_str, nonetype_ok, lower, upper = 'float', True, 1.0, None
255
263
  elif key == "growing.perturb_trust_region_step":
256
264
  type_str, nonetype_ok, lower, upper = 'bool', False, None, None
265
+ elif key == "dykstra.d_tol":
266
+ type_str, nonetype_ok, lower, upper = 'float', False, 0.0, None
267
+ elif key == "dykstra.max_iters":
268
+ type_str, nonetype_ok, lower, upper = 'int', False, 0, None
269
+ elif key == "matrix_rank.r_tol":
270
+ type_str, nonetype_ok, lower, upper = 'float', False, 0.0, None
257
271
  else:
258
272
  assert False, "ParameterList.param_type() has unknown key: %s" % key
259
273
  return type_str, nonetype_ok, lower, upper
dfols/solver.py CHANGED
@@ -43,6 +43,8 @@ from .util import *
43
43
 
44
44
  __all__ = ['solve']
45
45
 
46
+ module_logger = logging.getLogger(__name__)
47
+
46
48
 
47
49
  # A container for the results of the optimization routine
48
50
  class OptimResults(object):
@@ -93,7 +95,7 @@ class OptimResults(object):
93
95
  return output
94
96
 
95
97
 
96
- def solve_main(objfun, x0, args, xl, xu, npt, rhobeg, rhoend, maxfun, nruns_so_far, nf_so_far, nx_so_far, nsamples, params,
98
+ def solve_main(objfun, x0, args, xl, xu, projections, npt, rhobeg, rhoend, maxfun, nruns_so_far, nf_so_far, nx_so_far, nsamples, params,
97
99
  diagnostic_info, scaling_changes, r0_avg_old=None, r0_nsamples_old=None, default_growing_method_set_by_user=None,
98
100
  do_logging=True, print_progress=False):
99
101
  # Evaluate at x0 (keep nf, nx correct and check for f < 1e-12)
@@ -153,14 +155,14 @@ def solve_main(objfun, x0, args, xl, xu, npt, rhobeg, rhoend, maxfun, nruns_so_f
153
155
  # However, this can fail for m<n, so need to use an alternative method (perturb_trust_region_step)
154
156
  if m < len(x0):
155
157
  if do_logging:
156
- logging.debug("Inverse problem (m<n), switching default growing method")
158
+ module_logger.debug("Inverse problem (m<n), switching default growing method")
157
159
  params('growing.full_rank.use_full_rank_interp', new_value=False)
158
160
  params('growing.perturb_trust_region_step', new_value=True)
159
161
  if not params.params_changed['growing.delta_scale_new_dirns']:
160
162
  params('growing.delta_scale_new_dirns', new_value=0.1)
161
163
 
162
164
  # Initialise controller
163
- control = Controller(objfun, args, x0, r0_avg, num_samples_run, xl, xu, npt, rhobeg, rhoend, nf, nx, maxfun,
165
+ control = Controller(objfun, args, x0, r0_avg, num_samples_run, xl, xu, projections, npt, rhobeg, rhoend, nf, nx, maxfun,
164
166
  params, scaling_changes, do_logging)
165
167
 
166
168
  # Initialise interpolation set
@@ -169,11 +171,11 @@ def solve_main(objfun, x0, args, xl, xu, npt, rhobeg, rhoend, maxfun, nruns_so_f
169
171
  npt - 1) # cap at npt
170
172
  if params("init.random_initial_directions"):
171
173
  if do_logging:
172
- logging.info("Initialising (random directions)")
174
+ module_logger.info("Initialising (random directions)")
173
175
  exit_info = control.initialise_random_directions(number_of_samples, num_directions, params)
174
176
  else:
175
177
  if do_logging:
176
- logging.info("Initialising (coordinate directions)")
178
+ module_logger.info("Initialising (coordinate directions)")
177
179
  exit_info = control.initialise_coordinate_directions(number_of_samples, num_directions, params)
178
180
  if exit_info is not None:
179
181
  x, rvec, f, jacmin, nsamples = control.model.get_final_results()
@@ -195,18 +197,18 @@ def solve_main(objfun, x0, args, xl, xu, npt, rhobeg, rhoend, maxfun, nruns_so_f
195
197
  # ------------------------------------------
196
198
  current_iter = -1
197
199
  if do_logging:
198
- logging.info("Beginning main loop")
200
+ module_logger.info("Beginning main loop")
199
201
  if print_progress:
200
202
  print("{:^5}{:^7}{:^10}{:^10}{:^10}{:^10}{:^7}".format("Run", "Iter", "Obj", "Grad", "Delta", "rho", "Evals"))
201
203
  while True:
202
204
  current_iter += 1
203
205
 
204
206
  if do_logging:
205
- logging.debug("*** Iter %g (delta = %g, rho = %g) ***" % (current_iter, control.delta, control.rho))
207
+ module_logger.debug("*** Iter %g (delta = %g, rho = %g) ***" % (current_iter, control.delta, control.rho))
206
208
 
207
209
  if (not finished_growing) and control.model.npt() >= control.model.num_pts:
208
210
  if do_logging:
209
- logging.info("Finished growing init set")
211
+ module_logger.info("Finished growing init set")
210
212
  finished_growing = True
211
213
 
212
214
  if params("growing.reset_delta"):
@@ -217,7 +219,7 @@ def solve_main(objfun, x0, args, xl, xu, npt, rhobeg, rhoend, maxfun, nruns_so_f
217
219
 
218
220
  if not finished_growing:
219
221
  if do_logging:
220
- logging.debug("Main loop: still growing (have %g of %g pts)" % (control.model.npt(), control.model.num_pts))
222
+ module_logger.debug("Main loop: still growing (have %g of %g pts)" % (control.model.npt(), control.model.num_pts))
221
223
 
222
224
  # Noise level exit check
223
225
  if finished_growing and params("noise.quit_on_noise_level") and control.all_values_within_noise_level(params):
@@ -247,7 +249,8 @@ def solve_main(objfun, x0, args, xl, xu, npt, rhobeg, rhoend, maxfun, nruns_so_f
247
249
  min_sing_val=params("growing.full_rank.min_sing_val"),
248
250
  sing_val_frac=params("growing.full_rank.svd_scale_factor"),
249
251
  max_jac_cond=params("growing.full_rank.svd_max_jac_cond"),
250
- get_chg_J=params("restarts.use_restarts") and params("restarts.auto_detect"))
252
+ get_chg_J=params("restarts.use_restarts") and params("restarts.auto_detect"),
253
+ throw_error_on_nans=params("interpolation.throw_error_on_nans"))
251
254
  if not interp_ok:
252
255
  if params("restarts.use_restarts") and params("restarts.use_soft_restarts"):
253
256
  number_of_samples = max(nsamples(control.delta, control.rho, current_iter, nruns_so_far), 1)
@@ -270,9 +273,9 @@ def solve_main(objfun, x0, args, xl, xu, npt, rhobeg, rhoend, maxfun, nruns_so_f
270
273
 
271
274
 
272
275
  # Trust region step
273
- d, gopt, H, gnew, crvmin = control.trust_region_step()
276
+ d, gopt, H, gnew, crvmin = control.trust_region_step(params)
274
277
  if do_logging:
275
- logging.debug("Trust region step is d = " + str(d))
278
+ module_logger.debug("Trust region step is d = " + str(d))
276
279
  xnew = control.model.xopt() + d
277
280
  dnorm = min(LA.norm(d), control.delta)
278
281
 
@@ -288,7 +291,7 @@ def solve_main(objfun, x0, args, xl, xu, npt, rhobeg, rhoend, maxfun, nruns_so_f
288
291
 
289
292
  if dnorm < params("general.safety_step_thresh") * control.rho and not finished_growing and params("growing.safety.do_safety_step"):
290
293
  if do_logging:
291
- logging.debug("Safety step during growing phase")
294
+ module_logger.debug("Safety step during growing phase")
292
295
 
293
296
  if params("logging.save_diagnostic_info"):
294
297
  diagnostic_info.update_ratio(np.nan)
@@ -366,7 +369,7 @@ def solve_main(objfun, x0, args, xl, xu, npt, rhobeg, rhoend, maxfun, nruns_so_f
366
369
  elif dnorm < params("general.safety_step_thresh") * control.rho and finished_growing:
367
370
  # (start safety step)
368
371
  if do_logging:
369
- logging.debug("Safety step (main phase)")
372
+ module_logger.debug("Safety step (main phase)")
370
373
 
371
374
  if params("logging.save_diagnostic_info"):
372
375
  diagnostic_info.update_ratio(np.nan)
@@ -410,12 +413,12 @@ def solve_main(objfun, x0, args, xl, xu, npt, rhobeg, rhoend, maxfun, nruns_so_f
410
413
  # Reduce rho
411
414
  control.reduce_rho(current_iter, params)
412
415
  if do_logging:
413
- logging.info("New rho = %g after %i function evaluations" % (control.rho, control.nf))
416
+ module_logger.info("New rho = %g after %i function evaluations" % (control.rho, control.nf))
414
417
  if control.n() < params("logging.n_to_print_whole_x_vector"):
415
- logging.debug("Best so far: f = %.15g at x = " % (control.model.fopt())
418
+ module_logger.debug("Best so far: f = %.15g at x = " % (control.model.fopt())
416
419
  + str(control.model.xopt(abs_coordinates=True)))
417
420
  else:
418
- logging.debug("Best so far: f = %.15g at x = [...]" % (control.model.fopt()))
421
+ module_logger.debug("Best so far: f = %.15g at x = [...]" % (control.model.fopt()))
419
422
  continue # next iteration
420
423
  else:
421
424
  # Quit on rho=rhoend
@@ -455,18 +458,18 @@ def solve_main(objfun, x0, args, xl, xu, npt, rhobeg, rhoend, maxfun, nruns_so_f
455
458
  else:
456
459
  # (start trust region step)
457
460
  if do_logging:
458
- logging.debug("Standard trust region step")
461
+ module_logger.debug("Standard trust region step")
459
462
 
460
463
  # If growing, optionally perturb the trust region step in a new direction
461
464
  if not finished_growing and params("growing.perturb_trust_region_step"):
462
465
  step_length = params('growing.delta_scale_new_dirns') * control.delta
463
466
  dirn = control.get_new_direction_for_growing(step_length)
464
467
  if do_logging:
465
- logging.debug("Perturbing trust region with step = %s" % str(dirn))
468
+ module_logger.debug("Perturbing trust region with step = %s" % str(dirn))
466
469
  d += dirn
467
470
  xnew += dirn
468
471
  if do_logging:
469
- logging.debug("New trust region step = %s" % str(d))
472
+ module_logger.debug("New trust region step = %s" % str(d))
470
473
 
471
474
  # If finished growing, add chgJ and delta to restart auto-detect set
472
475
  if finished_growing and params("restarts.use_restarts") and params("restarts.auto_detect"):
@@ -513,6 +516,17 @@ def solve_main(objfun, x0, args, xl, xu, npt, rhobeg, rhoend, maxfun, nruns_so_f
513
516
  x = control.model.as_absolute_coordinates(xnew)
514
517
  number_of_samples = max(nsamples(control.delta, control.rho, current_iter, nruns_so_far), 1)
515
518
  rvec_list, f_list, num_samples_run, exit_info = control.evaluate_objective(x, number_of_samples, params)
519
+ if np.any(np.isnan(rvec_list)):
520
+ # Just exit without saving the current point
521
+ # We should be able to do a hard restart though, because it's unlikely
522
+ # that we will get the same trust-region step after expanding the radius/re-initialising
523
+ module_logger.warning("NaN encountered in evaluation of trust-region step")
524
+ if params("interpolation.throw_error_on_nans"):
525
+ raise np.linalg.LinAlgError("NaN encountered in objective evaluations")
526
+
527
+ exit_info = ExitInformation(EXIT_EVAL_ERROR, "NaN received from objective function evaluation")
528
+ nruns_so_far += 1
529
+ break # quit
516
530
  if exit_info is not None:
517
531
  if num_samples_run > 0:
518
532
  control.model.save_point(x, np.mean(rvec_list[:num_samples_run, :], axis=0), num_samples_run,
@@ -545,7 +559,7 @@ def solve_main(objfun, x0, args, xl, xu, npt, rhobeg, rhoend, maxfun, nruns_so_f
545
559
 
546
560
  # Update delta
547
561
  if do_logging:
548
- logging.debug("Ratio = %g" % ratio)
562
+ module_logger.debug("Ratio = %g" % ratio)
549
563
  if params("logging.save_diagnostic_info"):
550
564
  diagnostic_info.update_ratio(ratio)
551
565
  diagnostic_info.update_slow_iter(-1) # n/a, unless otherwise update
@@ -603,7 +617,7 @@ def solve_main(objfun, x0, args, xl, xu, npt, rhobeg, rhoend, maxfun, nruns_so_f
603
617
  knew = control.model.npt()
604
618
 
605
619
  if do_logging:
606
- logging.debug("Updating with knew = %i" % knew)
620
+ module_logger.debug("Updating with knew = %i" % knew)
607
621
  control.model.change_point(knew, xnew, rvec_list[0, :]) # expect step, not absolute x
608
622
  for i in range(1, num_samples_run):
609
623
  control.model.add_new_sample(knew, rvec_extra=rvec_list[i, :])
@@ -615,7 +629,7 @@ def solve_main(objfun, x0, args, xl, xu, npt, rhobeg, rhoend, maxfun, nruns_so_f
615
629
  diagnostic_info.update_slow_iter(1 if this_iter_slow else 0)
616
630
  if finished_growing and should_terminate:
617
631
  if do_logging:
618
- logging.info("Slow iteration - terminating/restarting")
632
+ module_logger.info("Slow iteration - terminating/restarting")
619
633
  if params("restarts.use_restarts") and params("restarts.use_soft_restarts"):
620
634
  number_of_samples = max(nsamples(control.delta, control.rho, current_iter, nruns_so_far), 1)
621
635
  exit_info = control.soft_restart(number_of_samples, nruns_so_far, params,
@@ -649,7 +663,7 @@ def solve_main(objfun, x0, args, xl, xu, npt, rhobeg, rhoend, maxfun, nruns_so_f
649
663
  # While growing, (optionally) add new directions
650
664
  if not finished_growing and params("growing.num_new_dirns_each_iter") > 0:
651
665
  if do_logging:
652
- logging.debug("Still growing: adding %g new directions" % params("growing.num_new_dirns_each_iter"))
666
+ module_logger.debug("Still growing: adding %g new directions" % params("growing.num_new_dirns_each_iter"))
653
667
  number_of_samples = max(nsamples(control.delta, control.rho, current_iter, nruns_so_far), 1)
654
668
  exit_info = control.add_new_direction_while_growing(number_of_samples, params)
655
669
  if exit_info is not None:
@@ -682,7 +696,7 @@ def solve_main(objfun, x0, args, xl, xu, npt, rhobeg, rhoend, maxfun, nruns_so_f
682
696
  control.model.npt() - 1) # cap at number of points
683
697
  if finished_growing and ratio > 0.0 and num_regression_steps > 0:
684
698
  if do_logging:
685
- logging.info("Regression: moving %g points" % num_regression_steps)
699
+ module_logger.info("Regression: moving %g points" % num_regression_steps)
686
700
  number_of_samples = max(nsamples(control.delta, control.rho, current_iter, nruns_so_far), 1)
687
701
  if params("regression.momentum_extra_steps"): # move points as random extra steps
688
702
  exit_info = control.move_furthest_points_momentum(d, number_of_samples, num_regression_steps, params)
@@ -729,7 +743,7 @@ def solve_main(objfun, x0, args, xl, xu, npt, rhobeg, rhoend, maxfun, nruns_so_f
729
743
  np.log(np.maximum(restart_auto_detect_chgJ, 1e-15)))
730
744
 
731
745
  if do_logging:
732
- logging.debug("Iter %g: (slope, intercept, r_value) = (%g, %g, %g)" % (current_iter, slope, intercept, r_value))
746
+ module_logger.debug("Iter %g: (slope, intercept, r_value) = (%g, %g, %g)" % (current_iter, slope, intercept, r_value))
733
747
  if slope > params("restarts.auto_detect.min_chgJ_slope") \
734
748
  and r_value > params("restarts.auto_detect.min_correl"):
735
749
  # increasing trend, with at least some positive correlation
@@ -740,9 +754,9 @@ def solve_main(objfun, x0, args, xl, xu, npt, rhobeg, rhoend, maxfun, nruns_so_f
740
754
  # Data available (full NumPy vectors of fixed length): restart_auto_detect_delta, restart_auto_detect_chgJ
741
755
  if do_restart and params("restarts.use_soft_restarts"):
742
756
  if do_logging:
743
- logging.info("Auto detection: need to do a restart")
744
- logging.debug("delta history = %s" % str(restart_auto_detect_delta))
745
- logging.debug("chgJ history = %s" % str(restart_auto_detect_chgJ))
757
+ module_logger.info("Auto detection: need to do a restart")
758
+ module_logger.debug("delta history = %s" % str(restart_auto_detect_delta))
759
+ module_logger.debug("chgJ history = %s" % str(restart_auto_detect_chgJ))
746
760
  number_of_samples = max(nsamples(control.delta, control.rho, current_iter, nruns_so_far), 1)
747
761
  exit_info = control.soft_restart(number_of_samples, nruns_so_far, params,
748
762
  x_in_abs_coords_to_save=None, rvec_to_save=None,
@@ -759,7 +773,7 @@ def solve_main(objfun, x0, args, xl, xu, npt, rhobeg, rhoend, maxfun, nruns_so_f
759
773
  continue # next iteration
760
774
  elif do_restart:
761
775
  if do_logging:
762
- logging.info("Auto detection: need to do a restart")
776
+ module_logger.info("Auto detection: need to do a restart")
763
777
  exit_info = ExitInformation(EXIT_AUTO_DETECT_RESTART_WARNING, "Auto-detected restart")
764
778
  nruns_so_far += 1
765
779
  break # quit
@@ -767,7 +781,7 @@ def solve_main(objfun, x0, args, xl, xu, npt, rhobeg, rhoend, maxfun, nruns_so_f
767
781
 
768
782
  # Otherwise (ratio < eta1 = 0.1), check & fix geometry
769
783
  if do_logging:
770
- logging.debug("Checking and possibly improving geometry (unsuccessful step)")
784
+ module_logger.debug("Checking and possibly improving geometry (unsuccessful step)")
771
785
  distsq = max((2.0 * control.delta) ** 2, (10.0 * control.rho) ** 2)
772
786
  update_delta = False
773
787
  number_of_samples = max(nsamples(control.delta, control.rho, current_iter, nruns_so_far), 1)
@@ -812,12 +826,12 @@ def solve_main(objfun, x0, args, xl, xu, npt, rhobeg, rhoend, maxfun, nruns_so_f
812
826
  # Reduce rho
813
827
  control.reduce_rho(current_iter, params)
814
828
  if do_logging:
815
- logging.info("New rho = %g after %i function evaluations" % (control.rho, control.nf))
829
+ module_logger.info("New rho = %g after %i function evaluations" % (control.rho, control.nf))
816
830
  if control.n() < params("logging.n_to_print_whole_x_vector"):
817
- logging.debug("Best so far: f = %.15g at x = " % (control.model.fopt())
831
+ module_logger.debug("Best so far: f = %.15g at x = " % (control.model.fopt())
818
832
  + str(control.model.xopt(abs_coordinates=True)))
819
833
  else:
820
- logging.debug("Best so far: f = %.15g at x = [...]" % (control.model.fopt()))
834
+ module_logger.debug("Best so far: f = %.15g at x = [...]" % (control.model.fopt()))
821
835
  continue # next iteration
822
836
  else:
823
837
  # Quit on rho=rhoend
@@ -845,14 +859,14 @@ def solve_main(objfun, x0, args, xl, xu, npt, rhobeg, rhoend, maxfun, nruns_so_f
845
859
  # Quit & return the important information
846
860
  x, rvec, f, jacmin, nsamples = control.model.get_final_results()
847
861
  if do_logging:
848
- logging.debug("At return from DFO-LS, number of function evals = %i" % nf)
849
- logging.debug("Smallest objective value = %.15g at x = " % f + str(x))
862
+ module_logger.debug("At return from DFO-LS, number of function evals = %i" % nf)
863
+ module_logger.debug("Smallest objective value = %.15g at x = " % f + str(x))
850
864
  return x, rvec, f, jacmin, nsamples, control.nf, control.nx, nruns_so_far, exit_info, diagnostic_info
851
865
 
852
866
 
853
- def solve(objfun, x0, args=(), bounds=None, npt=None, rhobeg=None, rhoend=1e-8, maxfun=None, nsamples=None, user_params=None,
867
+ def solve(objfun, x0, args=(), bounds=None, projections=[], npt=None, rhobeg=None, rhoend=1e-8, maxfun=None, nsamples=None, user_params=None,
854
868
  objfun_has_noise=False, scaling_within_bounds=False, do_logging=True, print_progress=False):
855
- x0 = x0.astype(np.float)
869
+ x0 = x0.astype(float)
856
870
  n = len(x0)
857
871
 
858
872
  # Set missing inputs (if not specified) to some sensible defaults
@@ -861,13 +875,17 @@ def solve(objfun, x0, args=(), bounds=None, npt=None, rhobeg=None, rhoend=1e-8,
861
875
  xu = None
862
876
  else:
863
877
  assert len(bounds) == 2, "bounds must be a 2-tuple of (lower, upper), where both are arrays of size(x0)"
864
- xl = bounds[0].astype(np.float) if bounds[0] is not None else None
865
- xu = bounds[1].astype(np.float) if bounds[1] is not None else None
878
+ xl = bounds[0].astype(float) if bounds[0] is not None else None
879
+ xu = bounds[1].astype(float) if bounds[1] is not None else None
866
880
 
867
881
  if (xl is None or xu is None) and scaling_within_bounds:
868
882
  scaling_within_bounds = False
869
883
  warnings.warn("Ignoring scaling_within_bounds=True for unconstrained problem/1-sided bounds", RuntimeWarning)
870
884
 
885
+ if projections and scaling_within_bounds:
886
+ scaling_within_bounds = False
887
+ warnings.warn("Ignoring scaling_within_bounds=True for problem with arbitrary constraints", RuntimeWarning)
888
+
871
889
  if xl is None:
872
890
  xl = -1e20 * np.ones((n,)) # unconstrained
873
891
  if xu is None:
@@ -881,6 +899,18 @@ def solve(objfun, x0, args=(), bounds=None, npt=None, rhobeg=None, rhoend=1e-8,
881
899
  if nsamples is None:
882
900
  nsamples = lambda delta, rho, iter, nruns: 1 # no averaging
883
901
 
902
+ # If using arbitrary constraints, create projection from bounds
903
+ if projections:
904
+ xlb = xl.copy()
905
+ xub = xu.copy()
906
+ bproj = lambda w: pbox(w,xlb,xub)
907
+ projections = list(projections)
908
+ projections.append(bproj)
909
+
910
+ # since using arbitrary constraints, don't constrain otherwise
911
+ xl = -1e20 * np.ones((n,))
912
+ xu = 1e20 * np.ones((n,))
913
+
884
914
  # Set parameters
885
915
  params = ParameterList(int(n), int(npt), int(maxfun), objfun_has_noise=objfun_has_noise) # make sure int, not np.int
886
916
  if user_params is not None:
@@ -975,6 +1005,13 @@ def solve(objfun, x0, args=(), bounds=None, npt=None, rhobeg=None, rhoend=1e-8,
975
1005
  results = OptimResults(None, None, None, None, 0, 0, 0, exit_flag, exit_msg)
976
1006
  return results
977
1007
 
1008
+ # Enforce arbitrary constraint bounds on x0
1009
+ if projections:
1010
+ xp = dykstra(projections,x0,max_iter=params("dykstra.max_iters"),tol=params("dykstra.d_tol"))
1011
+ if not np.allclose(xp,x0):
1012
+ warnings.warn("x0 not feasible w.r.t given constraints, adjusting", RuntimeWarning)
1013
+ x0 = xp.copy()
1014
+
978
1015
  # Enforce lower & upper bounds on x0
979
1016
  idx = (x0 <= xl)
980
1017
  if np.any(idx):
@@ -992,7 +1029,7 @@ def solve(objfun, x0, args=(), bounds=None, npt=None, rhobeg=None, rhoend=1e-8,
992
1029
  nf = 0
993
1030
  nx = 0
994
1031
  xmin, rmin, fmin, jacmin, nsamples_min, nf, nx, nruns, exit_info, diagnostic_info = \
995
- solve_main(objfun, x0, args, xl, xu, npt, rhobeg, rhoend, maxfun, nruns, nf, nx, nsamples, params,
1032
+ solve_main(objfun, x0, args, xl, xu, projections, npt, rhobeg, rhoend, maxfun, nruns, nf, nx, nsamples, params,
996
1033
  diagnostic_info, scaling_changes, default_growing_method_set_by_user=default_growing_method_set_by_user,
997
1034
  do_logging=do_logging, print_progress=print_progress)
998
1035
 
@@ -1007,28 +1044,28 @@ def solve(objfun, x0, args=(), bounds=None, npt=None, rhobeg=None, rhoend=1e-8,
1007
1044
  npt = min(npt, params("restarts.max_npt"))
1008
1045
 
1009
1046
  if do_logging:
1010
- logging.info("Restarting from finish point (f = %g) after %g function evals; using rhobeg = %g and rhoend = %g"
1047
+ module_logger.info("Restarting from finish point (f = %g) after %g function evals; using rhobeg = %g and rhoend = %g"
1011
1048
  % (fmin, nf, rhobeg, rhoend))
1012
1049
  if params("restarts.hard.use_old_rk"):
1013
1050
  xmin2, rmin2, fmin2, jacmin2, nsamples2, nf, nx, nruns, exit_info, diagnostic_info = \
1014
- solve_main(objfun, xmin, args, xl, xu, npt, rhobeg, rhoend, maxfun, nruns, nf, nx, nsamples, params,
1051
+ solve_main(objfun, xmin, args, xl, xu, projections, npt, rhobeg, rhoend, maxfun, nruns, nf, nx, nsamples, params,
1015
1052
  diagnostic_info, scaling_changes, r0_avg_old=rmin, r0_nsamples_old=nsamples_min,
1016
1053
  do_logging=do_logging, print_progress=print_progress)
1017
1054
  else:
1018
1055
  xmin2, rmin2, fmin2, jacmin2, nsamples2, nf, nx, nruns, exit_info, diagnostic_info = \
1019
- solve_main(objfun, xmin, args, xl, xu, npt, rhobeg, rhoend, maxfun, nruns, nf, nx, nsamples, params,
1056
+ solve_main(objfun, xmin, args, xl, xu, projections, npt, rhobeg, rhoend, maxfun, nruns, nf, nx, nsamples, params,
1020
1057
  diagnostic_info, scaling_changes, do_logging=do_logging, print_progress=print_progress)
1021
1058
 
1022
1059
  if fmin2 < fmin or np.isnan(fmin):
1023
1060
  if do_logging:
1024
- logging.info("Successful run with new f = %s compared to old f = %s" % (fmin2, fmin))
1061
+ module_logger.info("Successful run with new f = %s compared to old f = %s" % (fmin2, fmin))
1025
1062
  last_successful_run = nruns
1026
1063
  (xmin, rmin, fmin, nsamples_min) = (xmin2, rmin2, fmin2, nsamples2)
1027
1064
  if jacmin2 is not None: # may be None if finished during setup phase, in which case just use old Jacobian
1028
1065
  jacmin = jacmin2
1029
1066
  else:
1030
1067
  if do_logging:
1031
- logging.info("Unsuccessful run with new f = %s compared to old f = %s" % (fmin2, fmin))
1068
+ module_logger.info("Unsuccessful run with new f = %s compared to old f = %s" % (fmin2, fmin))
1032
1069
 
1033
1070
  if nruns - last_successful_run >= params("restarts.max_unsuccessful_restarts"):
1034
1071
  exit_info = ExitInformation(EXIT_SUCCESS, "Reached maximum number of unsuccessful restarts")
@@ -1046,7 +1083,7 @@ def solve(objfun, x0, args=(), bounds=None, npt=None, rhobeg=None, rhoend=1e-8,
1046
1083
  results.diagnostic_info = df
1047
1084
 
1048
1085
  if do_logging:
1049
- logging.info("Did a total of %g run(s)" % nruns)
1086
+ module_logger.info("Did a total of %g run(s)" % nruns)
1050
1087
 
1051
1088
  return results
1052
1089
 
dfols/trust_region.py CHANGED
@@ -11,6 +11,15 @@ produces a new vector d which (approximately) solves the trust region subproblem
11
11
  The other outputs: gnew is the gradient of the model at d, and crvmin has
12
12
  information about the curvature of the model at the solution.
13
13
 
14
+ For handling arbitrary constraints, the call is
15
+ d, gnew, crvmin = ctrsbox(xopt, g, H, projections, delta)
16
+ which produces a new vector d approximately solving the constrained trust region subproblem:
17
+ min_{d} g'*d + 0.5*d'*H*d
18
+ s.t. ||d|| <= delta
19
+ xopt + d is feasible w.r.t. the constraint set C
20
+ The other outputs: gnew is the gradient of the model at d, and crvmin has
21
+ information about the curvature of the model at the solution.
22
+
14
23
  We also provide a function for maximising the absolute value of a linear function
15
24
  inside a similar trust region - this is useful for geometry steps.
16
25
  The call
@@ -23,6 +32,13 @@ With this value, the variable d=x-xbase solves the problem
23
32
  min_s abs(c + g' * d)
24
33
  s.t. lower <= xbase + d <= upper
25
34
  ||d|| <= delta
35
+ Again, we have a version of this for handling arbitrary constraints
36
+ The call
37
+ x = ctrsbox_geometry(xbase, c, g, projections, Delta)
38
+ Solves
39
+ min_s abs(c + g' * d)
40
+ s.t. xbase + d is feasible w.r.t. the constraint set C
41
+ ||d|| <= delta
26
42
 
27
43
  Notes
28
44
  ----
@@ -63,13 +79,77 @@ except ImportError:
63
79
  # Fall back to Python implementation
64
80
  USE_FORTRAN = False
65
81
 
82
+ from .util import dykstra, pball, pbox, sumsq, model_value
66
83
 
67
- from .util import sumsq
84
+ __all__ = ['ctrsbox', 'ctrsbox_geometry', 'trsbox', 'trsbox_geometry']
68
85
 
86
+ ZERO_THRESH = 1e-14
69
87
 
70
- __all__ = ['trsbox', 'trsbox_geometry']
88
+ def ctrsbox(xopt, g, H, projections, delta, d_max_iters=100, d_tol=1e-10, use_fortran=USE_FORTRAN):
89
+ n = xopt.size
90
+ assert xopt.shape == (n,), "xopt has wrong shape (should be vector)"
91
+ assert g.shape == (n,), "g and xopt have incompatible sizes"
92
+ assert len(H.shape) == 2, "H must be a matrix"
93
+ assert H.shape == (n,n), "H and xopt have incompatible sizes"
94
+ assert np.allclose(H, H.T), "H must be symmetric"
95
+ assert delta > 0.0, "delta must be strictly positive"
71
96
 
72
- ZERO_THRESH = 1e-14
97
+ d = np.zeros((n,))
98
+ gnew = g.copy()
99
+ gy = g.copy()
100
+ crvmin = -1.0
101
+ y = d.copy()
102
+ eta = 1.2 # L backtrack scaling factor
103
+ t = 1
104
+
105
+ # Initial guess of L is norm(Hessian)
106
+ L = np.linalg.norm(H, 2)
107
+
108
+ # trust region is a ball of radius delta around xopt
109
+ trproj = lambda w: pball(w, xopt, delta)
110
+
111
+ # combine trust region constraints with user-entered constraints
112
+ P = list(projections) # make a copy of the projections list
113
+ P.append(trproj)
114
+ def proj(d0):
115
+ p = dykstra(P, xopt+d0, max_iter=d_max_iters, tol=d_tol)
116
+ # we want the step only, so we subtract xopt
117
+ # from the new point: proj(xk+d) - xk
118
+ return p - xopt
119
+
120
+ MAX_LOOP_ITERS = 100 * n ** 2
121
+
122
+ # projected GD loop
123
+ for ii in range(MAX_LOOP_ITERS):
124
+ w = y - (1/L)*gy
125
+ prev_d = d.copy()
126
+ d = proj(w)
127
+
128
+ # size of step taken
129
+ s = d - prev_d
130
+ stplen = np.linalg.norm(s)
131
+
132
+ # update true gradient
133
+ gnew += H.dot(s)
134
+
135
+ # update CRVMIN
136
+ crv = s.dot(H).dot(s)/sumsq(s) if sumsq(s) >= ZERO_THRESH else crvmin
137
+ crvmin = min(crvmin, crv) if crvmin != -1.0 else crv
138
+
139
+ # exit condition
140
+ if stplen <= ZERO_THRESH:
141
+ break
142
+
143
+ # momentum update
144
+ prev_t = t
145
+ t = (1 + np.sqrt(1 + 4 * t ** 2))/2
146
+ prev_y = y.copy()
147
+ y = d + s*(prev_t - 1)/t
148
+
149
+ # update gradient w.r.t y
150
+ gy += H.dot(y - prev_y)
151
+
152
+ return d, gnew, crvmin
73
153
 
74
154
 
75
155
  def trsbox(xopt, g, H, sl, su, delta, use_fortran=USE_FORTRAN):
@@ -103,7 +183,7 @@ def trsbox(xopt, g, H, sl, su, delta, use_fortran=USE_FORTRAN):
103
183
  iterc = 0
104
184
  nact = 0 # number of fixed variables
105
185
 
106
- xbdi = np.zeros((n,), dtype=np.int) # fix x_i at bounds? [values -1, 0, 1]
186
+ xbdi = np.zeros((n,), dtype=int) # fix x_i at bounds? [values -1, 0, 1]
107
187
  xbdi[(xopt <= sl) & (g >= 0.0)] = -1
108
188
  xbdi[(xopt >= su) & (g <= 0.0)] = 1
109
189
 
@@ -405,8 +485,63 @@ def ball_step(x0, g, Delta):
405
485
  if sqrt(gsqnorm) < ZERO_THRESH: # Error catching: if g=0, make no step
406
486
  return 0.0
407
487
  else:
408
- return (sqrt(gdotx0**2 + gsqnorm*(Delta**2 - x0sqnorm)) - gdotx0) / gsqnorm
488
+ # Sqrt had negative input on prob 46 in OG DFOLS with noise
489
+ # print("Inside of the sqrt:", gdotx0**2 + gsqnorm*(Delta**2 - x0sqnorm))
490
+ # Got Inside of the sqrt: -3.608971127647144e-42
491
+ # Added max(0,...) here
492
+ return (sqrt(np.maximum(0,gdotx0**2 + gsqnorm*(Delta**2 - x0sqnorm))) - gdotx0) / gsqnorm
493
+
494
+ def ctrsbox_linear(xbase, g, projections, Delta, d_max_iters=100, d_tol=1e-10, use_fortran=USE_FORTRAN):
495
+ # Solve the convex program:
496
+ # min_d g' * d
497
+ # s.t. xbase + d is feasible w.r.t. constraint set C
498
+ # ||d||^2 <= Delta^2
499
+
500
+ n = g.size
501
+ d = np.zeros((n,))
502
+ y = d.copy()
503
+ t = 1
504
+ dirn = -g
505
+ cons_dirns = []
506
+
507
+ # If g[i] = 0, never step along this direction
508
+ constant_directions = np.where(np.abs(dirn) < ZERO_THRESH)[0]
509
+ dirn[constant_directions] = 0.0
510
+
511
+ # trust region is a ball of radius delta centered around xbase
512
+ trproj = lambda w: pball(w, xbase, Delta)
409
513
 
514
+ # combine trust region constraints with user-entered constraints
515
+ P = list(projections) # make a copy of the projections list
516
+ P.append(trproj)
517
+ def proj(d0):
518
+ p = dykstra(P, xbase + d0, max_iter=d_max_iters, tol=d_tol)
519
+ # we want the step only, so we subtract
520
+ # xbase from the new point: proj(xk + d) - xk
521
+ return p - xbase
522
+
523
+ MAX_LOOP_ITERS = 100 * n ** 2
524
+
525
+ # projected GD loop
526
+ for ii in range(MAX_LOOP_ITERS):
527
+ w = y + dirn
528
+ prev_d = d.copy()
529
+ d = proj(w)
530
+
531
+ s = d - prev_d
532
+ stplen = np.linalg.norm(s)
533
+
534
+ # exit condition
535
+ if stplen <= ZERO_THRESH:
536
+ break
537
+
538
+ # 'momentum' update
539
+ prev_t = t
540
+ t = (1 + np.sqrt(1 + 4 * t ** 2))/2
541
+ prev_y = y.copy()
542
+ y = d + s*(prev_t - 1)/t
543
+
544
+ return d
410
545
 
411
546
  def trsbox_linear(g, a_in, b_in, Delta, use_fortran=USE_FORTRAN):
412
547
  # Solve the convex program:
@@ -466,6 +601,22 @@ def trsbox_linear(g, a_in, b_in, Delta, use_fortran=USE_FORTRAN):
466
601
  dirn[idx_hit] = 0.0 # no more searching this direction
467
602
  return x
468
603
 
604
+ def ctrsbox_geometry(xbase, c, g, projections, Delta, d_max_iters=100, d_tol=1e-10, use_fortran=USE_FORTRAN):
605
+ # Given a Lagrange polynomial defined by: L(x) = c + g' * (x - xbase)
606
+ # Maximise |L(x)| in a box + trust region - that is, solve:
607
+ # max_x abs(c + g' * (x - xbase))
608
+ # s.t. x is feasible w.r.t constraint set C
609
+ # ||x-xbase|| <= Delta
610
+ # Setting s = x-xbase (or x = xbase + s), this is equivalent to:
611
+ # max_s abs(c + g' * s)
612
+ # s.t. xbase + s is is feasible w.r.t constraint set C
613
+ # ||s|| <= Delta
614
+ smin = ctrsbox_linear(xbase, g, projections, Delta, d_max_iters=100, d_tol=1e-10, use_fortran=use_fortran) # minimise g' * s
615
+ smax = ctrsbox_linear(xbase, -g, projections, Delta, d_max_iters=100, d_tol=1e-10, use_fortran=use_fortran) # maximise g' * s
616
+ if abs(c + np.dot(g, smin)) >= abs(c + np.dot(g, smax)): # choose the one with largest absolute value
617
+ return smin
618
+ else:
619
+ return smax
469
620
 
470
621
  def trsbox_geometry(xbase, c, g, lower, upper, Delta, use_fortran=USE_FORTRAN):
471
622
  # Given a Lagrange polynomial defined by: L(x) = c + g' * (x - xbase)