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.
Files changed (46) hide show
  1. pynumerics/__init__.py +73 -0
  2. pynumerics/_bin/Microsoft.CodeAnalysis.CSharp.dll +0 -0
  3. pynumerics/_bin/Microsoft.CodeAnalysis.LICENSE.txt +27 -0
  4. pynumerics/_bin/Microsoft.CodeAnalysis.dll +0 -0
  5. pynumerics/_bin/Numerics.dll +0 -0
  6. pynumerics/_bin/pynumerics.runtimeconfig.json +15 -0
  7. pynumerics/_coverage.py +3825 -0
  8. pynumerics/_util.py +237 -0
  9. pynumerics/_version.py +40 -0
  10. pynumerics/bootstrap.py +593 -0
  11. pynumerics/callbacks.py +791 -0
  12. pynumerics/copulas.py +910 -0
  13. pynumerics/data.py +2233 -0
  14. pynumerics/differentiation.py +348 -0
  15. pynumerics/distributions.py +4043 -0
  16. pynumerics/download.py +623 -0
  17. pynumerics/enums.py +119 -0
  18. pynumerics/errors.py +245 -0
  19. pynumerics/functions.py +1142 -0
  20. pynumerics/goodness_of_fit.py +699 -0
  21. pynumerics/graphs.py +556 -0
  22. pynumerics/hypothesis_tests.py +445 -0
  23. pynumerics/identity.py +144 -0
  24. pynumerics/integration.py +677 -0
  25. pynumerics/linalg.py +1408 -0
  26. pynumerics/machine_learning.py +1335 -0
  27. pynumerics/marshal.py +733 -0
  28. pynumerics/mathematics.py +1053 -0
  29. pynumerics/mcmc.py +1406 -0
  30. pynumerics/multivariate.py +1539 -0
  31. pynumerics/ode.py +156 -0
  32. pynumerics/optimization.py +1736 -0
  33. pynumerics/probability.py +966 -0
  34. pynumerics/py.typed +0 -0
  35. pynumerics/regional.py +522 -0
  36. pynumerics/roots.py +274 -0
  37. pynumerics/runtime.py +384 -0
  38. pynumerics/sampling.py +1758 -0
  39. pynumerics/special.py +1079 -0
  40. pynumerics/statistics.py +1958 -0
  41. pynumerics/timeseries.py +1326 -0
  42. pynumerics-2.3.0.dist-info/METADATA +142 -0
  43. pynumerics-2.3.0.dist-info/RECORD +46 -0
  44. pynumerics-2.3.0.dist-info/WHEEL +5 -0
  45. pynumerics-2.3.0.dist-info/licenses/LICENSE +10 -0
  46. 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)