aisp 0.3.21__py3-none-any.whl → 0.4.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.
- aisp/__init__.py +17 -4
- aisp/base/__init__.py +21 -2
- aisp/base/_classifier.py +4 -2
- aisp/base/_optimizer.py +188 -0
- aisp/base/mutation.py +86 -18
- aisp/base/populations.py +49 -0
- aisp/csa/__init__.py +12 -1
- aisp/csa/_cell.py +2 -2
- aisp/csa/_clonalg.py +369 -0
- aisp/ina/__init__.py +2 -2
- aisp/ina/_ai_network.py +5 -4
- aisp/ina/_base.py +0 -40
- aisp/nsa/__init__.py +7 -0
- aisp/utils/display.py +185 -0
- aisp/utils/sanitizers.py +53 -1
- aisp/utils/types.py +10 -2
- {aisp-0.3.21.dist-info → aisp-0.4.0.dist-info}/METADATA +2 -1
- aisp-0.4.0.dist-info/RECORD +35 -0
- aisp-0.3.21.dist-info/RECORD +0 -31
- {aisp-0.3.21.dist-info → aisp-0.4.0.dist-info}/WHEEL +0 -0
- {aisp-0.3.21.dist-info → aisp-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {aisp-0.3.21.dist-info → aisp-0.4.0.dist-info}/top_level.txt +0 -0
aisp/csa/_clonalg.py
ADDED
@@ -0,0 +1,369 @@
|
|
1
|
+
"""Clonal Selection Algorithm (CLONALG)."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import heapq
|
6
|
+
from typing import Optional, Callable, Dict, Literal
|
7
|
+
|
8
|
+
import numpy as np
|
9
|
+
import numpy.typing as npt
|
10
|
+
|
11
|
+
from ..utils.display import ProgressTable
|
12
|
+
from ..base import BaseOptimizer, set_seed_numba
|
13
|
+
from ..base.mutation import clone_and_mutate_binary, clone_and_mutate_ranged, \
|
14
|
+
clone_and_mutate_continuous, clone_and_mutate_permutation
|
15
|
+
from ..base.populations import generate_random_antibodies
|
16
|
+
from ..utils.sanitizers import sanitize_seed, sanitize_param, sanitize_bounds
|
17
|
+
from ..utils.types import FeatureTypeAll
|
18
|
+
|
19
|
+
|
20
|
+
class Clonalg(BaseOptimizer):
|
21
|
+
"""Clonal Selection Algorithm (CLONALG).
|
22
|
+
|
23
|
+
The Clonal Selection Algorithm (CSA) is an optimization algorithm inspired by the biological
|
24
|
+
process of clonal selection and expansion of antibodies in the immune system [1]_. This
|
25
|
+
implementation of CLONALG has been adapted for the minimization or maximization of cost
|
26
|
+
functions in binary, continuous, ranged-value, and permutation problems.
|
27
|
+
|
28
|
+
|
29
|
+
Parameters
|
30
|
+
----------
|
31
|
+
problem_size : int
|
32
|
+
Dimension of the problem to be minimized.
|
33
|
+
N : int, default=50
|
34
|
+
Number of memory cells (antibodies) in the population.
|
35
|
+
rate_clonal : float, default=10
|
36
|
+
Maximum number of possible clones of a cell. This value is multiplied by
|
37
|
+
cell_affinity to determine the number of clones.
|
38
|
+
rate_hypermutation : float, default=0.75
|
39
|
+
Rate of mutated clones, used as a scalar factor.
|
40
|
+
n_diversity_injection : int, default=5
|
41
|
+
Number of new random memory cells injected to maintain diversity.
|
42
|
+
selection_size : int, default=5
|
43
|
+
Number of the best antibodies selected for cloning.
|
44
|
+
affinity_function : Optional[Callable[..., npt.NDArray]], default=None
|
45
|
+
Objective function to evaluate candidate solutions in minimizing the problem.
|
46
|
+
feature_type : FeatureTypeAll, default='ranged-features'
|
47
|
+
Type of problem samples: binary, continuous, or based on value ranges.
|
48
|
+
Specifies the type of features: "continuous-features", "binary-features",
|
49
|
+
"ranged-features", or "permutation-features".
|
50
|
+
bounds : Optional[Dict], default=None
|
51
|
+
Definition of search limits when ``feature_type='ranged-features'``.
|
52
|
+
Can be provided in two ways:
|
53
|
+
|
54
|
+
* Fixed values: ``{'low': float, 'high': float}``
|
55
|
+
Values are replicated across all dimensions, generating equal limits for each
|
56
|
+
dimension.
|
57
|
+
* Arrays: ``{'low': list, 'high': list}``
|
58
|
+
Each dimension has specific limits. Both arrays must be
|
59
|
+
``problem_size``.
|
60
|
+
|
61
|
+
mode : Literal["min", "max"], default="min"
|
62
|
+
Defines whether the algorithm minimizes or maximizes the cost function.
|
63
|
+
seed : Optional[int], default=None
|
64
|
+
Seed for random generation of detector values. If None, the value is random.
|
65
|
+
|
66
|
+
Notes
|
67
|
+
-----
|
68
|
+
This CLONALG implementation contains some changes based on the AISP context, for general
|
69
|
+
application to various problems, which may produce results different from the standard or
|
70
|
+
specific implementation. This adaptation aims to generalize CLONALG to minimization and
|
71
|
+
maximization tasks, in addition to supporting continuous, discrete, and permutation problems.
|
72
|
+
|
73
|
+
References
|
74
|
+
----------
|
75
|
+
.. [1] BROWNLEE, Jason. Clonal Selection Algorithm. Clever Algorithms: Nature-inspired
|
76
|
+
Programming Recipes., 2011. Available at:
|
77
|
+
https://cleveralgorithms.com/nature-inspired/immune/clonal_selection_algorithm.html
|
78
|
+
"""
|
79
|
+
|
80
|
+
def __init__(
|
81
|
+
self,
|
82
|
+
problem_size: int,
|
83
|
+
N: int = 50,
|
84
|
+
rate_clonal: int = 10,
|
85
|
+
rate_hypermutation: float = 0.75,
|
86
|
+
n_diversity_injection: int = 5,
|
87
|
+
selection_size: int = 5,
|
88
|
+
affinity_function: Optional[Callable[..., npt.NDArray]] = None,
|
89
|
+
feature_type: FeatureTypeAll = 'ranged-features',
|
90
|
+
bounds: Optional[Dict] = None,
|
91
|
+
mode: Literal["min", "max"] = "min",
|
92
|
+
seed: Optional[int] = None
|
93
|
+
):
|
94
|
+
super().__init__()
|
95
|
+
self.problem_size = sanitize_param(problem_size, 1, lambda x: x > 0)
|
96
|
+
self.N: int = sanitize_param(N, 50, lambda x: x > 0)
|
97
|
+
self.rate_clonal: int = sanitize_param(rate_clonal, 10, lambda x: x > 0)
|
98
|
+
self.rate_hypermutation: np.float64 = np.float64(
|
99
|
+
sanitize_param(
|
100
|
+
rate_hypermutation, 0.75, lambda x: x > 0
|
101
|
+
)
|
102
|
+
)
|
103
|
+
self.n_diversity_injection: int = sanitize_param(
|
104
|
+
n_diversity_injection, 5, lambda x: x > 0
|
105
|
+
)
|
106
|
+
self.selection_size: int = sanitize_param(
|
107
|
+
selection_size, 5, lambda x: x > 0
|
108
|
+
)
|
109
|
+
self._affinity_function = affinity_function
|
110
|
+
self.feature_type: FeatureTypeAll = feature_type
|
111
|
+
|
112
|
+
self._bounds = None
|
113
|
+
self._bounds_extend_cache = None
|
114
|
+
self.bounds = bounds
|
115
|
+
|
116
|
+
self.mode: Literal["min", "max"] = sanitize_param(
|
117
|
+
mode,
|
118
|
+
"min",
|
119
|
+
lambda x: x == "max"
|
120
|
+
)
|
121
|
+
|
122
|
+
self.seed: Optional[int] = sanitize_seed(seed)
|
123
|
+
if self.seed is not None:
|
124
|
+
np.random.seed(self.seed)
|
125
|
+
set_seed_numba(self.seed)
|
126
|
+
|
127
|
+
self.population = None
|
128
|
+
|
129
|
+
@property
|
130
|
+
def bounds(self) -> Optional[Dict]:
|
131
|
+
"""Getter for the bounds attribute."""
|
132
|
+
return self._bounds
|
133
|
+
|
134
|
+
@bounds.setter
|
135
|
+
def bounds(self, value: Optional[Dict]):
|
136
|
+
"""Setter for the bounds attribute."""
|
137
|
+
if self.feature_type == 'ranged-features':
|
138
|
+
self._bounds = sanitize_bounds(value, self.problem_size)
|
139
|
+
low_bounds = np.array(self._bounds['low'])
|
140
|
+
high_bounds = np.array(self._bounds['high'])
|
141
|
+
self._bounds_extend_cache = np.array([low_bounds, high_bounds])
|
142
|
+
else:
|
143
|
+
self._bounds = None
|
144
|
+
self._bounds_extend_cache = None
|
145
|
+
|
146
|
+
def optimize(
|
147
|
+
self,
|
148
|
+
max_iters: int = 50,
|
149
|
+
n_iter_no_change=10,
|
150
|
+
verbose: bool = True
|
151
|
+
) -> npt.NDArray:
|
152
|
+
"""Execute the optimization process and return the population.
|
153
|
+
|
154
|
+
Parameters
|
155
|
+
----------
|
156
|
+
max_iters : int, default=50
|
157
|
+
Maximum number of interactions when searching for the best solution using clonalg.
|
158
|
+
n_iter_no_change: int, default=10
|
159
|
+
the maximum number of iterations without updating the best cell
|
160
|
+
verbose : bool, default=True
|
161
|
+
Feedback on interactions, indicating the best antibody.
|
162
|
+
|
163
|
+
Returns
|
164
|
+
-------
|
165
|
+
population : npt.NDArray
|
166
|
+
Antibody population after clonal expansion.
|
167
|
+
"""
|
168
|
+
self.reset()
|
169
|
+
self.population = self._init_population_antibodies()
|
170
|
+
|
171
|
+
t = 1
|
172
|
+
antibodies = [(antibody, self.affinity_function(antibody)) for antibody in self.population]
|
173
|
+
best_cost = None
|
174
|
+
stop = 0
|
175
|
+
progress = ProgressTable(
|
176
|
+
{
|
177
|
+
"Iteration": 11,
|
178
|
+
f"Best Affinity ({self.mode})": 25,
|
179
|
+
"Worse Affinity": 20,
|
180
|
+
"Stagnation": 17},
|
181
|
+
verbose
|
182
|
+
)
|
183
|
+
|
184
|
+
while t <= max_iters:
|
185
|
+
p_select = self._select_top_antibodies(self.selection_size, antibodies)
|
186
|
+
self._record_best(p_select[0][1], p_select[0][0])
|
187
|
+
|
188
|
+
clones = self._clone_and_hypermutation(p_select)
|
189
|
+
|
190
|
+
p_rand = [
|
191
|
+
(antibody, self.affinity_function(antibody))
|
192
|
+
for antibody in self._diversity_introduction()
|
193
|
+
]
|
194
|
+
antibodies = p_select
|
195
|
+
antibodies.extend(clones)
|
196
|
+
antibodies = self._select_top_antibodies(
|
197
|
+
self.N - self.n_diversity_injection, antibodies
|
198
|
+
)
|
199
|
+
antibodies.extend(p_rand)
|
200
|
+
if len(antibodies) > self.N:
|
201
|
+
antibodies = self._select_top_antibodies(self.N, antibodies)
|
202
|
+
if best_cost == self.best_cost:
|
203
|
+
stop += 1
|
204
|
+
else:
|
205
|
+
stop = 0
|
206
|
+
best_cost = self.best_cost
|
207
|
+
progress.update(
|
208
|
+
{
|
209
|
+
"Iteration": t,
|
210
|
+
f"Best Affinity ({self.mode})": f"{self.best_cost:>25.6f}",
|
211
|
+
"Worse Affinity": f"{antibodies[-1][1]:>20.6f}",
|
212
|
+
"Stagnation": stop
|
213
|
+
}
|
214
|
+
)
|
215
|
+
if stop == n_iter_no_change:
|
216
|
+
break
|
217
|
+
|
218
|
+
t += 1
|
219
|
+
progress.finish()
|
220
|
+
self.population = np.array([antibody for antibody, _ in antibodies]).astype(dtype=float)
|
221
|
+
return self.population
|
222
|
+
|
223
|
+
def _select_top_antibodies(self, n: int, antibodies: list[tuple]) -> list[tuple]:
|
224
|
+
"""Select the antibodies with the highest or lowest values, depending on the mode.
|
225
|
+
|
226
|
+
Parameters
|
227
|
+
----------
|
228
|
+
n : int
|
229
|
+
Number of antibodies to select.
|
230
|
+
antibodies : list[tuple]
|
231
|
+
Representing the antibodies and their associated score.
|
232
|
+
|
233
|
+
Returns
|
234
|
+
-------
|
235
|
+
List containing the `n` antibodies selected according to the defined min or max
|
236
|
+
criterion.
|
237
|
+
"""
|
238
|
+
if self.mode == "max":
|
239
|
+
return heapq.nlargest(n, antibodies, key=lambda x: x[1])
|
240
|
+
|
241
|
+
return heapq.nsmallest(n, antibodies, key=lambda x: x[1])
|
242
|
+
|
243
|
+
def affinity_function(self, solution: npt.NDArray) -> np.float64:
|
244
|
+
"""
|
245
|
+
Evaluate the affinity of a candidate cell.
|
246
|
+
|
247
|
+
Parameters
|
248
|
+
----------
|
249
|
+
solution : npt.NDArray
|
250
|
+
Candidate solution to evaluate.
|
251
|
+
|
252
|
+
Returns
|
253
|
+
-------
|
254
|
+
affinity : float
|
255
|
+
Affinity value associated with the given cell.
|
256
|
+
|
257
|
+
Raises
|
258
|
+
------
|
259
|
+
NotImplementedError
|
260
|
+
If no affinity function has been provided.
|
261
|
+
"""
|
262
|
+
if not callable(self._affinity_function):
|
263
|
+
raise NotImplementedError(
|
264
|
+
"No affinity function to evaluate the candidate cell was provided."
|
265
|
+
)
|
266
|
+
return np.float64(self._affinity_function(solution))
|
267
|
+
|
268
|
+
def _init_population_antibodies(self) -> npt.NDArray:
|
269
|
+
"""Initialize the antibody set of the population randomly.
|
270
|
+
|
271
|
+
Returns
|
272
|
+
-------
|
273
|
+
npt.NDArray
|
274
|
+
List of initialized antibodies.
|
275
|
+
"""
|
276
|
+
return generate_random_antibodies(
|
277
|
+
self.N,
|
278
|
+
self.problem_size,
|
279
|
+
self.feature_type,
|
280
|
+
self._bounds_extend_cache
|
281
|
+
)
|
282
|
+
|
283
|
+
def _diversity_introduction(self):
|
284
|
+
"""Introduce diversity into the antibody population.
|
285
|
+
|
286
|
+
Returns
|
287
|
+
-------
|
288
|
+
npt.NDArray
|
289
|
+
Array of new random antibodies for diversity introduction.
|
290
|
+
"""
|
291
|
+
return generate_random_antibodies(
|
292
|
+
self.n_diversity_injection,
|
293
|
+
self.problem_size,
|
294
|
+
self.feature_type,
|
295
|
+
self._bounds_extend_cache
|
296
|
+
)
|
297
|
+
|
298
|
+
def _clone_and_mutate(
|
299
|
+
self,
|
300
|
+
antibody: npt.NDArray,
|
301
|
+
n_clone: int,
|
302
|
+
rate_hypermutation: float
|
303
|
+
) -> npt.NDArray:
|
304
|
+
"""
|
305
|
+
Generate mutated clones from an antibody, based on the feature type.
|
306
|
+
|
307
|
+
Parameters
|
308
|
+
----------
|
309
|
+
antibody : npt.NDArray
|
310
|
+
Original antibody vector to be cloned and mutated.
|
311
|
+
n_clone : int
|
312
|
+
Number of clones to generate.
|
313
|
+
|
314
|
+
Returns
|
315
|
+
-------
|
316
|
+
npt.NDArray
|
317
|
+
Array of shape (n_clone, len(antibody)) containing mutated clones
|
318
|
+
"""
|
319
|
+
if self.feature_type == "binary-features":
|
320
|
+
return clone_and_mutate_binary(antibody, n_clone)
|
321
|
+
if self.feature_type == "ranged-features" and self._bounds_extend_cache is not None:
|
322
|
+
return clone_and_mutate_ranged(
|
323
|
+
antibody, n_clone, self._bounds_extend_cache, rate_hypermutation
|
324
|
+
)
|
325
|
+
if self.feature_type == "permutation-features":
|
326
|
+
return clone_and_mutate_permutation(antibody, n_clone, rate_hypermutation)
|
327
|
+
return clone_and_mutate_continuous(antibody, n_clone, rate_hypermutation)
|
328
|
+
|
329
|
+
def _clone_and_hypermutation(
|
330
|
+
self,
|
331
|
+
population: list[tuple]
|
332
|
+
) -> list:
|
333
|
+
"""Clone and hypermutate the population's antibodies.
|
334
|
+
|
335
|
+
The clone list is returned with the clones and their affinities with respect to the cost
|
336
|
+
function.
|
337
|
+
|
338
|
+
Parameters
|
339
|
+
----------
|
340
|
+
population: list
|
341
|
+
The list of antibodies (solutions) to be evaluated and cloned.
|
342
|
+
|
343
|
+
Returns
|
344
|
+
-------
|
345
|
+
list[npt.NDArray]
|
346
|
+
List of mutated clones.
|
347
|
+
"""
|
348
|
+
clonal_m = []
|
349
|
+
min_affinity = min(item[1] for item in population)
|
350
|
+
max_affinity = max(item[1] for item in population)
|
351
|
+
affinity_range = max_affinity - min_affinity
|
352
|
+
|
353
|
+
for antibody, affinity in population:
|
354
|
+
if affinity_range == 0:
|
355
|
+
normalized_affinity = 1
|
356
|
+
else:
|
357
|
+
normalized_affinity = (affinity - min_affinity) / affinity_range
|
358
|
+
if self.mode == "min":
|
359
|
+
normalized_affinity = max(0.0, 1.0 - normalized_affinity)
|
360
|
+
|
361
|
+
num_clones = max(0, int(self.rate_clonal * normalized_affinity))
|
362
|
+
clones = self._clone_and_mutate(
|
363
|
+
antibody,
|
364
|
+
num_clones,
|
365
|
+
1 - np.exp(-self.rate_hypermutation * normalized_affinity)
|
366
|
+
)
|
367
|
+
clonal_m.extend(clones)
|
368
|
+
|
369
|
+
return [(clone, self.affinity_function(clone)) for clone in clonal_m]
|
aisp/ina/__init__.py
CHANGED
@@ -4,8 +4,8 @@ This module implements algorithms based on Network Theory Algorithms proposed by
|
|
4
4
|
|
5
5
|
Classes
|
6
6
|
-------
|
7
|
-
AiNet
|
8
|
-
|
7
|
+
AiNet : Artificial Immune Network.
|
8
|
+
An unsupervised learning algorithm for clustering, based on the theory of immune networks.
|
9
9
|
"""
|
10
10
|
|
11
11
|
from ._ai_network import AiNet
|
aisp/ina/_ai_network.py
CHANGED
@@ -16,6 +16,7 @@ from ._base import BaseAiNet
|
|
16
16
|
from ..base import set_seed_numba
|
17
17
|
from ..base.mutation import clone_and_mutate_binary, clone_and_mutate_continuous, \
|
18
18
|
clone_and_mutate_ranged
|
19
|
+
from ..base.populations import generate_random_antibodies
|
19
20
|
from ..utils.distance import hamming, compute_metric_distance, get_metric_code
|
20
21
|
from ..utils.sanitizers import sanitize_choice, sanitize_param, sanitize_seed
|
21
22
|
from ..utils.types import FeatureType, MetricType
|
@@ -293,7 +294,7 @@ class AiNet(BaseAiNet):
|
|
293
294
|
npt.NDArray
|
294
295
|
List of initialized memories.
|
295
296
|
"""
|
296
|
-
return
|
297
|
+
return generate_random_antibodies(
|
297
298
|
self.N,
|
298
299
|
self._n_features,
|
299
300
|
self._feature_type,
|
@@ -402,7 +403,7 @@ class AiNet(BaseAiNet):
|
|
402
403
|
npt.NDArray
|
403
404
|
Array of new random antibodies for diversity introduction.
|
404
405
|
"""
|
405
|
-
return
|
406
|
+
return generate_random_antibodies(
|
406
407
|
self.n_diversity_injection,
|
407
408
|
self._n_features,
|
408
409
|
self._feature_type,
|
@@ -478,8 +479,8 @@ class AiNet(BaseAiNet):
|
|
478
479
|
if self._feature_type == "binary-features":
|
479
480
|
return clone_and_mutate_binary(antibody, n_clone)
|
480
481
|
if self._feature_type == "ranged-features" and self._bounds is not None:
|
481
|
-
return clone_and_mutate_ranged(antibody, n_clone, self._bounds)
|
482
|
-
return clone_and_mutate_continuous(antibody, n_clone)
|
482
|
+
return clone_and_mutate_ranged(antibody, n_clone, self._bounds, np.float64(1.0))
|
483
|
+
return clone_and_mutate_continuous(antibody, n_clone, np.float64(1.0))
|
483
484
|
|
484
485
|
def _build_mst(self):
|
485
486
|
"""Construct the Minimum Spanning Tree (MST) for the antibody population.
|
aisp/ina/_base.py
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
"""Base Class for Network Theory Algorithms."""
|
2
2
|
|
3
3
|
from abc import ABC
|
4
|
-
from typing import Optional
|
5
4
|
|
6
5
|
import numpy as np
|
7
6
|
from numpy import typing as npt
|
@@ -82,42 +81,3 @@ class BaseAiNet(BaseClusterer, ABC):
|
|
82
81
|
raise ValueError(
|
83
82
|
"The array X contains values that are not composed only of 0 and 1."
|
84
83
|
)
|
85
|
-
|
86
|
-
@staticmethod
|
87
|
-
def _generate_random_antibodies(
|
88
|
-
n_samples: int,
|
89
|
-
n_features: int,
|
90
|
-
feature_type: FeatureType = "continuous-features",
|
91
|
-
bounds: Optional[npt.NDArray[np.float64]] = None
|
92
|
-
) -> npt.NDArray:
|
93
|
-
"""
|
94
|
-
Generate a random antibody population.
|
95
|
-
|
96
|
-
Parameters
|
97
|
-
----------
|
98
|
-
n_samples : int
|
99
|
-
Number of antibodies (samples) to generate.
|
100
|
-
n_features : int
|
101
|
-
Number of features (dimensions) for each antibody.
|
102
|
-
feature_type : FeatureType, default="continuous-features"
|
103
|
-
Specifies the type of features: "continuous-features", "binary-features",
|
104
|
-
or "ranged-features".
|
105
|
-
bounds : np.ndarray
|
106
|
-
Array (n_features, 2) with min and max per dimension.
|
107
|
-
|
108
|
-
Returns
|
109
|
-
-------
|
110
|
-
npt.NDArray
|
111
|
-
Array of shape (n_samples, n_features) containing the generated antibodies.
|
112
|
-
Data type depends on the feature_type type (float for continuous/ranged, bool for
|
113
|
-
binary).
|
114
|
-
"""
|
115
|
-
if n_features <= 0:
|
116
|
-
raise ValueError("Number of features must be greater than zero.")
|
117
|
-
|
118
|
-
if feature_type == "binary-features":
|
119
|
-
return np.random.randint(0, 2, size=(n_samples, n_features)).astype(np.bool_)
|
120
|
-
if feature_type == "ranged-features" and bounds is not None:
|
121
|
-
return np.random.uniform(low=bounds[0], high=bounds[1], size=(n_samples, n_features))
|
122
|
-
|
123
|
-
return np.random.random_sample(size=(n_samples, n_features))
|
aisp/nsa/__init__.py
CHANGED
@@ -3,6 +3,13 @@
|
|
3
3
|
NSAs simulate the maturation process of T-cells in the immune system, where these cells learn to
|
4
4
|
distinguish between self and non-self. Only T-cells capable of recognizing non-self elements are
|
5
5
|
preserved.
|
6
|
+
|
7
|
+
Classes
|
8
|
+
-------
|
9
|
+
RNSA : Real-valued Negative Selection Algorithm.
|
10
|
+
A supervised learning algorithm for classification that uses real-valued detectors.
|
11
|
+
BNSA : Binary Negative Selection Algorithm.
|
12
|
+
A supervised learning algorithm for classification that uses binary detectors.
|
6
13
|
"""
|
7
14
|
|
8
15
|
from ._binary_negative_selection import BNSA
|
aisp/utils/display.py
ADDED
@@ -0,0 +1,185 @@
|
|
1
|
+
"""Utility functions for displaying algorithm information."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import locale
|
6
|
+
import sys
|
7
|
+
import time
|
8
|
+
from typing import Mapping, Union
|
9
|
+
|
10
|
+
|
11
|
+
def _supports_box_drawing() -> bool:
|
12
|
+
"""
|
13
|
+
Check if the terminal supports boxed characters.
|
14
|
+
|
15
|
+
Returns
|
16
|
+
-------
|
17
|
+
bool
|
18
|
+
True if the terminal likely supports boxed characters, False otherwise.
|
19
|
+
"""
|
20
|
+
enc = (sys.stdout.encoding or locale.getpreferredencoding(False)).lower()
|
21
|
+
if not enc.startswith("utf"):
|
22
|
+
return False
|
23
|
+
try:
|
24
|
+
"┌".encode(enc)
|
25
|
+
except UnicodeEncodeError:
|
26
|
+
return False
|
27
|
+
return True
|
28
|
+
|
29
|
+
|
30
|
+
class TableFormatter:
|
31
|
+
"""
|
32
|
+
Format tabular data into strings for display in the console.
|
33
|
+
|
34
|
+
Parameters
|
35
|
+
----------
|
36
|
+
headers : Mapping[str, int]
|
37
|
+
Mapping of column names to their respective widths, in the format
|
38
|
+
{column_name: column_width}.
|
39
|
+
"""
|
40
|
+
|
41
|
+
def __init__(self, headers: Mapping[str, int]) -> None:
|
42
|
+
if not headers or not isinstance(headers, Mapping):
|
43
|
+
raise ValueError("'headers' must be a non-empty dictionary.")
|
44
|
+
self.headers: Mapping[str, int] = headers
|
45
|
+
self._ascii_only = not _supports_box_drawing()
|
46
|
+
|
47
|
+
def _border(self, left: str, middle: str, right: str, line: str, new_line: bool = True) -> str:
|
48
|
+
"""
|
49
|
+
Create a horizontal border for the table.
|
50
|
+
|
51
|
+
Parameters
|
52
|
+
----------
|
53
|
+
left: str
|
54
|
+
Character on the left side of the border.
|
55
|
+
middle: str
|
56
|
+
Character separator between columns.
|
57
|
+
right: str
|
58
|
+
Character on the right side of the border.
|
59
|
+
line: str
|
60
|
+
Character used to fill the border.
|
61
|
+
new_line: bool, optional
|
62
|
+
If True, adds a line break before the border (default is True).
|
63
|
+
|
64
|
+
Returns
|
65
|
+
-------
|
66
|
+
str
|
67
|
+
String representing the horizontal border.
|
68
|
+
"""
|
69
|
+
nl = '\n' if new_line else ''
|
70
|
+
return f"{nl}{left}{middle.join(line * w for w in self.headers.values())}{right}"
|
71
|
+
|
72
|
+
def get_header(self):
|
73
|
+
"""
|
74
|
+
Generate the table header, including the top border, column headings, and separator line.
|
75
|
+
|
76
|
+
Returns
|
77
|
+
-------
|
78
|
+
str
|
79
|
+
Formatted string of the table header.
|
80
|
+
"""
|
81
|
+
if self._ascii_only:
|
82
|
+
top = self._border("+", "+", "+", "-")
|
83
|
+
sep = self._border("+", "+", "+", "-")
|
84
|
+
cell_border = "|"
|
85
|
+
else:
|
86
|
+
top = self._border("┌", "┬", "┐", "─")
|
87
|
+
sep = self._border("├", "┼", "┤", "─")
|
88
|
+
cell_border = "│"
|
89
|
+
|
90
|
+
titles = '\n' + cell_border + cell_border.join(
|
91
|
+
f"{h:^{self.headers[h]}}" for h in self.headers
|
92
|
+
) + cell_border
|
93
|
+
|
94
|
+
return top + titles + sep
|
95
|
+
|
96
|
+
def get_row(self, values: Mapping[str, Union[str, int, float]]):
|
97
|
+
"""
|
98
|
+
Generate a formatted row for the table data.
|
99
|
+
|
100
|
+
Parameters
|
101
|
+
----------
|
102
|
+
values : Mapping[str, Union[str, int, float]]
|
103
|
+
Dictionary with values for each column, in the format
|
104
|
+
{column_name: value}.
|
105
|
+
|
106
|
+
Returns
|
107
|
+
-------
|
108
|
+
str
|
109
|
+
Formatted string of the table row.
|
110
|
+
"""
|
111
|
+
border = "|" if self._ascii_only else "│"
|
112
|
+
row = border + border.join(
|
113
|
+
f"{values.get(h, ''):^{self.headers[h]}}" for h in self.headers
|
114
|
+
) + border
|
115
|
+
|
116
|
+
return row
|
117
|
+
|
118
|
+
def get_bottom(self, new_line: bool = False):
|
119
|
+
"""
|
120
|
+
Generate the table's bottom border.
|
121
|
+
|
122
|
+
Parameters
|
123
|
+
----------
|
124
|
+
new_line : bool, Optional
|
125
|
+
If True, adds a line break before the border (default is False).
|
126
|
+
|
127
|
+
Returns
|
128
|
+
-------
|
129
|
+
str
|
130
|
+
Formatted string for the bottom border.
|
131
|
+
"""
|
132
|
+
if self._ascii_only:
|
133
|
+
bottom = self._border("+", "+", "+", "-", new_line)
|
134
|
+
else:
|
135
|
+
bottom = self._border("└", "┴", "┘", "─", new_line)
|
136
|
+
return bottom
|
137
|
+
|
138
|
+
|
139
|
+
class ProgressTable(TableFormatter):
|
140
|
+
"""
|
141
|
+
Display a formatted table in the console to track the algorithm's progress.
|
142
|
+
|
143
|
+
Parameters
|
144
|
+
----------
|
145
|
+
headers : Mapping[str, int]
|
146
|
+
Mapping {column_name: column_width}.
|
147
|
+
verbose : bool, default=True
|
148
|
+
If False, prints nothing to the terminal.
|
149
|
+
"""
|
150
|
+
|
151
|
+
def __init__(self, headers: Mapping[str, int], verbose: bool = True) -> None:
|
152
|
+
super().__init__(headers)
|
153
|
+
if not headers or not isinstance(headers, Mapping):
|
154
|
+
raise ValueError("'headers' must be a non-empty dictionary.")
|
155
|
+
self.verbose: bool = verbose
|
156
|
+
self.headers: Mapping[str, int] = headers
|
157
|
+
self._ascii_only = not _supports_box_drawing()
|
158
|
+
if self.verbose:
|
159
|
+
self._print_header()
|
160
|
+
self._start = time.perf_counter()
|
161
|
+
|
162
|
+
def _print_header(self) -> None:
|
163
|
+
"""Print the table header."""
|
164
|
+
print(self.get_header())
|
165
|
+
|
166
|
+
def update(self, values: Mapping[str, Union[str, int, float]]) -> None:
|
167
|
+
"""
|
168
|
+
Add a new row of values to the table.
|
169
|
+
|
170
|
+
Parameters
|
171
|
+
----------
|
172
|
+
values: Mapping[str, Union[str, int, float]]
|
173
|
+
Keys must match the columns defined in headers.
|
174
|
+
"""
|
175
|
+
if not self.verbose:
|
176
|
+
return
|
177
|
+
print(self.get_row(values))
|
178
|
+
|
179
|
+
def finish(self) -> None:
|
180
|
+
"""End the table display, printing the bottom border and total time."""
|
181
|
+
if not self.verbose:
|
182
|
+
return
|
183
|
+
|
184
|
+
print(self.get_bottom())
|
185
|
+
print(f"Total time: {time.perf_counter() - self._start:.6f} seconds")
|