Skip to content

Commit ddbb869

Browse files
authored
Add option to use nonlinear constraints (#346)
1 parent d04b369 commit ddbb869

File tree

16 files changed

+1223
-35
lines changed

16 files changed

+1223
-35
lines changed

CHANGES.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ Breaking changes
3333
``estimagic.OptimizeLogReader``.
3434
- Convenience functions to create namedtuples are removed from ``estimagic.utilities``.
3535

36-
36+
- :gh:`346` Add option to use nonlinear constraints (:ghuser:`timmens`)
3737
- :gh:`345` Moves estimation_table to new latex functionality of pandas
3838
(:ghuser:`mpetrosian`)
3939
- :gh:`344` Adds pytree support to slice_plot (:ghuser:`janosg`)

docs/source/explanations/optimization/implementation_of_constraints.rst

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ constraints into constrained optimizers. Reparametrization and penalties. Below
2424
explain what both approaches are, why we chose the reparametrization approach over
2525
penalties and which reparametrizations we are using for each type of constraint.
2626

27+
.. note::
28+
29+
In this text we focus on constraints that can solved by estimagic via bijective and
30+
differentiable transformations. General nonlinear constraints do not fall into this
31+
category. If you want to use nonlinear constraints you can still do so, but
32+
estimagic will simply pass the constraints to your chosen optimizer. See
33+
:ref:`constraints` for more details.
34+
2735

2836
Possible approaches
2937
-------------------
@@ -32,9 +40,9 @@ Possible approaches
3240
Reparametrizations
3341
~~~~~~~~~~~~~~~~~~
3442

35-
In the reparametrization approach need to find an invertible mapping :math:`g` such as
36-
well as two :math:`k'` dimensional vectors :math:`l` and :math:`u` such that:
37-
43+
In the reparametrization approach we need to find an invertible mapping
44+
:math:`g : \mathbb{R}^{k'} \to \mathbb{R}^k`, and two new bounds :math:`l'` and
45+
:math:`u'` such that:
3846

3947
.. math::
4048
@@ -175,7 +183,7 @@ A suitable choice of :math:`\mathbf{\tilde{X}}` and :math:`\mathbf{M}` are:
175183
.. math::
176184
177185
\mathbf{\tilde{X}} \equiv \{(\tilde{x}_1, \tilde{x}_2)^T \mid \mathbf{\tilde{x}}_1
178-
\in \mathbb{R}^{k}$ \text{ and } \mathbf{l} \leq \mathbf{\tilde{x}}_2 \leq \mathbf{l}\}
186+
\in \mathbb{R}^{k} \text{ and } \mathbf{l} \leq \mathbf{\tilde{x}}_2 \leq \mathbf{l}\}
179187
180188
\mathbf{M} =
181189
\left[ {\begin{array}{cc}

docs/source/explanations/optimization/internal_optimizers.rst

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ this interface. The mandatory conditions for an internal optimizer function are:
4343
first derivative jointly
4444
- lower_bounds: for lower bounds in form of a 1d numpy array
4545
- upper_bounds: for upper bounds in form of a 1d numpy array
46+
- nonlinear_constraints: for nonlinear constraints in form a list of dictionaries
4647

4748
Of course, algorithms that do not need a certain argument (e.g. unbounded or
4849
derivative free ones) do not need those arguments at all.
@@ -98,6 +99,53 @@ different optimizers. However, complete transparency is possible and we try to d
9899
the exact meaning of all options for all optimizers.
99100

100101

102+
Nonlinear constraints
103+
---------------------
104+
105+
Estimagic can pass nonlinear constraints to the internal optimizer. The internal
106+
interface for nonlinear constraints is as follows.
107+
108+
A nonlinear constraint is a ``list`` of ``dict`` 's, where each ``dict`` represents a
109+
group of constraints. In each group the constraint function can potentially be
110+
multi-dimensional. We distinguish between equality and inequality constraints, which is
111+
signalled by a dict entry ``type`` that takes values ``"eq"`` and ``"ineq"``. The
112+
constraint function, which takes as input an internal parameter vector, is stored under
113+
the entry ``fun``, while the Jacobian of that function is stored at ``jac``. The
114+
tolerance for the constraints is stored under ``tol``. At last, the number of
115+
constraints in each group is specified under ``n_constr``. An example list with one
116+
constraint that would be passed to the internal optimizer is given by
117+
118+
.. code-block::
119+
120+
constraints = [
121+
{
122+
"type": "ineq",
123+
"n_constr": 1,
124+
"tol": 1e-5,
125+
"fun": lambda x: x**3,
126+
"jac": lambda x: 3 * x**2,
127+
}
128+
]
129+
130+
131+
.. note::
132+
133+
**Equality.** Internal equality constraints assume that the constraint is met when the function is
134+
zero. That is
135+
136+
.. math::
137+
138+
0 = g(x) \in \mathbb{R}^m .
139+
140+
**Inequality.** Internal inequality constraints assume that the constraint is met when the function is
141+
greater or equal to zero. That is
142+
143+
.. math::
144+
145+
0 \leq g(x) \in \mathbb{R}^m .
146+
147+
148+
101149
Other conventions
102150
-----------------
103151

docs/source/how_to_guides/optimization/how_to_specify_constraints.rst

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,25 @@ Constraints vs bounds
1010

1111
Estimagic distinguishes between bounds and constraints. Bounds are lower and upper
1212
bounds for parameters. In the literature they are sometimes called box constraints.
13-
Bounds are specified as "lower_bound" and "upper_bound" column of a params DataFrame
14-
or as pytrees via the ``lower_bounds`` and ``upper_bounds`` argument to ``maximize`` and
15-
``minimize``.
13+
Bounds are specified as ``lower_bounds`` and ``upper_bounds`` argument to ``maximize``
14+
and ``minimize``.
1615

1716
Examples with bounds can be found in `this tutorial`_.
1817

1918
.. _this tutorial: ../../getting_started/first_optimization_with_estimagic.ipynb
2019

21-
Constraints are more general constraints on the parameters. This ranges from rather
22-
simple ones (e.g. Parameters are fixed to a value, a group of parameters is required
23-
to be equal) to more complex ones (like general linear constraints).
20+
To specify more general constraints on the parameters you use can use the argument
21+
``constraints``. This ranges from rather simple ones (e.g. parameters are fixed to a
22+
value, a group of parameters is required to be equal) to more complex ones (like general
23+
linear constraints, or even nonlinear constraints).
2424

2525
Can you use constraints with all optimizers?
2626
============================================
2727

28-
We implement constraints via reparametrizations. Details are explained `here`_. This
29-
means that you can use all of the constraints with any optimizer that supports
30-
bounds. Some constraints (e.g. fixing parameters) can even be used with optimizers
31-
that do not support bounds.
28+
With the exception of general nonlinear constraints, we implement constraints via
29+
reparametrizations. Details are explained `here`_. This means that you can use all of
30+
the constraints with any optimizer that supports bounds. Some constraints (e.g. fixing
31+
parameters) can even be used with optimizers that do not support bounds.
3232

3333
.. _here: ../../explanations/optimization/implementation_of_constraints.rst
3434

@@ -288,8 +288,8 @@ flat numpy array are explained in the next section.
288288
typically it is more convenient to use the special cases instead of expressing
289289
them as a linear constraint. Internally, it will make no difference.
290290

291-
Let's impose the constraint that the sum of the average of the first four parameters
292-
is at least 0.95.
291+
Let's impose the constraint that the average of the first four parameters is at
292+
least 0.95.
293293

294294
.. code-block:: python
295295
@@ -315,6 +315,44 @@ flat numpy array are explained in the next section.
315315
can also be arrays (or even pytrees) with bounds and weights for each selected
316316
parameter.
317317

318+
.. tabbed:: nonlinear
319+
320+
.. warning::
321+
322+
General nonlinear constraints that are specified via a black-box constraint
323+
function can only be used if you choose an optimizer that supports it.
324+
The feature is currently supported by the algorithms:
325+
326+
* ``ipopt``
327+
* ``nlopt``: ``cobyla``, ``slsqp``, ``isres``, ``mma``
328+
* ``scipy``: ``cobyla``, ``slsqp``, ``trust_constr``
329+
330+
You can use nonlinear constraints to express restrictions of the form
331+
``lower_bound <= func(x) <= upper_bound`` or
332+
``func(x) = value`` where ``x`` are the selected parameters and ``func`` is the
333+
constraint function.
334+
335+
Let's impose the constraint that the product of all but the last parameter is 1.
336+
337+
.. code-block:: python
338+
339+
res = minimize(
340+
criterion=criterion,
341+
params=np.ones(6),
342+
algorithm="scipy_slsqp",
343+
constraints={
344+
"type": "nonlinear",
345+
"selector": lambda x: x[:-1],
346+
"func": lambda x: np.prod(x),
347+
"value": 1.0,
348+
},
349+
)
350+
351+
This yields:
352+
353+
>>> array([ 1.31, 1.16, 1.01, 0.87, 0.75, -0. ])
354+
355+
Where the product of the all but the last parameters is equal to 1.
318356

319357

320358
Imposing multiple constraints at once

src/estimagic/optimization/algo_options.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,13 @@
179179
180180
"""
181181

182+
CONSTRAINTS_ABSOLUTE_TOLERANCE = 1e-5
183+
"""float: Allowed tolerance of the equality and inequality constraints for values to be
184+
considered 'feasible'.
185+
186+
"""
187+
188+
182189
"""
183190
-------------------------
184191
Trust Region Parameters

src/estimagic/optimization/cyipopt_optimizers.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ def ipopt(
2424
lower_bounds,
2525
upper_bounds,
2626
*,
27+
# nonlinear constraints
28+
nonlinear_constraints=(),
2729
# convergence criteria
2830
convergence_relative_criterion_tolerance=CONVERGENCE_RELATIVE_CRITERION_TOLERANCE,
2931
dual_inf_tol=1.0,
@@ -494,7 +496,7 @@ def ipopt(
494496
x0=x,
495497
bounds=_get_scipy_bounds(lower_bounds, upper_bounds),
496498
jac=derivative,
497-
constraints=(),
499+
constraints=nonlinear_constraints,
498500
tol=convergence_relative_criterion_tolerance,
499501
options=options,
500502
)

src/estimagic/optimization/get_algorithm.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ def get_final_algorithm(
4949
valid_kwargs,
5050
lower_bounds,
5151
upper_bounds,
52+
nonlinear_constraints,
5253
algo_options,
5354
logging,
5455
db_kwargs,
@@ -64,6 +65,8 @@ def get_final_algorithm(
6465
algorithm function.
6566
lower_bounds (np.ndarray): 1d numpy array with lower bounds.
6667
upper_bounds (np.ndarray): 1d numpy array with upper bounds.
68+
nonlinear_constraints (list[dict]): List of dictionaries, each containing the
69+
specification of a nonlinear constraint.
6770
algo_options (dict): Dictionary with additional keyword arguments for the
6871
algorithm. Entries that are not used by the algorithm are ignored with a
6972
warning.
@@ -80,6 +83,7 @@ def get_final_algorithm(
8083
algo_options=algo_options,
8184
lower_bounds=lower_bounds,
8285
upper_bounds=upper_bounds,
86+
nonlinear_constraints=nonlinear_constraints,
8387
algo_name=algo_name,
8488
valid_kwargs=valid_kwargs,
8589
)
@@ -253,6 +257,7 @@ def _adjust_options_to_algorithm(
253257
algo_options,
254258
lower_bounds,
255259
upper_bounds,
260+
nonlinear_constraints,
256261
algo_name,
257262
valid_kwargs,
258263
):
@@ -291,4 +296,7 @@ def _adjust_options_to_algorithm(
291296
if "upper_bounds" in valid_kwargs:
292297
reduced["upper_bounds"] = upper_bounds
293298

299+
if "nonlinear_constraints" in valid_kwargs:
300+
reduced["nonlinear_constraints"] = nonlinear_constraints
301+
294302
return reduced

0 commit comments

Comments
 (0)