DFO-LS 1.2__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.

@@ -1,41 +1,53 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: DFO-LS
3
- Version: 1.2
3
+ Version: 1.4.1
4
4
  Summary: A flexible derivative-free solver for (bound constrained) nonlinear least-squares minimization
5
- Home-page: https://github.com/numericalalgorithmsgroup/dfols/
6
- Author: Lindon Roberts
7
- Author-email: lindon.roberts@maths.ox.ac.uk
8
- License: GNU GPL
9
- Download-URL: https://github.com/numericalalgorithmsgroup/dfols/archive/v1.2.tar.gz
10
- Keywords: mathematics derivative free optimization nonlinear least squares
11
- Platform: UNKNOWN
5
+ Author-email: Lindon Roberts <lindon.roberts@sydney.edu.au>
6
+ Maintainer-email: Lindon Roberts <lindon.roberts@sydney.edu.au>
7
+ License: GPL-3.0-or-later
8
+ Project-URL: Homepage, https://github.com/numericalalgorithmsgroup/dfols
9
+ Project-URL: Download, https://github.com/numericalalgorithmsgroup/dfols/releases/
10
+ Project-URL: Bug Tracker, https://github.com/numericalalgorithmsgroup/dfols/issues/
11
+ Project-URL: Documentation, https://numericalalgorithmsgroup.github.io/dfols/
12
+ Project-URL: Source Code, https://github.com/numericalalgorithmsgroup/dfols
13
+ Keywords: mathematics,optimization,least squares,derivative free optimization,nonlinear least squares
12
14
  Classifier: Development Status :: 5 - Production/Stable
13
15
  Classifier: Environment :: Console
14
16
  Classifier: Framework :: IPython
15
17
  Classifier: Framework :: Jupyter
16
- Classifier: Intended Audience :: Financial and Insurance Industry
17
18
  Classifier: Intended Audience :: Science/Research
18
- Classifier: License :: OSI Approved :: GNU General Public License (GPL)
19
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
19
20
  Classifier: Operating System :: MacOS
20
21
  Classifier: Operating System :: Microsoft :: Windows
21
- Classifier: Operating System :: POSIX
22
22
  Classifier: Operating System :: Unix
23
23
  Classifier: Programming Language :: Python
24
- Classifier: Programming Language :: Python :: 2
25
24
  Classifier: Programming Language :: Python :: 3
25
+ Classifier: Programming Language :: Python :: 3.9
26
+ Classifier: Programming Language :: Python :: 3.10
27
+ Classifier: Programming Language :: Python :: 3.11
28
+ Classifier: Programming Language :: Python :: 3.12
26
29
  Classifier: Topic :: Scientific/Engineering
27
30
  Classifier: Topic :: Scientific/Engineering :: Mathematics
28
- Requires-Dist: numpy (>=1.11)
29
- Requires-Dist: scipy (>=0.18)
30
- Requires-Dist: pandas (>=0.17)
31
- Requires-Dist: trustregion (>=1.1)
31
+ Requires-Python: >=3.9
32
+ Description-Content-Type: text/x-rst
33
+ License-File: LICENSE.txt
34
+ Requires-Dist: setuptools
35
+ Requires-Dist: numpy
36
+ Requires-Dist: scipy >=1.11
37
+ Requires-Dist: pandas
38
+ Provides-Extra: dev
39
+ Requires-Dist: pytest ; extra == 'dev'
40
+ Requires-Dist: Sphinx ; extra == 'dev'
41
+ Requires-Dist: sphinx-rtd-theme ; extra == 'dev'
42
+ Provides-Extra: trustregion
43
+ Requires-Dist: trustregion >=1.1 ; extra == 'trustregion'
32
44
 
33
45
  ===================================================
34
46
  DFO-LS: Derivative-Free Optimizer for Least-Squares
35
47
  ===================================================
36
48
 
37
- .. image:: https://travis-ci.org/numericalalgorithmsgroup/dfols.svg?branch=master
38
- :target: https://travis-ci.org/numericalalgorithmsgroup/dfols
49
+ .. image:: https://github.com/numericalalgorithmsgroup/dfols/actions/workflows/python_testing.yml/badge.svg
50
+ :target: https://github.com/numericalalgorithmsgroup/dfols/actions
39
51
  :alt: Build Status
40
52
 
41
53
  .. image:: https://img.shields.io/badge/License-GPL%20v3-blue.svg
@@ -49,6 +61,10 @@ DFO-LS: Derivative-Free Optimizer for Least-Squares
49
61
  .. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.2630426.svg
50
62
  :target: https://doi.org/10.5281/zenodo.2630426
51
63
  :alt: DOI:10.5281/zenodo.2630426
64
+
65
+ .. image:: https://static.pepy.tech/personalized-badge/dfo-ls?period=total&units=international_system&left_color=black&right_color=green&left_text=Downloads
66
+ :target: https://pepy.tech/project/dfo-ls
67
+ :alt: Total downloads
52
68
 
53
69
  DFO-LS is a flexible package for solving nonlinear least-squares minimization, without requiring derivatives of the objective. It is particularly useful when evaluations of the objective function are expensive and/or noisy. DFO-LS is more flexible version of `DFO-GN <https://github.com/numericalalgorithmsgroup/dfogn>`_.
54
70
 
@@ -66,36 +82,49 @@ If you use DFO-LS in a paper, please cite:
66
82
 
67
83
  Cartis, C., Fiala, J., Marteau, B. and Roberts, L., `Improving the Flexibility and Robustness of Model-Based Derivative-Free Optimization Solvers <https://doi.org/10.1145/3338517>`_, *ACM Transactions on Mathematical Software*, 45:3 (2019), pp. 32:1-32:41.
68
84
 
85
+ If you use DFO-LS for problems with constraints, including bound constraints, please also cite:
86
+
87
+ Hough, M. and Roberts, L., `Model-Based Derivative-Free Methods for Convex-Constrained Optimization <https://doi.org/10.1137/21M1460971>`_, *SIAM Journal on Optimization*, 21:4 (2022), pp. 2552-2579.
88
+
69
89
  Requirements
70
90
  ------------
71
91
  DFO-LS requires the following software to be installed:
72
92
 
73
- * Python 2.7 or Python 3 (http://www.python.org/)
74
- * Fortran compiler (e.g. `gfortran <https://gcc.gnu.org/wiki/GFortran>`_), required by the `trustregion <https://github.com/lindonroberts/trust-region>`_ package.
93
+ * Python 3.9 or higher (http://www.python.org/)
75
94
 
76
95
  Additionally, the following python packages should be installed (these will be installed automatically if using *pip*, see `Installation using pip`_):
77
96
 
78
- * NumPy 1.11 or higher (http://www.numpy.org/)
79
- * SciPy 0.18 or higher (http://www.scipy.org/)
80
- * Pandas 0.17 or higher (http://pandas.pydata.org/)
97
+ * NumPy (http://www.numpy.org/)
98
+ * SciPy version 1.11 or higher (http://www.scipy.org/)
99
+ * Pandas (http://pandas.pydata.org/)
100
+
101
+ **Optional package:** DFO-LS versions 1.2 and higher also support the `trustregion <https://github.com/lindonroberts/trust-region>`_ package for fast trust-region subproblem solutions. To install this, make sure you have a Fortran compiler (e.g. `gfortran <https://gcc.gnu.org/wiki/GFortran>`_) and NumPy installed, then run :code:`pip install trustregion`. You do not have to have trustregion installed for DFO-LS to work, and it is not installed by default.
102
+
103
+ Installation using conda
104
+ ------------------------
105
+ DFO-LS can be directly installed in Anaconda environments using `conda-forge <https://anaconda.org/conda-forge/dfo-ls>`_:
106
+
107
+ .. code-block:: bash
108
+
109
+ $ conda install -c conda-forge dfo-ls
81
110
 
82
111
  Installation using pip
83
112
  ----------------------
84
113
  For easy installation, use `pip <http://www.pip-installer.org/>`_ as root:
85
114
 
86
- .. code-block:: bash
115
+ .. code-block:: bash
87
116
 
88
117
  $ [sudo] pip install DFO-LS
89
118
 
90
119
  or alternatively *easy_install*:
91
120
 
92
- .. code-block:: bash
121
+ .. code-block:: bash
93
122
 
94
123
  $ [sudo] easy_install DFO-LS
95
124
 
96
125
  If you do not have root privileges or you want to install DFO-LS for your private use, you can use:
97
126
 
98
- .. code-block:: bash
127
+ .. code-block:: bash
99
128
 
100
129
  $ pip install --user DFO-LS
101
130
 
@@ -103,7 +132,7 @@ which will install DFO-LS in your home directory.
103
132
 
104
133
  Note that if an older install of DFO-LS is present on your system you can use:
105
134
 
106
- .. code-block:: bash
135
+ .. code-block:: bash
107
136
 
108
137
  $ [sudo] pip install --upgrade DFO-LS
109
138
 
@@ -132,7 +161,7 @@ If you do not have root privileges or you want to install DFO-LS for your privat
132
161
 
133
162
  instead.
134
163
 
135
- To upgrade DFO-LS to the latest version, navigate to the top-level directory (i.e. the one containing :code:`setup.py`) and rerun the installation using :code:`pip`, as above:
164
+ To upgrade DFO-LS to the latest version, navigate to the top-level directory (i.e. the one containing :code:`pyproject.toml`) and rerun the installation using :code:`pip`, as above:
136
165
 
137
166
  .. code-block:: bash
138
167
 
@@ -141,11 +170,12 @@ To upgrade DFO-LS to the latest version, navigate to the top-level directory (i.
141
170
 
142
171
  Testing
143
172
  -------
144
- If you installed DFO-LS manually, you can test your installation by running:
173
+ If you installed DFO-LS manually, you can test your installation using the pytest package:
145
174
 
146
175
  .. code-block:: bash
147
176
 
148
- $ python setup.py test
177
+ $ pip install pytest
178
+ $ python -m pytest --pyargs dfols
149
179
 
150
180
  Alternatively, the HTML documentation provides some simple examples of how to run DFO-LS.
151
181
 
@@ -165,10 +195,8 @@ If DFO-LS was installed manually you have to remove the installed files by hand
165
195
 
166
196
  Bugs
167
197
  ----
168
- Please report any bugs using GitHub's issue tracker.
198
+ Please report any bugs using `GitHub's issue tracker <https://github.com/numericalalgorithmsgroup/dfols/issues>`_.
169
199
 
170
200
  License
171
201
  -------
172
202
  This algorithm is released under the GNU GPL license. Please `contact NAG <http://www.nag.com/content/worldwide-contact-information>`_ for alternative licensing.
173
-
174
-
@@ -0,0 +1,14 @@
1
+ dfols/__init__.py,sha256=D-x5glfZFfJ8-bdjA-4k4JFTDu1Eylaz3EL4GSH28eI,1605
2
+ dfols/controller.py,sha256=LSeHZoKaKUEYgB1_2subjKskHJ8mWccMbn-LOpxJ7LM,42769
3
+ dfols/diagnostic_info.py,sha256=2kEUkL-MS4eDENUf1r2hOWsntP8OxMDKi_kyHmrC9V4,6081
4
+ dfols/hessian.py,sha256=sExx4J4KoGwHItbthX2odosB2ONbQFvLdlcod7PIh4k,4262
5
+ dfols/model.py,sha256=q70zuqocNtsaXzNjWHcTdrS209BdQt4uY0GNtp0qlI8,18809
6
+ dfols/params.py,sha256=_Va1ybnQDIzWaXvImcSeH8xnNE_A2zpAfBgDG74sc5c,17557
7
+ dfols/solver.py,sha256=IKg3xWPLYlOW_zuTc_-HY_3ZvdDEfkyxARerERUQHlU,61264
8
+ dfols/trust_region.py,sha256=hRKQx0fpSxol7dLZO0yrT7O5IDptPPSnDvxKQNZ3r0M,24603
9
+ dfols/util.py,sha256=ysdIHTkrkWwCRKuGffofehKl-t5dT3sD9dfy0muI4ZI,9852
10
+ DFO_LS-1.4.1.dist-info/LICENSE.txt,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
11
+ DFO_LS-1.4.1.dist-info/METADATA,sha256=RR6KhJi4Ae_1PES8Bpzqm3AYK2w12V-2MyDyjaCDe80,8552
12
+ DFO_LS-1.4.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
13
+ DFO_LS-1.4.1.dist-info/top_level.txt,sha256=UfxRhaDN8HQx2_l17KbrDrERJ90OCN7VKkDMpYYbRLU,6
14
+ DFO_LS-1.4.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.34.2)
2
+ Generator: bdist_wheel (0.43.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
dfols/__init__.py CHANGED
@@ -7,8 +7,7 @@ nonlinear least-squares solver which only requires function values.
7
7
 
8
8
  It solves the nonlinear least-squares problem:
9
9
  min_{x} f(x) = r1(x)**2 + ... + rm(x)**2,
10
- subject to the (optional) bounds
11
- lb <= x <= ub,
10
+ (optionally) subject to finitely many convex constraints,
12
11
  where each function ri(x) is differentiable, possibly nonconvex.
13
12
  Since the derivatives of ri(x) are never required or approximated,
14
13
  the solver works when the evaluation of ri(x) is noisy.
@@ -39,10 +38,10 @@ alternative licensing.
39
38
  # Ensure compatibility with Python 2
40
39
  from __future__ import absolute_import, division, print_function, unicode_literals
41
40
 
42
- from .version import __version__
43
- __all__ = ['__version__']
41
+ # DFO-LS version
42
+ __version__ = '1.4.1'
44
43
 
45
44
  # Main solver & exit flags
46
45
  from .solver import *
47
- __all__ += ['solve']
46
+ __all__ = ['solve']
48
47
 
dfols/controller.py CHANGED
@@ -41,8 +41,11 @@ from .util import *
41
41
 
42
42
  __all__ = ['Controller', 'ExitInformation', 'EXIT_SLOW_WARNING', 'EXIT_MAXFUN_WARNING', 'EXIT_SUCCESS',
43
43
  'EXIT_INPUT_ERROR', 'EXIT_TR_INCREASE_ERROR', 'EXIT_LINALG_ERROR', 'EXIT_FALSE_SUCCESS_WARNING',
44
- 'EXIT_AUTO_DETECT_RESTART_WARNING']
44
+ 'EXIT_AUTO_DETECT_RESTART_WARNING', 'EXIT_EVAL_ERROR']
45
45
 
46
+ module_logger = logging.getLogger(__name__)
47
+
48
+ EXIT_TR_INCREASE_WARNING = 5 # warning, TR increase in proj constrained case - likely due to multiple active constraints
46
49
  EXIT_AUTO_DETECT_RESTART_WARNING = 4 # warning, auto-detected restart criteria
47
50
  EXIT_FALSE_SUCCESS_WARNING = 3 # warning, maximum fake successful steps reached
48
51
  EXIT_SLOW_WARNING = 2 # warning, maximum number of slow (successful) iterations reached
@@ -51,6 +54,7 @@ EXIT_SUCCESS = 0 # successful finish (rho=rhoend, sufficient objective reductio
51
54
  EXIT_INPUT_ERROR = -1 # error, bad inputs
52
55
  EXIT_TR_INCREASE_ERROR = -2 # error, trust region step increased model value
53
56
  EXIT_LINALG_ERROR = -3 # error, linalg error (singular matrix encountered)
57
+ EXIT_EVAL_ERROR = -4 # error, objective evaluation error (e.g. nan result received)
54
58
 
55
59
 
56
60
  class ExitInformation(object):
@@ -70,6 +74,8 @@ class ExitInformation(object):
70
74
  return "Warning (slow progress): " + self.msg
71
75
  elif self.flag == EXIT_MAXFUN_WARNING:
72
76
  return "Warning (max evals): " + self.msg
77
+ elif self.flag == EXIT_TR_INCREASE_WARNING:
78
+ return "Warning (trust region increase): " + self.msg
73
79
  elif self.flag == EXIT_INPUT_ERROR:
74
80
  return "Error (bad input): " + self.msg
75
81
  elif self.flag == EXIT_TR_INCREASE_ERROR:
@@ -78,11 +84,13 @@ class ExitInformation(object):
78
84
  return "Error (linear algebra): " + self.msg
79
85
  elif self.flag == EXIT_FALSE_SUCCESS_WARNING:
80
86
  return "Warning (max false good steps): " + self.msg
87
+ elif self.flag == EXIT_EVAL_ERROR:
88
+ return "Error (function evaluation): " + self.msg
81
89
  else:
82
90
  return "Unknown exit flag: " + self.msg
83
91
 
84
92
  def able_to_do_restart(self):
85
- if self.flag in [EXIT_TR_INCREASE_ERROR, EXIT_LINALG_ERROR, EXIT_SLOW_WARNING, EXIT_AUTO_DETECT_RESTART_WARNING]:
93
+ if self.flag in [EXIT_TR_INCREASE_ERROR, EXIT_TR_INCREASE_WARNING, EXIT_LINALG_ERROR, EXIT_SLOW_WARNING, EXIT_AUTO_DETECT_RESTART_WARNING, EXIT_EVAL_ERROR]:
86
94
  return True
87
95
  elif self.flag in [EXIT_MAXFUN_WARNING, EXIT_INPUT_ERROR]:
88
96
  return False
@@ -92,13 +100,13 @@ class ExitInformation(object):
92
100
 
93
101
 
94
102
  class Controller(object):
95
- def __init__(self, objfun, args, x0, r0, r0_nsamples, xl, xu, npt, rhobeg, rhoend, nf, nx, maxfun, params,
103
+ def __init__(self, objfun, args, x0, r0, r0_nsamples, xl, xu, projections, npt, rhobeg, rhoend, nf, nx, maxfun, params,
96
104
  scaling_changes, do_logging):
97
105
  self.do_logging = do_logging
98
106
  self.objfun = objfun
99
107
  self.args = args
100
108
  self.maxfun = maxfun
101
- self.model = Model(npt, x0, r0, xl, xu, r0_nsamples, precondition=params("interpolation.precondition"),
109
+ self.model = Model(npt, x0, r0, xl, xu, projections, r0_nsamples, precondition=params("interpolation.precondition"),
102
110
  abs_tol = params("model.abs_tol"), rel_tol = params("model.rel_tol"), do_logging=do_logging)
103
111
  self.nf = nf
104
112
  self.nx = nx
@@ -107,9 +115,6 @@ class Controller(object):
107
115
  self.rho = rhobeg
108
116
  self.rhoend = rhoend
109
117
  self.diffs = [0.0, 0.0, 0.0]
110
- self.last_iters_step_taken = []
111
- self.last_fopts_step_taken = []
112
- self.num_slow_iters = 0
113
118
  self.finished_growing = False
114
119
  self.finished_halfway_growing = False
115
120
  # For measuing slow iterations
@@ -134,12 +139,113 @@ class Controller(object):
134
139
 
135
140
  def initialise_coordinate_directions(self, number_of_samples, num_directions, params):
136
141
  if self.do_logging:
137
- logging.debug("Initialising with coordinate directions")
142
+ module_logger.debug("Initialising with coordinate directions")
138
143
  # self.model already has x0 evaluated, so only need to initialise the other points
139
144
  # num_directions = params("growing.ndirs_initial")
140
145
  assert self.model.num_pts <= (self.n() + 1) * (self.n() + 2) // 2, "prelim: must have npt <= (n+1)(n+2)/2"
141
146
  assert 1 <= num_directions < self.model.num_pts, "Initialisation: must have 1 <= ndirs_initial < npt"
142
147
 
148
+
149
+ if self.model.projections:
150
+ D = np.zeros((self.n(),self.n()))
151
+ k = 0
152
+ while k < self.n():
153
+ ek = np.zeros(self.n())
154
+ ek[k] = 1
155
+ p = np.dot(ek,min(1,self.delta))
156
+ yk = dykstra(self.model.projections, self.model.xbase + p, max_iter=params("dykstra.max_iters"), tol=params("dykstra.d_tol"))
157
+ D[k,:] = yk - self.model.xbase
158
+
159
+ k += 1 # move on to next point
160
+
161
+ # Have at least one L.D. vector, try negative direction on bad one first
162
+ k = 0
163
+ mr_tol = params("matrix_rank.r_tol")
164
+ D_rank, diag = qr_rank(D,tol=mr_tol)
165
+ while D_rank != num_directions and k < self.n():
166
+ if diag[k] < mr_tol:
167
+ ek = np.zeros(self.n())
168
+ ek[k] = 1
169
+ p = -np.dot(ek,min(1,self.delta))
170
+ yk = dykstra(self.model.projections, self.model.xbase + p, max_iter=params("dykstra.max_iters"), tol=params("dykstra.d_tol"))
171
+ dk = D[k,:].copy()
172
+ D[k,:] = yk - self.model.xbase
173
+ D_rank2, _diag2 = qr_rank(D,tol=params("matrix_rank.r_tol"))
174
+ if D_rank2 <= D_rank:
175
+ # Did not improve rank, revert change
176
+ D[k,:] = dk
177
+ # rank was improved, update D_rank for next comparison
178
+ D_rank = D_rank2
179
+ k += 1
180
+
181
+ # Try random combination of negatives...
182
+ k = 0
183
+ slctr = np.random.randint(0, 1+1, self.n()) # generate rand binary "selector" array
184
+ D_rank, diag = qr_rank(D,tol=params("matrix_rank.r_tol"))
185
+ while D_rank != num_directions and k < 100*self.n():
186
+ if slctr[k%self.n()] == 1: # if selector says make -ve, make -ve
187
+ ek = np.zeros(self.n())
188
+ ek[k%self.n()] = 1
189
+ p = -np.dot(ek,min(1,self.delta))
190
+ yk = dykstra(self.model.projections, self.model.xbase + p, max_iter=params("dykstra.max_iters"), tol=params("dykstra.d_tol"))
191
+ dk = D[k%self.n(),:].copy()
192
+ D[k%self.n(),:] = yk - self.model.xbase
193
+ D_rank2, _diag2 = qr_rank(D,tol=params("matrix_rank.r_tol"))
194
+ if D_rank2 <= D_rank:
195
+ # Did not improve rank, revert change
196
+ D[k%self.n(),:] = dk
197
+ # rank was improved, update D_rank for next comparison
198
+ D_rank = D_rank2
199
+
200
+ # Go again
201
+ slctr = np.random.randint(0, 1+1, self.n())
202
+ k += 1
203
+
204
+ # Set still not L.I? Try random directions
205
+ i = 0
206
+ D_rank, diag = qr_rank(D,tol=params("matrix_rank.r_tol"))
207
+ while D_rank != num_directions and i <= 100*num_directions:
208
+ k = 0
209
+ while k < self.n():
210
+ if diag[k] < mr_tol:
211
+ p = np.random.normal(size=self.n())
212
+ p = p/np.linalg.norm(p)
213
+ p = np.dot(p,min(1,self.delta))
214
+ yk = dykstra(self.model.projections, self.model.xbase + p, max_iter=params("dykstra.max_iters"), tol=params("dykstra.d_tol"))
215
+ dk = D[k,:].copy()
216
+ D[k,:] = yk - self.model.xbase
217
+ D_rank2, _diag2 = qr_rank(D,tol=params("matrix_rank.r_tol"))
218
+ if D_rank2 <= D_rank:
219
+ # Did not improve rank, revert change
220
+ D[k,:] = dk
221
+ # rank was improved, update D_rank for next comparison
222
+ D_rank = D_rank2
223
+ k += 1
224
+ i += 1
225
+
226
+ if D_rank != num_directions:
227
+ raise RuntimeError("Unable to generate suitable initial directions")
228
+
229
+ # we have a L.I set of interpolation points
230
+ for k in range(0,self.n()):
231
+ # Evaluate objective at this new point
232
+ x = self.model.as_absolute_coordinates(D[k, :])
233
+ rvec_list, f_list, num_samples_run, exit_info = self.evaluate_objective(x, number_of_samples, params)
234
+
235
+ # Handle exit conditions (f < min obj value or maxfun reached)
236
+ if exit_info is not None:
237
+ if num_samples_run > 0:
238
+ self.model.save_point(x, np.mean(rvec_list[:num_samples_run, :], axis=0), num_samples_run,
239
+ x_in_abs_coords=True)
240
+ return exit_info # return & quit
241
+
242
+ # Otherwise, add new results (increments model.npt_so_far)
243
+ self.model.change_point(k+1, x - self.model.xbase, rvec_list[0, :]) # expect step, not absolute x
244
+ for i in range(1, num_samples_run):
245
+ self.model.add_new_sample(k+1, rvec_extra=rvec_list[i, :])
246
+
247
+ return None # return & continue
248
+
143
249
  at_lower_boundary = (self.model.sl > -0.01 * self.delta) # sl = xl - x0, should be -ve, actually < -rhobeg
144
250
  at_upper_boundary = (self.model.su < 0.01 * self.delta) # su = xu - x0, should be +ve, actually > rhobeg
145
251
 
@@ -150,17 +256,19 @@ class Controller(object):
150
256
  # k = 2n+1, ..., (n+1)(n+2)/2 --> off-diagonal directions
151
257
  if 1 <= k < self.n() + 1: # first step along coord directions
152
258
  dirn = k - 1 # direction to move in (0,...,n-1)
153
- stepa = self.delta if not at_upper_boundary[dirn] else -self.delta
259
+ stepa = self.delta if not at_upper_boundary[dirn] else -self.delta # take a +delta step if at lower, -delta if at upper
154
260
  stepb = None
155
- xpts_added[k, dirn] = stepa
261
+ xpts_added[k, dirn] = stepa # set new (relative) point to the step since we haven't done any moving, so relative point is all zeros.
156
262
 
157
263
  elif self.n() + 1 <= k < 2 * self.n() + 1: # second step along coord directions
158
264
  dirn = k - self.n() - 1 # direction to move in (0,...,n-1)
159
- stepa = xpts_added[k - self.n(), dirn]
160
- stepb = -self.delta
265
+ stepa = xpts_added[k - self.n(), dirn] # previous step
266
+ stepb = -self.delta # new step
161
267
  if at_lower_boundary[dirn]:
268
+ # if at lower boundary, set the second step to be +ve
162
269
  stepb = min(2.0 * self.delta, self.model.su[dirn]) # su = xu - x0, should be +ve
163
270
  if at_upper_boundary[dirn]:
271
+ # if at upper boundary, set the second step to be -ve
164
272
  stepb = max(-2.0 * self.delta, self.model.sl[dirn]) # sl = xl - x0, should be -ve
165
273
  xpts_added[k, dirn] = stepb
166
274
 
@@ -208,7 +316,7 @@ class Controller(object):
208
316
 
209
317
  def initialise_random_directions(self, number_of_samples, num_directions, params):
210
318
  if self.do_logging:
211
- logging.debug("Initialising with random orthogonal directions")
319
+ module_logger.debug("Initialising with random orthogonal directions")
212
320
  # self.model already has x0 evaluated, so only need to initialise the other points
213
321
  assert 1 <= num_directions < self.model.num_pts, "Initialisation: must have 1 <= ndirs_initial < npt"
214
322
 
@@ -328,20 +436,28 @@ class Controller(object):
328
436
 
329
437
  return dirn * (step_length / LA.norm(dirn))
330
438
 
331
- def trust_region_step(self):
439
+ def trust_region_step(self, params):
332
440
  # Build model for full least squares objectives
333
441
  gopt, H = self.model.build_full_model()
334
- d, gnew, crvmin = trsbox(self.model.xopt(), gopt, H, self.model.sl, self.model.su, self.delta)
442
+ if self.model.projections:
443
+ d, gnew, crvmin = ctrsbox(self.model.xopt(abs_coordinates=True), gopt, H, self.model.projections, self.delta, d_max_iters=params("dykstra.max_iters"), d_tol=params("dykstra.d_tol"))
444
+ else:
445
+ d, gnew, crvmin = trsbox(self.model.xopt(), gopt, H, self.model.sl, self.model.su, self.delta)
335
446
  return d, gopt, H, gnew, crvmin
336
447
 
337
448
  def geometry_step(self, knew, adelt, number_of_samples, params):
338
449
  if self.do_logging:
339
- logging.debug("Running geometry-fixing step")
450
+ module_logger.debug("Running geometry-fixing step")
340
451
  try:
341
452
  c, g = self.model.lagrange_gradient(knew)
342
453
  # c = 1.0 if knew == self.model.kopt else 0.0 # based at xopt, just like d
343
- # Solve problem: bounds are sl <= xnew <= su, and ||xnew-xopt|| <= adelt
344
- xnew = trsbox_geometry(self.model.xopt(), c, g, np.minimum(self.model.sl, 0.0), np.maximum(self.model.su, 0.0), adelt)
454
+ if self.model.projections:
455
+ # Solve problem: use projection onto arbitrary constraints, and ||xnew-xopt|| <= adelt
456
+ step = ctrsbox_geometry(self.model.xopt(abs_coordinates=True), c, g, self.model.projections, adelt, d_max_iters=params("dykstra.max_iters"), d_tol=params("dykstra.d_tol"))
457
+ xnew = self.model.xopt() + step
458
+ else:
459
+ # Solve problem: bounds are sl <= xnew <= su, and ||xnew-xopt|| <= adelt
460
+ xnew = trsbox_geometry(self.model.xopt(), c, g, np.minimum(self.model.sl, 0.0), np.maximum(self.model.su, 0.0), adelt)
345
461
  except LA.LinAlgError:
346
462
  exit_info = ExitInformation(EXIT_LINALG_ERROR, "Singular matrix encountered in geometry step")
347
463
  return exit_info # didn't fix geometry - return & quit
@@ -502,13 +618,16 @@ class Controller(object):
502
618
  def calculate_ratio(self, current_iter, rvec_list, d, gopt, H):
503
619
  exit_info = None
504
620
  f = sumsq(np.mean(rvec_list, axis=0)) # estimate actual objective value
505
- pred_reduction = - model_value(gopt, H, d)
621
+ pred_reduction = - model_value(gopt, H, d) # negative of m since m(0) = 0
506
622
  actual_reduction = self.model.fopt() - f
507
623
  self.diffs = [abs(actual_reduction - pred_reduction), self.diffs[0], self.diffs[1]]
508
624
  if min(sqrt(sumsq(d)), self.delta) > self.rho: # if ||d|| >= rho, successful!
509
625
  self.last_successful_iter = current_iter
510
626
  if pred_reduction < 0.0:
511
- exit_info = ExitInformation(EXIT_TR_INCREASE_ERROR, "Trust region step gave model increase")
627
+ if len(self.model.projections) > 1: # if we are using multiple projections, only warn since likely due to constraint intersection
628
+ exit_info = ExitInformation(EXIT_TR_INCREASE_WARNING, "Either multiple constraints are active or trust region step gave model increase")
629
+ else:
630
+ exit_info = ExitInformation(EXIT_TR_INCREASE_ERROR, "Either rust region step gave model increase")
512
631
 
513
632
  ratio = actual_reduction / pred_reduction
514
633
  return ratio, exit_info
@@ -529,12 +648,12 @@ class Controller(object):
529
648
  if this_iter_slow:
530
649
  self.num_slow_iters += 1
531
650
  if self.do_logging:
532
- logging.info("Slow iteration (%g consecutive so far, max allowed %g)"
651
+ module_logger.info("Slow iteration (%g consecutive so far, max allowed %g)"
533
652
  % (self.num_slow_iters, params("slow.max_slow_iters")))
534
653
  else:
535
654
  self.num_slow_iters = 0
536
655
  if self.do_logging:
537
- logging.debug("Non-slow iteration")
656
+ module_logger.debug("Non-slow iteration")
538
657
  return this_iter_slow, self.num_slow_iters >= params("slow.max_slow_iters")
539
658
 
540
659
  def soft_restart(self, number_of_samples, nruns_so_far, params, x_in_abs_coords_to_save=None, rvec_to_save=None,
@@ -563,12 +682,17 @@ class Controller(object):
563
682
  self.model.nsamples[self.model.kopt], x_in_abs_coords=True)
564
683
 
565
684
  if self.do_logging:
566
- logging.info("Soft restart [currently, f = %g after %g function evals]" % (self.model.fopt(), self.nf))
685
+ module_logger.info("Soft restart [currently, f = %g after %g function evals]" % (self.model.fopt(), self.nf))
567
686
  # Resetting method: reset delta and rho, then move the closest 'num_steps' points to xk to improve geometry
568
687
  # Note: closest points because we are suddenly increasing delta & rho, so we want to encourage spreading out points
569
688
  self.delta = self.rhobeg
570
689
  self.rho = self.rhobeg
571
690
  self.diffs = [0.0, 0.0, 0.0]
691
+
692
+ # Forget history of slow iterations
693
+ self.last_iters_step_taken = []
694
+ self.last_fopts_step_taken = []
695
+ self.num_slow_iters = 0
572
696
 
573
697
  all_sq_dist = self.model.distances_to_xopt()[:self.model.npt()]
574
698
  closest_points = np.argsort(all_sq_dist)
@@ -615,7 +739,7 @@ class Controller(object):
615
739
  self.model.add_new_sample(self.model.npt() - 1, rvec_extra=rvec_list[i, :])
616
740
 
617
741
  if self.do_logging:
618
- logging.info("Soft restart: added %g new directions, npt is now %g" % (num_pts_to_add, self.model.npt()))
742
+ module_logger.info("Soft restart: added %g new directions, npt is now %g" % (num_pts_to_add, self.model.npt()))
619
743
 
620
744
  # Otherwise, we are doing a restart
621
745
  self.last_successful_iter = 0
dfols/hessian.py CHANGED
@@ -39,7 +39,7 @@ class Hessian(object):
39
39
  def __init__(self, n, vals=None):
40
40
  self.n = n
41
41
  if vals is None:
42
- self.hq = np.zeros((n * (n + 1) // 2,), dtype=np.float)
42
+ self.hq = np.zeros((n * (n + 1) // 2,), dtype=float)
43
43
  else:
44
44
  assert isinstance(vals, np.ndarray), "Can only set Hessian from NumPy array"
45
45
  assert len(vals.shape) in [1, 2], "Can only set Hessian from vector or matrix"
dfols/model.py CHANGED
@@ -36,12 +36,15 @@ 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
39
+ from .util import sumsq, dykstra
40
40
 
41
41
  __all__ = ['Model']
42
42
 
43
+ module_logger = logging.getLogger(__name__)
44
+
45
+
43
46
  class Model(object):
44
- def __init__(self, npt, x0, r0, xl, xu, r0_nsamples, n=None, m=None, abs_tol=1e-12, rel_tol=1e-20, precondition=True,
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,
45
48
  do_logging=True):
46
49
  if n is None:
47
50
  n = len(x0)
@@ -63,6 +66,7 @@ class Model(object):
63
66
  self.xbase = x0.copy()
64
67
  self.sl = xl - self.xbase # lower bound w.r.t. xbase (require xpt >= sl)
65
68
  self.su = xu - self.xbase # upper bound w.r.t. xbase (require xpt <= su)
69
+ self.projections = projections
66
70
  self.points = np.zeros((npt, n)) # interpolation points w.r.t. xbase
67
71
 
68
72
  # Function values
@@ -71,7 +75,7 @@ class Model(object):
71
75
  self.fval = np.inf * np.ones((npt, )) # overall objective value for each xpt
72
76
  self.fval[0] = sumsq(r0)
73
77
  self.kopt = 0 # index of current iterate (should be best value so far)
74
- self.nsamples = np.zeros((npt,), dtype=np.int) # number of samples used to evaluate objective at each point
78
+ self.nsamples = np.zeros((npt,), dtype=int) # number of samples used to evaluate objective at each point
75
79
  self.nsamples[0] = r0_nsamples
76
80
  self.fbeg = self.fval[0] # f(x0), saved to check for sufficient reduction
77
81
 
@@ -123,6 +127,8 @@ class Model(object):
123
127
  return np.minimum(np.maximum(self.sl, self.points[k, :].copy()), self.su)
124
128
  else:
125
129
  # Apply bounds and convert back to absolute coordinates
130
+ if self.projections:
131
+ return dykstra(self.projections, self.xbase + self.points[k,:])
126
132
  return self.xbase + np.minimum(np.maximum(self.sl, self.points[k, :]), self.su)
127
133
 
128
134
  def rvec(self, k):
@@ -133,8 +139,10 @@ class Model(object):
133
139
  assert 0 <= k < self.npt(), "Invalid index %g" % k
134
140
  return self.fval[k]
135
141
 
136
- def as_absolute_coordinates(self, x):
142
+ def as_absolute_coordinates(self, x, full_dykstra=False):
137
143
  # If x were an interpolation point, get the absolute coordinates of x
144
+ if self.projections:
145
+ return dykstra(self.projections, self.xbase + x)
138
146
  return self.xbase + np.minimum(np.maximum(self.sl, x), self.su)
139
147
 
140
148
  def xpt_directions(self, include_kopt=True):
@@ -301,12 +309,12 @@ class Model(object):
301
309
  return col_scale(LA.solve_triangular(self.R, Qb), self.right_scaling)
302
310
  else:
303
311
  if self.do_logging:
304
- logging.warning("model.solve_geom_system not using factorisation")
312
+ module_logger.warning("model.solve_geom_system not using factorisation")
305
313
  W, left_scaling, right_scaling = self.interpolation_matrix()
306
314
  return col_scale(LA.lstsq(W, col_scale(rhs * left_scaling))[0], right_scaling)
307
315
 
308
316
  def interpolate_mini_models_svd(self, verbose=False, make_full_rank=False, min_sing_val=1e-6, sing_val_frac=1.0, max_jac_cond=1e8,
309
- get_chg_J=False):
317
+ get_chg_J=False, throw_error_on_nans=False):
310
318
  W, left_scaling, right_scaling = self.interpolation_matrix()
311
319
  self.factorise_geom_system()
312
320
  ls_interp_cond_num = np.linalg.cond(W) if verbose else 0.0 # scipy.linalg does not have condition number!
@@ -327,12 +335,18 @@ class Model(object):
327
335
  self.model_jac = np.dot(self.model_jac, np.dot(Qhat, Qhat.T))
328
336
 
329
337
  rhs = self.fval_v[fval_row_idx, :] # size npt * m
338
+ if np.any(np.isnan(rhs)) and throw_error_on_nans:
339
+ if self.do_logging:
340
+ module_logger.warning("model.interpolate_mini_models_svd: NaNs encountered in objective evaluations, raising error")
341
+ raise np.linalg.LinAlgError("NaN encountered in objective evaluations")
330
342
  try:
331
343
  dg = self.solve_geom_system(rhs) # size (n+1)*m
332
344
  except LA.LinAlgError:
333
345
  return False, None, None, None, None # flag error
334
346
  except ValueError:
335
347
  return False, None, None, None, None # flag error (e.g. inf or NaN encountered)
348
+ if not np.all(np.isfinite(dg)): # another check for inf or NaN
349
+ return False, None, None, None, None
336
350
  J_old = self.model_jac.copy()
337
351
  self.model_jac = dg[1:,:].T
338
352
  self.model_const = dg[0,:] - np.dot(self.model_jac, xopt) # shift base to xbase