Skip to content

Commit 6d037a1

Browse files
ragonneaurgommerslucascolley
authored
ENH: add cobyqa to scipy.optimize. (scipy#19994)
Co-authored-by: Tom M. Ragonneau <t.ragonneau@gmail.com> Co-authored-by: Ralf Gommers <ralf.gommers@gmail.com> Co-authored-by: Lucas Colley <lucas.colley8@gmail.com>
1 parent 7e69656 commit 6d037a1

21 files changed

+482
-55
lines changed

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,6 @@
2323
[submodule "scipy/_lib/pocketfft"]
2424
path = scipy/_lib/pocketfft
2525
url = https://github.com/scipy/pocketfft
26+
[submodule "scipy/_lib/cobyqa"]
27+
path = scipy/_lib/cobyqa
28+
url = https://github.com/cobyqa/cobyqa.git

benchmarks/benchmarks/optimize.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -273,8 +273,8 @@ def bench_run(self, x0, methods=None, **minimizer_kwargs):
273273

274274
# L-BFGS-B, BFGS, trust-constr, SLSQP can use gradients, but examine
275275
# performance when numerical differentiation is used.
276-
fonly_methods = ["COBYLA", 'Powell', 'nelder-mead', 'L-BFGS-B', 'BFGS',
277-
'trust-constr', 'SLSQP']
276+
fonly_methods = ["COBYLA", 'COBYQA', 'Powell', 'nelder-mead',
277+
'L-BFGS-B', 'BFGS', 'trust-constr', 'SLSQP']
278278
for method in fonly_methods:
279279
if method not in methods:
280280
continue
@@ -316,7 +316,7 @@ class BenchSmoothUnbounded(Benchmark):
316316
['rosenbrock_slow', 'rosenbrock_nograd', 'rosenbrock', 'rosenbrock_tight',
317317
'simple_quadratic', 'asymmetric_quadratic',
318318
'sin_1d', 'booth', 'beale', 'LJ'],
319-
["COBYLA", 'Powell', 'nelder-mead',
319+
["COBYLA", 'COBYQA', 'Powell', 'nelder-mead',
320320
'L-BFGS-B', 'BFGS', 'CG', 'TNC', 'SLSQP',
321321
"Newton-CG", 'dogleg', 'trust-ncg', 'trust-exact',
322322
'trust-krylov', 'trust-constr'],
@@ -599,7 +599,8 @@ class BenchDFO(Benchmark):
599599

600600
params = [
601601
list(range(53)), # adjust which problems to solve
602-
["COBYLA", "SLSQP", "Powell", "nelder-mead", "L-BFGS-B", "BFGS",
602+
["COBYLA", "COBYQA", "SLSQP", "Powell", "nelder-mead", "L-BFGS-B",
603+
"BFGS",
603604
"trust-constr"], # note: methods must also be listed in bench_run
604605
["mean_nfev", "min_obj"], # defined in average_results
605606
]
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.. _optimize.minimize-cobyqa:
2+
3+
minimize(method='COBYQA')
4+
-------------------------
5+
6+
.. scipy-optimize:function:: scipy.optimize.minimize
7+
:impl: scipy.optimize._cobyqa_py._minimize_cobyqa
8+
:method: COBYQA

doc/source/tutorial/optimize.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -541,8 +541,8 @@ Constrained minimization of multivariate scalar functions (:func:`minimize`)
541541
----------------------------------------------------------------------------
542542

543543
The :func:`minimize` function provides algorithms for constrained minimization,
544-
namely ``'trust-constr'`` , ``'SLSQP'`` and ``'COBYLA'``. They require the constraints
545-
to be defined using slightly different structures. The method ``'trust-constr'`` requires
544+
namely ``'trust-constr'`` , ``'SLSQP'``, ``'COBYLA'``, and ``'COBYQA'``. They require the constraints
545+
to be defined using slightly different structures. The methods ``'trust-constr'`` and ``'COBYQA'`` require
546546
the constraints to be defined as a sequence of objects :func:`LinearConstraint` and
547547
:func:`NonlinearConstraint`. Methods ``'SLSQP'`` and ``'COBYLA'``, on the other hand,
548548
require constraints to be defined as a sequence of dictionaries, with keys

pytest.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[pytest]
22
addopts = -l
3-
norecursedirs = doc tools scipy/_lib/array_api_compat scipy/_lib/highs
3+
norecursedirs = doc tools scipy/_lib/array_api_compat scipy/_lib/cobyqa scipy/_lib/highs
44
junit_family=xunit2
55

66
filterwarnings =

scipy/_lib/cobyqa

Submodule cobyqa added at dee1a92

scipy/_lib/meson.build

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ endif
1414
if not fs.exists('pocketfft/README.md')
1515
error('Missing the `pocketfft` submodule! Run `git submodule update --init` to fix this.')
1616
endif
17+
if not fs.exists('cobyqa/README.rst')
18+
error('Missing the `cobyqa` submodule! Run `git submodule update --init` to fix this.')
19+
endif
1720

1821
_lib_pxd = [
1922
fs.copyfile('__init__.py'),
@@ -199,5 +202,36 @@ py3.install_sources(
199202
subdir: 'scipy/_lib/array_api_compat/torch',
200203
)
201204

205+
py3.install_sources(
206+
[
207+
'cobyqa/cobyqa/__init__.py',
208+
'cobyqa/cobyqa/framework.py',
209+
'cobyqa/cobyqa/main.py',
210+
'cobyqa/cobyqa/models.py',
211+
'cobyqa/cobyqa/problem.py',
212+
'cobyqa/cobyqa/settings.py',
213+
],
214+
subdir: 'scipy/_lib/cobyqa',
215+
)
216+
217+
py3.install_sources(
218+
[
219+
'cobyqa/cobyqa/subsolvers/__init__.py',
220+
'cobyqa/cobyqa/subsolvers/geometry.py',
221+
'cobyqa/cobyqa/subsolvers/optim.py',
222+
],
223+
subdir: 'scipy/_lib/cobyqa/subsolvers',
224+
)
225+
226+
py3.install_sources(
227+
[
228+
'cobyqa/cobyqa/utils/__init__.py',
229+
'cobyqa/cobyqa/utils/exceptions.py',
230+
'cobyqa/cobyqa/utils/math.py',
231+
'cobyqa/cobyqa/utils/versions.py',
232+
],
233+
subdir: 'scipy/_lib/cobyqa/utils',
234+
)
235+
202236
subdir('_uarray')
203237
subdir('tests')

scipy/optimize/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
optimize.minimize-lbfgsb
6666
optimize.minimize-tnc
6767
optimize.minimize-cobyla
68+
optimize.minimize-cobyqa
6869
optimize.minimize-slsqp
6970
optimize.minimize-trustconstr
7071
optimize.minimize-dogleg

scipy/optimize/_cobyqa_py.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import numpy as np
2+
3+
from ._optimize import _check_unknown_options
4+
5+
6+
def _minimize_cobyqa(fun, x0, args=(), bounds=None, constraints=(),
7+
callback=None, disp=False, maxfev=None, maxiter=None,
8+
f_target=-np.inf, feasibility_tol=1e-8,
9+
initial_tr_radius=1.0, final_tr_radius=1e-6, scale=False,
10+
**unknown_options):
11+
"""
12+
Minimize a scalar function of one or more variables using the
13+
Constrained Optimization BY Quadratic Approximations (COBYQA) algorithm.
14+
15+
.. versionadded:: 1.14.0
16+
17+
Options
18+
-------
19+
disp : bool
20+
Set to True to print information about the optimization procedure.
21+
maxfev : int
22+
Maximum number of function evaluations.
23+
maxiter : int
24+
Maximum number of iterations.
25+
f_target : float
26+
Target value for the objective function. The optimization procedure is
27+
terminated when the objective function value of a feasible point (see
28+
`feasibility_tol` below) is less than or equal to this target.
29+
feasibility_tol : float
30+
Absolute tolerance for the constraint violation.
31+
initial_tr_radius : float
32+
Initial trust-region radius. Typically, this value should be in the
33+
order of one tenth of the greatest expected change to the variables.
34+
final_tr_radius : float
35+
Final trust-region radius. It should indicate the accuracy required in
36+
the final values of the variables. If provided, this option overrides
37+
the value of `tol` in the `minimize` function.
38+
scale : bool
39+
Set to True to scale the variables according to the bounds. If True and
40+
if all the lower and upper bounds are finite, the variables are scaled
41+
to be within the range :math:`[-1, 1]`. If any of the lower or upper
42+
bounds is infinite, the variables are not scaled.
43+
"""
44+
from .._lib.cobyqa import minimize # import here to avoid circular imports
45+
46+
_check_unknown_options(unknown_options)
47+
options = {
48+
'disp': bool(disp),
49+
'maxfev': int(maxfev) if maxfev is not None else 500 * len(x0),
50+
'maxiter': int(maxiter) if maxiter is not None else 1000 * len(x0),
51+
'target': float(f_target),
52+
'feasibility_tol': float(feasibility_tol),
53+
'radius_init': float(initial_tr_radius),
54+
'radius_final': float(final_tr_radius),
55+
'scale': bool(scale),
56+
}
57+
return minimize(fun, x0, args, bounds, constraints, callback, options)

scipy/optimize/_minimize.py

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from ._lbfgsb_py import _minimize_lbfgsb
3131
from ._tnc import _minimize_tnc
3232
from ._cobyla_py import _minimize_cobyla
33+
from ._cobyqa_py import _minimize_cobyqa
3334
from ._slsqp_py import _minimize_slsqp
3435
from ._constraints import (old_bound_to_new, new_bounds_to_old,
3536
old_constraint_to_new, new_constraint_to_old,
@@ -38,13 +39,14 @@
3839
from ._differentiable_functions import FD_METHODS
3940

4041
MINIMIZE_METHODS = ['nelder-mead', 'powell', 'cg', 'bfgs', 'newton-cg',
41-
'l-bfgs-b', 'tnc', 'cobyla', 'slsqp', 'trust-constr',
42-
'dogleg', 'trust-ncg', 'trust-exact', 'trust-krylov']
42+
'l-bfgs-b', 'tnc', 'cobyla', 'cobyqa', 'slsqp',
43+
'trust-constr', 'dogleg', 'trust-ncg', 'trust-exact',
44+
'trust-krylov']
4345

4446
# These methods support the new callback interface (passed an OptimizeResult)
4547
MINIMIZE_METHODS_NEW_CB = ['nelder-mead', 'powell', 'cg', 'bfgs', 'newton-cg',
4648
'l-bfgs-b', 'trust-constr', 'dogleg', 'trust-ncg',
47-
'trust-exact', 'trust-krylov']
49+
'trust-exact', 'trust-krylov', 'cobyqa']
4850

4951
MINIMIZE_SCALAR_METHODS = ['brent', 'bounded', 'golden']
5052

@@ -80,6 +82,7 @@ def minimize(fun, x0, args=(), method=None, jac=None, hess=None,
8082
- 'L-BFGS-B' :ref:`(see here) <optimize.minimize-lbfgsb>`
8183
- 'TNC' :ref:`(see here) <optimize.minimize-tnc>`
8284
- 'COBYLA' :ref:`(see here) <optimize.minimize-cobyla>`
85+
- 'COBYQA' :ref:`(see here) <optimize.minimize-cobyqa>`
8386
- 'SLSQP' :ref:`(see here) <optimize.minimize-slsqp>`
8487
- 'trust-constr':ref:`(see here) <optimize.minimize-trustconstr>`
8588
- 'dogleg' :ref:`(see here) <optimize.minimize-dogleg>`
@@ -146,15 +149,15 @@ def minimize(fun, x0, args=(), method=None, jac=None, hess=None,
146149
parameters.
147150
bounds : sequence or `Bounds`, optional
148151
Bounds on variables for Nelder-Mead, L-BFGS-B, TNC, SLSQP, Powell,
149-
trust-constr, and COBYLA methods. There are two ways to specify the
150-
bounds:
152+
trust-constr, COBYLA, and COBYQA methods. There are two ways to specify
153+
the bounds:
151154
152155
1. Instance of `Bounds` class.
153156
2. Sequence of ``(min, max)`` pairs for each element in `x`. None
154157
is used to specify no bound.
155158
156159
constraints : {Constraint, dict} or List of {Constraint, dict}, optional
157-
Constraints definition. Only for COBYLA, SLSQP and trust-constr.
160+
Constraints definition. Only for COBYLA, COBYQA, SLSQP and trust-constr.
158161
159162
Constraints for 'trust-constr' are defined as a single object or a
160163
list of objects specifying constraints to the optimization problem.
@@ -178,6 +181,8 @@ def minimize(fun, x0, args=(), method=None, jac=None, hess=None,
178181
Equality constraint means that the constraint function result is to
179182
be zero whereas inequality means that it is to be non-negative.
180183
Note that COBYLA only supports inequality constraints.
184+
185+
Constraints for COBYQA are defined as any of the above.
181186
tol : float, optional
182187
Tolerance for termination. When `tol` is specified, the selected
183188
minimization algorithm sets some relevant solver-specific tolerance(s)
@@ -335,6 +340,13 @@ def minimize(fun, x0, args=(), method=None, jac=None, hess=None,
335340
constraints functions 'fun' may return either a single number
336341
or an array or list of numbers.
337342
343+
Method :ref:`COBYQA <optimize.minimize-cobyqa>` uses the Constrained
344+
Optimization BY Quadratic Approximations (COBYQA) method [18]_. The
345+
algorithm is a derivative-free trust-region SQP method based on quadratic
346+
approximations to the objective function and each nonlinear constraint. The
347+
bounds are treated as unrelaxable constraints, in the sense that the
348+
algorithm always respects them throughout the optimization process.
349+
338350
Method :ref:`SLSQP <optimize.minimize-slsqp>` uses Sequential
339351
Least SQuares Programming to minimize a function of several
340352
variables with any combination of bounds, equality and inequality
@@ -466,6 +478,10 @@ def minimize(fun, x0, args=(), method=None, jac=None, hess=None,
466478
.. [17] Lalee, Marucha, Jorge Nocedal, and Todd Plantega. 1998. On the
467479
implementation of an algorithm for large-scale equality constrained
468480
optimization. SIAM Journal on Optimization 8.3: 682-706.
481+
.. [18] Ragonneau, T. M. *Model-Based Derivative-Free Optimization Methods
482+
and Software*. PhD thesis, Department of Applied Mathematics, The Hong
483+
Kong Polytechnic University, Hong Kong, China, 2022. URL:
484+
https://theses.lib.polyu.edu.hk/handle/200/12294.
469485
470486
Examples
471487
--------
@@ -558,7 +574,7 @@ def minimize(fun, x0, args=(), method=None, jac=None, hess=None,
558574
options = {}
559575
# check if optional parameters are supported by the selected method
560576
# - jac
561-
if meth in ('nelder-mead', 'powell', 'cobyla') and bool(jac):
577+
if meth in ('nelder-mead', 'powell', 'cobyla', 'cobyqa') and bool(jac):
562578
warn('Method %s does not use gradient information (jac).' % method,
563579
RuntimeWarning, stacklevel=2)
564580
# - hess
@@ -574,16 +590,17 @@ def minimize(fun, x0, args=(), method=None, jac=None, hess=None,
574590
'information (hessp).' % method,
575591
RuntimeWarning, stacklevel=2)
576592
# - constraints or bounds
577-
if (meth not in ('cobyla', 'slsqp', 'trust-constr', '_custom') and
593+
if (meth not in ('cobyla', 'cobyqa', 'slsqp', 'trust-constr', '_custom') and
578594
np.any(constraints)):
579595
warn('Method %s cannot handle constraints.' % method,
580596
RuntimeWarning, stacklevel=2)
581-
if meth not in ('nelder-mead', 'powell', 'l-bfgs-b', 'cobyla', 'slsqp',
582-
'tnc', 'trust-constr', '_custom') and bounds is not None:
597+
if meth not in (
598+
'nelder-mead', 'powell', 'l-bfgs-b', 'cobyla', 'cobyqa', 'slsqp',
599+
'tnc', 'trust-constr', '_custom') and bounds is not None:
583600
warn('Method %s cannot handle bounds.' % method,
584601
RuntimeWarning, stacklevel=2)
585602
# - return_all
586-
if (meth in ('l-bfgs-b', 'tnc', 'cobyla', 'slsqp') and
603+
if (meth in ('l-bfgs-b', 'tnc', 'cobyla', 'cobyqa', 'slsqp') and
587604
options.get('return_all', False)):
588605
warn('Method %s does not support the return_all option.' % method,
589606
RuntimeWarning, stacklevel=2)
@@ -624,6 +641,8 @@ def minimize(fun, x0, args=(), method=None, jac=None, hess=None,
624641
options.setdefault('gtol', tol)
625642
if meth in ('cobyla', '_custom'):
626643
options.setdefault('tol', tol)
644+
if meth == 'cobyqa':
645+
options.setdefault('final_tr_radius', tol)
627646
if meth == 'trust-constr':
628647
options.setdefault('xtol', tol)
629648
options.setdefault('gtol', tol)
@@ -718,6 +737,9 @@ def minimize(fun, x0, args=(), method=None, jac=None, hess=None,
718737
elif meth == 'cobyla':
719738
res = _minimize_cobyla(fun, x0, args, constraints, callback=callback,
720739
bounds=bounds, **options)
740+
elif meth == 'cobyqa':
741+
res = _minimize_cobyqa(fun, x0, args, bounds, constraints, callback,
742+
**options)
721743
elif meth == 'slsqp':
722744
res = _minimize_slsqp(fun, x0, args, jac, bounds,
723745
constraints, callback=callback, **options)
@@ -1016,7 +1038,8 @@ def _validate_bounds(bounds, x0, meth):
10161038

10171039
def standardize_bounds(bounds, x0, meth):
10181040
"""Converts bounds to the form required by the solver."""
1019-
if meth in {'trust-constr', 'powell', 'nelder-mead', 'cobyla', 'new'}:
1041+
if meth in {'trust-constr', 'powell', 'nelder-mead', 'cobyla', 'cobyqa',
1042+
'new'}:
10201043
if not isinstance(bounds, Bounds):
10211044
lb, ub = old_bound_to_new(bounds)
10221045
bounds = Bounds(lb, ub)

0 commit comments

Comments
 (0)