pynumerics 2.3.0__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.
- pynumerics/__init__.py +73 -0
- pynumerics/_bin/Microsoft.CodeAnalysis.CSharp.dll +0 -0
- pynumerics/_bin/Microsoft.CodeAnalysis.LICENSE.txt +27 -0
- pynumerics/_bin/Microsoft.CodeAnalysis.dll +0 -0
- pynumerics/_bin/Numerics.dll +0 -0
- pynumerics/_bin/pynumerics.runtimeconfig.json +15 -0
- pynumerics/_coverage.py +3825 -0
- pynumerics/_util.py +237 -0
- pynumerics/_version.py +40 -0
- pynumerics/bootstrap.py +593 -0
- pynumerics/callbacks.py +791 -0
- pynumerics/copulas.py +910 -0
- pynumerics/data.py +2233 -0
- pynumerics/differentiation.py +348 -0
- pynumerics/distributions.py +4043 -0
- pynumerics/download.py +623 -0
- pynumerics/enums.py +119 -0
- pynumerics/errors.py +245 -0
- pynumerics/functions.py +1142 -0
- pynumerics/goodness_of_fit.py +699 -0
- pynumerics/graphs.py +556 -0
- pynumerics/hypothesis_tests.py +445 -0
- pynumerics/identity.py +144 -0
- pynumerics/integration.py +677 -0
- pynumerics/linalg.py +1408 -0
- pynumerics/machine_learning.py +1335 -0
- pynumerics/marshal.py +733 -0
- pynumerics/mathematics.py +1053 -0
- pynumerics/mcmc.py +1406 -0
- pynumerics/multivariate.py +1539 -0
- pynumerics/ode.py +156 -0
- pynumerics/optimization.py +1736 -0
- pynumerics/probability.py +966 -0
- pynumerics/py.typed +0 -0
- pynumerics/regional.py +522 -0
- pynumerics/roots.py +274 -0
- pynumerics/runtime.py +384 -0
- pynumerics/sampling.py +1758 -0
- pynumerics/special.py +1079 -0
- pynumerics/statistics.py +1958 -0
- pynumerics/timeseries.py +1326 -0
- pynumerics-2.3.0.dist-info/METADATA +142 -0
- pynumerics-2.3.0.dist-info/RECORD +46 -0
- pynumerics-2.3.0.dist-info/WHEEL +5 -0
- pynumerics-2.3.0.dist-info/licenses/LICENSE +10 -0
- pynumerics-2.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1736 @@
|
|
|
1
|
+
"""Local, global, constrained and multi-objective optimization from ``Numerics.Mathematics.Optimization``.
|
|
2
|
+
|
|
3
|
+
Every optimizer class wraps the Numerics optimizer of the same name and shares the interface of
|
|
4
|
+
:class:`Optimizer`: settings are properties (and keyword arguments of the constructor), and
|
|
5
|
+
:meth:`Optimizer.minimize` and :meth:`Optimizer.maximize` run the Numerics search and return an
|
|
6
|
+
:class:`OptimizationResult` with the best parameters, the objective value, the counts, the status,
|
|
7
|
+
the Hessian and the best-so-far trace.
|
|
8
|
+
|
|
9
|
+
Objectives, gradients and constraint functions are Python callables of a float64 array, C# source
|
|
10
|
+
compiled by :func:`pynumerics.callbacks.compile_csharp`, or .NET delegates. A compiled or .NET
|
|
11
|
+
objective runs with no Python crossing per evaluation. With a Python callable the population
|
|
12
|
+
optimizers (:class:`DifferentialEvolution`, :class:`ParticleSwarm`) always evaluate serially, because
|
|
13
|
+
a Python callback holds the GIL (:func:`pynumerics.callbacks.apply_parallel_policy`); with a compiled
|
|
14
|
+
objective ``use_parallel=True`` evaluates each generation on several threads.
|
|
15
|
+
|
|
16
|
+
Settings left unset keep the Numerics defaults. Numerics minimizes internally: for a maximization it
|
|
17
|
+
stores the negated objective as the fitness, and :class:`OptimizationResult` reports the objective
|
|
18
|
+
value itself.
|
|
19
|
+
|
|
20
|
+
:class:`ParetoFront` (dominance, non-dominated sorting, crowding distance, hypervolume and the
|
|
21
|
+
generational distances) takes one row of objective values per point and minimizes every objective.
|
|
22
|
+
|
|
23
|
+
:func:`knapsack` solves the exact 0/1 knapsack, single or multi-constraint.
|
|
24
|
+
:class:`DynamicProgramming` solves finite-horizon and stationary discrete dynamic programs with
|
|
25
|
+
deterministic or stochastic transitions; its stage-value and transition functions are Python
|
|
26
|
+
callables, C# source or .NET delegates, and with a Python callable the solve runs serially
|
|
27
|
+
(:func:`pynumerics.callbacks.apply_parallel_policy`). The shortest-path solvers of the same
|
|
28
|
+
namespace are in :mod:`pynumerics.graphs`.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
from collections.abc import Callable, Sequence
|
|
34
|
+
from dataclasses import dataclass, field
|
|
35
|
+
from typing import Any
|
|
36
|
+
|
|
37
|
+
import numpy as np
|
|
38
|
+
|
|
39
|
+
from pynumerics import callbacks
|
|
40
|
+
from pynumerics._util import _opt, net_enum
|
|
41
|
+
from pynumerics.enums import NetEnum
|
|
42
|
+
from pynumerics.errors import translate_class, translate_public
|
|
43
|
+
from pynumerics.marshal import (
|
|
44
|
+
_int32_array,
|
|
45
|
+
_net_rectangular,
|
|
46
|
+
_numpy_rectangular,
|
|
47
|
+
to_net_doubles,
|
|
48
|
+
to_net_ints,
|
|
49
|
+
to_net_matrix,
|
|
50
|
+
to_numpy_doubles,
|
|
51
|
+
to_numpy_ints,
|
|
52
|
+
to_numpy_matrix,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class OptimizationStatus(NetEnum):
|
|
57
|
+
"""Optimizer outcomes (``Numerics.Mathematics.Optimization.OptimizationStatus``)."""
|
|
58
|
+
|
|
59
|
+
NONE = "None"
|
|
60
|
+
SUCCESS = "Success"
|
|
61
|
+
MAXIMUM_ITERATIONS_REACHED = "MaximumIterationsReached"
|
|
62
|
+
MAXIMUM_FUNCTION_EVALUATIONS_REACHED = "MaximumFunctionEvaluationsReached"
|
|
63
|
+
FAILURE = "Failure"
|
|
64
|
+
LINE_SEARCH_FAILED = "LineSearchFailed"
|
|
65
|
+
CANCELLED = "Cancelled"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class LocalMethod(NetEnum):
|
|
69
|
+
"""Local search methods of the multi-start and polishing steps (``LocalMethod``)."""
|
|
70
|
+
|
|
71
|
+
ADAM = "ADAM"
|
|
72
|
+
BFGS = "BFGS"
|
|
73
|
+
GRADIENT_DESCENT = "GradientDescent"
|
|
74
|
+
NELDER_MEAD = "NelderMead"
|
|
75
|
+
POWELL = "Powell"
|
|
76
|
+
LBFGSB = "LBFGSB"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ConstraintType(NetEnum):
|
|
80
|
+
"""Constraint relations (``Numerics.Mathematics.Optimization.ConstraintType``)."""
|
|
81
|
+
|
|
82
|
+
EQUAL_TO = "EqualTo"
|
|
83
|
+
GREATER_THAN_OR_EQUAL_TO = "GreaterThanOrEqualTo"
|
|
84
|
+
LESSER_THAN_OR_EQUAL_TO = "LesserThanOrEqualTo"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True, eq=False, slots=True)
|
|
88
|
+
class ParameterSet:
|
|
89
|
+
"""A parameter vector with its fitness and weight (``Numerics.Mathematics.Optimization.ParameterSet``).
|
|
90
|
+
|
|
91
|
+
Parameters
|
|
92
|
+
----------
|
|
93
|
+
values : numpy.ndarray
|
|
94
|
+
The parameter values.
|
|
95
|
+
fitness : float
|
|
96
|
+
The fitness (the minimized objective; the negated objective of a maximization).
|
|
97
|
+
weight : float, optional
|
|
98
|
+
The weight; default 0.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
values: np.ndarray
|
|
102
|
+
fitness: float
|
|
103
|
+
weight: float = 0.0
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def from_net(cls, net: Any) -> ParameterSet:
|
|
107
|
+
"""Copy a .NET ``ParameterSet`` into a :class:`ParameterSet`.
|
|
108
|
+
|
|
109
|
+
Parameters
|
|
110
|
+
----------
|
|
111
|
+
net : Numerics.Mathematics.Optimization.ParameterSet
|
|
112
|
+
The .NET value.
|
|
113
|
+
|
|
114
|
+
Returns
|
|
115
|
+
-------
|
|
116
|
+
ParameterSet
|
|
117
|
+
The copy; ``values`` is empty when the .NET values are null.
|
|
118
|
+
"""
|
|
119
|
+
values = net.Values
|
|
120
|
+
array = np.empty(0) if values is None else to_numpy_doubles(values)
|
|
121
|
+
return cls(array, float(net.Fitness), float(net.Weight))
|
|
122
|
+
|
|
123
|
+
def to_net(self) -> Any:
|
|
124
|
+
"""Return the .NET ``ParameterSet`` with these values, fitness and weight.
|
|
125
|
+
|
|
126
|
+
Returns
|
|
127
|
+
-------
|
|
128
|
+
Numerics.Mathematics.Optimization.ParameterSet
|
|
129
|
+
A new .NET value.
|
|
130
|
+
"""
|
|
131
|
+
return _opt().ParameterSet(to_net_doubles(self.values), float(self.fitness), float(self.weight))
|
|
132
|
+
|
|
133
|
+
def clone(self) -> ParameterSet:
|
|
134
|
+
"""Return a copy with its own values array (``ParameterSet.Clone``).
|
|
135
|
+
|
|
136
|
+
Returns
|
|
137
|
+
-------
|
|
138
|
+
ParameterSet
|
|
139
|
+
The copy.
|
|
140
|
+
"""
|
|
141
|
+
return ParameterSet(np.array(self.values, dtype=np.float64), self.fitness, self.weight)
|
|
142
|
+
|
|
143
|
+
def to_xml(self) -> str:
|
|
144
|
+
"""Return the Numerics XML form (``ParameterSet.ToXElement``).
|
|
145
|
+
|
|
146
|
+
Returns
|
|
147
|
+
-------
|
|
148
|
+
str
|
|
149
|
+
The ``ParameterSet`` element with its ``Values``, ``Fitness`` and ``Weight`` attributes.
|
|
150
|
+
"""
|
|
151
|
+
return str(self.to_net().ToXElement().ToString())
|
|
152
|
+
|
|
153
|
+
@classmethod
|
|
154
|
+
def from_xml(cls, xml: str) -> ParameterSet:
|
|
155
|
+
"""Read the Numerics XML form (the ``ParameterSet(XElement)`` constructor).
|
|
156
|
+
|
|
157
|
+
Parameters
|
|
158
|
+
----------
|
|
159
|
+
xml : str
|
|
160
|
+
A ``ParameterSet`` element, as written by :meth:`to_xml`.
|
|
161
|
+
|
|
162
|
+
Returns
|
|
163
|
+
-------
|
|
164
|
+
ParameterSet
|
|
165
|
+
The parameter set.
|
|
166
|
+
"""
|
|
167
|
+
opt = _opt()
|
|
168
|
+
from System.Xml.Linq import XElement # noqa: PLC0415 - the runtime is running
|
|
169
|
+
|
|
170
|
+
return cls.from_net(opt.ParameterSet(XElement.Parse(str(xml))))
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@dataclass(frozen=True, eq=False, slots=True)
|
|
174
|
+
class OptimizationProgress:
|
|
175
|
+
"""A progress report of a running optimizer (``OptimizationProgress``).
|
|
176
|
+
|
|
177
|
+
Parameters
|
|
178
|
+
----------
|
|
179
|
+
iterations : int
|
|
180
|
+
Iterations completed.
|
|
181
|
+
function_evaluations : int
|
|
182
|
+
Objective evaluations completed.
|
|
183
|
+
best_fitness : float
|
|
184
|
+
The best fitness so far (the negated objective during a maximization).
|
|
185
|
+
best_values : numpy.ndarray
|
|
186
|
+
The parameters of the best fitness.
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
iterations: int
|
|
190
|
+
function_evaluations: int
|
|
191
|
+
best_fitness: float
|
|
192
|
+
best_values: np.ndarray
|
|
193
|
+
|
|
194
|
+
@classmethod
|
|
195
|
+
def from_net(cls, net: Any) -> OptimizationProgress:
|
|
196
|
+
"""Copy a .NET ``OptimizationProgress``.
|
|
197
|
+
|
|
198
|
+
Parameters
|
|
199
|
+
----------
|
|
200
|
+
net : Numerics.Mathematics.Optimization.OptimizationProgress
|
|
201
|
+
The .NET report.
|
|
202
|
+
|
|
203
|
+
Returns
|
|
204
|
+
-------
|
|
205
|
+
OptimizationProgress
|
|
206
|
+
The copy.
|
|
207
|
+
"""
|
|
208
|
+
return cls(int(net.Iterations), int(net.FunctionEvaluations), float(net.BestFitness),
|
|
209
|
+
to_numpy_doubles(net.BestValues))
|
|
210
|
+
|
|
211
|
+
def to_net(self) -> Any:
|
|
212
|
+
"""Return the .NET ``OptimizationProgress`` with these values.
|
|
213
|
+
|
|
214
|
+
Returns
|
|
215
|
+
-------
|
|
216
|
+
Numerics.Mathematics.Optimization.OptimizationProgress
|
|
217
|
+
A new .NET report.
|
|
218
|
+
"""
|
|
219
|
+
return _opt().OptimizationProgress(int(self.iterations), int(self.function_evaluations),
|
|
220
|
+
float(self.best_fitness), to_net_doubles(self.best_values))
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
_TRACE_FLATTEN = """
|
|
224
|
+
static double[] Flatten(System.Collections.Generic.List<Numerics.Mathematics.Optimization.ParameterSet> trace)
|
|
225
|
+
{
|
|
226
|
+
int n = trace.Count, p = n == 0 ? 0 : trace[0].Values.Length;
|
|
227
|
+
var flat = new double[n * (p + 1)];
|
|
228
|
+
for (int i = 0; i < n; i++)
|
|
229
|
+
{
|
|
230
|
+
var item = trace[i];
|
|
231
|
+
System.Array.Copy(item.Values, 0, flat, i * (p + 1), p);
|
|
232
|
+
flat[i * (p + 1) + p] = item.Fitness;
|
|
233
|
+
}
|
|
234
|
+
return flat;
|
|
235
|
+
}
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
BULK_TRACE_LENGTH = 2000
|
|
239
|
+
"""Trace length from which a trace crosses in one copy through a compiled flattener, when Roslyn is staged."""
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _trace_arrays(net_trace: Any, scale: float, bulk: bool | None = None) -> tuple[np.ndarray, np.ndarray]:
|
|
243
|
+
"""Return the parameters and objective values of a .NET ``List<ParameterSet>`` trace.
|
|
244
|
+
|
|
245
|
+
Long traces cross in one copy through a compiled C# flattener when Roslyn is staged; short ones, or
|
|
246
|
+
any trace without Roslyn, cross one entry at a time. Both paths copy the same bits.
|
|
247
|
+
"""
|
|
248
|
+
count = int(net_trace.Count)
|
|
249
|
+
if count == 0:
|
|
250
|
+
return np.empty((0, 0)), np.empty(0)
|
|
251
|
+
if bulk is None:
|
|
252
|
+
bulk = count >= BULK_TRACE_LENGTH and callbacks.roslyn_available()
|
|
253
|
+
if bulk:
|
|
254
|
+
import System # noqa: PLC0415 - the runtime is running
|
|
255
|
+
from System.Collections.Generic import List # noqa: PLC0415
|
|
256
|
+
|
|
257
|
+
dtype = System.Func[List[_opt().ParameterSet], System.Array[System.Double]]
|
|
258
|
+
flat = to_numpy_doubles(callbacks.compile_csharp(_TRACE_FLATTEN, dtype)(net_trace))
|
|
259
|
+
table = flat.reshape(count, -1)
|
|
260
|
+
return np.ascontiguousarray(table[:, :-1]), scale * table[:, -1]
|
|
261
|
+
parameters, fitness = [], np.empty(count)
|
|
262
|
+
for i, item in enumerate(net_trace):
|
|
263
|
+
parameters.append(to_numpy_doubles(item.Values))
|
|
264
|
+
fitness[i] = float(item.Fitness)
|
|
265
|
+
return np.vstack(parameters), scale * fitness
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@dataclass(frozen=True, eq=False)
|
|
269
|
+
class OptimizationResult:
|
|
270
|
+
"""The outcome of an optimizer run, shared by every optimizer.
|
|
271
|
+
|
|
272
|
+
Parameters
|
|
273
|
+
----------
|
|
274
|
+
parameters : numpy.ndarray
|
|
275
|
+
The best parameters (``BestParameterSet.Values``).
|
|
276
|
+
value : float
|
|
277
|
+
The objective at ``parameters``: ``BestParameterSet.Fitness``, with the sign restored for a
|
|
278
|
+
maximization.
|
|
279
|
+
iterations : int
|
|
280
|
+
Iterations used (``Iterations``).
|
|
281
|
+
function_evaluations : int
|
|
282
|
+
Objective evaluations used (``FunctionEvaluations``).
|
|
283
|
+
status : OptimizationStatus
|
|
284
|
+
The outcome (``Status``).
|
|
285
|
+
maximized : bool, optional
|
|
286
|
+
Whether the run maximized; default False.
|
|
287
|
+
hessian : numpy.ndarray or None, optional
|
|
288
|
+
The Hessian of the objective at ``parameters`` when Numerics computed it (``ComputeHessian``).
|
|
289
|
+
net_trace : object, optional
|
|
290
|
+
The .NET ``ParameterSetTrace`` list that :attr:`trace_parameters` and :attr:`trace_values` copy on
|
|
291
|
+
first use.
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
parameters: np.ndarray
|
|
295
|
+
value: float
|
|
296
|
+
iterations: int
|
|
297
|
+
function_evaluations: int
|
|
298
|
+
status: OptimizationStatus
|
|
299
|
+
maximized: bool = False
|
|
300
|
+
hessian: np.ndarray | None = None
|
|
301
|
+
net_trace: Any = field(default=None, repr=False)
|
|
302
|
+
_cache: dict = field(default_factory=dict, repr=False, init=False)
|
|
303
|
+
|
|
304
|
+
@property
|
|
305
|
+
def success(self) -> bool:
|
|
306
|
+
"""Whether the status is ``SUCCESS``."""
|
|
307
|
+
return self.status is OptimizationStatus.SUCCESS
|
|
308
|
+
|
|
309
|
+
def _trace(self) -> tuple[np.ndarray, np.ndarray]:
|
|
310
|
+
"""Copy the .NET trace once and cache it."""
|
|
311
|
+
if "trace" not in self._cache:
|
|
312
|
+
if self.net_trace is None:
|
|
313
|
+
self._cache["trace"] = (np.empty((0, 0)), np.empty(0))
|
|
314
|
+
else:
|
|
315
|
+
self._cache["trace"] = _trace_arrays(self.net_trace, -1.0 if self.maximized else 1.0)
|
|
316
|
+
return self._cache["trace"]
|
|
317
|
+
|
|
318
|
+
@property
|
|
319
|
+
def trace_parameters(self) -> np.ndarray:
|
|
320
|
+
"""The best parameters after each evaluation, one row per evaluation (``ParameterSetTrace``)."""
|
|
321
|
+
return self._trace()[0]
|
|
322
|
+
|
|
323
|
+
@property
|
|
324
|
+
def trace_values(self) -> np.ndarray:
|
|
325
|
+
"""The best objective value after each evaluation, with the sign restored for a maximization."""
|
|
326
|
+
return self._trace()[1]
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _vector(fn: Any) -> Any:
|
|
330
|
+
"""Return an objective or constraint function as a ``Func<double[], double>``."""
|
|
331
|
+
return callbacks.vector_function(fn)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _gradient(fn: Any) -> Any:
|
|
335
|
+
"""Return a gradient as a ``Func<double[], double[]>``, or None."""
|
|
336
|
+
return None if fn is None else callbacks.to_delegate(fn, "Func<double[],double[]>")
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _setting(name: str, kind: type, doc: str) -> property:
|
|
340
|
+
"""Return a read-write property mirroring the .NET property ``name``, converted with ``kind``."""
|
|
341
|
+
|
|
342
|
+
def fget(self: Any) -> Any:
|
|
343
|
+
return kind(getattr(self.net, name))
|
|
344
|
+
|
|
345
|
+
def fset(self: Any, value: Any) -> None:
|
|
346
|
+
setattr(self.net, name, kind(value))
|
|
347
|
+
|
|
348
|
+
return property(fget, fset, doc=doc)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _array(name: str, doc: str) -> property:
|
|
352
|
+
"""Return a read-only property copying the .NET ``double[]`` property ``name`` (null gives None)."""
|
|
353
|
+
|
|
354
|
+
def fget(self: Any) -> np.ndarray | None:
|
|
355
|
+
values = getattr(self.net, name)
|
|
356
|
+
return None if values is None else to_numpy_doubles(values)
|
|
357
|
+
|
|
358
|
+
return property(fget, doc=doc)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _method_setting(name: str, doc: str) -> property:
|
|
362
|
+
"""Return a read-write :class:`LocalMethod` property mirroring the .NET property ``name``."""
|
|
363
|
+
|
|
364
|
+
def fget(self: Any) -> LocalMethod:
|
|
365
|
+
return LocalMethod(str(getattr(self.net, name)))
|
|
366
|
+
|
|
367
|
+
def fset(self: Any, value: Any) -> None:
|
|
368
|
+
setattr(self.net, name, net_enum(_opt().LocalMethod, LocalMethod(value).value))
|
|
369
|
+
|
|
370
|
+
return property(fget, fset, doc=doc)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
_REPORTER: dict[str, Any] = {}
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _progress_reporter(fn: Callable[[OptimizationProgress], Any]) -> Any:
|
|
377
|
+
"""Return an ``IProgress<OptimizationProgress>`` that calls ``fn`` with each report, synchronously."""
|
|
378
|
+
reporter = _REPORTER.get("type")
|
|
379
|
+
if reporter is None:
|
|
380
|
+
opt = _opt()
|
|
381
|
+
from System import IProgress # noqa: PLC0415 - the runtime is running
|
|
382
|
+
|
|
383
|
+
class _Reporter(IProgress[opt.OptimizationProgress]):
|
|
384
|
+
__namespace__ = "pynumerics.optimization"
|
|
385
|
+
|
|
386
|
+
def Report(self, value: Any) -> None: # noqa: N802 - the .NET interface member
|
|
387
|
+
self.callback(OptimizationProgress.from_net(value))
|
|
388
|
+
|
|
389
|
+
reporter = _REPORTER["type"] = _Reporter
|
|
390
|
+
instance = reporter()
|
|
391
|
+
instance.callback = fn
|
|
392
|
+
return instance
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
class Optimizer:
|
|
396
|
+
"""The settings, run methods and results every optimizer shares (``Optimizer``).
|
|
397
|
+
|
|
398
|
+
Construct one of the concrete optimizers; this base class only wraps an existing .NET optimizer.
|
|
399
|
+
|
|
400
|
+
Parameters
|
|
401
|
+
----------
|
|
402
|
+
net : Numerics.Mathematics.Optimization.Optimizer
|
|
403
|
+
The .NET optimizer to wrap.
|
|
404
|
+
functions : sequence, optional
|
|
405
|
+
The callables given to it, for the parallel policy.
|
|
406
|
+
|
|
407
|
+
Attributes
|
|
408
|
+
----------
|
|
409
|
+
net : Numerics.Mathematics.Optimization.Optimizer
|
|
410
|
+
The wrapped .NET optimizer.
|
|
411
|
+
"""
|
|
412
|
+
|
|
413
|
+
def __init__(self, net: Any, functions: Sequence[Any] = ()) -> None:
|
|
414
|
+
self.net = net
|
|
415
|
+
self._functions = tuple(functions)
|
|
416
|
+
self._progress: Callable[[OptimizationProgress], Any] | None = None
|
|
417
|
+
self._maximized = False
|
|
418
|
+
callbacks.apply_parallel_policy(net, *self._functions)
|
|
419
|
+
|
|
420
|
+
def _configure(self, settings: dict[str, Any]) -> None:
|
|
421
|
+
"""Set each keyword setting through its property (None keeps the default), then re-apply the policy."""
|
|
422
|
+
for name, value in settings.items():
|
|
423
|
+
member = getattr(type(self), name, None)
|
|
424
|
+
if not isinstance(member, property) or member.fset is None:
|
|
425
|
+
raise TypeError(f"{type(self).__name__} has no setting {name!r}.")
|
|
426
|
+
if value is not None:
|
|
427
|
+
setattr(self, name, value)
|
|
428
|
+
callbacks.apply_parallel_policy(self.net, *self._functions)
|
|
429
|
+
|
|
430
|
+
max_iterations = _setting("MaxIterations", int, "Iteration limit (``MaxIterations``).")
|
|
431
|
+
max_function_evaluations = _setting("MaxFunctionEvaluations", int,
|
|
432
|
+
"Objective evaluation limit (``MaxFunctionEvaluations``).")
|
|
433
|
+
absolute_tolerance = _setting("AbsoluteTolerance", float, "Absolute convergence tolerance (``AbsoluteTolerance``).")
|
|
434
|
+
relative_tolerance = _setting("RelativeTolerance", float, "Relative convergence tolerance (``RelativeTolerance``).")
|
|
435
|
+
report_failure = _setting("ReportFailure", bool, "Whether a failed search raises (``ReportFailure``).")
|
|
436
|
+
record_traces = _setting("RecordTraces", bool, "Whether the best-so-far trace is recorded (``RecordTraces``).")
|
|
437
|
+
compute_hessian = _setting("ComputeHessian", bool,
|
|
438
|
+
"Whether the Hessian is computed after convergence (``ComputeHessian``).")
|
|
439
|
+
progress_interval = _setting("ProgressInterval", int,
|
|
440
|
+
"Evaluations between progress reports (``ProgressInterval``).")
|
|
441
|
+
|
|
442
|
+
@property
|
|
443
|
+
def cancellation_token(self) -> Any:
|
|
444
|
+
"""The ``System.Threading.CancellationToken`` that stops the search (``CancellationToken``)."""
|
|
445
|
+
return self.net.CancellationToken
|
|
446
|
+
|
|
447
|
+
@cancellation_token.setter
|
|
448
|
+
def cancellation_token(self, token: Any) -> None:
|
|
449
|
+
self.net.CancellationToken = token
|
|
450
|
+
|
|
451
|
+
@property
|
|
452
|
+
def progress(self) -> Callable[[OptimizationProgress], Any] | None:
|
|
453
|
+
"""A callable given an :class:`OptimizationProgress` every ``progress_interval`` evaluations (``Progress``).
|
|
454
|
+
|
|
455
|
+
It runs synchronously on the optimizer thread. None, the default, turns reporting off.
|
|
456
|
+
"""
|
|
457
|
+
return self._progress
|
|
458
|
+
|
|
459
|
+
@progress.setter
|
|
460
|
+
def progress(self, fn: Callable[[OptimizationProgress], Any] | None) -> None:
|
|
461
|
+
self.net.Progress = None if fn is None else _progress_reporter(fn)
|
|
462
|
+
self._progress = fn
|
|
463
|
+
|
|
464
|
+
@property
|
|
465
|
+
def objective_function(self) -> Any:
|
|
466
|
+
"""The objective as the .NET ``Func<double[], double>`` Numerics calls (``ObjectiveFunction``).
|
|
467
|
+
|
|
468
|
+
Setting it accepts a Python callable, C# source or a delegate, and re-applies the parallel policy.
|
|
469
|
+
"""
|
|
470
|
+
return self.net.ObjectiveFunction
|
|
471
|
+
|
|
472
|
+
@objective_function.setter
|
|
473
|
+
def objective_function(self, fn: Any) -> None:
|
|
474
|
+
self.net.ObjectiveFunction = _vector(fn)
|
|
475
|
+
self._functions = (fn, *self._functions[1:])
|
|
476
|
+
callbacks.apply_parallel_policy(self.net, *self._functions)
|
|
477
|
+
|
|
478
|
+
@property
|
|
479
|
+
def number_of_parameters(self) -> int:
|
|
480
|
+
"""The number of parameters (``NumberOfParameters``)."""
|
|
481
|
+
return int(self.net.NumberOfParameters)
|
|
482
|
+
|
|
483
|
+
@property
|
|
484
|
+
def iterations(self) -> int:
|
|
485
|
+
"""Iterations used by the last run (``Iterations``)."""
|
|
486
|
+
return int(self.net.Iterations)
|
|
487
|
+
|
|
488
|
+
@property
|
|
489
|
+
def function_evaluations(self) -> int:
|
|
490
|
+
"""Objective evaluations used by the last run (``FunctionEvaluations``)."""
|
|
491
|
+
return int(self.net.FunctionEvaluations)
|
|
492
|
+
|
|
493
|
+
@property
|
|
494
|
+
def status(self) -> OptimizationStatus:
|
|
495
|
+
"""The outcome of the last run (``Status``)."""
|
|
496
|
+
return OptimizationStatus(str(self.net.Status))
|
|
497
|
+
|
|
498
|
+
@property
|
|
499
|
+
def best_parameter_set(self) -> ParameterSet:
|
|
500
|
+
"""The best parameters and fitness (``BestParameterSet``; the fitness is negated after a maximization)."""
|
|
501
|
+
return ParameterSet.from_net(self.net.BestParameterSet)
|
|
502
|
+
|
|
503
|
+
@property
|
|
504
|
+
def parameter_set_trace(self) -> list[ParameterSet]:
|
|
505
|
+
"""The best parameter set after each evaluation, fitness as Numerics stores it (``ParameterSetTrace``)."""
|
|
506
|
+
return [ParameterSet.from_net(item) for item in self.net.ParameterSetTrace]
|
|
507
|
+
|
|
508
|
+
@property
|
|
509
|
+
def hessian(self) -> np.ndarray | None:
|
|
510
|
+
"""The Hessian at the best parameters, or None when it was not computed (``Hessian``)."""
|
|
511
|
+
matrix = self.net.Hessian
|
|
512
|
+
return None if matrix is None else to_numpy_matrix(matrix.ToArray())
|
|
513
|
+
|
|
514
|
+
@property
|
|
515
|
+
def result(self) -> OptimizationResult:
|
|
516
|
+
"""The :class:`OptimizationResult` of the last run."""
|
|
517
|
+
best = self.net.BestParameterSet
|
|
518
|
+
values = best.Values
|
|
519
|
+
scale = -1.0 if self._maximized else 1.0
|
|
520
|
+
return OptimizationResult(np.empty(0) if values is None else to_numpy_doubles(values),
|
|
521
|
+
scale * float(best.Fitness), self.iterations, self.function_evaluations,
|
|
522
|
+
self.status, self._maximized, self.hessian, self.net.ParameterSetTrace)
|
|
523
|
+
|
|
524
|
+
def minimize(self) -> OptimizationResult:
|
|
525
|
+
"""Minimize the objective (``Minimize``).
|
|
526
|
+
|
|
527
|
+
Returns
|
|
528
|
+
-------
|
|
529
|
+
OptimizationResult
|
|
530
|
+
The best parameters, the objective value, the counts and the status.
|
|
531
|
+
|
|
532
|
+
Raises
|
|
533
|
+
------
|
|
534
|
+
RuntimeError
|
|
535
|
+
Whatever Numerics throws, translated by ``pynumerics.errors``, for example when the search
|
|
536
|
+
fails with ``report_failure`` set.
|
|
537
|
+
"""
|
|
538
|
+
self._maximized = False
|
|
539
|
+
self.net.Minimize()
|
|
540
|
+
return self.result
|
|
541
|
+
|
|
542
|
+
def maximize(self) -> OptimizationResult:
|
|
543
|
+
"""Maximize the objective (``Maximize``).
|
|
544
|
+
|
|
545
|
+
Returns
|
|
546
|
+
-------
|
|
547
|
+
OptimizationResult
|
|
548
|
+
The best parameters, the objective value (not negated), the counts and the status.
|
|
549
|
+
|
|
550
|
+
Raises
|
|
551
|
+
------
|
|
552
|
+
RuntimeError
|
|
553
|
+
Whatever Numerics throws, translated by ``pynumerics.errors``.
|
|
554
|
+
"""
|
|
555
|
+
self._maximized = True
|
|
556
|
+
self.net.Maximize()
|
|
557
|
+
return self.result
|
|
558
|
+
|
|
559
|
+
def clear_results(self) -> None:
|
|
560
|
+
"""Reset the counts, best parameter set, trace, status and Hessian (``ClearResults``)."""
|
|
561
|
+
self.net.ClearResults()
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def _bounds(*arrays: Any) -> list[Any]:
|
|
565
|
+
"""Copy parameter vectors to ``System.Double[]``."""
|
|
566
|
+
return [to_net_doubles(a) for a in arrays]
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
class _BoundedSearch(Optimizer):
|
|
570
|
+
"""Bounds shared by the multidimensional optimizers."""
|
|
571
|
+
|
|
572
|
+
lower_bounds = _array("LowerBounds", "The parameter lower bounds (``LowerBounds``).")
|
|
573
|
+
upper_bounds = _array("UpperBounds", "The parameter upper bounds (``UpperBounds``).")
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
class _LocalSearch(_BoundedSearch):
|
|
577
|
+
"""Initial values shared by the local optimizers."""
|
|
578
|
+
|
|
579
|
+
initial_values = _array("InitialValues", "The starting parameters (``InitialValues``).")
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
class _GradientSearch(_LocalSearch):
|
|
583
|
+
"""The optional analytic gradient of the gradient-based local optimizers."""
|
|
584
|
+
|
|
585
|
+
@property
|
|
586
|
+
def gradient(self) -> Any:
|
|
587
|
+
"""The analytic gradient as a .NET ``Func<double[], double[]>``, or None for finite differences (``Gradient``).
|
|
588
|
+
|
|
589
|
+
Setting it accepts a Python callable returning an array, C# source or a delegate.
|
|
590
|
+
"""
|
|
591
|
+
return self.net.Gradient
|
|
592
|
+
|
|
593
|
+
@gradient.setter
|
|
594
|
+
def gradient(self, fn: Any) -> None:
|
|
595
|
+
self.net.Gradient = _gradient(fn)
|
|
596
|
+
self._functions = (self._functions[0], fn)
|
|
597
|
+
callbacks.apply_parallel_policy(self.net, *self._functions)
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
class _ScalarSearch(Optimizer):
|
|
601
|
+
"""The interval of the one-dimensional optimizers."""
|
|
602
|
+
|
|
603
|
+
@property
|
|
604
|
+
def lower_bound(self) -> float:
|
|
605
|
+
"""The lower end of the search interval (``LowerBound``)."""
|
|
606
|
+
return float(self.net.LowerBound)
|
|
607
|
+
|
|
608
|
+
@property
|
|
609
|
+
def upper_bound(self) -> float:
|
|
610
|
+
"""The upper end of the search interval (``UpperBound``)."""
|
|
611
|
+
return float(self.net.UpperBound)
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
class BrentSearch(_ScalarSearch):
|
|
615
|
+
"""Brent's one-dimensional minimizer on a bounded interval (``BrentSearch``).
|
|
616
|
+
|
|
617
|
+
The search is a port of ``scipy.optimize.minimize_scalar(method="bounded")``. Its step tolerance is
|
|
618
|
+
``relative_tolerance * |x| + absolute_tolerance / 3``; the constructor sets ``absolute_tolerance`` to about
|
|
619
|
+
3.3e-19, so the default is ``1e-8 * |x| + 1.1e-19``. With ``relative_tolerance=math.sqrt(2.2e-16)``,
|
|
620
|
+
``absolute_tolerance=1e-5`` and ``max_function_evaluations=500`` (SciPy's ``xatol`` and ``maxiter``
|
|
621
|
+
defaults) the minimizer is bitwise equal to SciPy's.
|
|
622
|
+
|
|
623
|
+
Parameters
|
|
624
|
+
----------
|
|
625
|
+
objective : callable, str or System.Delegate
|
|
626
|
+
Objective of one float (``Func<double, double>``).
|
|
627
|
+
lower_bound, upper_bound : float
|
|
628
|
+
The search interval.
|
|
629
|
+
**settings
|
|
630
|
+
Initial values of any settings property, for example ``relative_tolerance``.
|
|
631
|
+
"""
|
|
632
|
+
|
|
633
|
+
def __init__(self, objective: Any, lower_bound: float, upper_bound: float, **settings: Any) -> None:
|
|
634
|
+
super().__init__(_opt().BrentSearch(callbacks.scalar_function(objective), float(lower_bound),
|
|
635
|
+
float(upper_bound)), (objective,))
|
|
636
|
+
self._configure(settings)
|
|
637
|
+
|
|
638
|
+
def bracket(self, s: float = 1e-2, k: float = 2.0) -> tuple[float, float]:
|
|
639
|
+
"""Move the interval downhill until it brackets a minimum (``Bracket``).
|
|
640
|
+
|
|
641
|
+
Parameters
|
|
642
|
+
----------
|
|
643
|
+
s : float, optional
|
|
644
|
+
Initial step; default 1e-2, as in Numerics.
|
|
645
|
+
k : float, optional
|
|
646
|
+
Step growth factor; default 2, as in Numerics.
|
|
647
|
+
|
|
648
|
+
Returns
|
|
649
|
+
-------
|
|
650
|
+
tuple of float
|
|
651
|
+
The new ``(lower_bound, upper_bound)``.
|
|
652
|
+
"""
|
|
653
|
+
self.net.Bracket(float(s), float(k))
|
|
654
|
+
return self.lower_bound, self.upper_bound
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
class GoldenSection(_ScalarSearch):
|
|
658
|
+
"""Golden-section one-dimensional minimizer on a bounded interval (``GoldenSection``).
|
|
659
|
+
|
|
660
|
+
Parameters
|
|
661
|
+
----------
|
|
662
|
+
objective : callable, str or System.Delegate
|
|
663
|
+
Objective of one float (``Func<double, double>``).
|
|
664
|
+
lower_bound, upper_bound : float
|
|
665
|
+
The search interval.
|
|
666
|
+
**settings
|
|
667
|
+
Initial values of any settings property.
|
|
668
|
+
"""
|
|
669
|
+
|
|
670
|
+
def __init__(self, objective: Any, lower_bound: float, upper_bound: float, **settings: Any) -> None:
|
|
671
|
+
super().__init__(_opt().GoldenSection(callbacks.scalar_function(objective), float(lower_bound),
|
|
672
|
+
float(upper_bound)), (objective,))
|
|
673
|
+
self._configure(settings)
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
class NelderMead(_LocalSearch):
|
|
677
|
+
"""Bounded Nelder-Mead downhill simplex (``NelderMead``).
|
|
678
|
+
|
|
679
|
+
Parameters
|
|
680
|
+
----------
|
|
681
|
+
objective : callable, str or System.Delegate
|
|
682
|
+
Objective of a float64 array (``Func<double[], double>``).
|
|
683
|
+
initial_values, lower_bounds, upper_bounds : array_like
|
|
684
|
+
Start point and bounds, one entry per parameter.
|
|
685
|
+
**settings
|
|
686
|
+
Initial values of any settings property, for example ``simplex_scale``.
|
|
687
|
+
"""
|
|
688
|
+
|
|
689
|
+
def __init__(self, objective: Any, initial_values: Any, lower_bounds: Any, upper_bounds: Any,
|
|
690
|
+
**settings: Any) -> None:
|
|
691
|
+
arrays = _bounds(initial_values, lower_bounds, upper_bounds)
|
|
692
|
+
super().__init__(_opt().NelderMead(_vector(objective), int(arrays[0].Length), *arrays), (objective,))
|
|
693
|
+
self._configure(settings)
|
|
694
|
+
|
|
695
|
+
simplex_scale = _setting("SimplexScale", float,
|
|
696
|
+
"Initial simplex size as a fraction of the range (``SimplexScale``).")
|
|
697
|
+
enable_start_point_probe = _setting("EnableStartPointProbe", bool,
|
|
698
|
+
"Whether probes near the start may replace it (``EnableStartPointProbe``).")
|
|
699
|
+
start_point_probe_count = _setting("StartPointProbeCount", int,
|
|
700
|
+
"Number of start probes (``StartPointProbeCount``).")
|
|
701
|
+
start_point_probe_radius = _setting("StartPointProbeRadius", float,
|
|
702
|
+
"Start probe radius as a fraction of the range (``StartPointProbeRadius``).")
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
class Powell(_LocalSearch):
|
|
706
|
+
"""Bounded Powell conjugate-direction search (``Powell``).
|
|
707
|
+
|
|
708
|
+
Parameters
|
|
709
|
+
----------
|
|
710
|
+
objective : callable, str or System.Delegate
|
|
711
|
+
Objective of a float64 array.
|
|
712
|
+
initial_values, lower_bounds, upper_bounds : array_like
|
|
713
|
+
Start point and bounds.
|
|
714
|
+
**settings
|
|
715
|
+
Initial values of any settings property.
|
|
716
|
+
"""
|
|
717
|
+
|
|
718
|
+
def __init__(self, objective: Any, initial_values: Any, lower_bounds: Any, upper_bounds: Any,
|
|
719
|
+
**settings: Any) -> None:
|
|
720
|
+
arrays = _bounds(initial_values, lower_bounds, upper_bounds)
|
|
721
|
+
super().__init__(_opt().Powell(_vector(objective), int(arrays[0].Length), *arrays), (objective,))
|
|
722
|
+
self._configure(settings)
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
class BFGS(_GradientSearch):
|
|
726
|
+
"""Bounded BFGS quasi-Newton search (``BFGS``).
|
|
727
|
+
|
|
728
|
+
Parameters
|
|
729
|
+
----------
|
|
730
|
+
objective : callable, str or System.Delegate
|
|
731
|
+
Objective of a float64 array.
|
|
732
|
+
initial_values, lower_bounds, upper_bounds : array_like
|
|
733
|
+
Start point and bounds.
|
|
734
|
+
gradient : callable, str or System.Delegate, optional
|
|
735
|
+
Analytic gradient returning an array; None, the default, uses finite differences.
|
|
736
|
+
**settings
|
|
737
|
+
Initial values of any settings property, for example ``scale_initial_inverse_hessian``.
|
|
738
|
+
"""
|
|
739
|
+
|
|
740
|
+
def __init__(self, objective: Any, initial_values: Any, lower_bounds: Any, upper_bounds: Any,
|
|
741
|
+
gradient: Any = None, **settings: Any) -> None:
|
|
742
|
+
arrays = _bounds(initial_values, lower_bounds, upper_bounds)
|
|
743
|
+
super().__init__(_opt().BFGS(_vector(objective), int(arrays[0].Length), *arrays, _gradient(gradient)),
|
|
744
|
+
(objective, gradient))
|
|
745
|
+
self._configure(settings)
|
|
746
|
+
|
|
747
|
+
scale_initial_inverse_hessian = _setting(
|
|
748
|
+
"ScaleInitialInverseHessian", bool,
|
|
749
|
+
"Whether the first inverse Hessian is scaled (``ScaleInitialInverseHessian``).")
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
class LBFGSB(_GradientSearch):
|
|
753
|
+
"""Limited-memory BFGS with bound constraints (``LBFGSB``).
|
|
754
|
+
|
|
755
|
+
Parameters
|
|
756
|
+
----------
|
|
757
|
+
objective : callable, str or System.Delegate
|
|
758
|
+
Objective of a float64 array.
|
|
759
|
+
initial_values, lower_bounds, upper_bounds : array_like
|
|
760
|
+
Start point and bounds.
|
|
761
|
+
gradient : callable, str or System.Delegate, optional
|
|
762
|
+
Analytic gradient; None, the default, uses finite differences.
|
|
763
|
+
**settings
|
|
764
|
+
Initial values of any settings property, for example ``memory_size``.
|
|
765
|
+
"""
|
|
766
|
+
|
|
767
|
+
def __init__(self, objective: Any, initial_values: Any, lower_bounds: Any, upper_bounds: Any,
|
|
768
|
+
gradient: Any = None, **settings: Any) -> None:
|
|
769
|
+
arrays = _bounds(initial_values, lower_bounds, upper_bounds)
|
|
770
|
+
super().__init__(_opt().LBFGSB(_vector(objective), int(arrays[0].Length), *arrays, _gradient(gradient)),
|
|
771
|
+
(objective, gradient))
|
|
772
|
+
self._configure(settings)
|
|
773
|
+
|
|
774
|
+
memory_size = _setting("MemorySize", int, "Number of correction pairs kept (``MemorySize``).")
|
|
775
|
+
max_line_search_steps = _setting("MaxLineSearchSteps", int, "Line-search step limit (``MaxLineSearchSteps``).")
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
class GradientDescent(_GradientSearch):
|
|
779
|
+
"""Bounded gradient descent (``GradientDescent``).
|
|
780
|
+
|
|
781
|
+
Parameters
|
|
782
|
+
----------
|
|
783
|
+
objective : callable, str or System.Delegate
|
|
784
|
+
Objective of a float64 array.
|
|
785
|
+
initial_values, lower_bounds, upper_bounds : array_like
|
|
786
|
+
Start point and bounds.
|
|
787
|
+
alpha : float, optional
|
|
788
|
+
Learning rate; None keeps the Numerics default.
|
|
789
|
+
gradient : callable, str or System.Delegate, optional
|
|
790
|
+
Analytic gradient; None, the default, uses finite differences.
|
|
791
|
+
**settings
|
|
792
|
+
Initial values of any settings property.
|
|
793
|
+
"""
|
|
794
|
+
|
|
795
|
+
def __init__(self, objective: Any, initial_values: Any, lower_bounds: Any, upper_bounds: Any,
|
|
796
|
+
alpha: float | None = None, gradient: Any = None, **settings: Any) -> None:
|
|
797
|
+
arrays = _bounds(initial_values, lower_bounds, upper_bounds)
|
|
798
|
+
net = _opt().GradientDescent(_vector(objective), int(arrays[0].Length), *arrays)
|
|
799
|
+
net.Gradient = _gradient(gradient)
|
|
800
|
+
super().__init__(net, (objective, gradient))
|
|
801
|
+
self._configure({"alpha": alpha, **settings})
|
|
802
|
+
|
|
803
|
+
alpha = _setting("Alpha", float, "Learning rate (``Alpha``).")
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
class ADAM(_GradientSearch):
|
|
807
|
+
"""Bounded Adam optimizer (``ADAM``).
|
|
808
|
+
|
|
809
|
+
Parameters
|
|
810
|
+
----------
|
|
811
|
+
objective : callable, str or System.Delegate
|
|
812
|
+
Objective of a float64 array.
|
|
813
|
+
initial_values, lower_bounds, upper_bounds : array_like
|
|
814
|
+
Start point and bounds.
|
|
815
|
+
alpha : float, optional
|
|
816
|
+
Learning rate; None keeps the Numerics default.
|
|
817
|
+
gradient : callable, str or System.Delegate, optional
|
|
818
|
+
Analytic gradient; None, the default, uses finite differences.
|
|
819
|
+
**settings
|
|
820
|
+
Initial values of any settings property, for example ``beta1``.
|
|
821
|
+
"""
|
|
822
|
+
|
|
823
|
+
def __init__(self, objective: Any, initial_values: Any, lower_bounds: Any, upper_bounds: Any,
|
|
824
|
+
alpha: float | None = None, gradient: Any = None, **settings: Any) -> None:
|
|
825
|
+
arrays = _bounds(initial_values, lower_bounds, upper_bounds)
|
|
826
|
+
net = _opt().ADAM(_vector(objective), int(arrays[0].Length), *arrays)
|
|
827
|
+
net.Gradient = _gradient(gradient)
|
|
828
|
+
super().__init__(net, (objective, gradient))
|
|
829
|
+
self._configure({"alpha": alpha, **settings})
|
|
830
|
+
|
|
831
|
+
alpha = _setting("Alpha", float, "Learning rate (``Alpha``).")
|
|
832
|
+
beta1 = _setting("Beta1", float, "First-moment decay rate (``Beta1``).")
|
|
833
|
+
beta2 = _setting("Beta2", float, "Second-moment decay rate (``Beta2``).")
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
class _GlobalSearch(_BoundedSearch):
|
|
837
|
+
"""The seed and polishing settings of the global optimizers."""
|
|
838
|
+
|
|
839
|
+
prng_seed = _setting("PRNGSeed", int, "Random seed (``PRNGSeed``).")
|
|
840
|
+
polish = _setting("Polish", bool, "Whether a local search polishes the best point (``Polish``).")
|
|
841
|
+
polish_method = _method_setting("PolishMethod", "The local method of the polishing step (``PolishMethod``).")
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
class _PopulationSearch(_GlobalSearch):
|
|
845
|
+
"""Population size and parallel evaluation of the population optimizers."""
|
|
846
|
+
|
|
847
|
+
population_size = _setting("PopulationSize", int, "Population size (``PopulationSize``).")
|
|
848
|
+
max_degree_of_parallelism = _setting(
|
|
849
|
+
"MaxDegreeOfParallelism", int,
|
|
850
|
+
"Concurrent evaluations when parallel, or -1 for no limit (``MaxDegreeOfParallelism``).")
|
|
851
|
+
|
|
852
|
+
@property
|
|
853
|
+
def use_parallel(self) -> bool:
|
|
854
|
+
"""Whether each generation is evaluated on several threads (``UseParallel``; Numerics default False).
|
|
855
|
+
|
|
856
|
+
Parallel evaluation runs the deferred (synchronous) variant of the search, whose result differs
|
|
857
|
+
from the serial search but not with the thread count. It stays off with a Python objective, which
|
|
858
|
+
serializes on the GIL.
|
|
859
|
+
"""
|
|
860
|
+
return bool(self.net.UseParallel)
|
|
861
|
+
|
|
862
|
+
@use_parallel.setter
|
|
863
|
+
def use_parallel(self, value: bool) -> None:
|
|
864
|
+
self.net.UseParallel = bool(value)
|
|
865
|
+
callbacks.apply_parallel_policy(self.net, *self._functions)
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
class DifferentialEvolution(_PopulationSearch):
|
|
869
|
+
"""Differential evolution (``DifferentialEvolution``).
|
|
870
|
+
|
|
871
|
+
Parameters
|
|
872
|
+
----------
|
|
873
|
+
objective : callable, str or System.Delegate
|
|
874
|
+
Objective of a float64 array.
|
|
875
|
+
lower_bounds, upper_bounds : array_like
|
|
876
|
+
Parameter bounds.
|
|
877
|
+
**settings
|
|
878
|
+
Initial values of any settings property, for example ``population_size`` or ``use_parallel``.
|
|
879
|
+
"""
|
|
880
|
+
|
|
881
|
+
def __init__(self, objective: Any, lower_bounds: Any, upper_bounds: Any, **settings: Any) -> None:
|
|
882
|
+
arrays = _bounds(lower_bounds, upper_bounds)
|
|
883
|
+
super().__init__(_opt().DifferentialEvolution(_vector(objective), int(arrays[0].Length), *arrays),
|
|
884
|
+
(objective,))
|
|
885
|
+
self._configure(settings)
|
|
886
|
+
|
|
887
|
+
mutation = _setting("Mutation", float, "Mutation (differential weight) factor (``Mutation``).")
|
|
888
|
+
dither_rate = _setting("DitherRate", float, "Mutation dithering rate (``DitherRate``).")
|
|
889
|
+
crossover_probability = _setting("CrossoverProbability", float,
|
|
890
|
+
"Crossover probability (``CrossoverProbability``).")
|
|
891
|
+
|
|
892
|
+
@property
|
|
893
|
+
def initial_population(self) -> np.ndarray | None:
|
|
894
|
+
"""Optional starting population, one row per member (``InitialPopulation``); None draws a Latin hypercube."""
|
|
895
|
+
population = self.net.InitialPopulation
|
|
896
|
+
return None if population is None else np.vstack([to_numpy_doubles(row) for row in population])
|
|
897
|
+
|
|
898
|
+
@initial_population.setter
|
|
899
|
+
def initial_population(self, rows: Any) -> None:
|
|
900
|
+
if rows is None:
|
|
901
|
+
self.net.InitialPopulation = None
|
|
902
|
+
return
|
|
903
|
+
import System # noqa: PLC0415 - the runtime is running
|
|
904
|
+
from System.Collections.Generic import List # noqa: PLC0415
|
|
905
|
+
|
|
906
|
+
population = List[System.Array[System.Double]]()
|
|
907
|
+
for row in np.atleast_2d(np.asarray(rows, dtype=np.float64)):
|
|
908
|
+
population.Add(to_net_doubles(row))
|
|
909
|
+
self.net.InitialPopulation = population
|
|
910
|
+
|
|
911
|
+
@property
|
|
912
|
+
def integer_parameters(self) -> np.ndarray | None:
|
|
913
|
+
"""Optional flags marking integer-valued parameters (``IntegerParameters``)."""
|
|
914
|
+
flags = self.net.IntegerParameters
|
|
915
|
+
return None if flags is None else np.array([bool(f) for f in flags])
|
|
916
|
+
|
|
917
|
+
@integer_parameters.setter
|
|
918
|
+
def integer_parameters(self, flags: Any) -> None:
|
|
919
|
+
import System # noqa: PLC0415 - the runtime is running
|
|
920
|
+
|
|
921
|
+
self.net.IntegerParameters = None if flags is None else System.Array[System.Boolean](
|
|
922
|
+
[bool(f) for f in flags])
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
class ParticleSwarm(_PopulationSearch):
|
|
926
|
+
"""Particle swarm optimization (``ParticleSwarm``).
|
|
927
|
+
|
|
928
|
+
Parameters
|
|
929
|
+
----------
|
|
930
|
+
objective : callable, str or System.Delegate
|
|
931
|
+
Objective of a float64 array.
|
|
932
|
+
lower_bounds, upper_bounds : array_like
|
|
933
|
+
Parameter bounds.
|
|
934
|
+
**settings
|
|
935
|
+
Initial values of any settings property, for example ``population_size``.
|
|
936
|
+
"""
|
|
937
|
+
|
|
938
|
+
def __init__(self, objective: Any, lower_bounds: Any, upper_bounds: Any, **settings: Any) -> None:
|
|
939
|
+
arrays = _bounds(lower_bounds, upper_bounds)
|
|
940
|
+
super().__init__(_opt().ParticleSwarm(_vector(objective), int(arrays[0].Length), *arrays), (objective,))
|
|
941
|
+
self._configure(settings)
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
class _StartedGlobalSearch(_GlobalSearch):
|
|
945
|
+
"""The optional start point of CMA-ES and DDS."""
|
|
946
|
+
|
|
947
|
+
initial_values = _array("InitialValues", "The optional starting parameters (``InitialValues``).")
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
def _started(factory: Any, objective: Any, lower_bounds: Any, upper_bounds: Any, initial_values: Any) -> Any:
|
|
951
|
+
"""Build a global optimizer with or without its optional start point."""
|
|
952
|
+
lower, upper = _bounds(lower_bounds, upper_bounds)
|
|
953
|
+
if initial_values is None:
|
|
954
|
+
return factory(_vector(objective), int(lower.Length), lower, upper)
|
|
955
|
+
return factory(_vector(objective), int(lower.Length), to_net_doubles(initial_values), lower, upper)
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
class CMAES(_StartedGlobalSearch):
|
|
959
|
+
"""Covariance matrix adaptation evolution strategy with restarts (``CMAES``).
|
|
960
|
+
|
|
961
|
+
Parameters
|
|
962
|
+
----------
|
|
963
|
+
objective : callable, str or System.Delegate
|
|
964
|
+
Objective of a float64 array.
|
|
965
|
+
lower_bounds, upper_bounds : array_like
|
|
966
|
+
Parameter bounds.
|
|
967
|
+
initial_values : array_like, optional
|
|
968
|
+
Start point; None lets Numerics choose it.
|
|
969
|
+
**settings
|
|
970
|
+
Initial values of any settings property, for example ``restarts``.
|
|
971
|
+
"""
|
|
972
|
+
|
|
973
|
+
def __init__(self, objective: Any, lower_bounds: Any, upper_bounds: Any, initial_values: Any = None,
|
|
974
|
+
**settings: Any) -> None:
|
|
975
|
+
super().__init__(_started(_opt().CMAES, objective, lower_bounds, upper_bounds, initial_values), (objective,))
|
|
976
|
+
self._configure(settings)
|
|
977
|
+
|
|
978
|
+
population_size = _setting("PopulationSize", int, "Offspring per generation (``PopulationSize``).")
|
|
979
|
+
initial_step_size = _setting("InitialStepSize", float,
|
|
980
|
+
"Initial step size as a fraction of the range (``InitialStepSize``).")
|
|
981
|
+
restarts = _setting("Restarts", int, "Maximum number of restarts (``Restarts``).")
|
|
982
|
+
|
|
983
|
+
@property
|
|
984
|
+
def restarts_performed(self) -> int:
|
|
985
|
+
"""Restarts performed by the last run (``RestartsPerformed``)."""
|
|
986
|
+
return int(self.net.RestartsPerformed)
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
class DynamicallyDimensionedSearch(_StartedGlobalSearch):
|
|
990
|
+
"""Dynamically dimensioned search (``DynamicallyDimensionedSearch``).
|
|
991
|
+
|
|
992
|
+
Parameters
|
|
993
|
+
----------
|
|
994
|
+
objective : callable, str or System.Delegate
|
|
995
|
+
Objective of a float64 array.
|
|
996
|
+
lower_bounds, upper_bounds : array_like
|
|
997
|
+
Parameter bounds.
|
|
998
|
+
initial_values : array_like, optional
|
|
999
|
+
Start point; None lets Numerics choose it.
|
|
1000
|
+
**settings
|
|
1001
|
+
Initial values of any settings property, for example ``evaluation_budget``.
|
|
1002
|
+
"""
|
|
1003
|
+
|
|
1004
|
+
def __init__(self, objective: Any, lower_bounds: Any, upper_bounds: Any, initial_values: Any = None,
|
|
1005
|
+
**settings: Any) -> None:
|
|
1006
|
+
super().__init__(_started(_opt().DynamicallyDimensionedSearch, objective, lower_bounds, upper_bounds,
|
|
1007
|
+
initial_values), (objective,))
|
|
1008
|
+
self._configure(settings)
|
|
1009
|
+
|
|
1010
|
+
evaluation_budget = _setting("EvaluationBudget", int, "Objective evaluation budget (``EvaluationBudget``).")
|
|
1011
|
+
perturbation_size = _setting("PerturbationSize", float,
|
|
1012
|
+
"Perturbation size as a fraction of the range (``PerturbationSize``).")
|
|
1013
|
+
|
|
1014
|
+
|
|
1015
|
+
class ShuffledComplexEvolution(_GlobalSearch):
|
|
1016
|
+
"""Shuffled complex evolution, SCE-UA (``ShuffledComplexEvolution``).
|
|
1017
|
+
|
|
1018
|
+
Parameters
|
|
1019
|
+
----------
|
|
1020
|
+
objective : callable, str or System.Delegate
|
|
1021
|
+
Objective of a float64 array.
|
|
1022
|
+
lower_bounds, upper_bounds : array_like
|
|
1023
|
+
Parameter bounds.
|
|
1024
|
+
**settings
|
|
1025
|
+
Initial values of any settings property, for example ``complexes``.
|
|
1026
|
+
"""
|
|
1027
|
+
|
|
1028
|
+
def __init__(self, objective: Any, lower_bounds: Any, upper_bounds: Any, **settings: Any) -> None:
|
|
1029
|
+
arrays = _bounds(lower_bounds, upper_bounds)
|
|
1030
|
+
super().__init__(_opt().ShuffledComplexEvolution(_vector(objective), int(arrays[0].Length), *arrays),
|
|
1031
|
+
(objective,))
|
|
1032
|
+
self._configure(settings)
|
|
1033
|
+
|
|
1034
|
+
complexes = _setting("Complexes", int, "Number of complexes (``Complexes``).")
|
|
1035
|
+
cce_iterations = _setting("CCEIterations", int,
|
|
1036
|
+
"Competitive complex evolution steps per shuffle (``CCEIterations``).")
|
|
1037
|
+
tolerance_steps = _setting("ToleranceSteps", int,
|
|
1038
|
+
"Shuffles without improvement before stopping (``ToleranceSteps``).")
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
class SimulatedAnnealing(_GlobalSearch):
|
|
1042
|
+
"""Simulated annealing (``SimulatedAnnealing``).
|
|
1043
|
+
|
|
1044
|
+
Parameters
|
|
1045
|
+
----------
|
|
1046
|
+
objective : callable, str or System.Delegate
|
|
1047
|
+
Objective of a float64 array.
|
|
1048
|
+
lower_bounds, upper_bounds : array_like
|
|
1049
|
+
Parameter bounds.
|
|
1050
|
+
**settings
|
|
1051
|
+
Initial values of any settings property, for example ``initial_temperature``.
|
|
1052
|
+
"""
|
|
1053
|
+
|
|
1054
|
+
def __init__(self, objective: Any, lower_bounds: Any, upper_bounds: Any, **settings: Any) -> None:
|
|
1055
|
+
arrays = _bounds(lower_bounds, upper_bounds)
|
|
1056
|
+
super().__init__(_opt().SimulatedAnnealing(_vector(objective), int(arrays[0].Length), *arrays), (objective,))
|
|
1057
|
+
self._configure(settings)
|
|
1058
|
+
|
|
1059
|
+
initial_temperature = _setting("InitialTemperature", float, "Starting temperature (``InitialTemperature``).")
|
|
1060
|
+
min_temperature = _setting("MinTemperature", float, "Final temperature (``MinTemperature``).")
|
|
1061
|
+
cooling_rate = _setting("CoolingRate", float, "Temperature reduction factor (``CoolingRate``).")
|
|
1062
|
+
update_cycles = _setting("UpdateCycles", int, "Step-size updates per temperature (``UpdateCycles``).")
|
|
1063
|
+
temperature_cycles = _setting("TemperatureCycles", int, "Moves per step-size update (``TemperatureCycles``).")
|
|
1064
|
+
tolerance_steps = _setting("ToleranceSteps", int,
|
|
1065
|
+
"Temperatures without improvement before stopping (``ToleranceSteps``).")
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
class _MultiStartSearch(_LocalSearch):
|
|
1069
|
+
"""The local method and tolerances of the multi-start optimizers."""
|
|
1070
|
+
|
|
1071
|
+
prng_seed = _setting("PRNGSeed", int, "Random seed (``PRNGSeed``).")
|
|
1072
|
+
method = _method_setting("Method", "The local search method (``Method``).")
|
|
1073
|
+
local_absolute_tolerance = _setting("LocalAbsoluteTolerance", float,
|
|
1074
|
+
"Absolute tolerance of each local search (``LocalAbsoluteTolerance``).")
|
|
1075
|
+
local_relative_tolerance = _setting("LocalRelativeTolerance", float,
|
|
1076
|
+
"Relative tolerance of each local search (``LocalRelativeTolerance``).")
|
|
1077
|
+
polish = _setting("Polish", bool, "Whether the best point is polished at the end (``Polish``).")
|
|
1078
|
+
|
|
1079
|
+
|
|
1080
|
+
def _multi_start(factory: Any, objective: Any, initial_values: Any, lower_bounds: Any, upper_bounds: Any,
|
|
1081
|
+
method: Any) -> Any:
|
|
1082
|
+
"""Build MultiStart or MLSL, keeping the Numerics default method when ``method`` is None."""
|
|
1083
|
+
arrays = _bounds(initial_values, lower_bounds, upper_bounds)
|
|
1084
|
+
if method is None:
|
|
1085
|
+
return factory(_vector(objective), int(arrays[0].Length), *arrays)
|
|
1086
|
+
return factory(_vector(objective), int(arrays[0].Length), *arrays,
|
|
1087
|
+
net_enum(_opt().LocalMethod, LocalMethod(method).value))
|
|
1088
|
+
|
|
1089
|
+
|
|
1090
|
+
class MultiStart(_MultiStartSearch):
|
|
1091
|
+
"""Multi-start local search (``MultiStart``).
|
|
1092
|
+
|
|
1093
|
+
Parameters
|
|
1094
|
+
----------
|
|
1095
|
+
objective : callable, str or System.Delegate
|
|
1096
|
+
Objective of a float64 array.
|
|
1097
|
+
initial_values, lower_bounds, upper_bounds : array_like
|
|
1098
|
+
First start point and bounds.
|
|
1099
|
+
method : LocalMethod or str, optional
|
|
1100
|
+
The local method; None keeps the Numerics default.
|
|
1101
|
+
**settings
|
|
1102
|
+
Initial values of any settings property; ``max_iterations`` is the number of starts.
|
|
1103
|
+
"""
|
|
1104
|
+
|
|
1105
|
+
def __init__(self, objective: Any, initial_values: Any, lower_bounds: Any, upper_bounds: Any,
|
|
1106
|
+
method: Any = None, **settings: Any) -> None:
|
|
1107
|
+
super().__init__(_multi_start(_opt().MultiStart, objective, initial_values, lower_bounds, upper_bounds,
|
|
1108
|
+
method), (objective,))
|
|
1109
|
+
self._configure(settings)
|
|
1110
|
+
|
|
1111
|
+
|
|
1112
|
+
@dataclass(frozen=True, eq=False, slots=True)
|
|
1113
|
+
class SamplePoint:
|
|
1114
|
+
"""A sampled point of MLSL (``MLSL.SamplePoint``).
|
|
1115
|
+
|
|
1116
|
+
Parameters
|
|
1117
|
+
----------
|
|
1118
|
+
parameter_set : ParameterSet
|
|
1119
|
+
The point and its fitness.
|
|
1120
|
+
minimized : bool, optional
|
|
1121
|
+
Whether a local search started from it; default False.
|
|
1122
|
+
"""
|
|
1123
|
+
|
|
1124
|
+
parameter_set: ParameterSet
|
|
1125
|
+
minimized: bool = False
|
|
1126
|
+
|
|
1127
|
+
@classmethod
|
|
1128
|
+
def from_net(cls, net: Any) -> SamplePoint:
|
|
1129
|
+
"""Copy a .NET ``MLSL.SamplePoint``.
|
|
1130
|
+
|
|
1131
|
+
Parameters
|
|
1132
|
+
----------
|
|
1133
|
+
net : Numerics.Mathematics.Optimization.MLSL.SamplePoint
|
|
1134
|
+
The .NET point.
|
|
1135
|
+
|
|
1136
|
+
Returns
|
|
1137
|
+
-------
|
|
1138
|
+
SamplePoint
|
|
1139
|
+
The copy.
|
|
1140
|
+
"""
|
|
1141
|
+
return cls(ParameterSet.from_net(net.ParameterSet), bool(net.Minimized))
|
|
1142
|
+
|
|
1143
|
+
|
|
1144
|
+
class MLSL(_MultiStartSearch):
|
|
1145
|
+
"""Multi-level single linkage global search (``MLSL``).
|
|
1146
|
+
|
|
1147
|
+
Parameters
|
|
1148
|
+
----------
|
|
1149
|
+
objective : callable, str or System.Delegate
|
|
1150
|
+
Objective of a float64 array.
|
|
1151
|
+
initial_values, lower_bounds, upper_bounds : array_like
|
|
1152
|
+
First start point and bounds.
|
|
1153
|
+
method : LocalMethod or str, optional
|
|
1154
|
+
The local method; None keeps the Numerics default.
|
|
1155
|
+
**settings
|
|
1156
|
+
Initial values of any settings property, for example ``sample_size``.
|
|
1157
|
+
"""
|
|
1158
|
+
|
|
1159
|
+
def __init__(self, objective: Any, initial_values: Any, lower_bounds: Any, upper_bounds: Any,
|
|
1160
|
+
method: Any = None, **settings: Any) -> None:
|
|
1161
|
+
super().__init__(_multi_start(_opt().MLSL, objective, initial_values, lower_bounds, upper_bounds, method),
|
|
1162
|
+
(objective,))
|
|
1163
|
+
self._configure(settings)
|
|
1164
|
+
|
|
1165
|
+
sample_size = _setting("SampleSize", int, "Points sampled per iteration (``SampleSize``).")
|
|
1166
|
+
gamma = _setting("Gamma", float, "Fraction of the best samples that may start local searches (``Gamma``).")
|
|
1167
|
+
sigma = _setting("Sigma", float, "Critical-distance scale (``Sigma``).")
|
|
1168
|
+
min_no_improvement = _setting("MinNoImprovement", int,
|
|
1169
|
+
"Iterations without improvement before stopping may begin (``MinNoImprovement``).")
|
|
1170
|
+
max_no_improvement = _setting("MaxNoImprovement", int,
|
|
1171
|
+
"Iterations without improvement that stop the search (``MaxNoImprovement``).")
|
|
1172
|
+
|
|
1173
|
+
@property
|
|
1174
|
+
def sampled_points(self) -> list[SamplePoint]:
|
|
1175
|
+
"""The points sampled by the last run (``SampledPoints``)."""
|
|
1176
|
+
points = self.net.SampledPoints
|
|
1177
|
+
return [] if points is None else [SamplePoint.from_net(p) for p in points]
|
|
1178
|
+
|
|
1179
|
+
@property
|
|
1180
|
+
def local_minimums(self) -> list[SamplePoint]:
|
|
1181
|
+
"""The local minima found by the last run (``LocalMinimums``)."""
|
|
1182
|
+
points = self.net.LocalMinimums
|
|
1183
|
+
return [] if points is None else [SamplePoint.from_net(p) for p in points]
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
|
+
class Constraint:
|
|
1187
|
+
"""A constraint ``g(x) relation value`` for :class:`AugmentedLagrange` (``Constraint``).
|
|
1188
|
+
|
|
1189
|
+
Parameters
|
|
1190
|
+
----------
|
|
1191
|
+
function : callable, str or System.Delegate
|
|
1192
|
+
The constraint function g of a float64 array.
|
|
1193
|
+
number_of_parameters : int
|
|
1194
|
+
The number of parameters.
|
|
1195
|
+
value : float
|
|
1196
|
+
The right-hand side.
|
|
1197
|
+
type : ConstraintType or str
|
|
1198
|
+
The relation: g(x) = value, g(x) >= value or g(x) <= value.
|
|
1199
|
+
tolerance : float, optional
|
|
1200
|
+
The feasibility tolerance; None keeps the Numerics default.
|
|
1201
|
+
|
|
1202
|
+
Attributes
|
|
1203
|
+
----------
|
|
1204
|
+
net : Numerics.Mathematics.Optimization.Constraint
|
|
1205
|
+
The wrapped .NET constraint.
|
|
1206
|
+
"""
|
|
1207
|
+
|
|
1208
|
+
def __init__(self, function: Any, number_of_parameters: int, value: float, type: Any, # noqa: A002
|
|
1209
|
+
tolerance: float | None = None) -> None:
|
|
1210
|
+
opt = _opt()
|
|
1211
|
+
relation = net_enum(opt.ConstraintType, ConstraintType(type).value)
|
|
1212
|
+
arguments = [_vector(function), int(number_of_parameters), float(value), relation]
|
|
1213
|
+
if tolerance is not None:
|
|
1214
|
+
arguments.append(float(tolerance))
|
|
1215
|
+
self.net = opt.Constraint(*arguments)
|
|
1216
|
+
self._python_function = function
|
|
1217
|
+
|
|
1218
|
+
@property
|
|
1219
|
+
def type(self) -> ConstraintType:
|
|
1220
|
+
"""The relation (``Type``)."""
|
|
1221
|
+
return ConstraintType(str(self.net.Type))
|
|
1222
|
+
|
|
1223
|
+
@property
|
|
1224
|
+
def number_of_parameters(self) -> int:
|
|
1225
|
+
"""The number of parameters (``NumberOfParameters``)."""
|
|
1226
|
+
return int(self.net.NumberOfParameters)
|
|
1227
|
+
|
|
1228
|
+
@property
|
|
1229
|
+
def function(self) -> Any:
|
|
1230
|
+
"""The constraint function as the .NET ``Func<double[], double>`` (``Function``)."""
|
|
1231
|
+
return self.net.Function
|
|
1232
|
+
|
|
1233
|
+
@property
|
|
1234
|
+
def value(self) -> float:
|
|
1235
|
+
"""The right-hand side (``Value``)."""
|
|
1236
|
+
return float(self.net.Value)
|
|
1237
|
+
|
|
1238
|
+
@property
|
|
1239
|
+
def tolerance(self) -> float:
|
|
1240
|
+
"""The feasibility tolerance (``Tolerance``)."""
|
|
1241
|
+
return float(self.net.Tolerance)
|
|
1242
|
+
|
|
1243
|
+
|
|
1244
|
+
class AugmentedLagrange(Optimizer):
|
|
1245
|
+
"""Augmented Lagrangian constrained optimization around an inner optimizer (``AugmentedLagrange``).
|
|
1246
|
+
|
|
1247
|
+
Numerics replaces the inner optimizer's objective with the augmented Lagrangian of ``objective``
|
|
1248
|
+
and the constraints; its bounds, start point and settings are kept. The parallel policy is applied
|
|
1249
|
+
to the inner optimizer for ``objective`` and every constraint function.
|
|
1250
|
+
|
|
1251
|
+
Parameters
|
|
1252
|
+
----------
|
|
1253
|
+
objective : callable, str or System.Delegate
|
|
1254
|
+
Objective of a float64 array.
|
|
1255
|
+
optimizer : Optimizer
|
|
1256
|
+
The inner optimizer, for example :class:`BFGS` or :class:`DifferentialEvolution`.
|
|
1257
|
+
constraints : sequence of Constraint
|
|
1258
|
+
At least one constraint.
|
|
1259
|
+
**settings
|
|
1260
|
+
Initial values of any settings property.
|
|
1261
|
+
"""
|
|
1262
|
+
|
|
1263
|
+
def __init__(self, objective: Any, optimizer: Optimizer, constraints: Sequence[Constraint],
|
|
1264
|
+
**settings: Any) -> None:
|
|
1265
|
+
opt = _opt()
|
|
1266
|
+
from System.Collections.Generic import List # noqa: PLC0415 - the runtime is running
|
|
1267
|
+
|
|
1268
|
+
items = List[opt.IConstraint]()
|
|
1269
|
+
for constraint in constraints:
|
|
1270
|
+
items.Add(constraint.net)
|
|
1271
|
+
functions = (objective, *(c._python_function for c in constraints))
|
|
1272
|
+
net = opt.AugmentedLagrange(_vector(objective), optimizer.net, items)
|
|
1273
|
+
optimizer._functions = functions
|
|
1274
|
+
callbacks.apply_parallel_policy(optimizer.net, *functions)
|
|
1275
|
+
self._inner = optimizer
|
|
1276
|
+
self._constraints = tuple(constraints)
|
|
1277
|
+
super().__init__(net, functions)
|
|
1278
|
+
self._configure(settings)
|
|
1279
|
+
|
|
1280
|
+
@property
|
|
1281
|
+
def optimizer(self) -> Optimizer:
|
|
1282
|
+
"""The inner optimizer (``Optimizer``)."""
|
|
1283
|
+
return self._inner
|
|
1284
|
+
|
|
1285
|
+
@property
|
|
1286
|
+
def constraints(self) -> tuple[Constraint, ...]:
|
|
1287
|
+
"""The constraints (``Constraints``)."""
|
|
1288
|
+
return self._constraints
|
|
1289
|
+
|
|
1290
|
+
@property
|
|
1291
|
+
def lambda_(self) -> np.ndarray:
|
|
1292
|
+
"""The equality multipliers of the last run (``Lambda``)."""
|
|
1293
|
+
return np.array([float(v) for v in self.net.Lambda])
|
|
1294
|
+
|
|
1295
|
+
@property
|
|
1296
|
+
def mu(self) -> np.ndarray:
|
|
1297
|
+
"""The less-than inequality multipliers of the last run (``Mu``)."""
|
|
1298
|
+
return np.array([float(v) for v in self.net.Mu])
|
|
1299
|
+
|
|
1300
|
+
@property
|
|
1301
|
+
def nu(self) -> np.ndarray:
|
|
1302
|
+
"""The greater-than inequality multipliers of the last run (``Nu``)."""
|
|
1303
|
+
return np.array([float(v) for v in self.net.Nu])
|
|
1304
|
+
|
|
1305
|
+
|
|
1306
|
+
class ParetoFront:
|
|
1307
|
+
"""Pareto-front utilities for minimization (``Numerics.Mathematics.Optimization.ParetoFront``).
|
|
1308
|
+
|
|
1309
|
+
Every function takes points as a two-dimensional array with one row of objective values per
|
|
1310
|
+
point; negate an objective to maximize it.
|
|
1311
|
+
"""
|
|
1312
|
+
|
|
1313
|
+
@staticmethod
|
|
1314
|
+
def dominates(a: Any, b: Any) -> bool:
|
|
1315
|
+
"""Return whether point ``a`` Pareto dominates point ``b`` (``ParetoFront.Dominates``).
|
|
1316
|
+
|
|
1317
|
+
Parameters
|
|
1318
|
+
----------
|
|
1319
|
+
a, b : array_like
|
|
1320
|
+
Objective values of the two points.
|
|
1321
|
+
|
|
1322
|
+
Returns
|
|
1323
|
+
-------
|
|
1324
|
+
bool
|
|
1325
|
+
True when ``a`` is no worse in every objective and better in one.
|
|
1326
|
+
"""
|
|
1327
|
+
return bool(_opt().ParetoFront.Dominates(to_net_doubles(a), to_net_doubles(b)))
|
|
1328
|
+
|
|
1329
|
+
@staticmethod
|
|
1330
|
+
def non_dominated_sort(points: Any) -> list[np.ndarray]:
|
|
1331
|
+
"""Sort points into non-dominated fronts (``ParetoFront.NonDominatedSort``).
|
|
1332
|
+
|
|
1333
|
+
Parameters
|
|
1334
|
+
----------
|
|
1335
|
+
points : array_like
|
|
1336
|
+
One row of objective values per point.
|
|
1337
|
+
|
|
1338
|
+
Returns
|
|
1339
|
+
-------
|
|
1340
|
+
list of numpy.ndarray
|
|
1341
|
+
The fronts, front 0 first, each the ascending int32 point indices.
|
|
1342
|
+
"""
|
|
1343
|
+
fronts = _opt().ParetoFront.NonDominatedSort(to_net_matrix(points))
|
|
1344
|
+
return [to_numpy_ints(front.ToArray()) for front in fronts]
|
|
1345
|
+
|
|
1346
|
+
@staticmethod
|
|
1347
|
+
def non_dominated_indices(points: Any) -> np.ndarray:
|
|
1348
|
+
"""Return the indices of the non-dominated points (``ParetoFront.NonDominatedIndices``).
|
|
1349
|
+
|
|
1350
|
+
Parameters
|
|
1351
|
+
----------
|
|
1352
|
+
points : array_like
|
|
1353
|
+
One row of objective values per point.
|
|
1354
|
+
|
|
1355
|
+
Returns
|
|
1356
|
+
-------
|
|
1357
|
+
numpy.ndarray
|
|
1358
|
+
The ascending int32 indices of front 0.
|
|
1359
|
+
"""
|
|
1360
|
+
return to_numpy_ints(_opt().ParetoFront.NonDominatedIndices(to_net_matrix(points)))
|
|
1361
|
+
|
|
1362
|
+
@staticmethod
|
|
1363
|
+
def crowding_distance(points: Any, front_indices: Any = None) -> np.ndarray:
|
|
1364
|
+
"""Return the NSGA-II crowding distance of the points of one front (``ParetoFront.CrowdingDistance``).
|
|
1365
|
+
|
|
1366
|
+
Parameters
|
|
1367
|
+
----------
|
|
1368
|
+
points : array_like
|
|
1369
|
+
The front, or with ``front_indices`` the whole population; one row per point.
|
|
1370
|
+
front_indices : array_like of int, optional
|
|
1371
|
+
Indices of the front members in ``points``.
|
|
1372
|
+
|
|
1373
|
+
Returns
|
|
1374
|
+
-------
|
|
1375
|
+
numpy.ndarray
|
|
1376
|
+
One distance per front member; boundary points are infinite.
|
|
1377
|
+
"""
|
|
1378
|
+
pareto = _opt().ParetoFront
|
|
1379
|
+
if front_indices is None:
|
|
1380
|
+
return to_numpy_doubles(pareto.CrowdingDistance(to_net_matrix(points)))
|
|
1381
|
+
import System # noqa: PLC0415 - the runtime is running
|
|
1382
|
+
|
|
1383
|
+
rows = np.atleast_2d(np.asarray(points, dtype=np.float64))
|
|
1384
|
+
jagged = System.Array[System.Array[System.Double]]([to_net_doubles(r) for r in rows])
|
|
1385
|
+
return to_numpy_doubles(pareto.CrowdingDistance(jagged, to_net_ints(front_indices)))
|
|
1386
|
+
|
|
1387
|
+
@staticmethod
|
|
1388
|
+
def hypervolume(points: Any, reference_point: Any) -> float:
|
|
1389
|
+
"""Return the exact hypervolume the points dominate up to a reference point (``ParetoFront.Hypervolume``).
|
|
1390
|
+
|
|
1391
|
+
Parameters
|
|
1392
|
+
----------
|
|
1393
|
+
points : array_like
|
|
1394
|
+
One row of objective values per point.
|
|
1395
|
+
reference_point : array_like
|
|
1396
|
+
The reference point.
|
|
1397
|
+
|
|
1398
|
+
Returns
|
|
1399
|
+
-------
|
|
1400
|
+
float
|
|
1401
|
+
The hypervolume.
|
|
1402
|
+
"""
|
|
1403
|
+
return float(_opt().ParetoFront.Hypervolume(to_net_matrix(points), to_net_doubles(reference_point)))
|
|
1404
|
+
|
|
1405
|
+
@staticmethod
|
|
1406
|
+
def generational_distance(approximation: Any, reference_front: Any) -> float:
|
|
1407
|
+
"""Return the mean distance from the approximation to the reference front (``GenerationalDistance``).
|
|
1408
|
+
|
|
1409
|
+
Parameters
|
|
1410
|
+
----------
|
|
1411
|
+
approximation, reference_front : array_like
|
|
1412
|
+
One row of objective values per point.
|
|
1413
|
+
|
|
1414
|
+
Returns
|
|
1415
|
+
-------
|
|
1416
|
+
float
|
|
1417
|
+
The generational distance (pymoo ``GD`` without normalization).
|
|
1418
|
+
"""
|
|
1419
|
+
return float(_opt().ParetoFront.GenerationalDistance(to_net_matrix(approximation),
|
|
1420
|
+
to_net_matrix(reference_front)))
|
|
1421
|
+
|
|
1422
|
+
@staticmethod
|
|
1423
|
+
def inverted_generational_distance(approximation: Any, reference_front: Any) -> float:
|
|
1424
|
+
"""Return the mean distance from the reference front to the approximation (``InvertedGenerationalDistance``).
|
|
1425
|
+
|
|
1426
|
+
Parameters
|
|
1427
|
+
----------
|
|
1428
|
+
approximation, reference_front : array_like
|
|
1429
|
+
One row of objective values per point.
|
|
1430
|
+
|
|
1431
|
+
Returns
|
|
1432
|
+
-------
|
|
1433
|
+
float
|
|
1434
|
+
The inverted generational distance (pymoo ``IGD`` without normalization).
|
|
1435
|
+
"""
|
|
1436
|
+
return float(_opt().ParetoFront.InvertedGenerationalDistance(to_net_matrix(approximation),
|
|
1437
|
+
to_net_matrix(reference_front)))
|
|
1438
|
+
|
|
1439
|
+
|
|
1440
|
+
def _ints_matrix_to_numpy(table: Any) -> np.ndarray:
|
|
1441
|
+
"""Copy a .NET ``int[,]`` into a two-dimensional int32 array in one copy."""
|
|
1442
|
+
return _numpy_rectangular(table, np.int32)
|
|
1443
|
+
|
|
1444
|
+
|
|
1445
|
+
def _ints_matrix_to_net(values: Any) -> Any:
|
|
1446
|
+
"""Copy a two-dimensional integer array into a new .NET ``int[,]``, refusing values outside the int32 range."""
|
|
1447
|
+
array = np.asarray(values)
|
|
1448
|
+
if array.ndim != 2:
|
|
1449
|
+
raise ValueError(f"Expected a 2-D weight array; got shape {array.shape}.")
|
|
1450
|
+
return _net_rectangular(_int32_array(array))
|
|
1451
|
+
|
|
1452
|
+
|
|
1453
|
+
def knapsack(values: Any, weights: Any, capacity: Any) -> tuple[float, np.ndarray]:
|
|
1454
|
+
"""Solve the exact 0/1 knapsack by dynamic programming (``Knapsack.Solve``).
|
|
1455
|
+
|
|
1456
|
+
Parameters
|
|
1457
|
+
----------
|
|
1458
|
+
values : array_like
|
|
1459
|
+
The value of each item.
|
|
1460
|
+
weights : array_like of int
|
|
1461
|
+
The non-negative integer weight of each item, or a ``(items, constraints)`` array for the
|
|
1462
|
+
multi-constraint problem.
|
|
1463
|
+
capacity : int or array_like of int
|
|
1464
|
+
The capacity, or one capacity per constraint.
|
|
1465
|
+
|
|
1466
|
+
Returns
|
|
1467
|
+
-------
|
|
1468
|
+
tuple
|
|
1469
|
+
``(total_value, selected_items)``, the selected indices ascending as int32.
|
|
1470
|
+
|
|
1471
|
+
Raises
|
|
1472
|
+
------
|
|
1473
|
+
ValueError
|
|
1474
|
+
If a weight or capacity is a floating-point value that is not a finite integer, or if Numerics
|
|
1475
|
+
refuses the problem (for example a negative weight).
|
|
1476
|
+
"""
|
|
1477
|
+
for name, given in (("weights", weights), ("capacity", capacity)):
|
|
1478
|
+
array = np.asarray(given)
|
|
1479
|
+
if array.dtype.kind == "f" and not np.all(np.isfinite(array) & (array == np.floor(array))):
|
|
1480
|
+
raise ValueError(f"The knapsack {name} must be integers.")
|
|
1481
|
+
solver = _opt().Knapsack
|
|
1482
|
+
if np.ndim(weights) == 2:
|
|
1483
|
+
total, selected = solver.Solve(to_net_doubles(values), _ints_matrix_to_net(weights),
|
|
1484
|
+
to_net_ints(np.atleast_1d(capacity)), None)
|
|
1485
|
+
else:
|
|
1486
|
+
total, selected = solver.Solve(to_net_doubles(values), to_net_ints(weights), int(capacity), None)
|
|
1487
|
+
return float(total), to_numpy_ints(selected)
|
|
1488
|
+
|
|
1489
|
+
|
|
1490
|
+
@dataclass(frozen=True, eq=False, slots=True)
|
|
1491
|
+
class DynamicProgrammingPath:
|
|
1492
|
+
"""A forward simulation of a dynamic programming policy (``DynamicProgrammingPath``).
|
|
1493
|
+
|
|
1494
|
+
Parameters
|
|
1495
|
+
----------
|
|
1496
|
+
states : numpy.ndarray
|
|
1497
|
+
The state at each stage, T + 1 entries.
|
|
1498
|
+
decisions : numpy.ndarray
|
|
1499
|
+
The decision at each stage, T entries.
|
|
1500
|
+
stage_values : numpy.ndarray
|
|
1501
|
+
The stage cost or reward at each stage.
|
|
1502
|
+
total_value : float
|
|
1503
|
+
The total value, terminal value included.
|
|
1504
|
+
"""
|
|
1505
|
+
|
|
1506
|
+
states: np.ndarray
|
|
1507
|
+
decisions: np.ndarray
|
|
1508
|
+
stage_values: np.ndarray
|
|
1509
|
+
total_value: float
|
|
1510
|
+
|
|
1511
|
+
@classmethod
|
|
1512
|
+
def from_net(cls, net: Any) -> DynamicProgrammingPath:
|
|
1513
|
+
"""Copy a .NET ``DynamicProgrammingPath``.
|
|
1514
|
+
|
|
1515
|
+
Parameters
|
|
1516
|
+
----------
|
|
1517
|
+
net : Numerics.Mathematics.Optimization.DynamicProgrammingPath
|
|
1518
|
+
The .NET path.
|
|
1519
|
+
|
|
1520
|
+
Returns
|
|
1521
|
+
-------
|
|
1522
|
+
DynamicProgrammingPath
|
|
1523
|
+
The copy.
|
|
1524
|
+
"""
|
|
1525
|
+
return cls(to_numpy_ints(net.States), to_numpy_ints(net.Decisions), to_numpy_doubles(net.StageValues),
|
|
1526
|
+
float(net.TotalValue))
|
|
1527
|
+
|
|
1528
|
+
|
|
1529
|
+
class DynamicProgramming:
|
|
1530
|
+
"""Discrete dynamic programming over integer stages, states and decisions (``DynamicProgramming``).
|
|
1531
|
+
|
|
1532
|
+
Parameters
|
|
1533
|
+
----------
|
|
1534
|
+
stages, states, decisions : int
|
|
1535
|
+
The numbers of stages T, states S and decisions A.
|
|
1536
|
+
stage_value : callable, str or System.Delegate
|
|
1537
|
+
The stage cost or reward ``c(t, s, a)`` (``Func<int, int, int, double>``); a non-finite value
|
|
1538
|
+
marks the decision infeasible.
|
|
1539
|
+
transition : callable, str or System.Delegate
|
|
1540
|
+
Without ``max_outcomes``, the next state ``f(t, s, a)`` (``Func<int, int, int, int>``; -1 is
|
|
1541
|
+
infeasible). With ``max_outcomes``, a ``TransitionDistribution``
|
|
1542
|
+
``(t, s, a, next_states, probabilities) -> count`` that fills the two .NET arrays and returns
|
|
1543
|
+
the number of outcomes written.
|
|
1544
|
+
max_outcomes : int, optional
|
|
1545
|
+
The largest number of outcomes of a stochastic transition; None builds a deterministic problem.
|
|
1546
|
+
|
|
1547
|
+
Attributes
|
|
1548
|
+
----------
|
|
1549
|
+
net : Numerics.Mathematics.Optimization.DynamicProgramming
|
|
1550
|
+
The wrapped .NET problem.
|
|
1551
|
+
"""
|
|
1552
|
+
|
|
1553
|
+
def __init__(self, stages: int, states: int, decisions: int, stage_value: Any, transition: Any,
|
|
1554
|
+
max_outcomes: int | None = None) -> None:
|
|
1555
|
+
opt = _opt()
|
|
1556
|
+
value = callbacks.to_delegate(stage_value, "Func<int,int,int,double>")
|
|
1557
|
+
if max_outcomes is None:
|
|
1558
|
+
step = callbacks.to_delegate(transition, "Func<int,int,int,int>")
|
|
1559
|
+
self.net = opt.DynamicProgramming(int(stages), int(states), int(decisions), value, step)
|
|
1560
|
+
else:
|
|
1561
|
+
step = callbacks.to_delegate(transition, "TransitionDistribution")
|
|
1562
|
+
self.net = opt.DynamicProgramming(int(stages), int(states), int(decisions), value, step, int(max_outcomes))
|
|
1563
|
+
self._functions = (stage_value, transition)
|
|
1564
|
+
callbacks.apply_parallel_policy(self.net, *self._functions)
|
|
1565
|
+
|
|
1566
|
+
@property
|
|
1567
|
+
def stages(self) -> int:
|
|
1568
|
+
"""The number of stages T (``Stages``)."""
|
|
1569
|
+
return int(self.net.Stages)
|
|
1570
|
+
|
|
1571
|
+
@property
|
|
1572
|
+
def states(self) -> int:
|
|
1573
|
+
"""The number of states S (``States``)."""
|
|
1574
|
+
return int(self.net.States)
|
|
1575
|
+
|
|
1576
|
+
@property
|
|
1577
|
+
def decisions(self) -> int:
|
|
1578
|
+
"""The number of decisions A (``Decisions``)."""
|
|
1579
|
+
return int(self.net.Decisions)
|
|
1580
|
+
|
|
1581
|
+
@property
|
|
1582
|
+
def max_outcomes(self) -> int:
|
|
1583
|
+
"""The largest number of outcomes of a transition (``MaxOutcomes``)."""
|
|
1584
|
+
return int(self.net.MaxOutcomes)
|
|
1585
|
+
|
|
1586
|
+
@property
|
|
1587
|
+
def is_stochastic(self) -> bool:
|
|
1588
|
+
"""Whether the transitions are stochastic (``IsStochastic``)."""
|
|
1589
|
+
return bool(self.net.IsStochastic)
|
|
1590
|
+
|
|
1591
|
+
@property
|
|
1592
|
+
def maximize(self) -> bool:
|
|
1593
|
+
"""Whether the stage values are rewards to maximize (``Maximize``; default False)."""
|
|
1594
|
+
return bool(self.net.Maximize)
|
|
1595
|
+
|
|
1596
|
+
@maximize.setter
|
|
1597
|
+
def maximize(self, value: bool) -> None:
|
|
1598
|
+
self.net.Maximize = bool(value)
|
|
1599
|
+
|
|
1600
|
+
@property
|
|
1601
|
+
def discount_factor(self) -> float:
|
|
1602
|
+
"""The discount factor in (0, 1] (``DiscountFactor``; default 1)."""
|
|
1603
|
+
return float(self.net.DiscountFactor)
|
|
1604
|
+
|
|
1605
|
+
@discount_factor.setter
|
|
1606
|
+
def discount_factor(self, value: float) -> None:
|
|
1607
|
+
self.net.DiscountFactor = float(value)
|
|
1608
|
+
|
|
1609
|
+
@property
|
|
1610
|
+
def terminal_value(self) -> np.ndarray | None:
|
|
1611
|
+
"""The terminal value of each state, or None for zero (``TerminalValue``)."""
|
|
1612
|
+
values = self.net.TerminalValue
|
|
1613
|
+
return None if values is None else to_numpy_doubles(values)
|
|
1614
|
+
|
|
1615
|
+
@terminal_value.setter
|
|
1616
|
+
def terminal_value(self, values: Any) -> None:
|
|
1617
|
+
self.net.TerminalValue = None if values is None else to_net_doubles(values)
|
|
1618
|
+
|
|
1619
|
+
@property
|
|
1620
|
+
def use_parallel(self) -> bool:
|
|
1621
|
+
"""Whether the states of each stage are solved in parallel (``UseParallel``; default False).
|
|
1622
|
+
|
|
1623
|
+
The result equals the serial solve. It stays off with a Python callback.
|
|
1624
|
+
"""
|
|
1625
|
+
return bool(self.net.UseParallel)
|
|
1626
|
+
|
|
1627
|
+
@use_parallel.setter
|
|
1628
|
+
def use_parallel(self, value: bool) -> None:
|
|
1629
|
+
self.net.UseParallel = bool(value)
|
|
1630
|
+
callbacks.apply_parallel_policy(self.net, *self._functions)
|
|
1631
|
+
|
|
1632
|
+
@property
|
|
1633
|
+
def is_solved(self) -> bool:
|
|
1634
|
+
"""Whether :meth:`solve` has completed (``IsSolved``)."""
|
|
1635
|
+
return bool(self.net.IsSolved)
|
|
1636
|
+
|
|
1637
|
+
@property
|
|
1638
|
+
def value(self) -> np.ndarray:
|
|
1639
|
+
"""The optimal value table ``V[t, s]`` for t = 0..T; row T is the terminal value (``Value``)."""
|
|
1640
|
+
return to_numpy_matrix(self.net.Value)
|
|
1641
|
+
|
|
1642
|
+
@property
|
|
1643
|
+
def policy(self) -> np.ndarray:
|
|
1644
|
+
"""The optimal decision table ``[t, s]`` for t = 0..T-1, -1 where infeasible (``Policy``)."""
|
|
1645
|
+
return _ints_matrix_to_numpy(self.net.Policy)
|
|
1646
|
+
|
|
1647
|
+
@property
|
|
1648
|
+
def steady_state_value(self) -> np.ndarray:
|
|
1649
|
+
"""The stationary value of each state (``SteadyStateValue``)."""
|
|
1650
|
+
return to_numpy_doubles(self.net.SteadyStateValue)
|
|
1651
|
+
|
|
1652
|
+
@property
|
|
1653
|
+
def steady_state_policy(self) -> np.ndarray:
|
|
1654
|
+
"""The stationary decision of each state, -1 where infeasible (``SteadyStatePolicy``)."""
|
|
1655
|
+
return to_numpy_ints(self.net.SteadyStatePolicy)
|
|
1656
|
+
|
|
1657
|
+
@property
|
|
1658
|
+
def steady_state_iterations(self) -> int:
|
|
1659
|
+
"""Value-iteration sweeps of the last steady-state solve (``SteadyStateIterations``)."""
|
|
1660
|
+
return int(self.net.SteadyStateIterations)
|
|
1661
|
+
|
|
1662
|
+
@property
|
|
1663
|
+
def steady_state_converged(self) -> bool:
|
|
1664
|
+
"""Whether the last steady-state solve converged (``SteadyStateConverged``)."""
|
|
1665
|
+
return bool(self.net.SteadyStateConverged)
|
|
1666
|
+
|
|
1667
|
+
def solve(self) -> None:
|
|
1668
|
+
"""Solve the finite-horizon problem by backward recursion (``Solve``)."""
|
|
1669
|
+
self.net.Solve()
|
|
1670
|
+
|
|
1671
|
+
def solve_steady_state(self, tolerance: float = 1e-8, max_iterations: int = 10000) -> bool:
|
|
1672
|
+
"""Solve the stationary infinite-horizon problem by value iteration (``SolveSteadyState``).
|
|
1673
|
+
|
|
1674
|
+
Parameters
|
|
1675
|
+
----------
|
|
1676
|
+
tolerance : float, optional
|
|
1677
|
+
Convergence tolerance on the largest value change; default 1e-8, as in Numerics.
|
|
1678
|
+
max_iterations : int, optional
|
|
1679
|
+
Sweep limit; default 10,000, as in Numerics.
|
|
1680
|
+
|
|
1681
|
+
Returns
|
|
1682
|
+
-------
|
|
1683
|
+
bool
|
|
1684
|
+
True when the iteration converged.
|
|
1685
|
+
"""
|
|
1686
|
+
return bool(self.net.SolveSteadyState(float(tolerance), int(max_iterations)))
|
|
1687
|
+
|
|
1688
|
+
def simulate(self, initial_state: int, outcomes: Sequence[int] | None = None,
|
|
1689
|
+
seed: int | None = None) -> DynamicProgrammingPath:
|
|
1690
|
+
"""Simulate the optimal policy forward from a state (``Simulate``).
|
|
1691
|
+
|
|
1692
|
+
Parameters
|
|
1693
|
+
----------
|
|
1694
|
+
initial_state : int
|
|
1695
|
+
The state at stage 0.
|
|
1696
|
+
outcomes : sequence of int, optional
|
|
1697
|
+
The outcome index at each stage (for example an observed inflow class sequence).
|
|
1698
|
+
seed : int, optional
|
|
1699
|
+
Seed of the ``System.Random`` that samples each stochastic outcome.
|
|
1700
|
+
|
|
1701
|
+
Returns
|
|
1702
|
+
-------
|
|
1703
|
+
DynamicProgrammingPath
|
|
1704
|
+
The state and decision path. Without ``outcomes`` or ``seed`` the problem must be
|
|
1705
|
+
deterministic.
|
|
1706
|
+
|
|
1707
|
+
Raises
|
|
1708
|
+
------
|
|
1709
|
+
ValueError
|
|
1710
|
+
If both ``outcomes`` and ``seed`` are given.
|
|
1711
|
+
"""
|
|
1712
|
+
if outcomes is not None and seed is not None:
|
|
1713
|
+
raise ValueError("Give outcomes or seed, not both.")
|
|
1714
|
+
if outcomes is not None:
|
|
1715
|
+
path = self.net.Simulate(int(initial_state), to_net_ints(outcomes))
|
|
1716
|
+
elif seed is not None:
|
|
1717
|
+
import System # noqa: PLC0415 - the runtime is running
|
|
1718
|
+
|
|
1719
|
+
path = self.net.Simulate(int(initial_state), System.Random(int(seed)))
|
|
1720
|
+
else:
|
|
1721
|
+
path = self.net.Simulate(int(initial_state))
|
|
1722
|
+
return DynamicProgrammingPath.from_net(path)
|
|
1723
|
+
|
|
1724
|
+
|
|
1725
|
+
__all__ = [
|
|
1726
|
+
"ADAM", "BFGS", "BULK_TRACE_LENGTH", "CMAES", "LBFGSB", "MLSL", "AugmentedLagrange", "BrentSearch",
|
|
1727
|
+
"Constraint", "ConstraintType", "DifferentialEvolution", "DynamicProgramming", "DynamicProgrammingPath",
|
|
1728
|
+
"DynamicallyDimensionedSearch", "GoldenSection", "GradientDescent", "LocalMethod", "MultiStart", "NelderMead",
|
|
1729
|
+
"OptimizationProgress", "OptimizationResult", "OptimizationStatus", "Optimizer", "ParameterSet", "ParetoFront",
|
|
1730
|
+
"ParticleSwarm", "Powell", "SamplePoint", "ShuffledComplexEvolution", "SimulatedAnnealing", "knapsack",
|
|
1731
|
+
]
|
|
1732
|
+
|
|
1733
|
+
translate_public(globals())
|
|
1734
|
+
for _base in (_BoundedSearch, _LocalSearch, _GradientSearch, _ScalarSearch, _GlobalSearch, _PopulationSearch,
|
|
1735
|
+
_StartedGlobalSearch, _MultiStartSearch):
|
|
1736
|
+
translate_class(_base)
|