From ef6be3e5e0b8d4e7c65f8349e67fd34443218134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Paulo?= <88985821+Joao-Paulo-Silva@users.noreply.github.com> Date: Sat, 27 Jun 2026 15:26:47 -0300 Subject: [PATCH 01/15] fix: prevent import errors when retrieving the version in dev if `aisp` is not installed as a package --- aisp/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/aisp/__init__.py b/aisp/__init__.py index 1363c0f..1fa7127 100644 --- a/aisp/__init__.py +++ b/aisp/__init__.py @@ -26,12 +26,17 @@ https://ais-package.github.io/docs/intro """ -from importlib.metadata import version +from importlib.metadata import version, PackageNotFoundError from . import csa from . import ina from . import nsa +try: + __version__ = version('aisp') +except PackageNotFoundError: + __version__ = '0.dev' + __author__ = "AISP Development Team" -__version__ = version('aisp') + __all__ = ["csa", "nsa", "ina"] From dd4483bd1f3685bf26052e44b22d7357575138f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Paulo?= <88985821+Joao-Paulo-Silva@users.noreply.github.com> Date: Sun, 28 Jun 2026 17:38:55 -0300 Subject: [PATCH 02/15] feat: add simple validation functions --- aisp/utils/validation.py | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/aisp/utils/validation.py b/aisp/utils/validation.py index dfb11ec..ed7fb35 100644 --- a/aisp/utils/validation.py +++ b/aisp/utils/validation.py @@ -1,5 +1,4 @@ """Contains functions responsible for validating data types.""" - import numpy as np import numpy.typing as npt @@ -160,3 +159,32 @@ def check_value_range( raise ValueError( f"{name} must contain oly values within [{min_value}, {max_value}]." ) + + +def positive(arg): + if arg <= 0: + raise ValueError('Expected a positive value.') + return arg + + +def non_negative(arg): + if arg < 0: + raise ValueError('Expected a positive value.') + return arg + + +def between(low, high): + def validator(arg): + if not low <= arg <= high: + raise ValueError(f"Expected {low} <= value <= {high}.") + + return validator + + +def choice(choices: list): + def validator(arg): + if arg not in choices: + raise ValueError(f"Expected one of {choices}.") + return arg + + return validator From 0e6543d8ba02d63d906ee99e3e316aeec6ee3be8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Paulo?= <88985821+Joao-Paulo-Silva@users.noreply.github.com> Date: Tue, 30 Jun 2026 20:22:15 -0300 Subject: [PATCH 03/15] feat: add optional validation functions --- aisp/utils/validation.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/aisp/utils/validation.py b/aisp/utils/validation.py index ed7fb35..528d19f 100644 --- a/aisp/utils/validation.py +++ b/aisp/utils/validation.py @@ -169,7 +169,7 @@ def positive(arg): def non_negative(arg): if arg < 0: - raise ValueError('Expected a positive value.') + raise ValueError('Expected a non-negative value.') return arg @@ -177,7 +177,7 @@ def between(low, high): def validator(arg): if not low <= arg <= high: raise ValueError(f"Expected {low} <= value <= {high}.") - + return arg return validator @@ -188,3 +188,13 @@ def validator(arg): return arg return validator + + +def optional(validator): + def validate(arg): + if arg is None: + return None + return validator(arg) + + return validate + From 0ccfb7233d3a9b982441bdb4b314a45d8eaa0d80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Paulo?= <88985821+Joao-Paulo-Silva@users.noreply.github.com> Date: Tue, 30 Jun 2026 20:26:12 -0300 Subject: [PATCH 04/15] feat: add typing validation functions --- aisp/utils/validation.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/aisp/utils/validation.py b/aisp/utils/validation.py index 528d19f..f9b4c98 100644 --- a/aisp/utils/validation.py +++ b/aisp/utils/validation.py @@ -1,4 +1,7 @@ """Contains functions responsible for validating data types.""" +from numbers import Real +from typing import Collection, Callable, Any + import numpy as np import numpy.typing as npt @@ -161,27 +164,27 @@ def check_value_range( ) -def positive(arg): +def positive(arg: Real): if arg <= 0: raise ValueError('Expected a positive value.') return arg -def non_negative(arg): +def non_negative(arg: Real): if arg < 0: raise ValueError('Expected a non-negative value.') return arg -def between(low, high): - def validator(arg): +def between(low: Real, high: Real): + def validator(arg: Real): if not low <= arg <= high: raise ValueError(f"Expected {low} <= value <= {high}.") return arg return validator -def choice(choices: list): +def choice(choices: Collection): def validator(arg): if arg not in choices: raise ValueError(f"Expected one of {choices}.") @@ -190,7 +193,7 @@ def validator(arg): return validator -def optional(validator): +def optional(validator: Callable[[Any], Any]): def validate(arg): if arg is None: return None From b591cb328af3b2f50ae4ac3efe00cef8bb0b42e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Paulo?= <88985821+Joao-Paulo-Silva@users.noreply.github.com> Date: Tue, 30 Jun 2026 20:29:44 -0300 Subject: [PATCH 05/15] feat: add compose for multiple validation functions --- aisp/utils/validation.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/aisp/utils/validation.py b/aisp/utils/validation.py index f9b4c98..36a784c 100644 --- a/aisp/utils/validation.py +++ b/aisp/utils/validation.py @@ -201,3 +201,10 @@ def validate(arg): return validate +def compose(*validators): + def validate(arg): + for validator in validators: + arg = validator(arg) + return arg + + return validate \ No newline at end of file From b43e0b86bdb25d0220d715e155f144fc6c694567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Paulo?= <88985821+Joao-Paulo-Silva@users.noreply.github.com> Date: Tue, 30 Jun 2026 21:19:20 -0300 Subject: [PATCH 06/15] feat: add validate_parameters decorator for function arguments --- aisp/utils/validation.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/aisp/utils/validation.py b/aisp/utils/validation.py index 36a784c..dcbd513 100644 --- a/aisp/utils/validation.py +++ b/aisp/utils/validation.py @@ -1,4 +1,6 @@ """Contains functions responsible for validating data types.""" +import inspect +from functools import wraps from numbers import Real from typing import Collection, Callable, Any @@ -207,4 +209,28 @@ def validate(arg): arg = validator(arg) return arg - return validate \ No newline at end of file + return validate + + +def validate_parameters(**validators: Callable[[Any], Any]): + def decorator(func): + sig = inspect.signature(func) + unknown = validators.keys() - sig.parameters.keys() + if unknown: + raise TypeError(f"{func.__name__} has no parameter: {','.join(unknown)}") + + @wraps(func) + def wrapper(*args): + bound = sig.bind(*args) + bound.apply_defaults() + + for name, validator in validators.items(): + value = bound.arguments[name] + bound.arguments[name] = validator(value) + + return func(*bound.args) + + return wrapper + + return decorator + From 4fd9fbe8df4702691bc2861caf2b84ef5e42d222 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Paulo?= <88985821+Joao-Paulo-Silva@users.noreply.github.com> Date: Tue, 30 Jun 2026 21:38:18 -0300 Subject: [PATCH 07/15] fix: update validate_parameters to accept kwargs and prevent errors when the parameter is not found --- aisp/utils/validation.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/aisp/utils/validation.py b/aisp/utils/validation.py index dcbd513..0abae6e 100644 --- a/aisp/utils/validation.py +++ b/aisp/utils/validation.py @@ -212,23 +212,24 @@ def validate(arg): return validate -def validate_parameters(**validators: Callable[[Any], Any]): +def validate_parameters(**validators): def decorator(func): sig = inspect.signature(func) - unknown = validators.keys() - sig.parameters.keys() + valid_params = sig.parameters.keys() + unknown = {k: v for k, v in validators.items() if k not in valid_params} if unknown: raise TypeError(f"{func.__name__} has no parameter: {','.join(unknown)}") @wraps(func) - def wrapper(*args): - bound = sig.bind(*args) + def wrapper(*args, **kwargs): + bound = sig.bind(*args, **kwargs) bound.apply_defaults() for name, validator in validators.items(): value = bound.arguments[name] bound.arguments[name] = validator(value) - return func(*bound.args) + return func(*bound.args, **bound.kwargs) return wrapper From 3986a121888d70f474b4811ca064d107bcf8775f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Paulo?= <88985821+Joao-Paulo-Silva@users.noreply.github.com> Date: Tue, 30 Jun 2026 21:59:55 -0300 Subject: [PATCH 08/15] feat: include validated argument name in error messages --- aisp/utils/validation.py | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/aisp/utils/validation.py b/aisp/utils/validation.py index 0abae6e..55cb1ce 100644 --- a/aisp/utils/validation.py +++ b/aisp/utils/validation.py @@ -166,47 +166,53 @@ def check_value_range( ) -def positive(arg: Real): +def _validation_error(name, expected, value): + raise ValueError( + f"Invalid value for {name!r}: expected {expected}, got {value!r}." + ) + + +def positive(arg: Real, name: str): if arg <= 0: - raise ValueError('Expected a positive value.') + _validation_error(name, "must be positive", arg) return arg -def non_negative(arg: Real): +def non_negative(arg: Real, name): if arg < 0: - raise ValueError('Expected a non-negative value.') + _validation_error(name, "must be non-negative", arg) return arg def between(low: Real, high: Real): - def validator(arg: Real): + def validator(arg: Real, name: str): if not low <= arg <= high: - raise ValueError(f"Expected {low} <= value <= {high}.") + _validation_error(name, f"a value in [{low}, {high}]", arg) return arg return validator def choice(choices: Collection): - def validator(arg): + def validator(arg, name: str): if arg not in choices: - raise ValueError(f"Expected one of {choices}.") + _validation_error(name, f"one of {tuple(choices)}", arg) return arg return validator -def optional(validator: Callable[[Any], Any]): - def validate(arg): +def optional(validator: Callable[[Any, str], Any]): + def validate(arg, name: str): if arg is None: return None - return validator(arg) + return validator(arg, name) return validate def compose(*validators): - def validate(arg): + def validate(arg, name: str): for validator in validators: - arg = validator(arg) + arg = validator(arg, name) return arg return validate @@ -227,7 +233,7 @@ def wrapper(*args, **kwargs): for name, validator in validators.items(): value = bound.arguments[name] - bound.arguments[name] = validator(value) + bound.arguments[name] = validator(value, name) return func(*bound.args, **bound.kwargs) From b04492227ffaa23c19aac0b7faf5228430a498b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Paulo?= <88985821+Joao-Paulo-Silva@users.noreply.github.com> Date: Sat, 4 Jul 2026 09:17:45 -0300 Subject: [PATCH 09/15] refactor: remove compose and support validator lists in wrapper --- aisp/utils/validation.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/aisp/utils/validation.py b/aisp/utils/validation.py index 55cb1ce..b7ba8aa 100644 --- a/aisp/utils/validation.py +++ b/aisp/utils/validation.py @@ -189,6 +189,7 @@ def validator(arg: Real, name: str): if not low <= arg <= high: _validation_error(name, f"a value in [{low}, {high}]", arg) return arg + return validator @@ -209,14 +210,6 @@ def validate(arg, name: str): return validate -def compose(*validators): - def validate(arg, name: str): - for validator in validators: - arg = validator(arg, name) - return arg - - return validate - def validate_parameters(**validators): def decorator(func): @@ -233,11 +226,15 @@ def wrapper(*args, **kwargs): for name, validator in validators.items(): value = bound.arguments[name] - bound.arguments[name] = validator(value, name) + if isinstance(validator, (list, tuple)): + for fn in validator: + value = fn(value, name) + else: + value = validator(value, name) + bound.arguments[name] = value return func(*bound.args, **bound.kwargs) return wrapper return decorator - From ce878c4b708986f62c26194ee800a0dc0a630342 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Paulo?= <88985821+Joao-Paulo-Silva@users.noreply.github.com> Date: Sat, 4 Jul 2026 09:25:20 -0300 Subject: [PATCH 10/15] feat: add type validator --- aisp/utils/validation.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/aisp/utils/validation.py b/aisp/utils/validation.py index b7ba8aa..e982af6 100644 --- a/aisp/utils/validation.py +++ b/aisp/utils/validation.py @@ -211,6 +211,20 @@ def validate(arg, name: str): return validate +def is_type(expected_type): + def validate(arg, name: str): + if not isinstance(arg, expected_type): + if isinstance(expected_type, (tuple, list)): + expected = ', '.join(t.__name__ for t in expected_type) + else: + expected = expected_type.__name__ + raise TypeError(f"{name} must be of type {expected} got {type(arg).__name__}") + + return arg + + return validate + + def validate_parameters(**validators): def decorator(func): sig = inspect.signature(func) From d77e6165c8e334f4db60653e7bdfe63af9e793d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Paulo?= <88985821+Joao-Paulo-Silva@users.noreply.github.com> Date: Sat, 4 Jul 2026 09:31:57 -0300 Subject: [PATCH 11/15] feat: add collection support for optional validators --- aisp/utils/validation.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/aisp/utils/validation.py b/aisp/utils/validation.py index e982af6..5c2a2ac 100644 --- a/aisp/utils/validation.py +++ b/aisp/utils/validation.py @@ -202,10 +202,16 @@ def validator(arg, name: str): return validator -def optional(validator: Callable[[Any, str], Any]): +def optional(validator: Callable[[Any, str], Any] | Collection[Callable[[Any, str], Any]]): def validate(arg, name: str): if arg is None: return None + if isinstance(validator, (list, tuple)): + for fn in validator: + arg = fn(arg, name) + + return arg + return validator(arg, name) return validate From d902df6502adaabf15e09cab8e27167137229edf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Paulo?= <88985821+Joao-Paulo-Silva@users.noreply.github.com> Date: Sat, 4 Jul 2026 09:43:27 -0300 Subject: [PATCH 12/15] feat: add validation to BNSA for testing --- aisp/nsa/_binary_negative_selection.py | 27 ++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/aisp/nsa/_binary_negative_selection.py b/aisp/nsa/_binary_negative_selection.py index 1791a6b..2d675c6 100644 --- a/aisp/nsa/_binary_negative_selection.py +++ b/aisp/nsa/_binary_negative_selection.py @@ -2,6 +2,7 @@ from __future__ import annotations +from numbers import Real from typing import Dict, Literal, Optional, Union import numpy as np @@ -16,7 +17,7 @@ check_array_type, check_shape_match, check_binary_array, - check_feature_dimension, + check_feature_dimension, validate_parameters, positive, choice, between, optional, is_type, ) @@ -96,6 +97,16 @@ class BNSA(BaseClassifier): ['non-self' 'self'] """ + @validate_parameters( + N=(is_type(int), positive), + aff_thresh=(is_type(Real), between(0, 1)), + max_discards=(is_type(int), positive), + seed=optional((is_type(int), positive)), + no_label_sample_selection=( + is_type(str), + choice(["max_average_difference", "max_nearest_difference"]) + ), + ) def __init__( self, N: int = 100, @@ -106,20 +117,16 @@ def __init__( "max_average_difference", "max_nearest_difference" ] = "max_average_difference", ): - self.N: int = sanitize_param(N, 100, lambda x: x > 0) - self.aff_thresh: float = sanitize_param(aff_thresh, 0.1, lambda x: 0 < x < 1) - self.max_discards: int = sanitize_param(max_discards, 1000, lambda x: x > 0) + self.N: int = N + self.aff_thresh: float = aff_thresh + self.max_discards: int = max_discards - self.seed: Optional[int] = sanitize_seed(seed) + self.seed: Optional[int] = seed if self.seed is not None: np.random.seed(seed) - self.no_label_sample_selection: str = sanitize_param( - no_label_sample_selection, - "max_average_difference", - lambda x: x == "max_nearest_difference", - ) + self.no_label_sample_selection: str = no_label_sample_selection self.classes: Optional[npt.NDArray] = None self._detectors: Optional[dict] = None From 53c838d7058f57216283d249073f74e134e39717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Paulo?= <88985821+Joao-Paulo-Silva@users.noreply.github.com> Date: Sat, 4 Jul 2026 17:20:20 -0300 Subject: [PATCH 13/15] feat: add decorators to classes and remove sanitizers --- aisp/csa/_ai_recognition_sys.py | 56 ++++++++++++++---------- aisp/csa/_clonalg.py | 47 +++++++++++--------- aisp/ina/_ai_network.py | 59 +++++++++++++------------- aisp/nsa/_binary_negative_selection.py | 9 ++-- aisp/nsa/_negative_selection.py | 52 +++++++++++++++-------- 5 files changed, 130 insertions(+), 93 deletions(-) diff --git a/aisp/csa/_ai_recognition_sys.py b/aisp/csa/_ai_recognition_sys.py index 6c16ca7..5aeb694 100644 --- a/aisp/csa/_ai_recognition_sys.py +++ b/aisp/csa/_ai_recognition_sys.py @@ -3,6 +3,7 @@ from __future__ import annotations import random +from numbers import Real from operator import attrgetter from typing import List, Optional, Dict, Tuple, Any, Union @@ -18,7 +19,6 @@ from ..utils.distance import hamming, compute_metric_distance, get_metric_code from ..utils.multiclass import predict_knn_affinity from ..utils.random import set_seed_numba -from ..utils.sanitizers import sanitize_param, sanitize_seed, sanitize_choice from ..utils.types import FeatureType, MetricType from ..utils.validation import ( detect_vector_data_type, @@ -26,6 +26,12 @@ check_shape_match, check_feature_dimension, check_binary_array, + positive, + is_type, + between, + choice, + optional, + validate_parameters, ) @@ -111,6 +117,22 @@ class AIRS(BaseClassifier): [0 1] """ + @validate_parameters( + n_resources=(is_type(float), positive), + rate_clonal=(is_type(int), positive), + rate_mc_init=(is_type(Real), between(0, 1)), + rate_hypermutation=(is_type(float), positive), + affinity_threshold_scalar=(is_type(float), positive), + k=(is_type(int), positive), + max_iters=(is_type(int), positive), + resource_amplified=(is_type(float), positive), + metric=( + is_type(str), + choice(["manhattan", "minkowski", "euclidean"]) + ), + seed=optional((is_type(int), positive)), + p=(is_type(Real), positive), + ) def __init__( self, n_resources: float = 10, @@ -125,33 +147,23 @@ def __init__( seed: Optional[int] = None, p: float = 2.0, ) -> None: - self.n_resources: float = sanitize_param(n_resources, 10, lambda x: x >= 1) - self.rate_mc_init: float = sanitize_param( - rate_mc_init, 0.2, lambda x: 0 < x <= 1 - ) - self.rate_clonal: int = sanitize_param(rate_clonal, 10, lambda x: x > 0) - self.rate_hypermutation: float = sanitize_param( - rate_hypermutation, 0.75, lambda x: x > 0 - ) - self.affinity_threshold_scalar: float = sanitize_param( - affinity_threshold_scalar, 0.75, lambda x: x > 0 - ) - self.resource_amplified: float = sanitize_param( - resource_amplified, 1, lambda x: x > 1 - ) - self.k: int = sanitize_param(k, 3, lambda x: x > 0) - self.max_iters: int = sanitize_param(max_iters, 100, lambda x: x > 0) - self.seed: Optional[int] = sanitize_seed(seed) + self.n_resources: float = n_resources + self.rate_mc_init: float = rate_mc_init + self.rate_clonal: int = rate_clonal + self.rate_hypermutation: float = rate_hypermutation + self.affinity_threshold_scalar: float = affinity_threshold_scalar + self.resource_amplified: float = resource_amplified + self.k: int = k + self.max_iters: int = max_iters + self.metric = metric + self.seed: Optional[int] = seed if self.seed is not None: np.random.seed(self.seed) set_seed_numba(self.seed) + self.p: float = p self._feature_type: FeatureType = "continuous-features" - self.metric = sanitize_choice(metric, ["manhattan", "minkowski"], "euclidean") - - self.p: float = p - self._cells_memory: Optional[Dict[str | int, list[BCell]]] = None self._all_class_cell_vectors: Optional[List[Tuple[Any, np.ndarray]]] = None self.affinity_threshold: float = 0.0 diff --git a/aisp/csa/_clonalg.py b/aisp/csa/_clonalg.py index a32fb52..6a6ec20 100644 --- a/aisp/csa/_clonalg.py +++ b/aisp/csa/_clonalg.py @@ -3,11 +3,13 @@ from __future__ import annotations import heapq +from numbers import Real from typing import Optional, Callable, Dict, Literal, List import numpy as np import numpy.typing as npt +from ..utils.validation import validate_parameters, is_type, positive, between, choice, optional from ..base import BaseOptimizer from ..base.immune.cell import Antibody from ..base.immune.mutation import ( @@ -106,6 +108,22 @@ class Clonalg(BaseOptimizer): Best cost: 0.02623036956750724 """ + @validate_parameters( + problem_size=(is_type(int), positive), + N=(is_type(int), positive), + rate_clonal=(is_type(int), positive), + rate_hypermutation=(is_type(float), positive), + n_diversity_injection=(is_type(int), positive), + selection_size=(is_type(int), positive), + feature_type=( + is_type(str), + choice([ + "binary-features", "continuous-features", "ranged-features", "permutation-features" + ]) + ), + mode=(is_type(str), choice(["min", "max"])), + seed=optional((is_type(int), positive)) + ) def __init__( self, problem_size: int, @@ -121,34 +139,21 @@ def __init__( seed: Optional[int] = None, ): super().__init__() - self.problem_size = sanitize_param(problem_size, 1, lambda x: x > 0) - self.N: int = sanitize_param(N, 50, lambda x: x > 0) - self.rate_clonal: int = sanitize_param(rate_clonal, 10, lambda x: x > 0) - self.rate_hypermutation: np.float64 = np.float64( - sanitize_param( - rate_hypermutation, 1.0, lambda x: x > 0 - ) - ) - self.n_diversity_injection: int = sanitize_param( - n_diversity_injection, 5, lambda x: x > 0 - ) - self.selection_size: int = sanitize_param( - selection_size, 5, lambda x: x > 0 - ) + self.problem_size = problem_size + self.N: int = N + self.rate_clonal: int = rate_clonal + self.rate_hypermutation: float = rate_hypermutation + self.n_diversity_injection: int = n_diversity_injection + self.selection_size: int = selection_size self._affinity_function = affinity_function self.feature_type: FeatureTypeAll = feature_type + self.mode: Literal["min", "max"] = mode + self.seed: Optional[int] = seed self._bounds: Optional[Dict] = None self._bounds_extend_cache: Optional[np.ndarray] = None self.bounds = bounds - self.mode: Literal["min", "max"] = sanitize_param( - mode, - "min", - lambda x: x == "max" - ) - - self.seed: Optional[int] = sanitize_seed(seed) if self.seed is not None: np.random.seed(self.seed) set_seed_numba(self.seed) diff --git a/aisp/ina/_ai_network.py b/aisp/ina/_ai_network.py index 39ff917..cdf1217 100644 --- a/aisp/ina/_ai_network.py +++ b/aisp/ina/_ai_network.py @@ -2,6 +2,7 @@ from __future__ import annotations +from numbers import Real from typing import Optional, Dict, List, Tuple, Union import numpy as np @@ -28,7 +29,7 @@ detect_vector_data_type, check_array_type, check_feature_dimension, - check_binary_array, + check_binary_array, validate_parameters, is_type, positive, choice, optional, non_negative, ) @@ -124,11 +125,27 @@ class AiNet(BaseClusterer): [0 1] """ + @validate_parameters( + N=(is_type(int), positive), + n_clone=(is_type(int), positive), + top_clonal_memory_size=optional((is_type(int), positive)), + affinity_threshold=(is_type(Real), positive), + suppression_threshold=(is_type(Real), positive), + mst_inconsistency_factor=(is_type(Real), non_negative), + max_iterations=(is_type(int), positive), + k=(is_type(int), positive), + metric=( + is_type(str), + choice(["manhattan", "minkowski", "euclidean"]) + ), + seed=optional((is_type(int), positive)), + p=(is_type(Real), positive), + ) def __init__( self, N: int = 50, n_clone: int = 10, - top_clonal_memory_size: int = 5, + top_clonal_memory_size: Optional[int] = 5, n_diversity_injection: int = 5, affinity_threshold: float = 0.5, suppression_threshold: float = 0.5, @@ -140,39 +157,23 @@ def __init__( use_mst_clustering: bool = True, p: float = 2.0 ): - self.N: int = sanitize_param(N, 50, lambda x: x > 0) - self.n_clone: int = sanitize_param(n_clone, 10, lambda x: x > 0) - - self.top_clonal_memory_size: Optional[int] = None - if top_clonal_memory_size is not None: - self.top_clonal_memory_size = sanitize_param( - top_clonal_memory_size, 5, lambda x: x > 0 - ) - - self.n_diversity_injection: int = sanitize_param( - n_diversity_injection, 5, lambda x: x > 0 - ) - self.affinity_threshold: float = sanitize_param( - affinity_threshold, 0.5, lambda x: x > 0 - ) - self.suppression_threshold: float = sanitize_param( - suppression_threshold, 0.5, lambda x: x > 0 - ) - self.mst_inconsistency_factor: float = sanitize_param( - mst_inconsistency_factor, 2, lambda x: x >= 0 - ) - self.max_iterations: int = sanitize_param(max_iterations, 10, lambda x: x > 0) - self.k: int = sanitize_param(k, 1, lambda x: x > 0) - self.seed: Optional[int] = sanitize_seed(seed) + self.N: int = N + self.n_clone: int = n_clone + self.top_clonal_memory_size: Optional[int] = top_clonal_memory_size + self.n_diversity_injection: int = n_diversity_injection + self.affinity_threshold: float = affinity_threshold + self.suppression_threshold: float = suppression_threshold + self.mst_inconsistency_factor: float = mst_inconsistency_factor + self.max_iterations: int = max_iterations + self.k: int = k + self.seed: Optional[int] = seed self.use_mst_clustering: bool = use_mst_clustering if self.seed is not None: np.random.seed(self.seed) set_seed_numba(self.seed) self._feature_type: FeatureType = "continuous-features" - self.metric: str = sanitize_choice( - metric, ["euclidean", "manhattan", "minkowski"], "euclidean" - ) + self.metric: str = metric self.p: float = p self._metric_params = {} diff --git a/aisp/nsa/_binary_negative_selection.py b/aisp/nsa/_binary_negative_selection.py index 2d675c6..8ee8d55 100644 --- a/aisp/nsa/_binary_negative_selection.py +++ b/aisp/nsa/_binary_negative_selection.py @@ -12,7 +12,7 @@ from ..base import BaseClassifier from ..exceptions import MaxDiscardsReachedError, ModelNotFittedError from ..utils.display import ProgressBar -from ..utils.sanitizers import sanitize_seed, sanitize_param +from ..utils.random import set_seed_numba from ..utils.validation import ( check_array_type, check_shape_match, @@ -123,15 +123,16 @@ def __init__( self.seed: Optional[int] = seed - if self.seed is not None: - np.random.seed(seed) - self.no_label_sample_selection: str = no_label_sample_selection self.classes: Optional[npt.NDArray] = None self._detectors: Optional[dict] = None self._detectors_stack: Optional[npt.NDArray] = None + if self.seed is not None: + np.random.seed(seed) + set_seed_numba(self.seed) + @property def detectors(self) -> Optional[Dict[str | int, npt.NDArray[np.bool_]]]: """Return the trained detectors, organized by class.""" diff --git a/aisp/nsa/_negative_selection.py b/aisp/nsa/_negative_selection.py index 448c30f..6e7b979 100644 --- a/aisp/nsa/_negative_selection.py +++ b/aisp/nsa/_negative_selection.py @@ -2,6 +2,7 @@ from __future__ import annotations +from numbers import Real from typing import Dict, Literal, Optional, Union, List import numpy as np @@ -18,12 +19,16 @@ compute_metric_distance, ) from ..utils.random import set_seed_numba -from ..utils.sanitizers import sanitize_seed, sanitize_choice, sanitize_param from ..utils.validation import ( check_array_type, check_shape_match, check_feature_dimension, check_value_range, + validate_parameters, + positive, + is_type, + optional, + choice, ) @@ -130,6 +135,23 @@ class RNSA(BaseClassifier): ['non-self' 'non-self' 'non-self' 'non-self' 'non-self'] """ + @validate_parameters( + N=(is_type(int), positive), + r=(is_type(Real), positive), + rs=(is_type(Real), positive), + k=(is_type(int), positive), + metric=( + is_type(str), + choice(["manhattan", "minkowski", "euclidean"]) + ), + max_discards=(is_type(int), positive), + seed=optional((is_type(int), positive)), + algorithm=( + is_type(str), + choice(["default-NSA", "V-detector"]) + ), + p=(is_type(Real), positive), + ) def __init__( self, N: int = 100, @@ -144,22 +166,14 @@ def __init__( non_self_label: str = 'non-self', cell_bounds: bool = False ): - self.metric: str = sanitize_choice( - metric, ["manhattan", "minkowski"], "euclidean" - ) - self.seed: Optional[int] = sanitize_seed(seed) - if self.seed is not None: - np.random.seed(seed) - set_seed_numba(self.seed) - self.k: int = sanitize_param(k, 1, lambda x: x > 1) - self.N: int = sanitize_param(N, 100, lambda x: x >= 1) - self.r: float = sanitize_param(r, 0.05, lambda x: x > 0) - self.r_s: float = sanitize_param(r_s, 0.0001, lambda x: x > 0) - self.algorithm: str = sanitize_param( - algorithm, "default-NSA", lambda x: x == "V-detector" - ) - self.max_discards: int = sanitize_param(max_discards, 1000, lambda x: x > 0) - + self.metric: str = metric + self.seed: Optional[int] = seed + self.k: int = k + self.N: int = N + self.r: float = r + self.r_s: float = r_s + self.algorithm: str = algorithm + self.max_discards: int = max_discards self.p: float = p self.cell_bounds: bool = cell_bounds self.non_self_label: str = non_self_label @@ -167,6 +181,10 @@ def __init__( self._detectors: Optional[Dict[str | int, list[Detector]]] = None self.classes: Optional[npt.NDArray] = None + if self.seed is not None: + np.random.seed(seed) + set_seed_numba(self.seed) + @property def detectors(self) -> Optional[Dict[str | int, list[Detector]]]: """Returns the trained detectors, organized by class.""" From e1774fc7d50f0bc6f3f1eed0c72db904070a5f4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Paulo?= <88985821+Joao-Paulo-Silva@users.noreply.github.com> Date: Sat, 4 Jul 2026 20:34:18 -0300 Subject: [PATCH 14/15] fix: correct imports and parameter types --- aisp/csa/_ai_recognition_sys.py | 8 ++++---- aisp/csa/_clonalg.py | 6 +++--- aisp/ina/_ai_network.py | 1 - aisp/nsa/_negative_selection.py | 2 +- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/aisp/csa/_ai_recognition_sys.py b/aisp/csa/_ai_recognition_sys.py index 5aeb694..259f4c9 100644 --- a/aisp/csa/_ai_recognition_sys.py +++ b/aisp/csa/_ai_recognition_sys.py @@ -118,14 +118,14 @@ class AIRS(BaseClassifier): """ @validate_parameters( - n_resources=(is_type(float), positive), + n_resources=(is_type(Real), positive), rate_clonal=(is_type(int), positive), rate_mc_init=(is_type(Real), between(0, 1)), - rate_hypermutation=(is_type(float), positive), - affinity_threshold_scalar=(is_type(float), positive), + rate_hypermutation=(is_type(Real), positive), + affinity_threshold_scalar=(is_type(Real), positive), k=(is_type(int), positive), max_iters=(is_type(int), positive), - resource_amplified=(is_type(float), positive), + resource_amplified=(is_type(Real), positive), metric=( is_type(str), choice(["manhattan", "minkowski", "euclidean"]) diff --git a/aisp/csa/_clonalg.py b/aisp/csa/_clonalg.py index 6a6ec20..cc846f6 100644 --- a/aisp/csa/_clonalg.py +++ b/aisp/csa/_clonalg.py @@ -9,7 +9,6 @@ import numpy as np import numpy.typing as npt -from ..utils.validation import validate_parameters, is_type, positive, between, choice, optional from ..base import BaseOptimizer from ..base.immune.cell import Antibody from ..base.immune.mutation import ( @@ -21,8 +20,9 @@ from ..base.immune.populations import generate_random_antibodies from ..utils.display import ProgressTable from ..utils.random import set_seed_numba -from ..utils.sanitizers import sanitize_seed, sanitize_param, sanitize_bounds +from ..utils.sanitizers import sanitize_bounds from ..utils.types import FeatureTypeAll +from ..utils.validation import validate_parameters, is_type, positive, choice, optional class Clonalg(BaseOptimizer): @@ -112,7 +112,7 @@ class Clonalg(BaseOptimizer): problem_size=(is_type(int), positive), N=(is_type(int), positive), rate_clonal=(is_type(int), positive), - rate_hypermutation=(is_type(float), positive), + rate_hypermutation=(is_type(Real), positive), n_diversity_injection=(is_type(int), positive), selection_size=(is_type(int), positive), feature_type=( diff --git a/aisp/ina/_ai_network.py b/aisp/ina/_ai_network.py index cdf1217..23ca5c8 100644 --- a/aisp/ina/_ai_network.py +++ b/aisp/ina/_ai_network.py @@ -23,7 +23,6 @@ from ..utils.distance import hamming, compute_metric_distance, get_metric_code from ..utils.multiclass import predict_knn_affinity from ..utils.random import set_seed_numba -from ..utils.sanitizers import sanitize_choice, sanitize_param, sanitize_seed from ..utils.types import FeatureType, MetricType from ..utils.validation import ( detect_vector_data_type, diff --git a/aisp/nsa/_negative_selection.py b/aisp/nsa/_negative_selection.py index 6e7b979..9075145 100644 --- a/aisp/nsa/_negative_selection.py +++ b/aisp/nsa/_negative_selection.py @@ -138,7 +138,7 @@ class RNSA(BaseClassifier): @validate_parameters( N=(is_type(int), positive), r=(is_type(Real), positive), - rs=(is_type(Real), positive), + r_s=(is_type(Real), positive), k=(is_type(int), positive), metric=( is_type(str), From 97d43f5c19b7dd5c54658a86467490057b8f2dcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Paulo?= <88985821+Joao-Paulo-Silva@users.noreply.github.com> Date: Sat, 4 Jul 2026 20:35:29 -0300 Subject: [PATCH 15/15] docs: add docstrings and rename arg to value --- aisp/utils/validation.py | 63 ++++++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/aisp/utils/validation.py b/aisp/utils/validation.py index 5c2a2ac..6b1da36 100644 --- a/aisp/utils/validation.py +++ b/aisp/utils/validation.py @@ -172,67 +172,79 @@ def _validation_error(name, expected, value): ) -def positive(arg: Real, name: str): - if arg <= 0: - _validation_error(name, "must be positive", arg) - return arg +def positive(value: Real, name: str): + """Valida se o valor é positivo.""" + if value <= 0: + _validation_error(name, "must be positive", value) + return value -def non_negative(arg: Real, name): - if arg < 0: - _validation_error(name, "must be non-negative", arg) - return arg +def non_negative(value: Real, name): + """Valida se o valor é maior ou igual a zero.""" + if value < 0: + _validation_error(name, "must be non-negative", value) + return value def between(low: Real, high: Real): - def validator(arg: Real, name: str): - if not low <= arg <= high: - _validation_error(name, f"a value in [{low}, {high}]", arg) - return arg + """Cria um validador que verifica se um valor pertence a um intervalo.""" + def validator(value: Real, name: str): + """Valida se o valor está no intervalo definido.""" + if not low <= value <= high: + _validation_error(name, f"a value in [{low}, {high}]", value) + return value return validator def choice(choices: Collection): - def validator(arg, name: str): - if arg not in choices: - _validation_error(name, f"one of {tuple(choices)}", arg) - return arg + """Cria um validador que verifica se o valor pertence a um grupo.""" + def validator(value, name: str): + """Valida se o valor esta no grupo de valores permitidos.""" + if value not in choices: + _validation_error(name, f"one of {tuple(choices)}", value) + return value return validator def optional(validator: Callable[[Any, str], Any] | Collection[Callable[[Any, str], Any]]): - def validate(arg, name: str): - if arg is None: + """Torna um validadores opcionais.""" + def validate(value, name: str): + """Aplica a validação quando o valor não é None.""" + if value is None: return None if isinstance(validator, (list, tuple)): for fn in validator: - arg = fn(arg, name) + value = fn(value, name) - return arg + return value - return validator(arg, name) + return validator(value, name) return validate def is_type(expected_type): - def validate(arg, name: str): - if not isinstance(arg, expected_type): + """Cria um validador que verifica o tipo.""" + def validate(value, name: str): + """Valida se o valor possuir o tipo esperado.""" + if not isinstance(value, expected_type): if isinstance(expected_type, (tuple, list)): expected = ', '.join(t.__name__ for t in expected_type) else: expected = expected_type.__name__ - raise TypeError(f"{name} must be of type {expected} got {type(arg).__name__}") + raise TypeError(f"{name} must be of type {expected} got {type(value).__name__}") - return arg + return value return validate def validate_parameters(**validators): + """Cria um decorador para validar automaticamente os parâmetros de uma classe.""" def decorator(func): + """Decora uma função para validação.""" sig = inspect.signature(func) valid_params = sig.parameters.keys() unknown = {k: v for k, v in validators.items() if k not in valid_params} @@ -241,6 +253,7 @@ def decorator(func): @wraps(func) def wrapper(*args, **kwargs): + """Aplica as validações configuradas.""" bound = sig.bind(*args, **kwargs) bound.apply_defaults()