sigmaepsilon.math 2.2.1__tar.gz → 2.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/PKG-INFO +5 -3
  2. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/pyproject.toml +1 -1
  3. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/optimize/__init__.py +2 -1
  4. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/optimize/bga.py +19 -9
  5. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/optimize/ga.py +140 -49
  6. sigmaepsilon_math-2.2.2/src/sigmaepsilon/math/optimize/state.py +78 -0
  7. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/LICENSE +0 -0
  8. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/README.md +0 -0
  9. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/__init__.py +0 -0
  10. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/approx/__init__.py +0 -0
  11. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/approx/functions.py +0 -0
  12. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/approx/lagrange.py +0 -0
  13. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/approx/ls.py +0 -0
  14. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/approx/ls_approx.py +0 -0
  15. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/approx/ls_poly.py +0 -0
  16. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/approx/ls_preproc.py +0 -0
  17. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/approx/mls.py +0 -0
  18. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/arraysetops.py +0 -0
  19. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/decorate.py +0 -0
  20. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/downloads.py +0 -0
  21. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/function/__init__.py +0 -0
  22. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/function/function.py +0 -0
  23. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/function/functions.py +0 -0
  24. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/function/metafunction.py +0 -0
  25. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/function/relation.py +0 -0
  26. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/function/symutils.py +0 -0
  27. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/function/testfunction.py +0 -0
  28. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/function/utils.py +0 -0
  29. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/graph/__init__.py +0 -0
  30. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/graph/graph.py +0 -0
  31. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/graph/utils.py +0 -0
  32. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/hist.py +0 -0
  33. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/knn.py +0 -0
  34. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/linalg/__init__.py +0 -0
  35. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/linalg/abstract.py +0 -0
  36. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/linalg/exceptions.py +0 -0
  37. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/linalg/frame.py +0 -0
  38. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/linalg/imap.py +0 -0
  39. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/linalg/logical.py +0 -0
  40. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/linalg/meta.py +0 -0
  41. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/linalg/solve.py +0 -0
  42. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/linalg/sparse/__init__.py +0 -0
  43. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/linalg/sparse/csr.py +0 -0
  44. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/linalg/sparse/jaggedarray.py +0 -0
  45. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/linalg/sparse/utils.py +0 -0
  46. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/linalg/tensor.py +0 -0
  47. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/linalg/testing.py +0 -0
  48. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/linalg/tr.py +0 -0
  49. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/linalg/utils.py +0 -0
  50. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/linalg/vector.py +0 -0
  51. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/logical.py +0 -0
  52. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/mathtypes.py +0 -0
  53. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/metautils.py +0 -0
  54. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/numint.py +0 -0
  55. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/optimize/lp.py +0 -0
  56. {sigmaepsilon_math-2.2.1 → sigmaepsilon_math-2.2.2}/src/sigmaepsilon/math/utils.py +0 -0
@@ -1,9 +1,9 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: sigmaepsilon.math
3
- Version: 2.2.1
3
+ Version: 2.2.2
4
4
  Summary: A Python Library for Applied Mathematics in Physical Sciences.
5
- Home-page: https://github.com/sigma-epsilon/sigmaepsilon.math
6
5
  License: MIT
6
+ License-File: LICENSE
7
7
  Keywords: engineering,mathematics,optimization,vector,tensor,vector algebra,tensor algebra,symbolic mathematics,nonlinear optimization
8
8
  Author: Bence Balogh
9
9
  Author-email: bencebalogh@sigmaepsilon.com
@@ -18,6 +18,7 @@ Classifier: Programming Language :: Python :: 3.10
18
18
  Classifier: Programming Language :: Python :: 3.11
19
19
  Classifier: Programming Language :: Python :: 3.12
20
20
  Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
21
22
  Classifier: Programming Language :: Python :: 3 :: Only
22
23
  Requires-Dist: awkward (>2.0.8)
23
24
  Requires-Dist: numba (>=0.56.4)
@@ -29,6 +30,7 @@ Requires-Dist: sigmaepsilon-deepdict (>=3.0.1,<4.0.0)
29
30
  Requires-Dist: sympy (>=1.11.1)
30
31
  Requires-Dist: zipp (>=3.19.1)
31
32
  Project-URL: Documentation, https://sigmaepsilonmath.readthedocs.io/en/latest/?badge=latest
33
+ Project-URL: Homepage, https://github.com/sigma-epsilon/sigmaepsilon.math
32
34
  Project-URL: Repository, https://github.com/sigma-epsilon/sigmaepsilon.math
33
35
  Description-Content-Type: text/markdown
34
36
 
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "sigmaepsilon.math"
7
- version = "2.2.1"
7
+ version = "2.2.2"
8
8
  description = "A Python Library for Applied Mathematics in Physical Sciences."
9
9
  classifiers=[ # https://pypi.org/classifiers/
10
10
  "Development Status :: 5 - Production/Stable",
@@ -1,9 +1,10 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  from .bga import BinaryGeneticAlgorithm
3
3
  from .lp import LinearProgrammingProblem
4
-
4
+ from .state import OptimizerState
5
5
 
6
6
  __all__ = [
7
7
  "BinaryGeneticAlgorithm",
8
8
  "LinearProgrammingProblem",
9
+ "OptimizerState",
9
10
  ]
@@ -52,15 +52,13 @@ class BinaryGeneticAlgorithm(GeneticAlgorithm):
52
52
  ftol: float, Optional
53
53
  Torelance for floating point operations. Default is 1e-12.
54
54
  maxage: int, Optional
55
- The age is the number of generations a candidate spends at the top
56
- (being the best candidate). Setting an upper limit to this value is a kind
57
- of stopping criterion. Default is 5.
55
+ The age is the maximum number of generations a candidate spends at the top
56
+ (being the best candidate) before termination. Default is 5.
58
57
  minimize: bool, Optional
59
58
  If True, the objective function is minimized. Default is False.
60
59
 
61
60
  See Also
62
61
  --------
63
- :class:`~sigmaepsilon.math.optimize.ga.GeneticAlgorithm`
64
62
  :class:`~sigmaepsilon.math.optimize.ga.Genom`
65
63
 
66
64
  Examples
@@ -80,7 +78,7 @@ class BinaryGeneticAlgorithm(GeneticAlgorithm):
80
78
  >>> _ = bga.solve()
81
79
  >>> champion = bga.champion
82
80
  >>> x = champion.phenotype
83
- >>> fx = champion.fittness
81
+ >>> fx = champion.fitness
84
82
 
85
83
  """
86
84
 
@@ -167,16 +165,28 @@ class BinaryGeneticAlgorithm(GeneticAlgorithm):
167
165
  p = np.random.rand(self.dim * self.length)
168
166
  return np.where(p > self.p_m, child, 1 - child)
169
167
 
170
- def select(self, genotypes: ndarray, phenotypes: ndarray | None = None) -> ndarray:
168
+ def select(self, genotypes: ndarray | None = None, phenotypes: ndarray | None = None) -> ndarray:
171
169
  """
172
170
  Organizes a tournament and returns the genotypes of the winners.
171
+
172
+ .. note::
173
+ Providing either ``genotypes`` or ``phenotypes`` (or both) is not currently supported
174
+ and will raise a NotImplementedError. Only the default case (both None) is implemented.
173
175
  """
174
- fittness = self.evaluate(phenotypes)
175
- winners, others = self.divide(fittness)
176
+ if (genotypes is None) and (phenotypes is None):
177
+ fitness = self.fitness
178
+ genotypes = self.genotypes
179
+ else:
180
+ raise NotImplementedError(
181
+ "Selection with either 'genotypes' or 'phenotypes' (or both) provided is not implemented. "
182
+ "This branch is reached when at least one of these arguments is given to 'select', "
183
+ "but only the default case (both None) is currently supported."
184
+ )
185
+ winners, others = self.divide(fitness)
176
186
  winners = winners.tolist()
177
187
  while len(winners) < int(self.nPop / 2):
178
188
  candidates = np.random.choice(others, 3, replace=False)
179
- argsort = np.argsort([fittness[ID] for ID in candidates])
189
+ argsort = np.argsort([fitness[ID] for ID in candidates])
180
190
  winner = argsort[0] if self._minimize else argsort[-1]
181
191
  winners.append(candidates[winner])
182
192
  return np.array([genotypes[w] for w in winners], dtype=float)
@@ -1,22 +1,25 @@
1
1
  from abc import abstractmethod
2
2
  from typing import Iterable, Callable, Tuple, Generator
3
3
  from types import NoneType
4
+ from numbers import Number
4
5
  import operator
6
+ from enum import Enum, unique
5
7
 
6
8
  import numpy as np
7
9
  from numpy import ndarray
8
10
  from pydantic import BaseModel, Field
9
11
 
10
12
  from ..function import Function
13
+ from .state import OptimizerState
11
14
 
12
15
  __all__ = ["GeneticAlgorithm", "Genom"]
13
16
 
14
17
 
15
- def even(n):
18
+ def even(n: Number) -> bool:
16
19
  return n % 2 == 0
17
20
 
18
21
 
19
- def odd(n):
22
+ def odd(n: Number) -> bool:
20
23
  return not even(n)
21
24
 
22
25
 
@@ -27,7 +30,7 @@ class Genom(BaseModel):
27
30
 
28
31
  phenotype: list[float] = Field(default_factory=list)
29
32
  genotype: list[int] = Field(default_factory=list)
30
- fittness: float
33
+ fitness: float
31
34
  age: int = Field(default=0)
32
35
  index: int = Field(default=-1)
33
36
 
@@ -45,28 +48,38 @@ class Genom(BaseModel):
45
48
  raise TypeError(
46
49
  f"This operation is not supported between instances of {type(other)} and {type(self)}."
47
50
  )
48
- return np.all(self.fittness > other.fittness)
51
+ return np.all(self.fitness > other.fitness)
49
52
 
50
53
  def __lt__(self, other):
51
54
  if not isinstance(other, Genom):
52
55
  raise TypeError(
53
56
  f"This operation is not supported between instances of {type(other)} and {type(self)}."
54
57
  )
55
- return np.all(self.fittness < other.fittness)
58
+ return np.all(self.fitness < other.fitness)
56
59
 
57
60
  def __ge__(self, other):
58
61
  if not isinstance(other, Genom):
59
62
  raise TypeError(
60
63
  f"This operation is not supported between instances of {type(other)} and {type(self)}."
61
64
  )
62
- return np.all(self.fittness >= other.fittness)
65
+ return np.all(self.fitness >= other.fitness)
63
66
 
64
67
  def __le__(self, other):
65
68
  if not isinstance(other, Genom):
66
69
  raise TypeError(
67
70
  f"This operation is not supported between instances of {type(other)} and {type(self)}."
68
71
  )
69
- return np.all(self.fittness <= other.fittness)
72
+ return np.all(self.fitness <= other.fitness)
73
+
74
+ @property
75
+ def fittness(self) -> float: # pragma: no cover
76
+ import warnings
77
+ warnings.warn(
78
+ "'fittness' was a typo and is deprecated; it will be removed in a future version. Use 'fitness' instead.",
79
+ DeprecationWarning,
80
+ stacklevel=2,
81
+ )
82
+ return self.fitness
70
83
 
71
84
 
72
85
  class GeneticAlgorithm:
@@ -112,9 +125,8 @@ class GeneticAlgorithm:
112
125
  ftol: float, Optional
113
126
  Torelance for floating point operations. Default is 1e-12.
114
127
  maxage: int, Optional
115
- The age is the number of generations a candidate spends at the top
116
- (being the best candidate). Setting an upper limit to this value is a kind
117
- of stopping criterion. Default is 5.
128
+ The age is the maximum number of generations a candidate spends at the top
129
+ (being the best candidate). Default is 5.
118
130
  minimize: bool, Optional
119
131
  If True, the objective function is minimized. Default is False.
120
132
 
@@ -124,14 +136,23 @@ class GeneticAlgorithm:
124
136
  genetic algorithm can be wery demanding on the computational side. If the objective
125
137
  function takes a lot of time to evaluate, it is probably not a good idea to use a heuristic
126
138
  approach, unless you have a dedicated evaluator that is able to run efficiently for a large
127
- number of problems. If you want to customize the way the objective is evaluated, override
128
- the :func:`evaluate` method.
139
+ number of problems or if the long running time is not an issue. If you want to customize the way the
140
+ objective is evaluated, override the :func:`evaluate` method.
129
141
 
130
142
  See also
131
143
  --------
132
144
  :class:`~sigmaepsilon.math.optimize.bga.BinaryGeneticAlgorithm`
133
145
  """
134
146
 
147
+ @unique
148
+ class Status(Enum):
149
+ """Status codes for the genetic algorithm."""
150
+
151
+ INITIALIZED = 0
152
+ MAX_ITERATIONS_REACHED = 1
153
+ CONVERGED = 2
154
+ ERROR = -1
155
+
135
156
  __slots__ = [
136
157
  "fnc",
137
158
  "ranges",
@@ -142,17 +163,18 @@ class GeneticAlgorithm:
142
163
  "nPop",
143
164
  "_genotypes",
144
165
  "_phenotypes",
145
- "_fittness",
166
+ "_fitness",
146
167
  "_champion",
147
168
  "_evolver",
148
169
  "maxiter",
149
170
  "miniter",
150
171
  "elitism",
151
172
  "maxage",
152
- "nIter",
153
173
  "_is_symbolic_Function",
154
174
  "_celebrate_op",
155
175
  "_minimize",
176
+ "_state",
177
+ "_status",
156
178
  ]
157
179
 
158
180
  def __init__(
@@ -190,11 +212,13 @@ class GeneticAlgorithm:
190
212
  self.p_m = None
191
213
  self._genotypes = None
192
214
  self._pnenotypes = None
193
- self._fittness = None
215
+ self._fitness = None
194
216
  self._champion: Genom | NoneType = None
195
217
  self._celebrate_op = None
196
218
  self._minimize = False
197
219
  self.elitism = None
220
+ self._state = None
221
+ self._status = None
198
222
  self.set_solution_params(
199
223
  p_c=p_c,
200
224
  p_m=p_m,
@@ -206,6 +230,18 @@ class GeneticAlgorithm:
206
230
  )
207
231
  self.reset()
208
232
 
233
+ @property
234
+ def state(self) -> OptimizerState:
235
+ """
236
+ Returns the state of the optimizer.
237
+ """
238
+ return self._state
239
+
240
+ @property
241
+ def nIter(self) -> int: # pragma: no cover
242
+ """For backwards compatibility. Returns the number of iterations performed."""
243
+ return self.state.n_iter
244
+
209
245
  @property
210
246
  def champion(self) -> Genom:
211
247
  """
@@ -221,13 +257,13 @@ class GeneticAlgorithm:
221
257
  return self._genotypes
222
258
 
223
259
  @genotypes.setter
224
- def genotypes(self, value: Iterable):
260
+ def genotypes(self, value: Iterable) -> None:
225
261
  """
226
262
  Sets the genotypes of the population.
227
263
  """
228
264
  self._genotypes = value
229
265
  self._phenotypes = None
230
- self._fittness = None
266
+ self._fitness = None
231
267
 
232
268
  @property
233
269
  def phenotypes(self) -> Iterable:
@@ -240,25 +276,46 @@ class GeneticAlgorithm:
240
276
  self._phenotypes = self.decode(genotypes)
241
277
  return self._phenotypes
242
278
 
279
+
243
280
  @property
244
- def fittness(self) -> ndarray:
281
+ def fitness(self) -> ndarray:
245
282
  """
246
- Returns the actual fittness values of the population, or the fittness
283
+ Returns the actual fitness values of the population, or the fitness
247
284
  of the population described by the argument `phenotypes`.
248
285
  """
249
- if self._fittness is None:
250
- self._fittness = self.evaluate(self.phenotypes)
251
- return self._fittness
286
+ if self._fitness is not None:
287
+ return self._fitness
288
+
289
+ self._fitness = self.evaluate(self.phenotypes)
290
+ return self._fitness
291
+
292
+ @property
293
+ def fittness(self) -> ndarray: # pragma: no cover
294
+ import warnings
295
+ warnings.warn(
296
+ "'fittness' was a typo and is deprecated; it will be removed in a future version. Use 'fitness' instead.",
297
+ DeprecationWarning,
298
+ stacklevel=2,
299
+ )
300
+ return self.fitness
252
301
 
253
302
  def reset(self) -> "GeneticAlgorithm":
254
303
  """
255
304
  Resets the solver and returns the object. Only use it if you want to have
256
305
  a completely clean sheet. Also, the function is called for every object at
257
306
  instantiation.
307
+
308
+ Note
309
+ ----
310
+ This method resets the internal state of the genetic algorithm, including
311
+ the population and champion. Use this method when you want to start a new
312
+ optimization process from scratch.
258
313
  """
259
314
  self._evolver = self.evolver()
260
315
  self._evolver.send(None)
261
316
  self._champion = None
317
+ self._state = OptimizerState()
318
+ self._status = GeneticAlgorithm.Status.INITIALIZED
262
319
  return self
263
320
 
264
321
  def set_solution_params(self, **kwargs) -> "GeneticAlgorithm":
@@ -327,7 +384,7 @@ class GeneticAlgorithm:
327
384
  _ = yield
328
385
  yield self.genotypes
329
386
  while True:
330
- genotypes = self.select(self.genotypes, self.phenotypes)
387
+ genotypes = self.select()
331
388
  self.genotypes = self.populate(genotypes)
332
389
  yield self.genotypes
333
390
 
@@ -344,7 +401,7 @@ class GeneticAlgorithm:
344
401
 
345
402
  def solve(self, recycle: bool = False, **kwargs) -> Genom:
346
403
  """
347
- Solves the problem and returns the best phenotype.
404
+ Solves the problem and returns the champion.
348
405
 
349
406
  .. note::
350
407
  This class is designed for maximizing the objective function. To minimize it,
@@ -365,17 +422,39 @@ class GeneticAlgorithm:
365
422
  The best candidate.
366
423
 
367
424
  """
368
- self.reset() if not recycle else None
425
+ if not recycle:
426
+ self.reset()
369
427
  self.set_solution_params(**kwargs)
370
428
 
371
- nIter, finished = 0, False
429
+ finished = False
430
+ self._status = GeneticAlgorithm.Status.INITIALIZED
431
+
432
+ try:
433
+ while not finished:
434
+ self.evolve(1)
435
+ self.state.n_iter += 1
436
+ min_iter_reached = self.state.n_iter >= self.miniter
437
+ max_iter_reached = self.state.n_iter >= self.maxiter
438
+ finished = (
439
+ self.stopping_criteria() or max_iter_reached
440
+ ) and min_iter_reached
441
+
442
+ if finished:
443
+ if max_iter_reached:
444
+ self._status = GeneticAlgorithm.Status.MAX_ITERATIONS_REACHED
445
+ elif self.stopping_criteria():
446
+ self._status = GeneticAlgorithm.Status.CONVERGED
447
+ self._state.success = True
448
+ else: # pragma: no cover
449
+ self._state.success = False
450
+ except Exception as e:
451
+ self._state.success = False
452
+ self._state.message = str(e)
453
+ self._status = GeneticAlgorithm.Status.ERROR
454
+ raise e
455
+ finally:
456
+ self._state.stage = self._status.value
372
457
 
373
- while (not finished and nIter < self.maxiter) or (nIter < self.miniter):
374
- self.evolve(1)
375
- finished = self.stopping_criteria()
376
- nIter += 1
377
-
378
- self.nIter = nIter
379
458
  return self.champion
380
459
 
381
460
  def evaluate(self, phenotypes: Iterable | None = None) -> ndarray:
@@ -391,11 +470,19 @@ class GeneticAlgorithm:
391
470
  The phenotypes the objective function is to be evaluated for.
392
471
  Default is None.
393
472
  """
394
- phenotypes = self.phenotypes if phenotypes is None else phenotypes
395
- if self._is_symbolic_Function:
396
- return self.fnc(phenotypes.T)
397
- else:
398
- return np.array([self.fnc(x) for x in phenotypes], dtype=float)
473
+ try:
474
+ phenotypes = self.phenotypes if phenotypes is None else phenotypes
475
+ if self._is_symbolic_Function:
476
+ result = self.fnc(phenotypes.T)
477
+ else:
478
+ result = np.array([self.fnc(x) for x in phenotypes], dtype=float)
479
+ self.state.n_fev += len(phenotypes)
480
+ except Exception as e: # pragma: no cover
481
+ result = None
482
+ raise RuntimeError(
483
+ "Error during evaluation of the objective function."
484
+ ) from e
485
+ return result
399
486
 
400
487
  def best_phenotype(self) -> ndarray:
401
488
  """
@@ -420,13 +507,13 @@ class GeneticAlgorithm:
420
507
  the :func:`champion` property.
421
508
 
422
509
  """
423
- fittness = self.fittness
510
+ fitness = self.fitness
424
511
  argfunc = np.argmin if self._minimize else np.argmax
425
- index = argfunc(fittness)
512
+ index = argfunc(fitness)
426
513
  return Genom(
427
514
  phenotype=self.phenotypes[index],
428
515
  genotype=self.genotypes[index],
429
- fittness=fittness[index],
516
+ fitness=fitness[index],
430
517
  index=index,
431
518
  )
432
519
 
@@ -443,17 +530,19 @@ class GeneticAlgorithm:
443
530
  if has_new_champion:
444
531
  self._champion = genom
445
532
  self._champion.age = 0
533
+ self.state.x = self.champion.phenotype
534
+ self.state.fun = self.champion.fitness
446
535
  self._champion.age += 1
447
536
 
448
- def divide(self, fittness: ndarray | None = None) -> tuple[ndarray, ndarray]:
537
+ def divide(self, fitness: ndarray | None = None) -> tuple[ndarray, ndarray]:
449
538
  """
450
539
  Divides population to elit and others, and returns the corresponding
451
540
  index arrays.
452
541
 
453
542
  Parameters
454
543
  ----------
455
- fittness: numpy.ndarray, Optional
456
- Fittness values. If not provided, values from the latest
544
+ fitness: numpy.ndarray, Optional
545
+ Fitness values. If not provided, values from the latest
457
546
  evaluation are used. Default is None.
458
547
 
459
548
  Returns
@@ -463,14 +552,14 @@ class GeneticAlgorithm:
463
552
  list
464
553
  Indices of the members of the others.
465
554
  """
466
- fittness = self.fittness if fittness is None else fittness
467
- assert fittness is not None, "No available fittness data detected."
555
+ fitness = self.fitness if fitness is None else fitness
556
+ assert fitness is not None, "No available fitness data detected."
468
557
 
469
558
  if self.elitism is None:
470
559
  return [], list(range(self.nPop))
471
560
 
472
561
  if self.elitism is not None:
473
- argsort = np.argsort(fittness)
562
+ argsort = np.argsort(fitness)
474
563
  if not self._minimize:
475
564
  argsort = argsort[::-1]
476
565
 
@@ -566,9 +655,11 @@ class GeneticAlgorithm:
566
655
  ...
567
656
 
568
657
  @abstractmethod
569
- def select(self, genotypes: ndarray, phenotypes: ndarray | None = None) -> ndarray:
658
+ def select(
659
+ self, genotypes: ndarray, phenotypes: ndarray
660
+ ) -> ndarray:
570
661
  """
571
- Ought to implement dome kind of selection mechanism, eg. a roulette wheel,
572
- tournament or other.
662
+ Ought to implement some kind of selection mechanism, e.g., a roulette wheel,
663
+ tournament, or other. Both `genotypes` and `phenotypes` must be provided.
573
664
  """
574
665
  ...
@@ -0,0 +1,78 @@
1
+ from types import NoneType
2
+ from pydantic import BaseModel, Field
3
+ import numpy as np
4
+ from scipy.optimize import OptimizeResult
5
+
6
+
7
+ __all__ = ["OptimizerState"]
8
+
9
+
10
+ class OptimizerState(BaseModel):
11
+ """
12
+ Model representing the state of an optimizer.
13
+
14
+ Attributes
15
+ ----------
16
+ x : list[float] | NoneType
17
+ The current solution vector, or None if not set.
18
+ fun : float | NoneType
19
+ The value of the objective function at the current solution, or None if not set.
20
+ n_fev : int
21
+ The number of function evaluations performed.
22
+ n_jev : int
23
+ The number of Jacobian evaluations performed.
24
+ n_hev : int
25
+ The number of Hessian evaluations performed.
26
+ n_iter : int
27
+ The number of iterations completed.
28
+ success : bool
29
+ Whether or not the optimizer exited successfully.
30
+ message : str
31
+ A message describing the cause of the termination.
32
+ """
33
+
34
+ x: list[float] | NoneType = Field(
35
+ default=None, description="The current solution vector."
36
+ )
37
+ fun: float | NoneType = Field(
38
+ default=None,
39
+ description="The value of the objective function at the current solution.",
40
+ )
41
+ n_fev: int = Field(
42
+ default=0, description="The number of function evaluations performed."
43
+ )
44
+ n_jev: int = Field(
45
+ default=0, description="The number of Jacobian evaluations performed."
46
+ )
47
+ n_hev: int = Field(
48
+ default=0, description="The number of Hessian evaluations performed."
49
+ )
50
+ n_iter: int = Field(default=0, description="The number of iterations completed.")
51
+ success: bool = Field(
52
+ default=True, description="Indicates if the optimizer exited successfully."
53
+ )
54
+ message: str = Field(
55
+ default="", description="Description of the cause of termination."
56
+ )
57
+ stage: int | NoneType = Field(
58
+ default=None,
59
+ description=(
60
+ "Termination status of the optimizer. "
61
+ "Its value depends on the underlying solver. "
62
+ "Refer to the solver being used for more details."
63
+ ),
64
+ )
65
+
66
+ def to_scipy(self) -> OptimizeResult:
67
+ """Returns the state as a SciPy OptimizeResult object."""
68
+ return OptimizeResult(
69
+ x=np.array(self.x) if self.x is not None else None,
70
+ fun=self.fun,
71
+ status=self.stage,
72
+ nfev=self.n_fev,
73
+ njev=self.n_jev,
74
+ nhev=self.n_hev,
75
+ success=self.success,
76
+ message=self.message,
77
+ nit=self.n_iter,
78
+ )