aisp 0.2.0__py3-none-any.whl → 0.2.1__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/base/_classifier.py CHANGED
@@ -16,10 +16,10 @@ class BaseClassifier(ABC):
16
16
  ``get_params`` method.
17
17
  """
18
18
 
19
- classes: Optional[Union[npt.NDArray, list]] = None
19
+ classes: Union[npt.NDArray, list] = []
20
20
 
21
21
  @abstractmethod
22
- def fit(self, X: npt.NDArray, y: npt.NDArray, verbose: bool = True):
22
+ def fit(self, X: npt.NDArray, y: npt.NDArray, verbose: bool = True) -> "BaseClassifier":
23
23
  """
24
24
  Train the model using the input data X and corresponding labels y.
25
25
 
@@ -83,6 +83,10 @@ class BaseClassifier(ABC):
83
83
  if len(y) == 0:
84
84
  return 0
85
85
  y_pred = self.predict(X)
86
+
87
+ if y_pred is None:
88
+ return 0
89
+
86
90
  return accuracy_score(y, y_pred)
87
91
 
88
92
  def _slice_index_list_by_class(self, y: npt.NDArray) -> dict:
aisp/base/mutation.py CHANGED
@@ -84,3 +84,47 @@ def clone_and_mutate_binary(
84
84
  clone_set[i] = clone
85
85
 
86
86
  return clone_set
87
+
88
+
89
+ @njit([(types.float64[:], types.int64, types.float64[:, :])], cache=True)
90
+ def clone_and_mutate_ranged(
91
+ vector: npt.NDArray[np.float64],
92
+ n: int,
93
+ bounds: npt.NDArray[np.float64]
94
+ ) -> npt.NDArray[np.float64]:
95
+ """
96
+ Generate a set of mutated clones from a cell represented by custom ranges per dimension.
97
+
98
+ This function creates `n` clones of the input vector and applies random mutations to each of
99
+ them, simulating the process of clonal expansion in artificial immune systems. Each clone
100
+ will have a random number of mutations applied in distinct positions of the original vector.
101
+
102
+ Parameters
103
+ ----------
104
+ vector : npt.NDArray[np.bool_]
105
+ The original immune cell with binary values to be cloned and mutated.
106
+ n : int
107
+ The number of mutated clones to be generated.
108
+ bounds : np.ndarray
109
+ Array (n_features, 2) with min and max per dimension.
110
+
111
+ Returns
112
+ -------
113
+ clone_set : npt.NDArray
114
+ An Array(n, len(vector)) containing the `n` mutated clones of the original vector.
115
+ """
116
+ n_features = vector.shape[0]
117
+ clone_set = np.empty((n, n_features), dtype=np.float64)
118
+
119
+ for i in range(n):
120
+ n_mutations = np.random.randint(1, n_features)
121
+ clone = vector.copy()
122
+ position_mutations = np.random.permutation(n_features)[:n_mutations]
123
+ for j in range(n_mutations):
124
+ idx = position_mutations[j]
125
+ min_limit = bounds[idx, 0]
126
+ max_limit = bounds[idx, 1]
127
+ clone[idx] = np.random.uniform(min_limit, max_limit)
128
+ clone_set[i] = clone
129
+
130
+ return clone_set
@@ -4,16 +4,19 @@ import random
4
4
  from collections import Counter
5
5
  from heapq import nlargest
6
6
  from operator import attrgetter
7
- from typing import List, Literal, Optional, Dict
7
+ from typing import List, Optional, Dict
8
8
 
9
9
  import numpy as np
10
10
  import numpy.typing as npt
11
11
  from scipy.spatial.distance import pdist
12
12
  from tqdm import tqdm
13
13
 
14
+
14
15
  from ._cell import Cell
15
16
  from ..utils.sanitizers import sanitize_param, sanitize_seed, sanitize_choice
16
17
  from ..utils.distance import hamming, compute_metric_distance, get_metric_code
18
+ from ..utils.types import FeatureType, MetricType
19
+ from ..utils.validation import detect_vector_data_type
17
20
  from ._base import BaseAIRS
18
21
 
19
22
 
@@ -114,20 +117,12 @@ class AIRS(BaseAIRS):
114
117
  * ``'manhattan'`` ➜ The calculation of the distance is given by the expression:
115
118
  ( |x₁ – x₂| + |y₁ – y₂| + ... + |yn – yn|).
116
119
 
117
- algorithm : Literal["continuous-features", "binary-features"], default="continuous-features"
118
- Specifies the type of algorithm to use based on the nature of the input features:
119
-
120
- * ``continuous-features``: selects an algorithm designed for continuous data, which should
121
- be normalized within the range [0, 1].
122
-
123
- * ``binary-features``: selects an algorithm specialized for handling binary variables.
124
-
125
120
  seed : int
126
121
  Seed for the random generation of detector values. Defaults to None.
127
122
 
128
123
  **kwargs
129
124
  p : float
130
- This parameter stores the value of ``p`` used in the Minkowsks distance. The default
125
+ This parameter stores the value of ``p`` used in the Minkowski distance. The default
131
126
  is ``2``, which represents normalized Euclidean distance.\
132
127
  Different values of p lead to different variants of the Minkowski Distance.
133
128
 
@@ -160,11 +155,8 @@ class AIRS(BaseAIRS):
160
155
  k: int = 3,
161
156
  max_iters: int = 100,
162
157
  resource_amplified: float = 1.0,
163
- metric: Literal["manhattan", "minkowski", "euclidean"] = "euclidean",
164
- algorithm: Literal[
165
- "continuous-features", "binary-features"
166
- ] = "continuous-features",
167
- seed: int = None,
158
+ metric: MetricType = "euclidean",
159
+ seed: Optional[int] = None,
168
160
  **kwargs,
169
161
  ) -> None:
170
162
  self.n_resources: float = sanitize_param(n_resources, 10, lambda x: x >= 1)
@@ -183,35 +175,29 @@ class AIRS(BaseAIRS):
183
175
  )
184
176
  self.k: int = sanitize_param(k, 3, lambda x: x > 3)
185
177
  self.max_iters: int = sanitize_param(max_iters, 100, lambda x: x > 0)
186
- self.seed: int = sanitize_seed(seed)
178
+ self.seed: Optional[int] = sanitize_seed(seed)
187
179
  if self.seed is not None:
188
180
  np.random.seed(self.seed)
189
181
 
190
- self.algorithm: Literal["continuous-features", "binary-features"] = (
191
- sanitize_param(
192
- algorithm, "continuous-features", lambda x: x == "binary-features"
193
- )
194
- )
182
+ self._feature_type: FeatureType = "continuous-features"
195
183
 
196
- if algorithm == "binary-features":
197
- self.metric: str = "hamming"
198
- else:
199
- self.metric: str = sanitize_choice(
200
- metric, ["manhattan", "minkowski"], "euclidean"
201
- )
184
+ self.metric = sanitize_choice(
185
+ metric, ["manhattan", "minkowski"], "euclidean"
186
+ )
202
187
 
203
188
  self.p: np.float64 = np.float64(kwargs.get("p", 2.0))
204
189
 
205
190
  self._cells_memory = None
206
191
  self.affinity_threshold = 0.0
207
- self.classes = None
192
+ self.classes = []
193
+ self._bounds: Optional[npt.NDArray[np.float64]] = None
208
194
 
209
195
  @property
210
- def cells_memory(self) -> Dict[str, list[Cell]]:
196
+ def cells_memory(self) -> Optional[Dict[str, list[Cell]]]:
211
197
  """Returns the trained cells memory, organized by class."""
212
198
  return self._cells_memory
213
199
 
214
- def fit(self, X: npt.NDArray, y: npt.NDArray, verbose: bool = True):
200
+ def fit(self, X: npt.NDArray, y: npt.NDArray, verbose: bool = True) -> "AIRS":
215
201
  """
216
202
  Fit the model to the training data using the AIRS.
217
203
 
@@ -235,10 +221,16 @@ class AIRS(BaseAIRS):
235
221
  """
236
222
  progress = None
237
223
 
238
- super()._check_and_raise_exceptions_fit(X, y, self.algorithm)
224
+ self._feature_type = detect_vector_data_type(X)
239
225
 
240
- if self.algorithm == "binary-features":
241
- X = X.astype(np.bool_)
226
+ super()._check_and_raise_exceptions_fit(X, y)
227
+
228
+ match self._feature_type:
229
+ case "binary-features":
230
+ X = X.astype(np.bool_)
231
+ self.metric = "hamming"
232
+ case "ranged-features":
233
+ self._bounds = np.vstack([np.min(X, axis=0), np.max(X, axis=0)])
242
234
 
243
235
  self.classes = np.unique(y)
244
236
  sample_index = self._slice_index_list_by_class(y)
@@ -250,7 +242,7 @@ class AIRS(BaseAIRS):
250
242
  )
251
243
  pool_cells_classes = {}
252
244
  for _class_ in self.classes:
253
- if verbose:
245
+ if verbose and progress is not None:
254
246
  progress.set_description_str(
255
247
  f"Generating the memory cells for the {_class_} class:"
256
248
  )
@@ -267,7 +259,7 @@ class AIRS(BaseAIRS):
267
259
  for ai in x_class:
268
260
  # Calculating the stimulation of memory cells with aᵢ and selecting the largest
269
261
  # stimulation from the memory set.
270
- c_match = None
262
+ c_match = pool_c[0]
271
263
  match_stimulation = -1
272
264
  for cell in pool_c:
273
265
  stimulation = self._affinity(cell.vector, ai)
@@ -284,7 +276,7 @@ class AIRS(BaseAIRS):
284
276
 
285
277
  set_clones: npt.NDArray = c_match.hyper_clonal_mutate(
286
278
  int(self.rate_hypermutation * self.rate_clonal * match_stimulation),
287
- self.algorithm
279
+ self._feature_type
288
280
  )
289
281
 
290
282
  for clone in set_clones:
@@ -302,11 +294,11 @@ class AIRS(BaseAIRS):
302
294
  if self._affinity(c_candidate.vector, c_match.vector) < sufficiently_similar:
303
295
  pool_c.remove(c_match)
304
296
 
305
- if verbose:
297
+ if verbose and progress is not None:
306
298
  progress.update(1)
307
299
  pool_cells_classes[_class_] = pool_c
308
300
 
309
- if verbose:
301
+ if verbose and progress is not None:
310
302
  progress.set_description(
311
303
  f"\033[92m✔ Set of memory cells for classes ({', '.join(map(str, self.classes))}) "
312
304
  f"successfully generated\033[0m"
@@ -337,7 +329,7 @@ class AIRS(BaseAIRS):
337
329
  return None
338
330
 
339
331
  super()._check_and_raise_exceptions_predict(
340
- X, len(self._cells_memory[self.classes[0]][0].vector), self.algorithm
332
+ X, len(self._cells_memory[self.classes[0]][0].vector), self._feature_type
341
333
  )
342
334
 
343
335
  c: list = []
@@ -417,7 +409,7 @@ class AIRS(BaseAIRS):
417
409
  random_index = random.randint(0, len(arb_list) - 1)
418
410
  clone_arb = arb_list[random_index].hyper_clonal_mutate(
419
411
  int(self.rate_clonal * c_match_stimulation),
420
- self.algorithm
412
+ self._feature_type
421
413
  )
422
414
 
423
415
  arb_list = [
@@ -446,12 +438,12 @@ class AIRS(BaseAIRS):
446
438
  antigens_list : npt.NDArray
447
439
  List of training antigens.
448
440
  """
449
- if self.algorithm == "binary-features":
441
+ if self._feature_type == "binary-features":
450
442
  distances = pdist(antigens_list, metric="hamming")
451
- elif self.metric == "minkowski":
452
- distances = pdist(antigens_list, metric="minkowski", p=self.p)
453
443
  else:
454
- distances = pdist(antigens_list, metric=self.metric)
444
+ metric_kwargs = {'p': self.p} if self.metric == 'minkowski' else {}
445
+ distances = pdist(antigens_list, metric=self.metric, **metric_kwargs)
446
+
455
447
  n = antigens_list.shape[0]
456
448
  sum_affinity = np.sum(1.0 - (distances / (1.0 + distances)))
457
449
  self.affinity_threshold = 1.0 - (sum_affinity / ((n * (n - 1)) / 2))
@@ -473,7 +465,7 @@ class AIRS(BaseAIRS):
473
465
  The stimulus rate between the vectors.
474
466
  """
475
467
  distance: float
476
- if self.algorithm == "binary-features":
468
+ if self._feature_type == "binary-features":
477
469
  distance = hamming(u, v)
478
470
  else:
479
471
  distance = compute_metric_distance(
aisp/csa/_base.py CHANGED
@@ -1,12 +1,12 @@
1
1
  """Base Class for Clonal Selection Algorithm."""
2
2
 
3
3
  from abc import ABC
4
- from typing import Literal
5
4
 
6
5
  import numpy as np
7
6
  import numpy.typing as npt
8
7
 
9
- from aisp.exceptions import FeatureDimensionMismatch
8
+ from ..exceptions import FeatureDimensionMismatch
9
+ from ..utils.types import FeatureType
10
10
  from ..base import BaseClassifier
11
11
 
12
12
 
@@ -20,11 +20,8 @@ class BaseAIRS(BaseClassifier, ABC):
20
20
 
21
21
  @staticmethod
22
22
  def _check_and_raise_exceptions_fit(
23
- X: npt.NDArray = None,
24
- y: npt.NDArray = None,
25
- algorithm: Literal[
26
- "continuous-features", "binary-features"
27
- ] = "continuous-features"
23
+ X: npt.NDArray,
24
+ y: npt.NDArray
28
25
  ):
29
26
  """
30
27
  Verify the fit parameters and throw exceptions if the verification is not successful.
@@ -36,17 +33,11 @@ class BaseAIRS(BaseClassifier, ABC):
36
33
  [``N samples`` (rows)][``N features`` (columns)].
37
34
  y : npt.NDArray
38
35
  Array of target classes of ``X`` with [``N samples`` (lines)].
39
- algorithm : Literal["continuous-features", "binary-features"], default="continuous-features"
40
- Specifies the type of algorithm to use, depending on whether the input data has
41
- continuous or binary features.
42
36
 
43
37
  Raises
44
38
  ------
45
39
  TypeError:
46
40
  If X or y are not ndarrays or have incompatible shapes.
47
- ValueError
48
- If algorithm is binary-features and X contains values that are not composed only
49
- of 0 and 1.
50
41
  """
51
42
  if not isinstance(X, np.ndarray):
52
43
  if isinstance(X, list):
@@ -63,18 +54,12 @@ class BaseAIRS(BaseClassifier, ABC):
63
54
  "X does not have the same amount of sample for the output classes in y."
64
55
  )
65
56
 
66
- if algorithm == "binary-features" and not np.isin(X, [0, 1]).all():
67
- raise ValueError(
68
- "The array X contains values that are not composed only of 0 and 1."
69
- )
70
57
 
71
58
  @staticmethod
72
59
  def _check_and_raise_exceptions_predict(
73
- X: npt.NDArray = None,
60
+ X: npt.NDArray,
74
61
  expected: int = 0,
75
- algorithm: Literal[
76
- "continuous-features", "binary-features"
77
- ] = "continuous-features"
62
+ feature_type: FeatureType = "continuous-features"
78
63
  ) -> None:
79
64
  """
80
65
  Verify the predict parameters and throw exceptions if the verification is not successful.
@@ -86,8 +71,8 @@ class BaseAIRS(BaseClassifier, ABC):
86
71
  [``N samples`` (rows)][``N features`` (columns)].
87
72
  expected : int, default=0
88
73
  Expected number of features per sample (columns in X).
89
- algorithm : Literal["continuous-features", "binary-features"], default="continuous-features"
90
- Specifies the type of algorithm to use, depending on whether the input data has
74
+ feature_type : FeatureType, default="continuous-features"
75
+ Specifies the type of feature_type to use, depending on whether the input data has
91
76
  continuous or binary features.
92
77
 
93
78
  Raises
@@ -97,7 +82,7 @@ class BaseAIRS(BaseClassifier, ABC):
97
82
  FeatureDimensionMismatch
98
83
  If the number of features in X does not match the expected number.
99
84
  ValueError
100
- If algorithm is binary-features and X contains values that are not composed only
85
+ If feature_type is binary-features and X contains values that are not composed only
101
86
  of 0 and 1.
102
87
  """
103
88
  if not isinstance(X, (np.ndarray, list)):
@@ -109,7 +94,7 @@ class BaseAIRS(BaseClassifier, ABC):
109
94
  "X"
110
95
  )
111
96
 
112
- if algorithm != "binary-features":
97
+ if feature_type != "binary-features":
113
98
  return
114
99
 
115
100
  # Checks if matrix X contains only binary samples. Otherwise, raises an exception.
aisp/csa/_cell.py CHANGED
@@ -1,12 +1,17 @@
1
1
  """Represents a memory B-cell."""
2
2
 
3
3
  from dataclasses import dataclass
4
- from typing import Literal
4
+ from typing import Optional
5
5
 
6
6
  import numpy as np
7
7
  import numpy.typing as npt
8
8
 
9
- from ..base.mutation import clone_and_mutate_continuous, clone_and_mutate_binary
9
+ from ..base.mutation import (
10
+ clone_and_mutate_continuous,
11
+ clone_and_mutate_binary,
12
+ clone_and_mutate_ranged
13
+ )
14
+ from ..utils.types import FeatureType
10
15
 
11
16
 
12
17
  @dataclass(slots=True)
@@ -25,7 +30,8 @@ class Cell:
25
30
  def hyper_clonal_mutate(
26
31
  self,
27
32
  n: int,
28
- algorithm: Literal["continuous-features", "binary-features"] = "continuous-features"
33
+ feature_type: FeatureType = "continuous-features",
34
+ bounds: Optional[npt.NDArray[np.float64]] = None
29
35
  ) -> npt.NDArray:
30
36
  """
31
37
  Clones N features from a cell's features, generating a set of mutated vectors.
@@ -34,14 +40,22 @@ class Cell:
34
40
  ----------
35
41
  n : int
36
42
  Number of clones to be generated from mutations of the original cell.
37
- algorithm : Literal["continuous-features", "binary-features"], default="continuous-features"
38
- Specifies the type of algorithm to use based on the nature of the input features
43
+ feature_type : Literal["binary-features", "continuous-features", "ranged-features"]
44
+ Specifies the type of feature_type to use based on the nature of the input features
45
+ bounds : np.ndarray
46
+ Array (n_features, 2) with min and max per dimension.
39
47
 
40
48
  Returns
41
49
  -------
42
50
  npt.NDArray
43
51
  An array containing N mutated vectors from the original cell.
44
52
  """
45
- if algorithm == "binary-features":
53
+ if feature_type == "binary-features":
46
54
  return clone_and_mutate_binary(self.vector, n)
55
+ if feature_type == "ranged-features" and bounds is not None:
56
+ clone_and_mutate_ranged(self.vector, n, bounds)
47
57
  return clone_and_mutate_continuous(self.vector, n)
58
+
59
+ def __eq__(self, other):
60
+ """Check if two cells are equal."""
61
+ return np.array_equal(self.vector, other.vector)
aisp/exceptions.py CHANGED
@@ -1,5 +1,7 @@
1
1
  """Custom warnings and errors."""
2
2
 
3
+ from typing import Optional
4
+
3
5
 
4
6
  class MaxDiscardsReachedError(Exception):
5
7
  """Exception thrown when the maximum number of detector discards is reached."""
@@ -27,7 +29,7 @@ class FeatureDimensionMismatch(Exception):
27
29
  self,
28
30
  expected: int,
29
31
  received: int,
30
- variable_name: str = None
32
+ variable_name: Optional[str] = None
31
33
  ):
32
34
  parts = []
33
35
  if variable_name:
@@ -41,3 +43,17 @@ class FeatureDimensionMismatch(Exception):
41
43
  "and matches the expected shape for the model."
42
44
  )
43
45
  super().__init__(message)
46
+
47
+
48
+ class UnsupportedTypeError(Exception):
49
+ """
50
+ Exception raised when the input vector type is not supported.
51
+
52
+ This exception is thrown when the vector data type does not match any of the supported.
53
+ """
54
+
55
+ def __init__(self, message=None):
56
+ if message is None:
57
+ message = ("Type is not supported. Provide a binary, normalized, or bounded "
58
+ "continuous vector.")
59
+ super().__init__(message)
aisp/nsa/_base.py CHANGED
@@ -20,8 +20,8 @@ class BaseNSA(BaseClassifier, ABC):
20
20
 
21
21
  @staticmethod
22
22
  def _check_and_raise_exceptions_fit(
23
- X: npt.NDArray = None,
24
- y: npt.NDArray = None,
23
+ X: npt.NDArray,
24
+ y: npt.NDArray,
25
25
  _class_: Literal["RNSA", "BNSA"] = "RNSA",
26
26
  ) -> None:
27
27
  """Verify fit function parameters.
@@ -67,7 +67,7 @@ class BaseNSA(BaseClassifier, ABC):
67
67
 
68
68
  @staticmethod
69
69
  def _check_and_raise_exceptions_predict(
70
- X: npt.NDArray = None,
70
+ X: npt.NDArray,
71
71
  expected: int = 0,
72
72
  _class_: Literal["RNSA", "BNSA"] = "RNSA",
73
73
  ) -> None:
@@ -1,6 +1,6 @@
1
1
  """Negative Selection Algorithm."""
2
2
 
3
- from typing import Dict, Literal, Optional, Union
3
+ from typing import Any, Dict, Literal, Optional, Union
4
4
  from tqdm import tqdm
5
5
 
6
6
  import numpy as np
@@ -90,12 +90,12 @@ class RNSA(BaseNSA):
90
90
  k: int = 1,
91
91
  metric: Literal["manhattan", "minkowski", "euclidean"] = "euclidean",
92
92
  max_discards: int = 1000,
93
- seed: int = None,
93
+ seed: Optional[int] = None,
94
94
  algorithm: Literal["default-NSA", "V-detector"] = "default-NSA",
95
- **kwargs: Dict[str, Union[bool, str, float]],
95
+ **kwargs: Any,
96
96
  ):
97
- self.metric = sanitize_choice(metric, ["manhattan", "minkowski"], "euclidean")
98
- self.seed = sanitize_seed(seed)
97
+ self.metric: str = sanitize_choice(metric, ["manhattan", "minkowski"], "euclidean")
98
+ self.seed: Optional[int] = sanitize_seed(seed)
99
99
  if self.seed is not None:
100
100
  np.random.seed(seed)
101
101
  self.k: int = sanitize_param(k, 1, lambda x: x > 1)
@@ -108,20 +108,20 @@ class RNSA(BaseNSA):
108
108
  self.max_discards: int = sanitize_param(max_discards, 1000, lambda x: x > 0)
109
109
 
110
110
  # Retrieves the variables from kwargs.
111
- self.p: float = kwargs.get("p", 2)
112
- self.cell_bounds: bool = kwargs.get("cell_bounds", False)
113
- self.non_self_label: str = kwargs.get("non_self_label", "non-self")
111
+ self.p: np.float64 = np.float64(kwargs.get("p", 2))
112
+ self.cell_bounds: bool = bool(kwargs.get("cell_bounds", False))
113
+ self.non_self_label: str = str(kwargs.get("non_self_label", "non-self"))
114
114
 
115
115
  # Initializes the other class variables as None.
116
116
  self._detectors: Union[dict, None] = None
117
- self.classes: npt.NDArray = None
117
+ self.classes: Union[npt.NDArray, list] = []
118
118
 
119
119
  @property
120
- def detectors(self) -> Dict[str, list[Detector]]:
120
+ def detectors(self) -> Optional[Dict[str, list[Detector]]]:
121
121
  """Returns the trained detectors, organized by class."""
122
122
  return self._detectors
123
123
 
124
- def fit(self, X: npt.NDArray, y: npt.NDArray, verbose: bool = True):
124
+ def fit(self, X: npt.NDArray, y: npt.NDArray, verbose: bool = True) -> "RNSA":
125
125
  """
126
126
  Perform training according to X and y, using the negative selection method (NegativeSelect).
127
127
 
@@ -170,7 +170,7 @@ class RNSA(BaseNSA):
170
170
  discard_count = 0
171
171
  x_class = X[sample_index[_class_]]
172
172
  # Indicating which class the algorithm is currently processing for the progress bar.
173
- if verbose:
173
+ if verbose and progress is not None:
174
174
  progress.set_description_str(
175
175
  f"Generating the detectors for the {_class_} class:"
176
176
  )
@@ -183,11 +183,12 @@ class RNSA(BaseNSA):
183
183
  # If the detector is valid, add it to the list of valid detectors.
184
184
  if valid_detector is not False:
185
185
  discard_count = 0
186
- radius = (
187
- valid_detector[1] if self.algorithm == "V-detector" else None
188
- )
186
+ if self.algorithm == "V-detector" and isinstance(valid_detector, tuple):
187
+ radius = valid_detector[1]
188
+ else:
189
+ radius = None
189
190
  valid_detectors_set.append(Detector(vector_x, radius))
190
- if verbose:
191
+ if verbose and progress is not None:
191
192
  progress.update(1)
192
193
  else:
193
194
  discard_count += 1
@@ -197,7 +198,7 @@ class RNSA(BaseNSA):
197
198
  # Add detectors, with classes as keys in the dictionary.
198
199
  list_detectors_by_class[_class_] = valid_detectors_set
199
200
  # Notify completion of detector generation for the classes.
200
- if verbose:
201
+ if verbose and progress is not None:
201
202
  progress.set_description(
202
203
  f"\033[92m✔ Non-self detectors for classes ({', '.join(map(str, self.classes))}) "
203
204
  f"successfully generated\033[0m"
@@ -258,9 +259,7 @@ class RNSA(BaseNSA):
258
259
  elif not class_found:
259
260
  average_distance: dict = {}
260
261
  for _class_ in self.classes:
261
- detectores = list(
262
- map(lambda x: x.position, self._detectors[_class_])
263
- )
262
+ detectores = [x.position for x in self._detectors[_class_]]
264
263
  average_distance[_class_] = np.average(
265
264
  [self.__distance(detector, line) for detector in detectores]
266
265
  )
@@ -291,17 +290,17 @@ class RNSA(BaseNSA):
291
290
  # If self.k > 1, uses the k nearest neighbors (kNN); otherwise, checks the detector
292
291
  # without considering kNN.
293
292
  if self.k > 1:
294
- knn_list = []
293
+ knn_list: list = []
295
294
  for x in x_class:
296
295
  # Calculates the distance between the two vectors and adds it to the kNN list if
297
296
  # the distance is smaller than the largest distance in the list.
298
- knn_list = self.__compare_knearest_neighbors_list(
297
+ self.__compare_knearest_neighbors_list(
299
298
  knn_list, self.__distance(x, vector_x)
300
299
  )
301
300
  # If the average of the distances in the kNN list is less than the radius, Returns true.
302
301
  distance_mean = np.mean(knn_list)
303
302
  if self.algorithm == "V-detector":
304
- return self.__detector_is_valid_to_vdetector(distance_mean, vector_x)
303
+ return self.__detector_is_valid_to_vdetector(float(distance_mean), vector_x)
305
304
  if distance_mean > (self.r + self.r_s):
306
305
  return True
307
306
  else:
@@ -323,8 +322,8 @@ class RNSA(BaseNSA):
323
322
  return False # Detector is not valid!
324
323
 
325
324
  def __compare_knearest_neighbors_list(
326
- self, knn: npt.NDArray, distance: float
327
- ) -> npt.NDArray:
325
+ self, knn: list, distance: float
326
+ ) -> None:
328
327
  """
329
328
  Compare the k-nearest neighbor distance at position k=1 in the list knn.
330
329
 
@@ -336,17 +335,11 @@ class RNSA(BaseNSA):
336
335
  List of k-nearest neighbor distances.
337
336
  distance : float
338
337
  Distance to check.
339
-
340
- Returns
341
- -------
342
- knn : npt.NDArray
343
- Updated and sorted nearest neighbor list.
344
338
  """
345
339
  # If the number of distances in kNN is less than k, adds the distance.
346
340
  if len(knn) < self.k:
347
- knn = np.append(knn, distance)
341
+ knn.append(distance)
348
342
  knn.sort()
349
- return knn
350
343
 
351
344
  # Otherwise, add the distance if the new distance is smaller than the largest
352
345
  # distance in the list.
@@ -354,7 +347,6 @@ class RNSA(BaseNSA):
354
347
  knn[self.k - 1] = distance
355
348
  knn.sort()
356
349
 
357
- return knn
358
350
 
359
351
  def __compare_sample_to_detectors(self, line: npt.NDArray) -> Optional[str]:
360
352
  """
@@ -371,6 +363,9 @@ class RNSA(BaseNSA):
371
363
  Returns the predicted class with the detectors or None if the sample does not qualify
372
364
  for any class.
373
365
  """
366
+ if self._detectors is None:
367
+ return None
368
+
374
369
  # List to store the classes and the average distance between the detectors and the sample.
375
370
  possible_classes = []
376
371
  for _class_ in self.classes:
@@ -491,7 +486,7 @@ class BNSA(BaseNSA):
491
486
  N: int = 100,
492
487
  aff_thresh: float = 0.1,
493
488
  max_discards: int = 1000,
494
- seed: int = None,
489
+ seed: Optional[int] = None,
495
490
  no_label_sample_selection: Literal[
496
491
  "max_average_difference", "max_nearest_difference"
497
492
  ] = "max_average_difference",
@@ -500,27 +495,27 @@ class BNSA(BaseNSA):
500
495
  self.aff_thresh: float = sanitize_param(aff_thresh, 0.1, lambda x: 0 < x < 1)
501
496
  self.max_discards: float = sanitize_param(max_discards, 1000, lambda x: x > 0)
502
497
 
503
- self.seed = sanitize_seed(seed)
498
+ self.seed: Optional[int] = sanitize_seed(seed)
504
499
 
505
500
  if self.seed is not None:
506
501
  np.random.seed(seed)
507
502
 
508
- self.no_label_sample_selection: float = sanitize_param(
503
+ self.no_label_sample_selection: str = sanitize_param(
509
504
  no_label_sample_selection,
510
505
  "max_average_difference",
511
506
  lambda x: x == "nearest_difference",
512
507
  )
513
508
 
514
- self.classes: npt.NDArray = None
509
+ self.classes: Union[npt.NDArray, list] = []
515
510
  self._detectors: Optional[dict] = None
516
- self._detectors_stack: npt.NDArray = None
511
+ self._detectors_stack: Optional[npt.NDArray] = None
517
512
 
518
513
  @property
519
- def detectors(self) -> Dict[str, npt.NDArray[np.bool_]]:
514
+ def detectors(self) -> Optional[Dict[str, npt.NDArray[np.bool_]]]:
520
515
  """Returns the trained detectors, organized by class."""
521
516
  return self._detectors
522
517
 
523
- def fit(self, X: npt.NDArray, y: npt.NDArray, verbose: bool = True):
518
+ def fit(self, X: npt.NDArray, y: npt.NDArray, verbose: bool = True) -> "BNSA":
524
519
  """Training according to X and y, using the method negative selection method.
525
520
 
526
521
  Parameters
@@ -539,7 +534,7 @@ class BNSA(BaseNSA):
539
534
  Returns the instance it self.
540
535
  """
541
536
  super()._check_and_raise_exceptions_fit(X, y, "BNSA")
542
-
537
+ progress = None
543
538
  # Converts the entire array X to boolean
544
539
  X = X.astype(np.bool_)
545
540
 
@@ -562,7 +557,7 @@ class BNSA(BaseNSA):
562
557
  valid_detectors_set: list = []
563
558
  discard_count: int = 0
564
559
  # Updating the progress bar with the current class the algorithm is processing.
565
- if verbose:
560
+ if verbose and progress is not None:
566
561
  progress.set_description_str(
567
562
  f"Generating the detectors for the {_class_} class:"
568
563
  )
@@ -574,7 +569,7 @@ class BNSA(BaseNSA):
574
569
  if check_detector_bnsa_validity(x_class, vector_x, self.aff_thresh):
575
570
  discard_count = 0
576
571
  valid_detectors_set.append(vector_x)
577
- if verbose:
572
+ if verbose and progress is not None:
578
573
  progress.update(1)
579
574
  else:
580
575
  discard_count += 1
@@ -585,7 +580,7 @@ class BNSA(BaseNSA):
585
580
  list_detectors_by_class[_class_] = np.array(valid_detectors_set)
586
581
 
587
582
  # Notify the completion of detector generation for the classes.
588
- if verbose:
583
+ if verbose and progress is not None:
589
584
  progress.set_description(
590
585
  f"\033[92m✔ Non-self detectors for classes ({', '.join(map(str, self.classes))}) "
591
586
  f"successfully generated\033[0m"
@@ -613,7 +608,7 @@ class BNSA(BaseNSA):
613
608
  ``X``. Returns``None``: If there are no detectors for the prediction.
614
609
  """
615
610
  # If there are no detectors, Returns None.
616
- if self._detectors is None:
611
+ if self._detectors is None or self._detectors_stack is None:
617
612
  return None
618
613
 
619
614
  super()._check_and_raise_exceptions_predict(
@@ -664,6 +659,9 @@ class BNSA(BaseNSA):
664
659
  c : list
665
660
  List of predictions to be updated with the new classification.
666
661
  """
662
+ if self._detectors is None:
663
+ raise ValueError("Detectors is not initialized.")
664
+
667
665
  class_differences: dict = {}
668
666
  for _class_ in self.classes:
669
667
  distances = np.sum(line != self._detectors[_class_]) / self.N
aisp/utils/types.py ADDED
@@ -0,0 +1,31 @@
1
+ """
2
+ Defines type aliases used throughout the project to improve readability.
3
+
4
+ Type Aliases
5
+ ------------
6
+ FeatureType : Literal["binary-features", "continuous-features", "ranged-features"]
7
+ Specifies the type of features in the input data. Can be one of:
8
+ - "binary-features": Features with binary values (e.g., 0 or 1).
9
+ - "continuous-features": Features with continuous numeric values.
10
+ - "ranged-features": Features represented by ranges or intervals.
11
+
12
+ MetricType : Literal["manhattan", "minkowski", "euclidean"]
13
+ Specifies the distance metric to use for calculations. Possible values:
14
+ - "manhattan": The calculation of the distance is given by the expression:
15
+ √( (x₁ – x₂)² + (y₁ – y₂)² + ... + (yn – yn)²).
16
+ - "minkowski": The calculation of the distance is given by the expression:
17
+ ( |X₁ – Y₁|p + |X₂ – Y₂|p + ... + |Xn – Yn|p) ¹/ₚ.
18
+ - "euclidean": The calculation of the distance is given by the expression:
19
+ ( |x₁ – x₂| + |y₁ – y₂| + ... + |yn – yn|).
20
+ """
21
+
22
+
23
+ from typing import Literal, TypeAlias
24
+
25
+
26
+ FeatureType: TypeAlias = Literal[
27
+ "binary-features",
28
+ "continuous-features",
29
+ "ranged-features"
30
+ ]
31
+ MetricType: TypeAlias = Literal["manhattan", "minkowski", "euclidean"]
@@ -0,0 +1,47 @@
1
+ """Contains functions responsible for validating data types."""
2
+
3
+ import numpy as np
4
+ import numpy.typing as npt
5
+
6
+ from .types import FeatureType
7
+ from ..exceptions import UnsupportedTypeError
8
+
9
+
10
+ def detect_vector_data_type(
11
+ vector: npt.NDArray
12
+ ) -> FeatureType:
13
+ """
14
+ Detect the type of data in a vector.
15
+
16
+ The function detects if the vector contains data of type:
17
+ - "binary": binary data (boolean True/False or integer 0/1)
18
+ - "continuous": continuous data between 0.0 and 1.0 (float)
19
+ - "ranged": numerical data with values outside the normalized range (float)
20
+
21
+ Parameters
22
+ ----------
23
+ vector: npt.NDArray
24
+ An array containing the data to be classified.
25
+
26
+ Returns
27
+ -------
28
+ Literal["binary-features", "continuous-features", "ranged-features"]
29
+ The classified data type of the vector.
30
+
31
+ Raises
32
+ ------
33
+ UnsupportedDataTypeError
34
+ If the data type of the vector is not supported by the function.
35
+ """
36
+ if vector.dtype == np.bool_:
37
+ return "binary-features"
38
+
39
+ if np.issubdtype(vector.dtype, np.integer) and np.isin(vector, [0, 1]).all():
40
+ return "binary-features"
41
+
42
+ if np.issubdtype(vector.dtype, np.floating):
43
+ if np.all(vector >= 0.0) and np.all(vector <= 1.0):
44
+ return "continuous-features"
45
+ return "ranged-features"
46
+
47
+ raise UnsupportedTypeError()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aisp
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Package with techniques of artificial immune systems.
5
5
  Author-email: João Paulo da Silva Barros <jpsilvabarr@gmail.com>
6
6
  Maintainer-email: Alison Zille Lopes <alisonzille@gmail.com>
@@ -26,6 +26,11 @@ Requires-Dist: numpy>=1.22.4
26
26
  Requires-Dist: numba>=0.59.0
27
27
  Requires-Dist: scipy>=1.8.1
28
28
  Requires-Dist: tqdm>=4.64.1
29
+ Provides-Extra: dev
30
+ Requires-Dist: build>=1.2.2.post1; extra == "dev"
31
+ Requires-Dist: ipykernel>=6.29.5; extra == "dev"
32
+ Requires-Dist: twine>=5.1.1; extra == "dev"
33
+ Requires-Dist: pytest>=8.3.5; extra == "dev"
29
34
  Dynamic: license-file
30
35
 
31
36
  <div align = center>
@@ -0,0 +1,25 @@
1
+ aisp/__init__.py,sha256=N5aAyup46_tqU9cXfYfGuR3bdfAjcvaPc1xwFdGdD7A,112
2
+ aisp/exceptions.py,sha256=I9JaQx6p8Jo7qjwwcrqnuewQgyBdUnOSSZofPoBeDNE,1954
3
+ aisp/base/__init__.py,sha256=k2Ww9hej_32ekYhhCiYGEMLgOmDKwRt261HZ8rEurwA,102
4
+ aisp/base/_classifier.py,sha256=Ud8VLE7vNh1ddpNNg0RVET2RXCd7kvzvfvNKHKNn_GM,3734
5
+ aisp/base/mutation.py,sha256=A_AlGp8S4ooFEMW3Jgv0n0Y6tbhfusaMMWFsoH4HmD8,4762
6
+ aisp/csa/__init__.py,sha256=cJSKkbvNTpR_CKCL--h99fNPiMf3fJ73gFnZRq7uyVM,355
7
+ aisp/csa/_ai_immune_recognition_sys.py,sha256=_XqTHjqEO6sGZiIRlNNLe6Lz2PDFfDCtsbpucClvYmA,18878
8
+ aisp/csa/_base.py,sha256=jR1IIhGINn7DLo8V5iJinDn-wW-t6etcE39bAZnQylw,3595
9
+ aisp/csa/_cell.py,sha256=GUxnzvPyIbBm1YYkMhSx0tcV_oyDhJ7wAo5gtr_1CoY,1845
10
+ aisp/nsa/__init__.py,sha256=3cXuBmO-_Dp3-8ZG3Eu8e_bD1JDb-RH4Wu0UDNVD1bs,385
11
+ aisp/nsa/_base.py,sha256=3YKlZzA3yhP2uQHfhyKswbHUutlxkOR4wn6N10nSO-w,4119
12
+ aisp/nsa/_negative_selection.py,sha256=aMdbIrd4TdPxaAkHHY-HbbM5kd5f81HbE3DyB73ttX4,28467
13
+ aisp/nsa/_ns_core.py,sha256=SXkZL-p2VQygU4Pf6J5AP_yPzU4cR6aU6wx-e_vlm-c,5021
14
+ aisp/utils/__init__.py,sha256=RzpKhkg8nCZi4G0C4il97f3ESYs7Bbxq6EjTeOQQUGk,195
15
+ aisp/utils/_multiclass.py,sha256=nWd58ayVfxgdopBQc9b_xywkolJ2fGW3AN-JoD2A9Fw,1134
16
+ aisp/utils/distance.py,sha256=pIt76OUiwCry6eNEuWLYvUiW4KkeU6egjjnnmroFet8,6556
17
+ aisp/utils/metrics.py,sha256=zDAScDbHRnfu24alRcZ6fEIUaWNoCD-QCtOCFBWPPo8,1277
18
+ aisp/utils/sanitizers.py,sha256=u1GizdJ-RKfPWJLnuFiM09lpItZMhDR_EvK8YdVHwDk,1858
19
+ aisp/utils/types.py,sha256=KELzr1kSBT7hHdsABoIS1xmEBGj6gRSH5A5YNG36I_c,1324
20
+ aisp/utils/validation.py,sha256=ya7Y_6Lv7L6LAHC11EAfZRqqneCsOqrjG8i2EQFZcpA,1418
21
+ aisp-0.2.1.dist-info/licenses/LICENSE,sha256=fTqV5eBpeAZO0_jit8j4Ref9ikBSlHJ8xwj5TLg7gFk,7817
22
+ aisp-0.2.1.dist-info/METADATA,sha256=bhd0eOBVOuNN8gZ-jN3L4QWV17Qa0R2Kb-syKX5PK1U,4844
23
+ aisp-0.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
24
+ aisp-0.2.1.dist-info/top_level.txt,sha256=Q5aJi_rAVT5UNS1As0ZafoyS5dwNibnoyOYV7RWUB9s,5
25
+ aisp-0.2.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.8.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,23 +0,0 @@
1
- aisp/__init__.py,sha256=N5aAyup46_tqU9cXfYfGuR3bdfAjcvaPc1xwFdGdD7A,112
2
- aisp/exceptions.py,sha256=M2H_oM-ccIkDGpeFA3CyklZlgMcjTVvOCTGLU2sxFi8,1447
3
- aisp/base/__init__.py,sha256=k2Ww9hej_32ekYhhCiYGEMLgOmDKwRt261HZ8rEurwA,102
4
- aisp/base/_classifier.py,sha256=_HJiL1fCNqoB5KlNUN5pH9Yuu_btOze39h0SdnBw7ug,3672
5
- aisp/base/mutation.py,sha256=j_2WiZDxUS3KS4QgGXaFqoLVSxSz88BpLfZTjLuGaSU,3110
6
- aisp/csa/__init__.py,sha256=cJSKkbvNTpR_CKCL--h99fNPiMf3fJ73gFnZRq7uyVM,355
7
- aisp/csa/_ai_immune_recognition_sys.py,sha256=0f8DQzZ7lG69xCMI1jpR0QBKZ4oNvRXpayQMUekzC5o,19233
8
- aisp/csa/_base.py,sha256=y1OX0Z0ZGQu63fQmg1umMZ1110H8bkStP5NaGNOvgmY,4399
9
- aisp/csa/_cell.py,sha256=PhGdXKytRYnV97pmaLLKVhaV_OwU31-92URZVMszohY,1377
10
- aisp/nsa/__init__.py,sha256=3cXuBmO-_Dp3-8ZG3Eu8e_bD1JDb-RH4Wu0UDNVD1bs,385
11
- aisp/nsa/_base.py,sha256=D_N-VIESvGFhdf_A2NETV-JaZJ6ISankrbRzWXSMiXM,4140
12
- aisp/nsa/_negative_selection.py,sha256=-hqMspYvtPAb38qV1_NF5HmDCOGDWGL89BZ3M4eHiao,28141
13
- aisp/nsa/_ns_core.py,sha256=SXkZL-p2VQygU4Pf6J5AP_yPzU4cR6aU6wx-e_vlm-c,5021
14
- aisp/utils/__init__.py,sha256=RzpKhkg8nCZi4G0C4il97f3ESYs7Bbxq6EjTeOQQUGk,195
15
- aisp/utils/_multiclass.py,sha256=nWd58ayVfxgdopBQc9b_xywkolJ2fGW3AN-JoD2A9Fw,1134
16
- aisp/utils/distance.py,sha256=pIt76OUiwCry6eNEuWLYvUiW4KkeU6egjjnnmroFet8,6556
17
- aisp/utils/metrics.py,sha256=zDAScDbHRnfu24alRcZ6fEIUaWNoCD-QCtOCFBWPPo8,1277
18
- aisp/utils/sanitizers.py,sha256=u1GizdJ-RKfPWJLnuFiM09lpItZMhDR_EvK8YdVHwDk,1858
19
- aisp-0.2.0.dist-info/licenses/LICENSE,sha256=fTqV5eBpeAZO0_jit8j4Ref9ikBSlHJ8xwj5TLg7gFk,7817
20
- aisp-0.2.0.dist-info/METADATA,sha256=5jLC17E3FIl_8ewjhK5hkJQ7YsJFP_a-bMW8EHDCDSc,4631
21
- aisp-0.2.0.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
22
- aisp-0.2.0.dist-info/top_level.txt,sha256=Q5aJi_rAVT5UNS1As0ZafoyS5dwNibnoyOYV7RWUB9s,5
23
- aisp-0.2.0.dist-info/RECORD,,