DFO-LS 1.5.3__py3-none-any.whl → 1.6__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.

@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: DFO-LS
3
- Version: 1.5.3
3
+ Version: 1.6
4
4
  Summary: A flexible derivative-free solver for (bound constrained) nonlinear least-squares minimization
5
5
  Author-email: Lindon Roberts <lindon.roberts@sydney.edu.au>
6
6
  Maintainer-email: Lindon Roberts <lindon.roberts@sydney.edu.au>
@@ -33,14 +33,15 @@ Description-Content-Type: text/x-rst
33
33
  License-File: LICENSE.txt
34
34
  Requires-Dist: setuptools
35
35
  Requires-Dist: numpy
36
- Requires-Dist: scipy >=1.11
36
+ Requires-Dist: scipy>=1.11
37
37
  Requires-Dist: pandas
38
38
  Provides-Extra: dev
39
- Requires-Dist: pytest ; extra == 'dev'
40
- Requires-Dist: Sphinx ; extra == 'dev'
41
- Requires-Dist: sphinx-rtd-theme ; extra == 'dev'
39
+ Requires-Dist: pytest; extra == "dev"
40
+ Requires-Dist: Sphinx; extra == "dev"
41
+ Requires-Dist: sphinx-rtd-theme; extra == "dev"
42
42
  Provides-Extra: trustregion
43
- Requires-Dist: trustregion >=1.1 ; extra == 'trustregion'
43
+ Requires-Dist: trustregion>=1.1; extra == "trustregion"
44
+ Dynamic: license-file
44
45
 
45
46
  ===================================================
46
47
  DFO-LS: Derivative-Free Optimizer for Least-Squares
@@ -0,0 +1,15 @@
1
+ dfo_ls-1.6.dist-info/licenses/LICENSE.txt,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
2
+ dfols/__init__.py,sha256=Qmcjy68aqTr5qqgbslJ2l1OdSEl7kpoDA9F4kAp4QFQ,1689
3
+ dfols/controller.py,sha256=LHk8ES0JjsHeAixLxDxv_t08tLSRchgopagH8Trsn1c,55525
4
+ dfols/diagnostic_info.py,sha256=kEcFCjD2rk39XRa90ocEaQvJWc0wj_ZPpQkOulVIM-k,6106
5
+ dfols/evaluations_database.py,sha256=t9H8VA1sRClkh6y7EeJAyoKJxo6mW4Y2KUrat7NXSKQ,10245
6
+ dfols/hessian.py,sha256=sExx4J4KoGwHItbthX2odosB2ONbQFvLdlcod7PIh4k,4262
7
+ dfols/model.py,sha256=1Npj3fJvMv66bKu_RIzLLI-2tyzPWOsKuyv-YUjcv2c,20711
8
+ dfols/params.py,sha256=VGDvfDWxqhPEUWpNm4TtehzA5sw13m1hLs44WzK_5k0,18556
9
+ dfols/solver.py,sha256=gzH5SCrI1xHNzt40gcMnvIzWXm2aAOlIV0ZMHf1bItU,69314
10
+ dfols/trust_region.py,sha256=JbHLBDw7H88a3cIMuialh7kpMNGjL3Lp9JsjrBNpDWQ,28231
11
+ dfols/util.py,sha256=XYb42bc5X9nJtFT27sx6_tD_EcBbqOnCCjKy-1wLJxY,10725
12
+ dfo_ls-1.6.dist-info/METADATA,sha256=3ED5Qf0wCq95qtPXSrcidDtuavyVbw4aDzRjF5x27Yk,8083
13
+ dfo_ls-1.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
+ dfo_ls-1.6.dist-info/top_level.txt,sha256=UfxRhaDN8HQx2_l17KbrDrERJ90OCN7VKkDMpYYbRLU,6
15
+ dfo_ls-1.6.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.3.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
dfols/__init__.py CHANGED
@@ -39,9 +39,11 @@ alternative licensing.
39
39
  from __future__ import absolute_import, division, print_function, unicode_literals
40
40
 
41
41
  # DFO-LS version
42
- __version__ = '1.5.3'
42
+ __version__ = '1.6'
43
43
 
44
44
  # Main solver & exit flags
45
45
  from .solver import *
46
- __all__ = ['solve']
46
+ __all__ = ['solve', 'OptimResults']
47
47
 
48
+ from .evaluations_database import *
49
+ __all__ += ['EvaluationDatabase']
dfols/controller.py CHANGED
@@ -414,6 +414,48 @@ class Controller(object):
414
414
 
415
415
  return None
416
416
 
417
+ def initialise_from_database(self, eval_database, number_of_samples, params):
418
+ # Here, eval_database has at least one entry, and the base index has already been used
419
+ # to evaluate (x0,r0), which has already been added to self.model
420
+ # Now, find exactly n feasible perturbations (either from database or new evals) and add them to the model
421
+ base_idx, perturbation_idx, new_perturbations = eval_database.select_starting_evals(self.delta,
422
+ xl=self.model.xbase + self.model.sl,
423
+ xu=self.model.xbase + self.model.su,
424
+ projections=self.model.projections,
425
+ tol=params("database.new_direction_tol"),
426
+ dykstra_max_iters=params("dykstra.max_iters"),
427
+ dykstra_tol=params("dykstra.d_tol"))
428
+
429
+ # Add suitable pre-existing evaluations
430
+ for i, idx in enumerate(perturbation_idx):
431
+ module_logger.info("Adding pre-existing evaluation %g to initial model" % idx)
432
+ x, rx = eval_database.get_eval(idx)
433
+ self.model.change_point(i + 1, x - self.model.xbase, rx, -idx) # use eval_num = -idx
434
+
435
+ if new_perturbations is not None:
436
+ num_perturbations = new_perturbations.shape[0]
437
+ module_logger.debug("Adding %g new evaluations to initial model" % num_perturbations)
438
+ for i in range(num_perturbations):
439
+ new_point = (eval_database.get_x(base_idx) - self.model.xbase) + new_perturbations[i,:] # new_perturbations[i,:] has length <= self.delta
440
+
441
+ # Evaluate objective
442
+ x = self.model.as_absolute_coordinates(new_point)
443
+ rvec_list, obj_list, num_samples_run, exit_info = self.evaluate_objective(x, number_of_samples, params)
444
+
445
+ # Handle exit conditions (f < min obj value or maxfun reached)
446
+ if exit_info is not None:
447
+ if num_samples_run > 0:
448
+ self.model.save_point(x, np.mean(rvec_list[:num_samples_run, :], axis=0), num_samples_run,
449
+ self.nx, x_in_abs_coords=True)
450
+ return exit_info # return & quit
451
+
452
+ # Otherwise, add new results (increments model.npt_so_far)
453
+ self.model.change_point(len(perturbation_idx) + 1 + i, x - self.model.xbase, rvec_list[0, :], self.nx) # expect step, not absolute x
454
+ for j in range(1, num_samples_run):
455
+ self.model.add_new_sample(len(perturbation_idx) + 1 + i, rvec_extra=rvec_list[j, :])
456
+
457
+ return None
458
+
417
459
  def add_new_direction_while_growing(self, number_of_samples, params, min_num_steps=0):
418
460
  num_steps = max(params('growing.num_new_dirns_each_iter'), min_num_steps)
419
461
  step_length = params('growing.delta_scale_new_dirns') * self.delta
@@ -0,0 +1,208 @@
1
+ """
2
+ Class to create/store database of existing evaluations, and routines to select
3
+ existing evaluations to build an initial linear model
4
+ """
5
+ import logging
6
+ import numpy as np
7
+
8
+ from .util import apply_scaling, dykstra
9
+ from .trust_region import ctrsbox_geometry, trsbox_geometry
10
+
11
+ __all__ = ['EvaluationDatabase']
12
+
13
+ module_logger = logging.getLogger(__name__)
14
+
15
+
16
+ # Class to store set of evaluations (x, rx)
17
+ class EvaluationDatabase(object):
18
+ def __init__(self, eval_list=None, starting_eval=None):
19
+ # eval_list is a list of tuples (x, rx)
20
+ self._evals = []
21
+ if eval_list is not None:
22
+ for e in eval_list:
23
+ self._evals.append(e)
24
+
25
+ # Which evaluation index should be the starting point of the optimization?
26
+ self.starting_eval = None
27
+ if starting_eval is not None and 0 <= starting_eval <= len(self._evals):
28
+ self.starting_eval = starting_eval
29
+
30
+ def __len__(self):
31
+ return len(self._evals)
32
+
33
+ def append(self, x, rx, make_starting_eval=False):
34
+ self._evals.append((x, rx))
35
+ if make_starting_eval:
36
+ self.starting_eval = len(self) - 1
37
+
38
+ def set_starting_eval(self, index):
39
+ if 0 <= index < len(self):
40
+ self.starting_eval = index
41
+ else:
42
+ raise IndexError("Invalid index %g given current set of %g evaluations" % (index, len(self)))
43
+
44
+ def get_starting_eval_idx(self):
45
+ if len(self) == 0:
46
+ raise RuntimeError("No evaluations available, no suitable starting evaluation ")
47
+ elif self.starting_eval is None:
48
+ module_logger.warning("Starting evaluation index not set, using most recently appended evaluation")
49
+ self.starting_eval = len(self) - 1
50
+
51
+ return self.starting_eval
52
+
53
+ def get_eval(self, index):
54
+ # Return (x, rx) for given index
55
+ if 0 <= index < len(self):
56
+ return self._evals[index][0], self._evals[index][1]
57
+ else:
58
+ raise IndexError("Invalid index %g given current set of %g evaluations" % (index, len(self)))
59
+
60
+ def get_x(self, index):
61
+ return self.get_eval(index)[0]
62
+
63
+ def get_rx(self, index):
64
+ return self.get_eval(index)[1]
65
+
66
+ def apply_scaling(self, scaling_changes):
67
+ # Adjust all input x values based on scaling
68
+ if scaling_changes is not None:
69
+ for i in range(len(self)):
70
+ x, rx = self._evals[i]
71
+ self._evals[i] = (apply_scaling(x, scaling_changes), rx)
72
+ return
73
+
74
+ def select_starting_evals(self, delta, xl=None, xu=None, projections=[], tol=1e-8,
75
+ dykstra_max_iters=100, dykstra_tol=1e-10):
76
+ # Given a database 'evals' with prescribed starting index, and initial trust-region radius delta > 0
77
+ # determine a subset of the database to use
78
+
79
+ # The bounds xl <= x <= xu and projection list are used to determine where to evaluate any new points
80
+ # (ensuring they are feasible)
81
+
82
+ if delta <= 0.0:
83
+ raise RuntimeError("delta must be strictly positive")
84
+ if len(self) == 0:
85
+ raise RuntimeError("Need at least one evaluation to select starting evaluations")
86
+
87
+ base_idx = self.get_starting_eval_idx()
88
+ xbase = self.get_x(self.get_starting_eval_idx())
89
+ n = len(xbase)
90
+ module_logger.debug("Selecting starting evaluations from existing database")
91
+ module_logger.debug("Have %g evaluations to choose from" % len(self))
92
+ module_logger.debug("Using base index %g" % base_idx)
93
+
94
+ # For linear interpolation, we will use the matrix
95
+ # M = [[1, 0], [0, L]] where L has rows (xi-xbase)/delta
96
+ # So, just build a large matrix Lfull with everything
97
+ n_perturbations = len(self) - 1
98
+ Lfull = np.zeros((n_perturbations, n))
99
+ row_idx = 0
100
+ for i in range(n_perturbations + 1):
101
+ if i == base_idx:
102
+ continue
103
+ Lfull[row_idx, :] = (self.get_x(i) - xbase) / delta # Lfull[i,:] = (xi-xbase) / delta
104
+ row_idx += 1
105
+
106
+ xdist = np.linalg.norm(Lfull, axis=1) # xdist[i] = ||Lfull[i,:]|| = ||xi-xbase|| / delta
107
+ # module_logger.debug("xdist =", xdist)
108
+
109
+ # We ideally want xdist ~ 1, so reweight these distances based on that (large xdist_reweighted --> xdist ~ 1 --> good)
110
+ xdist_reweighted = 1.0 / np.maximum(xdist, 1.0 / xdist)
111
+ # module_logger.debug("xdist_reweighted =", xdist_reweighted)
112
+
113
+ if n_perturbations == 0:
114
+ module_logger.debug("Only one evaluation available, just selecting that")
115
+ return base_idx, [], delta * np.eye(n)
116
+
117
+ # Now, find as many good perturbations as we can
118
+ # Good = not too far from xbase (relative to delta) and sufficiently linearly independent
119
+ # from other selected perturbations (i.e. Lfull[perturbation_idx,:] well-conditioned
120
+ # and len(perturbation_idx) <= n
121
+ perturbation_idx = [] # what point indices to use as perturbations
122
+
123
+ for iter in range(min(n_perturbations, n)):
124
+ # Add one more good perturbation, if available
125
+ # Note: can only add at most the number of available perturbations, or n perturbations, whichever is smaller
126
+ if iter == 0:
127
+ # First perturbation: every direction is equally good, so pick the point closest to the
128
+ # trust-region boundary
129
+ idx = int(np.argmax(xdist_reweighted))
130
+ module_logger.debug("Adding index %g with ||xi-xbase|| / delta = %g" % (idx if idx < base_idx else idx+1, xdist[idx]))
131
+ perturbation_idx.append(idx)
132
+ else:
133
+ Q, R = np.linalg.qr(Lfull[perturbation_idx, :].T, mode='reduced')
134
+ # module_logger.debug("Current perturbation_idx =", perturbation_idx)
135
+ L_rem = Lfull @ (np.eye(n) - Q @ Q.T) # part of (xi-xbase)/delta orthogonal to current perturbations
136
+ # rem_size = fraction of original length ||xi-xbase||/delta that is orthogonal to current perturbations
137
+ # all entries are in [0,1], and is zero for already selected perturbations
138
+ rem_size = np.linalg.norm(L_rem, axis=1) / xdist
139
+ rem_size[perturbation_idx] = 0 # ensure this holds exactly
140
+ # module_logger.debug("rem_size =", rem_size)
141
+ # module_logger.debug("rem_size * xdist_reweighted =", rem_size * xdist_reweighted)
142
+
143
+ # We want a point with large rem_size and xdist ~ 1 (i.e. xdist_reweighted large)
144
+ idx = int(np.argmax(rem_size * xdist_reweighted))
145
+ if rem_size[idx] * xdist_reweighted[idx] > tol:
146
+ # This ensures new perturbation is sufficiently linearly independent of existing perturbations
147
+ # (and also ensures idx hasn't already been chosen)
148
+ module_logger.debug("Adding index %g" % (idx if idx < base_idx else idx+1))
149
+ perturbation_idx.append(idx)
150
+ else:
151
+ module_logger.debug("No more linearly independent directions, quitting")
152
+ break
153
+
154
+ # Find new linearly independent directions
155
+ if len(perturbation_idx) < n:
156
+ module_logger.debug("Selecting %g new linearly independent directions" % (n - len(perturbation_idx)))
157
+ Q, _ = np.linalg.qr(Lfull[perturbation_idx, :].T, mode='complete')
158
+ new_perturbations = delta * Q[:, len(perturbation_idx):].T
159
+
160
+ # Make perturbations feasible w.r.t. xl <= x <= xu and projections
161
+ # Note: if len(projections) > 0, then the projection list *already* includes bounds
162
+ # Don't need to make pre-existing evaluations feasible, since we already have r(x) for these
163
+
164
+ # Start construction of interpolation matrix for later
165
+ L = np.zeros((n, n), dtype=float)
166
+ L[:len(perturbation_idx), :] = Lfull[perturbation_idx, :]
167
+ L[len(perturbation_idx):, :] = new_perturbations / delta
168
+
169
+ # Since we already have a full set of linearly independent directions,
170
+ # we do this by moving each infeasible perturbation to a geometry-improving location
171
+ for i in range(new_perturbations.shape[0]):
172
+ xnew = xbase + new_perturbations[i, :]
173
+ # Check feasibility
174
+ if len(projections) == 0:
175
+ # Bounds only
176
+ feasible = np.all(xnew >= xl) and np.all(xnew <= xu)
177
+ else:
178
+ # Projections
179
+ xnew_C = dykstra(projections, xnew, max_iter=dykstra_max_iters, tol=dykstra_tol)
180
+ feasible = np.linalg.norm(xnew - xnew_C) < dykstra_tol
181
+
182
+ if feasible:
183
+ # Skip feasible points, nothing to do
184
+ continue
185
+
186
+ # If infeasible, build Lagrange polynomial and move to geometry-improving location in B(xbase,delta)
187
+ # which will automatically be feasible
188
+ module_logger.debug("Moving default %g-th new perturbation to ensure feasibility" % i)
189
+ c = 0.0 # Lagrange polynomial centered at xbase
190
+ ei = np.zeros((n,), dtype=float)
191
+ ei[len(perturbation_idx) + i] = 1.0
192
+ g = np.linalg.solve(L, ei) / delta # divide by delta because L is scaled by 1/delta
193
+ if len(projections) == 0:
194
+ new_perturbations[i, :] = trsbox_geometry(xbase, c, g, xl, xu, delta)
195
+ else:
196
+ new_perturbations[i, :] = ctrsbox_geometry(xbase, c, g, projections, delta)
197
+
198
+ # Update L after replacement
199
+ L[len(perturbation_idx) + i, :] = new_perturbations[i,:] / delta
200
+ else:
201
+ module_logger.debug("Full set of directions found, no need for new evaluations")
202
+ new_perturbations = None
203
+
204
+ # perturbation_idx in [0, ..., n_perturbations-1], reset to be actual indices
205
+ for i in range(len(perturbation_idx)):
206
+ if perturbation_idx[i] >= base_idx:
207
+ perturbation_idx[i] += 1
208
+ return base_idx, perturbation_idx, new_perturbations
dfols/params.py CHANGED
@@ -122,6 +122,9 @@ class ParameterList(object):
122
122
  self.params["func_tol.tr_step"] = 1-1e-1
123
123
  self.params["func_tol.max_iters"] = 500
124
124
  self.params["sfista.max_iters_scaling"] = 2.0
125
+
126
+ # Evaluation database
127
+ self.params["database.new_direction_tol"] = 1e-8
125
128
 
126
129
  self.params_changed = {}
127
130
  for p in self.params:
@@ -284,6 +287,8 @@ class ParameterList(object):
284
287
  type_str, nonetype_ok, lower, upper = 'int', False, 0, None
285
288
  elif key == "sfista.max_iters_scaling":
286
289
  type_str, nonetype_ok, lower, upper = 'float', False, 1.0, None
290
+ elif key == "database.new_direction_tol":
291
+ type_str, nonetype_ok, lower, upper = 'float', False, 0.0, None
287
292
  else:
288
293
  assert False, "ParameterList.param_type() has unknown key: %s" % key
289
294
  return type_str, nonetype_ok, lower, upper
dfols/solver.py CHANGED
@@ -32,16 +32,18 @@ import logging
32
32
  from math import sqrt
33
33
  import numpy as np
34
34
  import os
35
+ import pandas as pd
35
36
  import scipy.linalg as LA
36
37
  import scipy.stats as STAT
37
38
  import warnings
38
39
 
39
40
  from .controller import *
40
41
  from .diagnostic_info import *
42
+ from .evaluations_database import *
41
43
  from .params import *
42
44
  from .util import *
43
45
 
44
- __all__ = ['solve']
46
+ __all__ = ['solve', 'OptimResults']
45
47
 
46
48
  module_logger = logging.getLogger(__name__)
47
49
 
@@ -69,13 +71,16 @@ class OptimResults(object):
69
71
  self.EXIT_TR_INCREASE_ERROR = EXIT_TR_INCREASE_ERROR
70
72
  self.EXIT_LINALG_ERROR = EXIT_LINALG_ERROR
71
73
  self.EXIT_FALSE_SUCCESS_WARNING = EXIT_FALSE_SUCCESS_WARNING
74
+ self.max_resid_length_print = 20 # don't print self.resid in __str__ if length >= this value
75
+ self.max_jac_length_print = 40 # don't print self.jacobian in __str__ if length >= this value
76
+
72
77
 
73
78
  def __str__(self):
74
79
  # Result of calling print(soln)
75
80
  output = "****** DFO-LS Results ******\n"
76
81
  if self.flag != self.EXIT_INPUT_ERROR:
77
82
  output += "Solution xmin = %s\n" % str(self.x)
78
- if len(self.resid) < 100:
83
+ if len(self.resid) < self.max_resid_length_print:
79
84
  output += "Residual vector = %s\n" % str(self.resid)
80
85
  else:
81
86
  output += "Not showing residual vector because it is too long; check self.resid\n"
@@ -83,7 +88,7 @@ class OptimResults(object):
83
88
  output += "Needed %g objective evaluations (at %g points)\n" % (self.nf, self.nx)
84
89
  if self.nruns > 1:
85
90
  output += "Did a total of %g runs\n" % self.nruns
86
- if self.jacobian is not None and np.size(self.jacobian) < 200:
91
+ if self.jacobian is not None and np.size(self.jacobian) < self.max_jac_length_print:
87
92
  output += "Approximate Jacobian = %s\n" % str(self.jacobian)
88
93
  elif self.jacobian is None:
89
94
  output += "No Jacobian returned\n"
@@ -92,69 +97,136 @@ class OptimResults(object):
92
97
  if self.diagnostic_info is not None:
93
98
  output += "Diagnostic information available; check self.diagnostic_info\n"
94
99
  output += "Solution xmin was evaluation point %g\n" % self.xmin_eval_num
95
- if len(self.jacmin_eval_nums) < 100:
100
+ if self.jacmin_eval_nums is not None and len(self.jacmin_eval_nums) < self.max_resid_length_print:
96
101
  output += "Approximate Jacobian formed using evaluation points %s\n" % str(self.jacmin_eval_nums)
102
+ elif self.jacmin_eval_nums is None:
103
+ output += "Approximate Jacobian not formed using problem information, disregard\n"
104
+ else:
105
+ output += "Not showing Jacobian evaluation points because it is too long; check self.jacmin_eval_nums\n"
97
106
  output += "Exit flag = %g\n" % self.flag
98
107
  output += "%s\n" % self.msg
99
108
  output += "****************************\n"
100
109
  return output
110
+
111
+ def to_dict(self, replace_nan=True):
112
+ # Convert to a serializable dict object suitable for saving in a json file
113
+ # If replace_nan=True, convert all NaN entries to None
114
+ soln_dict = {}
115
+ soln_dict['x'] = self.x.tolist() if self.x is not None else None
116
+ soln_dict['resid'] = self.resid.tolist() if self.resid is not None else None
117
+ soln_dict['obj'] = float(self.obj)
118
+ soln_dict['jacobian'] = self.jacobian.tolist() if self.jacobian is not None else None
119
+ soln_dict['nf'] = int(self.nf)
120
+ soln_dict['nx'] = int(self.nx)
121
+ soln_dict['nruns'] = int(self.nruns)
122
+ soln_dict['flag'] = int(self.flag)
123
+ soln_dict['msg'] = str(self.msg)
124
+ soln_dict['diagnostic_info'] = self.diagnostic_info.to_dict() if self.diagnostic_info is not None else None
125
+ soln_dict['xmin_eval_num'] = int(self.xmin_eval_num)
126
+ soln_dict['jacmin_eval_nums'] = self.jacmin_eval_nums.tolist() if self.jacmin_eval_nums is not None else None
127
+ if replace_nan:
128
+ return replace_nan_with_none(soln_dict)
129
+ else:
130
+ return soln_dict
131
+
132
+ @staticmethod
133
+ def from_dict(soln_dict):
134
+ # Take a dict object containing OptimResults information, and return the relevant OptimResults object
135
+ # Input soln_dict should come from soln.to_dict()
136
+ # Note: np.array(mylist, dtype=float) automatically converts None to NaN
137
+ x = np.array(soln_dict['x'], dtype=float) if soln_dict['x'] is not None else None
138
+ resid = np.array(soln_dict['resid'], dtype=float) if soln_dict['resid'] is not None else None
139
+ obj = soln_dict['obj']
140
+ jacobian = np.array(soln_dict['jacobian'], dtype=float) if soln_dict['jacobian'] is not None else None
141
+ nf = soln_dict['nf']
142
+ nx = soln_dict['nx']
143
+ nruns = soln_dict['nruns']
144
+ flag = soln_dict['flag']
145
+ msg = soln_dict['msg']
146
+ xmin_eval_num = soln_dict['xmin_eval_num']
147
+ jacmin_eval_nums = np.array(soln_dict['jacmin_eval_nums'], dtype=int) if soln_dict['jacmin_eval_nums'] is not None else None
148
+
149
+ soln = OptimResults(x, resid, obj, jacobian, nf, nx, nruns, flag, msg, xmin_eval_num, jacmin_eval_nums)
150
+
151
+ if soln_dict['diagnostic_info'] is not None:
152
+ soln.diagnostic_info = pd.DataFrame.from_dict(soln_dict['diagnostic_info'])
153
+ return soln
101
154
 
102
155
 
103
156
  def solve_main(objfun, x0, argsf, xl, xu, projections, npt, rhobeg, rhoend, maxfun, nruns_so_far, nf_so_far, nx_so_far, nsamples, params,
104
157
  diagnostic_info, scaling_changes, h=None, lh=None, argsh=(), prox_uh=None, argsprox=None, r0_avg_old=None, r0_nsamples_old=None, default_growing_method_set_by_user=None,
105
158
  do_logging=True, print_progress=False):
159
+
160
+ if type(x0) == EvaluationDatabase:
161
+ x0_is_eval_database = True
162
+ x0_vec = x0.get_x(x0.get_starting_eval_idx())
163
+ else:
164
+ x0_vec = x0
165
+ x0_is_eval_database = False
166
+ n = len(x0_vec)
167
+
106
168
  # Evaluate at x0 (keep nf, nx correct and check for f < 1e-12)
107
169
  # The hard bit is determining what m = len(r0) should be, and allocating memory appropriately
108
170
  if r0_avg_old is None:
109
- number_of_samples = max(nsamples(rhobeg, rhobeg, 0, nruns_so_far), 1)
110
- # Evaluate the first time...
111
- nf = nf_so_far + 1
112
- nx = nx_so_far + 1
113
- r0, obj0 = eval_least_squares_with_regularisation(objfun, remove_scaling(x0, scaling_changes), h,
114
- argsf=argsf, argsh=argsh, verbose=do_logging, eval_num=nf, pt_num=nx,
115
- full_x_thresh=params("logging.n_to_print_whole_x_vector"),
116
- check_for_overflow=params("general.check_objfun_for_overflow"))
117
- m = len(r0)
118
-
119
- # Now we have m, we can evaluate the rest of the times
120
- rvec_list = np.zeros((number_of_samples, m))
121
- obj_list = np.zeros((number_of_samples,))
122
- rvec_list[0, :] = r0
123
- obj_list[0] = obj0
124
- num_samples_run = 1
125
171
  exit_info = None
172
+ if x0_is_eval_database:
173
+ # We have already got r(x0), so just extract this information
174
+ nf = nf_so_far
175
+ nx = nx_so_far
176
+ num_samples_run = 1
177
+ r0_avg = x0.get_rx(x0.get_starting_eval_idx())
178
+ m = len(r0_avg)
179
+ module_logger.info("Using pre-existing evaluation %g as starting point" % (x0.get_starting_eval_idx()))
180
+ else:
181
+ number_of_samples = max(nsamples(rhobeg, rhobeg, 0, nruns_so_far), 1)
182
+ # Evaluate the first time...
183
+ nf = nf_so_far + 1
184
+ nx = nx_so_far + 1
185
+ r0, obj0 = eval_least_squares_with_regularisation(objfun, remove_scaling(x0_vec, scaling_changes), h,
186
+ argsf=argsf, argsh=argsh, verbose=do_logging, eval_num=nf, pt_num=nx,
187
+ full_x_thresh=params("logging.n_to_print_whole_x_vector"),
188
+ check_for_overflow=params("general.check_objfun_for_overflow"))
189
+ m = len(r0)
190
+
191
+ # Now we have m, we can evaluate the rest of the times
192
+ rvec_list = np.zeros((number_of_samples, m))
193
+ obj_list = np.zeros((number_of_samples,))
194
+ rvec_list[0, :] = r0
195
+ obj_list[0] = obj0
196
+ num_samples_run = 1
197
+
198
+ for i in range(1, number_of_samples): # skip first eval - already did this
199
+ if nf >= maxfun:
200
+ exit_info = ExitInformation(EXIT_MAXFUN_WARNING, "Objective has been called MAXFUN times")
201
+ nruns_so_far += 1
202
+ break # stop evaluating at x0
126
203
 
127
- for i in range(1, number_of_samples): # skip first eval - already did this
128
- if nf >= maxfun:
129
- exit_info = ExitInformation(EXIT_MAXFUN_WARNING, "Objective has been called MAXFUN times")
130
- nruns_so_far += 1
131
- break # stop evaluating at x0
204
+ nf += 1
205
+ # Don't increment nx for x0 - we did this earlier
206
+ rvec_list[i, :], obj_list[i] = eval_least_squares_with_regularisation(objfun, remove_scaling(x0_vec, scaling_changes), h,
207
+ argsf=argsf, argsh=argsh, verbose=do_logging, eval_num=nf, pt_num=nx,
208
+ full_x_thresh=params("logging.n_to_print_whole_x_vector"),
209
+ check_for_overflow=params("general.check_objfun_for_overflow"))
210
+ num_samples_run += 1
132
211
 
133
- nf += 1
134
- # Don't increment nx for x0 - we did this earlier
135
- rvec_list[i, :], obj_list[i] = eval_least_squares_with_regularisation(objfun, remove_scaling(x0, scaling_changes), h,
136
- argsf=argsf, argsh=argsh, verbose=do_logging, eval_num=nf, pt_num=nx,
137
- full_x_thresh=params("logging.n_to_print_whole_x_vector"),
138
- check_for_overflow=params("general.check_objfun_for_overflow"))
139
- num_samples_run += 1
212
+ r0_avg = np.mean(rvec_list[:num_samples_run, :], axis=0)
140
213
 
141
- r0_avg = np.mean(rvec_list[:num_samples_run, :], axis=0)
142
214
  # NOTE: modify objvalue here
143
215
  if h is None:
144
216
  if sumsq(r0_avg) <= params("model.abs_tol"):
145
217
  exit_info = ExitInformation(EXIT_SUCCESS, "Objective is sufficiently small")
146
218
  else:
147
- if sumsq(r0_avg) + h(remove_scaling(x0, scaling_changes), *argsh)<= params("model.abs_tol"):
219
+ if sumsq(r0_avg) + h(remove_scaling(x0_vec, scaling_changes), *argsh)<= params("model.abs_tol"):
148
220
  exit_info = ExitInformation(EXIT_SUCCESS, "Objective is sufficiently small")
149
221
 
150
222
  if exit_info is not None:
151
223
  xmin_eval_num = 0
152
224
  jacmin_eval_nums = np.array([0], dtype=int)
153
- return x0, r0_avg, sumsq(r0_avg), None, num_samples_run, nf, nx, nruns_so_far+1, exit_info, diagnostic_info, xmin_eval_num, jacmin_eval_nums
225
+ return x0_vec, r0_avg, sumsq(r0_avg), None, num_samples_run, nf, nx, nruns_so_far+1, exit_info, diagnostic_info, xmin_eval_num, jacmin_eval_nums
154
226
 
155
227
  else: # have old r0 information (e.g. from previous restart), use this instead
156
228
 
157
- # m = len(r0_avg_old)
229
+ m = len(r0_avg_old)
158
230
  r0_avg = r0_avg_old
159
231
  num_samples_run = r0_nsamples_old
160
232
  nf = nf_so_far
@@ -164,7 +236,7 @@ def solve_main(objfun, x0, argsf, xl, xu, projections, npt, rhobeg, rhoend, maxf
164
236
  if default_growing_method_set_by_user is not None and (not default_growing_method_set_by_user):
165
237
  # If m>=n, the default growing method (use_full_rank_interp) is best
166
238
  # However, this can fail for m<n, so need to use an alternative method (perturb_trust_region_step)
167
- if m < len(x0):
239
+ if m < n:
168
240
  if do_logging:
169
241
  module_logger.debug("Inverse problem (m<n), switching default growing method")
170
242
  params('growing.full_rank.use_full_rank_interp', new_value=False)
@@ -173,25 +245,32 @@ def solve_main(objfun, x0, argsf, xl, xu, projections, npt, rhobeg, rhoend, maxf
173
245
  params('growing.delta_scale_new_dirns', new_value=0.1)
174
246
 
175
247
  # Initialise controller
176
- control = Controller(objfun, argsf, x0, r0_avg, num_samples_run, xl, xu, projections, npt, rhobeg, rhoend, nf, nx, maxfun,
248
+ control = Controller(objfun, argsf, x0_vec, r0_avg, num_samples_run, xl, xu, projections, npt, rhobeg, rhoend, nf, nx, maxfun,
177
249
  params, scaling_changes, do_logging, h=h, lh=lh, argsh=argsh, prox_uh=prox_uh, argsprox=argsprox)
178
250
 
179
251
  # Initialise interpolation set
180
252
  number_of_samples = max(nsamples(control.delta, control.rho, 0, nruns_so_far), 1)
181
253
  num_directions = min(params("growing.ndirs_initial") + params("restarts.hard.increase_ndirs_initial_amt") * nruns_so_far,
182
254
  npt - 1) # cap at npt
183
- if params("init.random_initial_directions"):
184
- if do_logging:
185
- module_logger.info("Initialising (random directions)")
186
- exit_info = control.initialise_random_directions(number_of_samples, num_directions, params)
255
+ if x0_is_eval_database:
256
+ if num_directions != n:
257
+ module_logger.warning("When evaluation database provided, we will always initialize with n+1 evaluations")
258
+ exit_info = control.initialise_from_database(x0, number_of_samples, params)
187
259
  else:
188
- if do_logging:
189
- module_logger.info("Initialising (coordinate directions)")
190
- exit_info = control.initialise_coordinate_directions(number_of_samples, num_directions, params)
260
+ if params("init.random_initial_directions"):
261
+ if do_logging:
262
+ module_logger.info("Initialising (random directions)")
263
+ exit_info = control.initialise_random_directions(number_of_samples, num_directions, params)
264
+ else:
265
+ if do_logging:
266
+ module_logger.info("Initialising (coordinate directions)")
267
+ exit_info = control.initialise_coordinate_directions(number_of_samples, num_directions, params)
191
268
  if exit_info is not None:
192
269
  x, rvec, obj, jacmin, nsamples, x_eval_num, jac_eval_nums = control.model.get_final_results()
193
270
  return x, rvec, obj, None, nsamples, control.nf, control.nx, nruns_so_far + 1, exit_info, diagnostic_info, x_eval_num, jac_eval_nums
194
271
 
272
+ # model.npt() = actual number of evaluations available to the model so far
273
+ # model.num_pts = desired interp set size >= n+1
195
274
  finished_growing = (control.model.npt() >= control.model.num_pts) # have we finished growing the initial set yet?
196
275
 
197
276
  # Save list of last N successful steps: whether they failed to be an improvement over fsave
@@ -893,8 +972,16 @@ def solve_main(objfun, x0, argsf, xl, xu, projections, npt, rhobeg, rhoend, maxf
893
972
 
894
973
  def solve(objfun, x0, h=None, lh=None, prox_uh=None, argsf=(), argsh=(), argsprox=(), bounds=None, projections=[], npt=None, rhobeg=None, rhoend=1e-8, maxfun=None, nsamples=None, user_params=None,
895
974
  objfun_has_noise=False, scaling_within_bounds=False, do_logging=True, print_progress=False):
896
- x0 = x0.astype(float)
897
- n = len(x0)
975
+
976
+ if type(x0) == EvaluationDatabase:
977
+ assert len(x0) > 0, "evaluation database x0 cannot be empty"
978
+ assert 0 <= x0.get_starting_eval_idx() < len(x0), "evaluation database must have valid starting index set"
979
+ x0_is_eval_database = True
980
+ n = len(x0.get_x(x0.get_starting_eval_idx()))
981
+ else:
982
+ x0 = np.array(x0).astype(float)
983
+ n = len(x0)
984
+ x0_is_eval_database = False
898
985
 
899
986
  # Set missing inputs (if not specified) to some sensible defaults
900
987
  if bounds is None:
@@ -920,7 +1007,8 @@ def solve(objfun, x0, h=None, lh=None, prox_uh=None, argsf=(), argsh=(), argspro
920
1007
  if npt is None:
921
1008
  npt = n + 1
922
1009
  if rhobeg is None:
923
- rhobeg = 0.1 if scaling_within_bounds else 0.1 * max(np.max(np.abs(x0)), 1.0)
1010
+ x0_norm = np.max(np.abs(x0.get_x(x0.get_starting_eval_idx()))) if x0_is_eval_database else np.max(np.abs(x0))
1011
+ rhobeg = 0.1 if scaling_within_bounds else 0.1 * max(x0_norm, 1.0)
924
1012
  if maxfun is None:
925
1013
  maxfun = min(100 * (n + 1), 1000) # 100 gradients, capped at 1000
926
1014
  if nsamples is None:
@@ -955,7 +1043,10 @@ def solve(objfun, x0, h=None, lh=None, prox_uh=None, argsf=(), argsh=(), argspro
955
1043
  scale = xu - xl
956
1044
  scaling_changes = (shift, scale)
957
1045
 
958
- x0 = apply_scaling(x0, scaling_changes)
1046
+ if x0_is_eval_database:
1047
+ x0.apply_scaling(scaling_changes)
1048
+ else:
1049
+ x0 = apply_scaling(x0, scaling_changes)
959
1050
  xl = apply_scaling(xl, scaling_changes)
960
1051
  xu = apply_scaling(xu, scaling_changes)
961
1052
 
@@ -984,13 +1075,19 @@ def solve(objfun, x0, h=None, lh=None, prox_uh=None, argsf=(), argsh=(), argspro
984
1075
  if exit_info is None and maxfun <= 0:
985
1076
  exit_info = ExitInformation(EXIT_INPUT_ERROR, "maxfun must be strictly positive")
986
1077
 
987
- if exit_info is None and np.shape(x0) != (n,):
988
- exit_info = ExitInformation(EXIT_INPUT_ERROR, "x0 must be a vector")
1078
+ if exit_info is None:
1079
+ if x0_is_eval_database:
1080
+ for i in range(len(x0)):
1081
+ if np.shape(x0.get_x(i)) != (n,):
1082
+ exit_info = ExitInformation(EXIT_INPUT_ERROR, "All input vectors x0 must have the same shape")
1083
+ else:
1084
+ if np.shape(x0) != (n,):
1085
+ exit_info = ExitInformation(EXIT_INPUT_ERROR, "x0 must be a vector")
989
1086
 
990
- if exit_info is None and np.shape(x0) != np.shape(xl):
1087
+ if exit_info is None and np.shape(xl) != (n,):
991
1088
  exit_info = ExitInformation(EXIT_INPUT_ERROR, "lower bounds must have same shape as x0")
992
1089
 
993
- if exit_info is None and np.shape(x0) != np.shape(xu):
1090
+ if exit_info is None and np.shape(xu) != (n,):
994
1091
  exit_info = ExitInformation(EXIT_INPUT_ERROR, "upper bounds must have same shape as x0")
995
1092
 
996
1093
  if exit_info is None and np.min(xu - xl) < 2.0 * rhobeg:
@@ -1041,22 +1138,24 @@ def solve(objfun, x0, h=None, lh=None, prox_uh=None, argsf=(), argsh=(), argspro
1041
1138
  return results
1042
1139
 
1043
1140
  # Enforce arbitrary constraint bounds on x0
1044
- if projections:
1045
- xp = dykstra(projections,x0,max_iter=params("dykstra.max_iters"),tol=params("dykstra.d_tol"))
1046
- if not np.allclose(xp,x0):
1047
- warnings.warn("x0 not feasible w.r.t given constraints, adjusting", RuntimeWarning)
1048
- x0 = xp.copy()
1049
-
1050
- # Enforce lower & upper bounds on x0
1051
- idx = (x0 < xl)
1052
- if np.any(idx):
1053
- warnings.warn("x0 below lower bound, adjusting", RuntimeWarning)
1054
- x0[idx] = xl[idx]
1055
-
1056
- idx = (x0 > xu)
1057
- if np.any(idx):
1058
- warnings.warn("x0 above upper bound, adjusting", RuntimeWarning)
1059
- x0[idx] = xu[idx]
1141
+ if not x0_is_eval_database:
1142
+ # Don't need to enforce any constraints for pre-existing evaluations (since we already have the objective value)
1143
+ if projections:
1144
+ xp = dykstra(projections,x0,max_iter=params("dykstra.max_iters"),tol=params("dykstra.d_tol"))
1145
+ if not np.allclose(xp,x0):
1146
+ warnings.warn("x0 not feasible w.r.t given constraints, adjusting", RuntimeWarning)
1147
+ x0 = xp.copy()
1148
+
1149
+ # Enforce lower & upper bounds on x0
1150
+ idx = (x0 < xl)
1151
+ if np.any(idx):
1152
+ warnings.warn("x0 below lower bound, adjusting", RuntimeWarning)
1153
+ x0[idx] = xl[idx]
1154
+
1155
+ idx = (x0 > xu)
1156
+ if np.any(idx):
1157
+ warnings.warn("x0 above upper bound, adjusting", RuntimeWarning)
1158
+ x0[idx] = xu[idx]
1060
1159
 
1061
1160
  # Call main solver (first time)
1062
1161
  diagnostic_info = DiagnosticInfo()
dfols/util.py CHANGED
@@ -26,13 +26,14 @@ alternative licensing.
26
26
  from __future__ import absolute_import, division, print_function, unicode_literals
27
27
 
28
28
  import logging
29
+ import math
29
30
  import numpy as np
30
31
  import scipy.linalg as LA
31
32
  import sys
32
33
 
33
34
 
34
35
  __all__ = ['sumsq', 'eval_least_squares_with_regularisation', 'model_value', 'random_orthog_directions_within_bounds',
35
- 'random_directions_within_bounds', 'apply_scaling', 'remove_scaling', 'pbox', 'pball', 'dykstra', 'qr_rank']
36
+ 'random_directions_within_bounds', 'apply_scaling', 'remove_scaling', 'pbox', 'pball', 'dykstra', 'qr_rank', 'replace_nan_with_none']
36
37
 
37
38
  module_logger = logging.getLogger(__name__)
38
39
 
@@ -268,3 +269,15 @@ def qr_rank(A,tol=1e-15):
268
269
  D = np.abs(np.diag(R))
269
270
  rank = np.sum(D > tol)
270
271
  return rank, D
272
+
273
+
274
+ def replace_nan_with_none(d):
275
+ # Replace Nan values in a dict/list with None (used for JSON serializing of OptimResults object)
276
+ if isinstance(d, dict):
277
+ return {k: replace_nan_with_none(v) for k, v in d.items()}
278
+ elif isinstance(d, list):
279
+ return [replace_nan_with_none(i) for i in d]
280
+ elif isinstance(d, float) and math.isnan(d):
281
+ return None
282
+ else:
283
+ return d
@@ -1,14 +0,0 @@
1
- dfols/__init__.py,sha256=19cgsqpElsxNRqwnyZbbQBw5vyZKUqHmu96PFM_rlvM,1605
2
- dfols/controller.py,sha256=Jffyao_z7wcQf1WEQtv2smnNew8HXGguWuUPLbgVuCc,52487
3
- dfols/diagnostic_info.py,sha256=kEcFCjD2rk39XRa90ocEaQvJWc0wj_ZPpQkOulVIM-k,6106
4
- dfols/hessian.py,sha256=sExx4J4KoGwHItbthX2odosB2ONbQFvLdlcod7PIh4k,4262
5
- dfols/model.py,sha256=1Npj3fJvMv66bKu_RIzLLI-2tyzPWOsKuyv-YUjcv2c,20711
6
- dfols/params.py,sha256=GzJGO0TByH1X3B0NbLOCOqmYG8dRiKPKjjX7or_fOqI,18342
7
- dfols/solver.py,sha256=NUzjOYxwTyabh1wxWnhpjmqgC4wppq8miLGtZ9PMeyA,64029
8
- dfols/trust_region.py,sha256=JbHLBDw7H88a3cIMuialh7kpMNGjL3Lp9JsjrBNpDWQ,28231
9
- dfols/util.py,sha256=efGVAKPb7YrHya1IOgyzacwa_h0u2jHHs5FhuxUlYDg,10282
10
- DFO_LS-1.5.3.dist-info/LICENSE.txt,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
11
- DFO_LS-1.5.3.dist-info/METADATA,sha256=dMHJW0Bv7rc0qoRGmaoTsorVaqgMVVOfqphFrBpO_mI,8069
12
- DFO_LS-1.5.3.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
13
- DFO_LS-1.5.3.dist-info/top_level.txt,sha256=UfxRhaDN8HQx2_l17KbrDrERJ90OCN7VKkDMpYYbRLU,6
14
- DFO_LS-1.5.3.dist-info/RECORD,,