better-optimize 0.0.0__tar.gz → 0.0.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: better_optimize
3
- Version: 0.0.0
3
+ Version: 0.0.2
4
4
  Summary: A drop-in replacement for scipy optimize functions with quality of life improvements
5
5
  License: MIT License
6
6
  Project-URL: Homepage, https://github.com/jessegrabowski/better_optimize
@@ -14,6 +14,7 @@ Classifier: Programming Language :: Python :: 3.11
14
14
  Classifier: Programming Language :: Python :: 3.12
15
15
  Classifier: Topic :: Scientific/Engineering
16
16
  Classifier: Topic :: Scientific/Engineering :: Mathematics
17
+ Requires-Python: <3.13,>=3.10
17
18
  Description-Content-Type: text/markdown
18
19
  Provides-Extra: tests
19
20
 
@@ -0,0 +1,17 @@
1
+ import logging
2
+
3
+ from better_optimize._version import get_versions
4
+ from better_optimize.minimize import minimize
5
+ from better_optimize.root import root
6
+
7
+ _log = logging.getLogger(__name__)
8
+
9
+ if not logging.root.handlers:
10
+ _log.setLevel(logging.INFO)
11
+ if len(_log.handlers) == 0:
12
+ handler = logging.StreamHandler()
13
+ _log.addHandler(handler)
14
+
15
+
16
+ __version__ = get_versions()["version"]
17
+ __all__ = ["minimize", "root"]
@@ -0,0 +1,21 @@
1
+
2
+ # This file was generated by 'versioneer.py' (0.29) from
3
+ # revision-control system data, or from the parent directory name of an
4
+ # unpacked source archive. Distribution tarballs contain a pre-generated copy
5
+ # of this file.
6
+
7
+ import json
8
+
9
+ version_json = '''
10
+ {
11
+ "date": "2024-08-19T18:20:27+0800",
12
+ "dirty": false,
13
+ "error": null,
14
+ "full-revisionid": "3595536b9c1ead4ca6ea916046e4379d86e36ecc",
15
+ "version": "0.0.2"
16
+ }
17
+ ''' # END VERSION_JSON
18
+
19
+
20
+ def get_versions():
21
+ return json.loads(version_json)
@@ -0,0 +1,359 @@
1
+ from typing import Literal
2
+
3
+ minimize_method = Literal[
4
+ "nelder-mead",
5
+ "powell",
6
+ "CG",
7
+ "BFGS",
8
+ "Newton-CG",
9
+ "L-BFGS-B",
10
+ "TNC",
11
+ "COBYLA",
12
+ "SLSQP",
13
+ "trust-constr",
14
+ "dogleg",
15
+ "trust-ncg",
16
+ "trust-exact",
17
+ "trust-krylov",
18
+ ]
19
+
20
+ root_method = Literal[
21
+ "hybr",
22
+ "lm",
23
+ "broyden1",
24
+ "broyden2",
25
+ "anderson",
26
+ "linearmixing",
27
+ "diagbroyden",
28
+ "excitingmixing",
29
+ "krylov",
30
+ "df-sane",
31
+ ]
32
+
33
+ TOLERANCES = ["xtol", "ftol", "gtol", "fatol", "xatol"]
34
+
35
+ MINIMIZE_MODE_KWARGS = {
36
+ "nelder-mead": {
37
+ "uses_grad": False,
38
+ "uses_hess": False,
39
+ "uses_hessp": False,
40
+ "valid_options": [
41
+ "disp",
42
+ "maxiter",
43
+ "maxfev",
44
+ "return_all",
45
+ "initial_simplex",
46
+ "xatol",
47
+ "fatol",
48
+ "adaptive",
49
+ "bounds",
50
+ ],
51
+ },
52
+ "powell": {
53
+ "uses_grad": False,
54
+ "uses_hess": False,
55
+ "uses_hessp": False,
56
+ "valid_options": ["disp", "xtol", "ftol", "maxiter", "maxfev", "direc", "return_all"],
57
+ },
58
+ "CG": {
59
+ "uses_grad": True,
60
+ "uses_hess": False,
61
+ "uses_hessp": False,
62
+ "valid_options": [
63
+ "disp",
64
+ "maxiter",
65
+ "gtol",
66
+ "norm",
67
+ "eps",
68
+ "return_all",
69
+ "finite_diff_rel_step",
70
+ "c1",
71
+ "c2",
72
+ ],
73
+ },
74
+ "BFGS": {
75
+ "uses_grad": True,
76
+ "uses_hess": False,
77
+ "uses_hessp": False,
78
+ "valid_options": [
79
+ "disp",
80
+ "maxiter",
81
+ "gtol",
82
+ "norm",
83
+ "eps",
84
+ "return_all",
85
+ "finite_diff_rel_step",
86
+ "xrtol",
87
+ "c1",
88
+ "c2",
89
+ "hess_inv0",
90
+ ],
91
+ },
92
+ "Newton-CG": {
93
+ "uses_grad": True,
94
+ "uses_hess": True,
95
+ "uses_hessp": True,
96
+ "valid_options": ["disp", "xtol", "maxiter", "eps", "return_all", "c1", "c2"],
97
+ },
98
+ "L-BFGS-B": {
99
+ "uses_grad": True,
100
+ "uses_hess": False,
101
+ "uses_hessp": False,
102
+ "valid_options": [
103
+ "disp",
104
+ "maxcor",
105
+ "ftol",
106
+ "gtol",
107
+ "eps",
108
+ "maxfun",
109
+ "maxiter",
110
+ "iprint",
111
+ "maxls",
112
+ "finite_diff_rel_step",
113
+ ],
114
+ },
115
+ "TNC": {
116
+ "uses_grad": True,
117
+ "uses_hess": False,
118
+ "uses_hessp": False,
119
+ "valid_options": [
120
+ "eps",
121
+ "scale",
122
+ "offset",
123
+ "disp",
124
+ "maxCGit",
125
+ "eta",
126
+ "stepmx",
127
+ "accuracy",
128
+ "minfev",
129
+ "ftol",
130
+ "xtol",
131
+ "gtol",
132
+ "rescale",
133
+ "finite_diff_rel_step",
134
+ "maxfun",
135
+ ],
136
+ },
137
+ "COBYLA": {
138
+ "uses_grad": False,
139
+ "uses_hess": False,
140
+ "uses_hessp": False,
141
+ "valid_options": ["rhobeg", "tol", "disp", "maxiter", "catol"],
142
+ },
143
+ "SLSQP": {
144
+ "uses_grad": True,
145
+ "uses_hess": False,
146
+ "uses_hessp": False,
147
+ "valid_options": [
148
+ "ftol",
149
+ "eps",
150
+ "disp",
151
+ "maxiter",
152
+ "finite_diff_rel_step",
153
+ ],
154
+ },
155
+ "trust-constr": {
156
+ "uses_grad": True,
157
+ "uses_hess": True,
158
+ "uses_hessp": True,
159
+ "valid_options": [
160
+ "gtol",
161
+ "xtol",
162
+ "barrier_tol",
163
+ "sparse_jacobian",
164
+ "initial_tr_radius",
165
+ "initial_constr_penalty",
166
+ "initial_barrier_parameter",
167
+ "initial_barrier_tolerance",
168
+ "factorization_method",
169
+ "finite_diff_rel_step",
170
+ "maxiter",
171
+ "verbose",
172
+ "disp",
173
+ ],
174
+ },
175
+ "dogleg": {
176
+ "uses_grad": True,
177
+ "uses_hess": True,
178
+ "uses_hessp": False,
179
+ "valid_options": ["initial_trust_radius", "max_trust_radius", "eta", "gtol"],
180
+ },
181
+ "trust-ncg": {
182
+ "uses_grad": True,
183
+ "uses_hess": True,
184
+ "uses_hessp": True,
185
+ "valid_options": ["initial_trust_radius", "max_trust_radius", "eta", "gtol"],
186
+ },
187
+ "trust-exact": {
188
+ "uses_grad": True,
189
+ "uses_hess": True,
190
+ "uses_hessp": False,
191
+ "valid_options": ["initial_trust_radius", "max_trust_radius", "eta", "gtol"],
192
+ },
193
+ "trust-krylov": {
194
+ "uses_grad": True,
195
+ "uses_hess": True,
196
+ "uses_hessp": True,
197
+ "valid_options": ["inexact"],
198
+ },
199
+ }
200
+
201
+ root_method = Literal[
202
+ "hybr",
203
+ "lm",
204
+ "broyden1",
205
+ "broyden2",
206
+ "anderson",
207
+ "linearmixing",
208
+ "diagbroyden",
209
+ "excitingmixing",
210
+ "krylov",
211
+ "df-sane",
212
+ ]
213
+
214
+ ROOT_MODE_KWARGS = {
215
+ "hybr": {
216
+ "uses_jac": True,
217
+ "valid_options": ["col_deriv", "xtol", "maxfev", "band", "eps", "factor", "diag"],
218
+ },
219
+ "lm": {
220
+ "uses_jac": True,
221
+ "valid_options": ["col_deriv", "ftol", "xtol", "gtol", "maxiter", "eps", "factor", "diag"],
222
+ },
223
+ "broyden1": {
224
+ "uses_jac": False,
225
+ "valid_options": [
226
+ "nit",
227
+ "disp",
228
+ "maxiter",
229
+ "ftol",
230
+ "fatol",
231
+ "xtol",
232
+ "xatol",
233
+ "tol_norm",
234
+ "line_search",
235
+ "max_rank",
236
+ "jac_options",
237
+ ],
238
+ "jac_options": ["alpha", "reduction_method", "to_retain"],
239
+ },
240
+ "broyden2": {
241
+ "uses_jac": False,
242
+ "valid_options": [
243
+ "nit",
244
+ "disp",
245
+ "maxiter",
246
+ "ftol",
247
+ "fatol",
248
+ "xtol",
249
+ "xatol",
250
+ "tol_norm",
251
+ "line_search",
252
+ "max_rank",
253
+ "jac_options",
254
+ ],
255
+ "jac_options": ["alpha", "reduction_method", "to_retain"],
256
+ },
257
+ "anderson": {
258
+ "uses_jac": False,
259
+ "valid_options": [
260
+ "nit",
261
+ "disp",
262
+ "maxiter",
263
+ "ftol",
264
+ "fatol",
265
+ "xtol",
266
+ "xatol",
267
+ "tol_norm",
268
+ "line_search",
269
+ "jac_options",
270
+ ],
271
+ "jac_options": ["alpha", "M", "w0"],
272
+ },
273
+ "linearmixing": {
274
+ "uses_jac": False,
275
+ "valid_options": [
276
+ "nit",
277
+ "disp",
278
+ "maxiter",
279
+ "ftol",
280
+ "fatol",
281
+ "xtol",
282
+ "xatol",
283
+ "tol_norm",
284
+ "line_search",
285
+ "jac_options",
286
+ ],
287
+ "jac_options": ["alpha"],
288
+ },
289
+ "diagbroyden": {
290
+ "uses_jac": False,
291
+ "valid_options": [
292
+ "nit",
293
+ "disp",
294
+ "maxiter",
295
+ "ftol",
296
+ "fatol",
297
+ "xtol",
298
+ "xatol",
299
+ "tol_norm",
300
+ "line_search",
301
+ "jac_options",
302
+ ],
303
+ "jac_options": ["alpha"],
304
+ },
305
+ "excitingmixing": {
306
+ "uses_jac": False,
307
+ "valid_options": [
308
+ "nit",
309
+ "disp",
310
+ "maxiter",
311
+ "ftol",
312
+ "fatol",
313
+ "xtol",
314
+ "xatol",
315
+ "tol_norm",
316
+ "line_search",
317
+ "jac_options",
318
+ ],
319
+ "jac_options": ["alpha", "alphamax"],
320
+ },
321
+ "krylov": {
322
+ "uses_jac": False,
323
+ "valid_options": [
324
+ "nit",
325
+ "disp",
326
+ "maxiter",
327
+ "ftol",
328
+ "fatol",
329
+ "xtol",
330
+ "xatol",
331
+ "tol_norm",
332
+ "line_search",
333
+ "jac_options",
334
+ ],
335
+ "jac_options": [
336
+ "rdiff",
337
+ "method",
338
+ "inner_M",
339
+ "inner_tol",
340
+ "inner_maxiter",
341
+ "outer_k",
342
+ ],
343
+ },
344
+ "df-sane": {
345
+ "uses_jac": False,
346
+ "valid_options": [
347
+ "ftol",
348
+ "fatol",
349
+ "fnorm",
350
+ "maxfev",
351
+ "disp",
352
+ "eta_strategy",
353
+ "sigma_eps",
354
+ "sigma_0",
355
+ "M",
356
+ "line_search",
357
+ ],
358
+ },
359
+ }
@@ -0,0 +1,105 @@
1
+ from collections.abc import Callable
2
+ from functools import partial
3
+
4
+ import numpy as np
5
+
6
+ from scipy.optimize import OptimizeResult
7
+ from scipy.optimize import minimize as sp_minimize
8
+ from scipy.sparse.linalg import LinearOperator
9
+
10
+ from better_optimize.constants import minimize_method
11
+ from better_optimize.utilities import (
12
+ check_f_is_fused,
13
+ determine_maxiter,
14
+ determine_tolerance,
15
+ kwargs_to_options,
16
+ validate_provided_functions_minimize,
17
+ )
18
+ from better_optimize.wrapper import ObjectiveWrapper, optimzer_early_stopping_wrapper
19
+
20
+
21
+ def minimize(
22
+ f: Callable[..., float | tuple[float, np.ndarray]],
23
+ x0: np.ndarray,
24
+ method: minimize_method,
25
+ jac: Callable[..., np.ndarray] | None = None,
26
+ hess: Callable[..., np.ndarray | LinearOperator] | None = None,
27
+ hessp: Callable[..., np.ndarray] | None = None,
28
+ progressbar: bool = True,
29
+ verbose: bool = True,
30
+ args: tuple | None = None,
31
+ **optimizer_kwargs,
32
+ ) -> OptimizeResult:
33
+ """
34
+ Solve a minimization problem using the scipy.optimize.minimize function.
35
+
36
+ Parameters
37
+ ----------
38
+ x0: np.ndarray
39
+ The initial values of the parameters to optimize
40
+ f: Callable
41
+ The objective function to minimize
42
+ args: tuple, optional
43
+ Additional arguments to pass to the objective function. Additional arguments are also passed to the gradient
44
+ and Hessian functions, if provided
45
+ jac: Callable, optional
46
+ The gradient of the objective function
47
+ hess: Callable, optional
48
+ The Hessian of the objective function
49
+ hessp: Callable, optional
50
+ The Hessian-vector product of the objective function
51
+ method: str
52
+ The optimization method to use
53
+ progressbar: bool
54
+ Whether to display a progress bar
55
+ verbose: bool
56
+ Whether to display verbose output
57
+ optimizer_kwargs
58
+ Additional keyword arguments to pass to the optimizer
59
+
60
+ Returns
61
+ -------
62
+ optimizer_result: OptimizeResult
63
+ Optimization result
64
+
65
+ """
66
+ has_fused_f_and_grad = check_f_is_fused(f, x0, args)
67
+ validate_provided_functions_minimize(
68
+ method, jac, hess, hessp, has_fused_f_and_grad, verbose=verbose
69
+ )
70
+
71
+ options = optimizer_kwargs.pop("options", {})
72
+ optimizer_kwargs["options"] = options
73
+
74
+ optimizer_kwargs = kwargs_to_options(optimizer_kwargs, method)
75
+ maxiter, optimizer_kwargs = determine_maxiter(optimizer_kwargs, method)
76
+ optimizer_kwargs = determine_tolerance(optimizer_kwargs, method)
77
+
78
+ # Test hessian function -- if it returns a LinearOperator, it can't be used inside the wrapper
79
+ args = () if args is None else args
80
+ use_hess = hess is not None and not isinstance(hess(x0, *args), LinearOperator)
81
+
82
+ objective = ObjectiveWrapper(
83
+ maxeval=maxiter,
84
+ f=f,
85
+ jac=jac,
86
+ hess=hess if use_hess else None,
87
+ args=args,
88
+ progressbar=progressbar,
89
+ has_fused_f_and_grad=has_fused_f_and_grad,
90
+ )
91
+
92
+ f_optim = partial(
93
+ sp_minimize,
94
+ fun=objective,
95
+ x0=x0,
96
+ method=method,
97
+ jac=True if has_fused_f_and_grad or jac is not None else None,
98
+ hess=hess,
99
+ hessp=hessp,
100
+ callback=objective.callback,
101
+ **optimizer_kwargs,
102
+ )
103
+
104
+ optimizer_result = optimzer_early_stopping_wrapper(f_optim)
105
+ return optimizer_result
@@ -0,0 +1,96 @@
1
+ from collections.abc import Callable
2
+ from functools import partial
3
+
4
+ import numpy as np
5
+
6
+ from scipy.optimize import OptimizeResult
7
+ from scipy.optimize import root as sp_root
8
+
9
+ from better_optimize.constants import root_method
10
+ from better_optimize.utilities import (
11
+ check_f_is_fused,
12
+ determine_maxiter,
13
+ determine_tolerance,
14
+ kwargs_to_jac_options,
15
+ kwargs_to_options,
16
+ validate_provided_functions_root,
17
+ )
18
+ from better_optimize.wrapper import ObjectiveWrapper, optimzer_early_stopping_wrapper
19
+
20
+
21
+ def root(
22
+ f: Callable[..., np.ndarray | tuple[np.ndarray, np.ndarray]],
23
+ x0: np.ndarray,
24
+ method: root_method,
25
+ jac: Callable[..., np.ndarray] | None = None,
26
+ progressbar: bool = True,
27
+ verbose: bool = True,
28
+ args: tuple | None = None,
29
+ **optimizer_kwargs,
30
+ ) -> OptimizeResult:
31
+ """
32
+ Solve a minimization problem using the scipy.optimize.minimize function.
33
+
34
+ Parameters
35
+ ----------
36
+ x0: np.ndarray
37
+ The initial values of the parameters to optimize
38
+ f: Callable
39
+ The objective function to minimize
40
+ args: tuple, optional
41
+ Additional arguments to pass to the objective function. Additional arguments are also passed to the gradient
42
+ and Hessian functions, if provided
43
+ jac: Callable, optional
44
+ The gradient of the objective function
45
+ hess: Callable, optional
46
+ The Hessian of the objective function
47
+ hessp: Callable, optional
48
+ The Hessian-vector product of the objective function
49
+ method: str
50
+ The optimization method to use
51
+ progressbar: bool
52
+ Whether to display a progress bar
53
+ verbose: bool
54
+ Whether to display verbose output
55
+ optimizer_kwargs
56
+ Additional keyword arguments to pass to the optimizer
57
+
58
+ Returns
59
+ -------
60
+ optimizer_result: OptimizeResult
61
+ Optimization result
62
+
63
+ """
64
+ has_fused_f_and_grad = check_f_is_fused(f, x0, args)
65
+ validate_provided_functions_root(
66
+ method, jac, has_fused_f_and_grad, has_fused_f_and_grad, verbose=verbose
67
+ )
68
+
69
+ options = optimizer_kwargs.pop("options", {})
70
+ optimizer_kwargs["options"] = options
71
+ optimizer_kwargs = kwargs_to_options(optimizer_kwargs, method)
72
+ optimizer_kwargs = kwargs_to_jac_options(optimizer_kwargs, method)
73
+
74
+ maxiter, optimizer_kwargs = determine_maxiter(optimizer_kwargs, method)
75
+ optimizer_kwargs = determine_tolerance(optimizer_kwargs, method)
76
+
77
+ objective = ObjectiveWrapper(
78
+ maxeval=maxiter,
79
+ f=f,
80
+ jac=jac,
81
+ args=args,
82
+ progressbar=progressbar,
83
+ has_fused_f_and_grad=has_fused_f_and_grad,
84
+ )
85
+
86
+ f_optim = partial(
87
+ sp_root,
88
+ fun=objective,
89
+ x0=x0,
90
+ method=method,
91
+ jac=True if has_fused_f_and_grad or jac is not None else None,
92
+ **optimizer_kwargs,
93
+ )
94
+
95
+ optimizer_result = optimzer_early_stopping_wrapper(f_optim)
96
+ return optimizer_result
@@ -0,0 +1,188 @@
1
+ import logging
2
+
3
+ from collections.abc import Callable
4
+ from copy import deepcopy
5
+
6
+ import numpy as np
7
+
8
+ from better_optimize.constants import (
9
+ MINIMIZE_MODE_KWARGS,
10
+ ROOT_MODE_KWARGS,
11
+ TOLERANCES,
12
+ minimize_method,
13
+ root_method,
14
+ )
15
+
16
+ _log = logging.getLogger(__name__)
17
+
18
+
19
+ def get_option_kwargs(method: minimize_method | root_method):
20
+ if method in MINIMIZE_MODE_KWARGS.keys():
21
+ options_kwargs = MINIMIZE_MODE_KWARGS[method]
22
+ elif method in ROOT_MODE_KWARGS.keys():
23
+ options_kwargs = ROOT_MODE_KWARGS[method]
24
+ else:
25
+ raise ValueError(f"Unknown method: {method}")
26
+
27
+ return options_kwargs
28
+
29
+
30
+ def validate_provided_functions_minimize(
31
+ method: minimize_method,
32
+ f_grad: Callable[[np.ndarray], np.ndarray] | None,
33
+ f_hess: Callable[[np.ndarray], np.ndarray] | None,
34
+ f_hessp: Callable[[np.ndarray], np.ndarray] | None,
35
+ has_fused_f_and_grad: bool,
36
+ verbose=True,
37
+ ) -> None:
38
+ has_grad, has_hess, has_hessp = map(lambda f: f is not None, [f_grad, f_hess, f_hessp])
39
+ uses_grad, uses_hess, uses_hessp, _ = MINIMIZE_MODE_KWARGS[method].values()
40
+
41
+ if has_fused_f_and_grad and has_grad:
42
+ _log.warning(
43
+ "Objective function returns a tuple (interpreted as (loss, gradient), but a gradient function was "
44
+ "also provided. The gradient function will be ignored."
45
+ )
46
+
47
+ elif has_fused_f_and_grad:
48
+ has_grad = True
49
+
50
+ if method not in MINIMIZE_MODE_KWARGS:
51
+ raise ValueError(
52
+ f"Method {method} not recognized. Must be one of {list(MINIMIZE_MODE_KWARGS.keys())}"
53
+ )
54
+
55
+ if has_hess and has_hessp:
56
+ raise ValueError(
57
+ "Cannot ask for Hessian and Hessian-vector product at the same time. For all algorithms "
58
+ "except trust-exact and dogleg, use_hessp is preferred."
59
+ )
60
+
61
+ if has_grad and not uses_grad and verbose:
62
+ _log.warning(
63
+ f"Gradient provided but not used by method {method}. Gradients will still be evaluated at each "
64
+ f"optimzer step and the norm will be reported as a diagnositc. For large problems, this may be "
65
+ f"computationally intensive."
66
+ )
67
+
68
+ if (has_hess and not uses_hess) or (has_hessp and not uses_hessp) and verbose:
69
+ _log.warning(
70
+ f"Hessian provided but not used by method {method}. The full hessian will still be evaluated at "
71
+ f"each optimzer step and the norm will be reported as a diagnositc. For large problems, this may "
72
+ f"be computationally intensive."
73
+ )
74
+
75
+ if has_hess and not has_hessp and uses_hessp and uses_hess:
76
+ _log.warning(
77
+ f"You provided a function to compute the full hessian, but method {method} allows the use of a "
78
+ f"hessian-vector product instead. Consider passing hessp instead -- this may be significantly "
79
+ f"more efficient."
80
+ )
81
+
82
+
83
+ def validate_provided_functions_root(
84
+ method: root_method, f, jac, has_fused_f_and_grad: bool, verbose: bool = True
85
+ ):
86
+ has_jac = jac is not None
87
+ info_dict = get_option_kwargs(method)
88
+ uses_jac = info_dict["uses_jac"]
89
+
90
+ if has_fused_f_and_grad and has_jac:
91
+ _log.warning(
92
+ "Objective function returns a tuple (interpreted as (loss, gradient), but a gradient function was "
93
+ "also provided. The gradient function will be ignored."
94
+ )
95
+
96
+ elif has_fused_f_and_grad:
97
+ has_jac = True
98
+
99
+ if has_jac and not uses_jac and verbose:
100
+ _log.warning(
101
+ f"Gradient provided but not used by method {method}. Gradients will still be evaluated at each "
102
+ f"optimzer step and the norm will be reported as a diagnositc. For large problems, this may be "
103
+ f"computationally intensive."
104
+ )
105
+
106
+
107
+ def check_f_is_fused(f, x0, args):
108
+ args = () if args is None else args
109
+ output = f(x0, *args)
110
+ if not isinstance(output, tuple | list):
111
+ return False
112
+
113
+ # If the output is a tuple, it should be length 2 (returning the value and the grad).If not, something is wrong
114
+ if len(output) != 2:
115
+ raise ValueError(
116
+ "Objective function should return either a scalar loss or a two-tuple of (loss, gradient)"
117
+ )
118
+ return True
119
+
120
+
121
+ def determine_maxiter(
122
+ optimizer_kwargs: dict, method: minimize_method | root_method
123
+ ) -> tuple[int, dict]:
124
+ MAXITER_KWARGS = ["maxiter", "maxfun", "maxfev"]
125
+ maxiter = optimizer_kwargs.pop("maxiter", 5000)
126
+ optimizer_kwargs["options"].update({"maxiter": maxiter})
127
+ maxiter_kwargs = [x for x in get_option_kwargs(method)["valid_options"] if x in MAXITER_KWARGS]
128
+
129
+ for kwarg in maxiter_kwargs:
130
+ if kwarg not in optimizer_kwargs["options"]:
131
+ optimizer_kwargs["options"][kwarg] = maxiter
132
+
133
+ return maxiter, optimizer_kwargs
134
+
135
+
136
+ def kwargs_to_options(optimizer_kwargs: dict, method: minimize_method | root_method) -> dict:
137
+ optimizer_kwargs = deepcopy(optimizer_kwargs)
138
+
139
+ NEVER_AUTO_PROMOTE = ["bounds", "tol", "jac_options"]
140
+ option_kwargs = get_option_kwargs(method)["valid_options"]
141
+
142
+ provided_kwargs = list(optimizer_kwargs.keys())
143
+ options = optimizer_kwargs.get("options", {})
144
+
145
+ for kwarg in option_kwargs:
146
+ if kwarg in provided_kwargs and kwarg not in NEVER_AUTO_PROMOTE:
147
+ options[kwarg] = optimizer_kwargs.pop(kwarg)
148
+
149
+ optimizer_kwargs["options"] = options
150
+ return optimizer_kwargs
151
+
152
+
153
+ def kwargs_to_jac_options(optimizer_kwargs: dict, method: root_method) -> dict:
154
+ provided_kwargs = list(optimizer_kwargs.keys())
155
+ method_jac_kwargs = get_option_kwargs(method).get("jac_options", None)
156
+
157
+ if method_jac_kwargs is None:
158
+ return optimizer_kwargs
159
+
160
+ optimizer_kwargs = deepcopy(optimizer_kwargs)
161
+
162
+ if "options" not in optimizer_kwargs:
163
+ optimizer_kwargs["options"] = {}
164
+
165
+ if (
166
+ any(kwarg in method_jac_kwargs for kwarg in provided_kwargs)
167
+ and "jac_options" not in optimizer_kwargs["options"]
168
+ ):
169
+ optimizer_kwargs["options"]["jac_options"] = {}
170
+
171
+ for kwarg in provided_kwargs:
172
+ if kwarg in method_jac_kwargs:
173
+ optimizer_kwargs["options"]["jac_options"][kwarg] = optimizer_kwargs.pop(kwarg)
174
+
175
+ return optimizer_kwargs
176
+
177
+
178
+ def determine_tolerance(optimizer_kwargs: dict, method: minimize_method | root_method) -> dict:
179
+ tol = optimizer_kwargs.pop("tol", 1e-8)
180
+ optimizer_kwargs["tol"] = tol
181
+ method_options = get_option_kwargs(method)["valid_options"]
182
+ method_tolerances = [tol_type for tol_type in method_options if tol_type in TOLERANCES]
183
+
184
+ for tol_name in method_tolerances:
185
+ if tol_name not in optimizer_kwargs["options"]:
186
+ optimizer_kwargs["options"][tol_name] = tol
187
+
188
+ return optimizer_kwargs
@@ -0,0 +1,180 @@
1
+ import logging
2
+
3
+ from collections.abc import Callable
4
+
5
+ import numpy as np
6
+
7
+ from rich.progress import (
8
+ BarColumn,
9
+ MofNCompleteColumn,
10
+ Progress,
11
+ SpinnerColumn,
12
+ TextColumn,
13
+ TimeElapsedColumn,
14
+ )
15
+ from rich.table import Column
16
+
17
+ _log = logging.getLogger(__name__)
18
+
19
+
20
+ class ObjectiveWrapper:
21
+ def __init__(
22
+ self,
23
+ f: Callable[[np.ndarray | float, ...], float | np.ndarray | tuple[float, np.ndarray]],
24
+ jac: Callable[[np.ndarray | float, ...], np.ndarray] | None = None,
25
+ hess: Callable[[np.ndarray | float, ...], np.ndarray] | None = None,
26
+ has_fused_f_and_grad: bool = False,
27
+ args: tuple | None = None,
28
+ maxeval: int = 5000,
29
+ progressbar: bool = True,
30
+ update_every: int = 10,
31
+ ):
32
+ self.n_eval = 0
33
+ self.maxeval = maxeval
34
+ self.args = args if args is not None else ()
35
+ self.f = lambda x: f(x, *self.args)
36
+ self.use_jac = False
37
+ self.use_hess = False
38
+ self.has_fused_f_and_grad = has_fused_f_and_grad
39
+
40
+ self.progress = None
41
+ self.task = None
42
+
43
+ self.update_every = update_every
44
+ self.interrupted = False
45
+ self.desc = "f = {task.fields[f_value]:,.5g}"
46
+
47
+ if jac is not None or has_fused_f_and_grad:
48
+ self.desc += ", ||grad|| = {task.fields[grad_norm]:,.5g}"
49
+ self.use_jac = True
50
+ self.f_jac = lambda x: jac(x, *self.args)
51
+
52
+ if hess is not None:
53
+ self.desc += ", ||hess|| = {task.fields[hess_norm]:,.5g}"
54
+ self.use_hess = True
55
+ self.f_hess = lambda x: hess(x, *self.args)
56
+
57
+ self.previous_x = None
58
+ self.progressbar = progressbar
59
+ if progressbar:
60
+ self.progress = self.initialize_progress_bar()
61
+ self.progress.start()
62
+
63
+ def step(self, x):
64
+ grad = None
65
+ hess = None
66
+ if self.has_fused_f_and_grad:
67
+ value, grad = self.f(x)
68
+ else:
69
+ value = self.f(x)
70
+
71
+ if self.use_jac and not self.has_fused_f_and_grad:
72
+ grad = self.f_jac(x)
73
+ if self.use_hess:
74
+ hess = self.f_hess(x)
75
+
76
+ if np.all(np.isfinite(x)):
77
+ self.previous_x = x
78
+
79
+ if self.n_eval % self.update_every == 0:
80
+ self.update_progressbar(value, grad, hess)
81
+
82
+ if self.n_eval > self.maxeval:
83
+ self.update_progressbar(value, grad, hess)
84
+ self.interrupted = True
85
+
86
+ self.n_eval += 1
87
+
88
+ if self.use_hess:
89
+ return value, grad, hess
90
+ elif self.use_jac:
91
+ return value, grad
92
+ else:
93
+ return value
94
+
95
+ def __call__(self, x):
96
+ try:
97
+ return self.step(x)
98
+ except (KeyboardInterrupt, StopIteration):
99
+ self.interrupted = True
100
+ return self.step(x)
101
+
102
+ def callback(self, *args):
103
+ if self.interrupted:
104
+ raise StopIteration
105
+
106
+ def update_progressbar(
107
+ self, value: float, grad: np.float64 = None, hess: np.float64 = None, completed=False
108
+ ) -> None:
109
+ if not self.progressbar:
110
+ return
111
+
112
+ if isinstance(value, np.ndarray):
113
+ value = (value**2).sum()
114
+ elif isinstance(value, list | tuple):
115
+ value = sum([x**2 for x in value])
116
+
117
+ value_dict = {"f_value": value}
118
+ if grad is not None:
119
+ grad_norm = np.linalg.norm(grad)
120
+ value_dict["grad_norm"] = grad_norm
121
+ if hess is not None:
122
+ hess_norm = np.linalg.norm(hess)
123
+ value_dict["hess_norm"] = hess_norm
124
+
125
+ if self.n_eval == 0:
126
+ self.task = self.progress.add_task("Optimizing", **value_dict)
127
+
128
+ if not completed:
129
+ self.progress.update(self.task, advance=self.update_every, **value_dict)
130
+ else:
131
+ self.progress.update(self.task, total=self.n_eval, completed=self.n_eval, **value_dict)
132
+
133
+ def initialize_progress_bar(self):
134
+ text_column = TextColumn("{task.description}", table_column=Column(ratio=1))
135
+ bar_column = BarColumn(bar_width=None, table_column=Column(ratio=2))
136
+ time_column = TimeElapsedColumn()
137
+ m_of_n = MofNCompleteColumn()
138
+ spinner = SpinnerColumn()
139
+
140
+ stat_column = TextColumn(self.desc, table_column=Column(ratio=1))
141
+
142
+ return Progress(
143
+ text_column, spinner, bar_column, time_column, m_of_n, stat_column, expand=False
144
+ )
145
+
146
+
147
+ def optimzer_early_stopping_wrapper(f_optim):
148
+ objective = f_optim.keywords["fun"]
149
+ progressbar = objective.progressbar
150
+
151
+ try:
152
+ # Do the optimization. This calls either optimize.root or optimize.minimize; all arguments are pre-configured
153
+ # at this point.
154
+ res = f_optim()
155
+ except Exception as e:
156
+ # Teardown the progress bar if necessary, then forward the error
157
+ if progressbar:
158
+ objective.progress.stop()
159
+ raise e
160
+
161
+ x_final = res.x
162
+
163
+ if progressbar:
164
+ # Evaluate the objective function one last time to get the final value, gradient, and hessian
165
+ # and update the progress bar to show the true final state
166
+ outputs = objective.step(x_final)
167
+ if not objective.use_jac and not objective.use_hess:
168
+ value = outputs
169
+ grad = None
170
+ hess = None
171
+ elif objective.use_jac and not objective.use_hess:
172
+ value, grad = outputs
173
+ hess = None
174
+ else:
175
+ value, grad, hess = outputs
176
+
177
+ objective.update_progressbar(value, grad, hess, completed=True)
178
+ objective.progress.stop()
179
+
180
+ return res
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: better-optimize
3
- Version: 0.0.0
3
+ Version: 0.0.2
4
4
  Summary: A drop-in replacement for scipy optimize functions with quality of life improvements
5
5
  License: MIT License
6
6
  Project-URL: Homepage, https://github.com/jessegrabowski/better_optimize
@@ -14,6 +14,7 @@ Classifier: Programming Language :: Python :: 3.11
14
14
  Classifier: Programming Language :: Python :: 3.12
15
15
  Classifier: Topic :: Scientific/Engineering
16
16
  Classifier: Topic :: Scientific/Engineering :: Mathematics
17
+ Requires-Python: <3.13,>=3.10
17
18
  Description-Content-Type: text/markdown
18
19
  Provides-Extra: tests
19
20
 
@@ -0,0 +1,15 @@
1
+ README.md
2
+ pyproject.toml
3
+ setup.py
4
+ better_optimize/__init__.py
5
+ better_optimize/_version.py
6
+ better_optimize/constants.py
7
+ better_optimize/minimize.py
8
+ better_optimize/root.py
9
+ better_optimize/utilities.py
10
+ better_optimize/wrapper.py
11
+ better_optimize.egg-info/PKG-INFO
12
+ better_optimize.egg-info/SOURCES.txt
13
+ better_optimize.egg-info/dependency_links.txt
14
+ better_optimize.egg-info/requires.txt
15
+ better_optimize.egg-info/top_level.txt
@@ -1,6 +1,15 @@
1
+ [build-system]
2
+ requires = [
3
+ "numpy",
4
+ "setuptools",
5
+ "versioneer[toml]",
6
+ ]
7
+ build-backend = "setuptools.build_meta"
8
+
1
9
  [project]
2
10
  name = 'better_optimize'
3
11
  dynamic = ['version']
12
+ requires-python = ">=3.10,<3.13"
4
13
  description = "A drop-in replacement for scipy optimize functions with quality of life improvements"
5
14
  readme = "README.md"
6
15
  license = {text = "MIT License"}
@@ -58,7 +67,7 @@ exclude_lines = [
58
67
  [tool.ruff]
59
68
  line-length = 100
60
69
  target-version = "py310"
61
- extend-exclude = ["better_optimize/_version.py"]
70
+ extend-exclude = ["better_optimize/_version.py", "setup.py"]
62
71
 
63
72
  [tool.ruff.lint]
64
73
  select = ["D", "E", "F", "I", "UP", "W", "RUF"]
@@ -0,0 +1,19 @@
1
+ from setuptools import find_packages, setup
2
+ from setuptools.dist import Distribution
3
+
4
+ import versioneer
5
+
6
+ dist = Distribution()
7
+ dist.parse_config_files()
8
+
9
+
10
+ NAME: str = dist.get_name() # type: ignore
11
+
12
+
13
+ if __name__ == "__main__":
14
+ setup(
15
+ name=NAME,
16
+ version=versioneer.get_version(),
17
+ cmdclass=versioneer.get_cmdclass(),
18
+ packages=find_packages(exclude=["tests*"]),
19
+ )
@@ -1,8 +0,0 @@
1
- README.md
2
- pyproject.toml
3
- setup.py
4
- better_optimize.egg-info/PKG-INFO
5
- better_optimize.egg-info/SOURCES.txt
6
- better_optimize.egg-info/dependency_links.txt
7
- better_optimize.egg-info/requires.txt
8
- better_optimize.egg-info/top_level.txt
@@ -1,3 +0,0 @@
1
- from setuptools import setup
2
-
3
- setup()