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/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
- Artificial Immune Network implementation for clustering.
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 self._generate_random_antibodies(
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 self._generate_random_antibodies(
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")