DFO-LS 1.4.1__py3-none-any.whl → 1.5.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/model.py CHANGED
@@ -36,7 +36,7 @@ import numpy as np
36
36
  import scipy.linalg as LA
37
37
 
38
38
  from .trust_region import trsbox_geometry
39
- from .util import sumsq, dykstra
39
+ from .util import sumsq, dykstra, remove_scaling
40
40
 
41
41
  __all__ = ['Model']
42
42
 
@@ -44,8 +44,8 @@ module_logger = logging.getLogger(__name__)
44
44
 
45
45
 
46
46
  class Model(object):
47
- def __init__(self, npt, x0, r0, xl, xu, projections, r0_nsamples, n=None, m=None, abs_tol=1e-12, rel_tol=1e-20, precondition=True,
48
- do_logging=True):
47
+ def __init__(self, npt, x0, r0, xl, xu, projections, r0_nsamples, h=None, argsh=(), n=None, m=None, abs_tol=1e-12, rel_tol=1e-20, precondition=True,
48
+ do_logging=True, scaling_changes=None):
49
49
  if n is None:
50
50
  n = len(x0)
51
51
  if m is None:
@@ -56,11 +56,15 @@ class Model(object):
56
56
  assert xu.shape == (n,), "xu has wrong shape (got %s, expect (%g,))" % (str(xu.shape), n)
57
57
  assert r0.shape == (m,), "r0 has wrong shape (got %s, expect (%g,))" % (str(r0.shape), m)
58
58
  self.do_logging = do_logging
59
+ self.scaling_changes = scaling_changes
59
60
  self.dim = n
60
61
  self.resid_dim = m
61
62
  self.num_pts = npt
62
63
  self.npt_so_far = 1 # number of points added so far (with function values)
63
64
 
65
+ self.h = h
66
+ self.argsh = argsh
67
+
64
68
  # Initialise to blank some useful stuff
65
69
  # Interpolation points
66
70
  self.xbase = x0.copy()
@@ -72,12 +76,17 @@ class Model(object):
72
76
  # Function values
73
77
  self.fval_v = np.inf * np.ones((npt, m)) # residuals for each xpt
74
78
  self.fval_v[0, :] = r0.copy()
75
- self.fval = np.inf * np.ones((npt, )) # overall objective value for each xpt
76
- self.fval[0] = sumsq(r0)
79
+
80
+ self.objval = np.inf * np.ones((npt, )) # overall objective value for each xpt
81
+ self.objval[0] = sumsq(r0)
82
+ if h is not None:
83
+ self.objval[0] += h(remove_scaling(x0, self.scaling_changes), *argsh)
77
84
  self.kopt = 0 # index of current iterate (should be best value so far)
78
85
  self.nsamples = np.zeros((npt,), dtype=int) # number of samples used to evaluate objective at each point
79
86
  self.nsamples[0] = r0_nsamples
80
- self.fbeg = self.fval[0] # f(x0), saved to check for sufficient reduction
87
+ self.objbeg = self.objval[0] # f(x0), saved to check for sufficient reduction
88
+ self.eval_num = np.zeros((npt,), dtype=int) # which evaluation number (1-indexed, nx not nf) is currently stored self.points[k,:]
89
+ self.eval_num[0] = 1
81
90
 
82
91
  # Termination criteria
83
92
  self.abs_tol = abs_tol
@@ -86,13 +95,16 @@ class Model(object):
86
95
  # Model information
87
96
  self.model_const = np.zeros((m, )) # constant term for model m(s) = c + J*s
88
97
  self.model_jac = np.zeros((m, n)) # Jacobian term for model m(s) = c + J*s
98
+ self.model_jac_eval_nums = None # which evaluation numbers (1-indexed, nx not nf) were used to build model_jac
89
99
 
90
100
  # Saved point (in absolute coordinates) - always check this value before quitting solver
91
101
  self.xsave = None
92
102
  self.rsave = None
93
- self.fsave = None
103
+ self.objsave = None
94
104
  self.jacsave = None
95
105
  self.nsamples_save = None
106
+ self.eval_num_save = None
107
+ self.jacsave_eval_nums = None
96
108
 
97
109
  # Factorisation of interpolation matrix
98
110
  self.factorisation_current = False
@@ -118,8 +130,8 @@ class Model(object):
118
130
  def ropt(self):
119
131
  return self.fval_v[self.kopt, :] # residuals for current iterate
120
132
 
121
- def fopt(self):
122
- return self.fval[self.kopt]
133
+ def objopt(self):
134
+ return self.objval[self.kopt]
123
135
 
124
136
  def xpt(self, k, abs_coordinates=False):
125
137
  assert 0 <= k < self.npt(), "Invalid index %g" % k
@@ -135,9 +147,9 @@ class Model(object):
135
147
  assert 0 <= k < self.npt(), "Invalid index %g" % k
136
148
  return self.fval_v[k, :]
137
149
 
138
- def fval(self, k):
150
+ def objval(self, k):
139
151
  assert 0 <= k < self.npt(), "Invalid index %g" % k
140
- return self.fval[k]
152
+ return self.objval[k]
141
153
 
142
154
  def as_absolute_coordinates(self, x, full_dykstra=False):
143
155
  # If x were an interpolation point, get the absolute coordinates of x
@@ -167,7 +179,7 @@ class Model(object):
167
179
  sq_distances[k] = sumsq(self.points[k, :] - xopt)
168
180
  return sq_distances
169
181
 
170
- def change_point(self, k, x, rvec, allow_kopt_update=True):
182
+ def change_point(self, k, x, rvec, eval_num, allow_kopt_update=True):
171
183
  # Update point k to x (w.r.t. xbase), with residual values fvec
172
184
  if k >= self.npt_so_far and self.npt_so_far < self.num_pts:
173
185
  assert k == self.npt_so_far, "Growing: updating wrong point"
@@ -177,18 +189,22 @@ class Model(object):
177
189
 
178
190
  self.points[k, :] = x.copy()
179
191
  self.fval_v[k, :] = rvec.copy()
180
- self.fval[k] = sumsq(rvec)
192
+ self.objval[k] = sumsq(rvec)
193
+ if self.h is not None:
194
+ self.objval[k] += self.h(remove_scaling(self.xbase + x, self.scaling_changes), *self.argsh)
181
195
  self.nsamples[k] = 1
196
+ self.eval_num[k] = eval_num
182
197
  self.factorisation_current = False
183
198
 
184
- if allow_kopt_update and self.fval[k] < self.fopt():
199
+ if allow_kopt_update and self.objval[k] < self.objopt():
185
200
  self.kopt = k
186
201
  return
187
202
 
188
203
  def swap_points(self, k1, k2):
189
204
  self.points[[k1, k2], :] = self.points[[k2, k1], :]
190
205
  self.fval_v[[k1, k2], :] = self.fval_v[[k2, k1], :]
191
- self.fval[[k1, k2]] = self.fval[[k2, k1]]
206
+ self.objval[[k1, k2]] = self.objval[[k2, k1]]
207
+ self.eval_num[[k1, k2]] = self.eval_num[[k2, k1]]
192
208
  if self.kopt == k1:
193
209
  self.kopt = k2
194
210
  elif self.kopt == k2:
@@ -201,22 +217,28 @@ class Model(object):
201
217
  assert 0 <= k < self.npt(), "Invalid index %g" % k
202
218
  t = float(self.nsamples[k]) / float(self.nsamples[k] + 1)
203
219
  self.fval_v[k, :] = t * self.fval_v[k, :] + (1 - t) * rvec_extra
204
- self.fval[k] = sumsq(self.fval_v[k, :])
220
+ # NOTE: how to sample when we have h? still at xpt(k), then add h(xpt(k)). Modify test if incorrect!
221
+ self.objval[k] = sumsq(self.fval_v[k, :])
222
+ if self.h is not None:
223
+ self.objval[k] += self.h(remove_scaling(self.xbase + self.points[k, :], self.scaling_changes), *self.argsh)
205
224
  self.nsamples[k] += 1
206
225
 
207
- self.kopt = np.argmin(self.fval[:self.npt()]) # make sure kopt is always the best value we have
226
+ self.kopt = np.argmin(self.objval[:self.npt()]) # make sure kopt is always the best value we have
208
227
  return
209
228
 
210
- def add_new_point(self, x, rvec):
229
+ def add_new_point(self, x, rvec, eval_num):
211
230
  self.points = np.append(self.points, x.reshape((1, self.n())), axis=0) # append row to xpt
212
231
  self.fval_v = np.append(self.fval_v, rvec.reshape((1, self.m())), axis=0) # append row to fval_v
213
- f = np.dot(rvec, rvec)
214
- self.fval = np.append(self.fval, f) # append entry to fval
232
+ obj = sumsq(rvec)
233
+ if self.h is not None:
234
+ obj += self.h(remove_scaling(self.xbase + x, self.scaling_changes), *self.argsh)
235
+ self.objval = np.append(self.objval, obj) # append entry to fval
215
236
  self.nsamples = np.append(self.nsamples, 1) # add new sample number
237
+ self.eval_num = np.append(self.eval_num, eval_num) # add new evaluation number
216
238
  self.num_pts += 1 # make sure npt is updated
217
239
  self.npt_so_far += 1
218
240
 
219
- if f < self.fopt():
241
+ if obj < self.objopt():
220
242
  self.kopt = self.npt() - 1
221
243
 
222
244
  self.factorisation_current = False
@@ -235,28 +257,33 @@ class Model(object):
235
257
  self.model_const += np.dot(self.model_jac, xbase_shift)
236
258
  return
237
259
 
238
- def save_point(self, x, rvec, nsamples, x_in_abs_coords=True):
239
- f = sumsq(rvec)
240
- if self.fsave is None or f <= self.fsave:
241
- self.xsave = x.copy() if x_in_abs_coords else self.as_absolute_coordinates(x)
260
+ def save_point(self, x, rvec, nsamples, eval_num, x_in_abs_coords=True):
261
+ xabs = x.copy() if x_in_abs_coords else self.as_absolute_coordinates(x)
262
+ obj = sumsq(rvec)
263
+ if self.h is not None:
264
+ obj += self.h(remove_scaling(xabs, self.scaling_changes), *self.argsh)
265
+ if self.objsave is None or obj <= self.objsave:
266
+ self.xsave = xabs
242
267
  self.rsave = rvec.copy()
243
- self.fsave = f
244
- self.jacsave = self.model_jac.copy()
268
+ self.objsave = obj
269
+ self.jacsave = self.model_jac.copy() if self.model_jac is not None else None
245
270
  self.nsamples_save = nsamples
271
+ self.eval_num_save = eval_num
272
+ self.jacsave_eval_nums = self.model_jac_eval_nums.copy() if self.model_jac_eval_nums is not None else None
246
273
  return True
247
274
  else:
248
275
  return False # this value is worse than what we have already - didn't save
249
276
 
250
277
  def get_final_results(self):
251
- # Return x and fval for optimal point (either from xsave+fsave or kopt)
252
- if self.fsave is None or self.fopt() <= self.fsave: # optimal has changed since xsave+fsave were last set
253
- return self.xopt(abs_coordinates=True).copy(), self.ropt().copy(), self.fopt(), self.model_jac.copy(), self.nsamples[self.kopt]
278
+ # Return x and objval for optimal point (either from xsave+objsave or kopt)
279
+ if self.objsave is None or self.objopt() <= self.objsave: # optimal has changed since xsave+objsave were last set
280
+ return self.xopt(abs_coordinates=True).copy(), self.ropt().copy(), self.objopt(), self.model_jac.copy(), self.nsamples[self.kopt], self.eval_num[self.kopt], self.model_jac_eval_nums
254
281
  else:
255
- return self.xsave.copy(), self.rsave.copy(), self.fsave, self.jacsave, self.nsamples_save
282
+ return self.xsave.copy(), self.rsave.copy(), self.objsave, self.jacsave, self.nsamples_save, self.eval_num_save, self.jacsave_eval_nums
256
283
 
257
284
  def min_objective_value(self):
258
285
  # Get termination criterion for f small: f <= abs_tol or f <= rel_tol * f0
259
- return max(self.abs_tol, self.rel_tol * self.fbeg)
286
+ return max(self.abs_tol, self.rel_tol * self.objbeg)
260
287
 
261
288
  def model_value(self, d, d_based_at_xopt=True, with_const_term=False):
262
289
  if d_based_at_xopt:
@@ -350,6 +377,7 @@ class Model(object):
350
377
  J_old = self.model_jac.copy()
351
378
  self.model_jac = dg[1:,:].T
352
379
  self.model_const = dg[0,:] - np.dot(self.model_jac, xopt) # shift base to xbase
380
+ self.model_jac_eval_nums = self.eval_num.copy()
353
381
  if verbose or get_chg_J:
354
382
  norm_J_error = np.linalg.norm(self.model_jac - J_old, ord='fro')**2
355
383
  linalg_resid = np.linalg.norm(W.dot(dg) - rhs)**2
@@ -375,7 +403,7 @@ class Model(object):
375
403
  return True, interp_error, sqrt(norm_J_error), linalg_resid, ls_interp_cond_num # flag ok
376
404
 
377
405
  def build_full_model(self):
378
- # Build full least squares objective model from mini-models
406
+ # Build full least squares model from mini-models
379
407
  # Centred around xopt
380
408
  r = self.model_const + np.dot(self.model_jac, self.xopt()) # constant term (for inexact interpolation)
381
409
  J = self.model_jac
dfols/params.py CHANGED
@@ -82,7 +82,7 @@ class ParameterList(object):
82
82
  self.params["restarts.use_soft_restarts"] = True
83
83
  self.params["restarts.soft.num_geom_steps"] = 3
84
84
  self.params["restarts.soft.move_xk"] = True
85
- self.params["restarts.soft.max_fake_successful_steps"] = maxfun # number ratio>0 steps below fsave allowed
85
+ self.params["restarts.soft.max_fake_successful_steps"] = maxfun # number ratio>0 steps below objsave allowed
86
86
  self.params["restarts.hard.use_old_rk"] = True # recycle r(xk) from previous run?
87
87
  self.params["restarts.increase_npt"] = False
88
88
  self.params["restarts.increase_npt_amt"] = 1
@@ -109,12 +109,20 @@ class ParameterList(object):
109
109
  self.params["growing.full_rank.min_sing_val"] = 1e-6 # absolute floor on singular values
110
110
  self.params["growing.full_rank.svd_max_jac_cond"] = 1e8 # maximum condition number of Jacobian
111
111
  self.params["growing.perturb_trust_region_step"] = False # add random direction onto TRS solution?
112
+
112
113
  # Dykstra's algorithm
113
114
  self.params["dykstra.d_tol"] = 1e-10
114
115
  self.params["dykstra.max_iters"] = 100
116
+
115
117
  # Matrix rank algorithm
116
118
  self.params["matrix_rank.r_tol"] = 1e-18
117
-
119
+
120
+ # Function tolerance when applying S-FISTA method
121
+ self.params["func_tol.criticality_measure"] = 1e-3
122
+ self.params["func_tol.tr_step"] = 1-1e-1
123
+ self.params["func_tol.max_iters"] = 500
124
+ self.params["sfista.max_iters_scaling"] = 2.0
125
+
118
126
  self.params_changed = {}
119
127
  for p in self.params:
120
128
  self.params_changed[p] = False
@@ -268,6 +276,14 @@ class ParameterList(object):
268
276
  type_str, nonetype_ok, lower, upper = 'int', False, 0, None
269
277
  elif key == "matrix_rank.r_tol":
270
278
  type_str, nonetype_ok, lower, upper = 'float', False, 0.0, None
279
+ elif key == "func_tol.criticality_measure":
280
+ type_str, nonetype_ok, lower, upper = 'float', False, 0.0, 1.0
281
+ elif key == "func_tol.tr_step":
282
+ type_str, nonetype_ok, lower, upper = 'float', False, 0.0, 1.0
283
+ elif key == "func_tol.max_iters":
284
+ type_str, nonetype_ok, lower, upper = 'int', False, 0, None
285
+ elif key == "sfista.max_iters_scaling":
286
+ type_str, nonetype_ok, lower, upper = 'float', False, 1.0, None
271
287
  else:
272
288
  assert False, "ParameterList.param_type() has unknown key: %s" % key
273
289
  return type_str, nonetype_ok, lower, upper