DFO-LS 1.5.3__tar.gz → 1.6__tar.gz
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.
- {dfo_ls-1.5.3 → dfo_ls-1.6}/DFO_LS.egg-info/PKG-INFO +3 -2
- {dfo_ls-1.5.3 → dfo_ls-1.6}/DFO_LS.egg-info/SOURCES.txt +1 -0
- {dfo_ls-1.5.3 → dfo_ls-1.6}/PKG-INFO +3 -2
- {dfo_ls-1.5.3 → dfo_ls-1.6}/dfols/__init__.py +4 -2
- {dfo_ls-1.5.3 → dfo_ls-1.6}/dfols/controller.py +42 -0
- dfo_ls-1.6/dfols/evaluations_database.py +208 -0
- {dfo_ls-1.5.3 → dfo_ls-1.6}/dfols/params.py +5 -0
- {dfo_ls-1.5.3 → dfo_ls-1.6}/dfols/solver.py +168 -69
- {dfo_ls-1.5.3 → dfo_ls-1.6}/dfols/util.py +14 -1
- {dfo_ls-1.5.3 → dfo_ls-1.6}/DFO_LS.egg-info/dependency_links.txt +0 -0
- {dfo_ls-1.5.3 → dfo_ls-1.6}/DFO_LS.egg-info/requires.txt +0 -0
- {dfo_ls-1.5.3 → dfo_ls-1.6}/DFO_LS.egg-info/top_level.txt +0 -0
- {dfo_ls-1.5.3 → dfo_ls-1.6}/LICENSE.txt +0 -0
- {dfo_ls-1.5.3 → dfo_ls-1.6}/MANIFEST.in +0 -0
- {dfo_ls-1.5.3 → dfo_ls-1.6}/README.rst +0 -0
- {dfo_ls-1.5.3 → dfo_ls-1.6}/dfols/diagnostic_info.py +0 -0
- {dfo_ls-1.5.3 → dfo_ls-1.6}/dfols/hessian.py +0 -0
- {dfo_ls-1.5.3 → dfo_ls-1.6}/dfols/model.py +0 -0
- {dfo_ls-1.5.3 → dfo_ls-1.6}/dfols/trust_region.py +0 -0
- {dfo_ls-1.5.3 → dfo_ls-1.6}/pyproject.toml +0 -0
- {dfo_ls-1.5.3 → dfo_ls-1.6}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: DFO-LS
|
|
3
|
-
Version: 1.
|
|
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>
|
|
@@ -41,6 +41,7 @@ Requires-Dist: Sphinx; extra == "dev"
|
|
|
41
41
|
Requires-Dist: sphinx-rtd-theme; extra == "dev"
|
|
42
42
|
Provides-Extra: trustregion
|
|
43
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: DFO-LS
|
|
3
|
-
Version: 1.
|
|
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>
|
|
@@ -41,6 +41,7 @@ Requires-Dist: Sphinx; extra == "dev"
|
|
|
41
41
|
Requires-Dist: sphinx-rtd-theme; extra == "dev"
|
|
42
42
|
Provides-Extra: trustregion
|
|
43
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
|
|
@@ -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.
|
|
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']
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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) <
|
|
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) <
|
|
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
|
+
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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 <
|
|
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,
|
|
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
|
|
184
|
-
if
|
|
185
|
-
module_logger.
|
|
186
|
-
exit_info = control.
|
|
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
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
897
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
988
|
-
|
|
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(
|
|
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(
|
|
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
|
|
1045
|
-
|
|
1046
|
-
if
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
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()
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|