ikn-library 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,31 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
16
+
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+
20
+ - uses: actions/setup-python@v5
21
+ with:
22
+ python-version: ${{ matrix.python-version }}
23
+
24
+ - name: Install package with dev dependencies
25
+ run: pip install -e ".[dev]"
26
+
27
+ - name: Lint with ruff
28
+ run: ruff check src tests
29
+
30
+ - name: Run tests
31
+ run: pytest
@@ -0,0 +1,60 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ jobs:
8
+ build:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+
13
+ - uses: actions/setup-python@v5
14
+ with:
15
+ python-version: "3.12"
16
+
17
+ - name: Build distributions
18
+ run: |
19
+ pip install build
20
+ python -m build
21
+
22
+ - uses: actions/upload-artifact@v4
23
+ with:
24
+ name: dist
25
+ path: dist/
26
+
27
+ publish:
28
+ needs: build
29
+ runs-on: ubuntu-latest
30
+ environment:
31
+ name: pypi
32
+ url: https://pypi.org/p/ikn-library
33
+ permissions:
34
+ id-token: write # required for PyPI Trusted Publishing (OIDC)
35
+
36
+ steps:
37
+ - uses: actions/download-artifact@v4
38
+ with:
39
+ name: dist
40
+ path: dist/
41
+
42
+ - name: Publish to PyPI
43
+ uses: pypa/gh-action-pypi-publish@release/v1
44
+
45
+ github-release:
46
+ needs: publish
47
+ runs-on: ubuntu-latest
48
+ permissions:
49
+ contents: write
50
+ steps:
51
+ - uses: actions/download-artifact@v4
52
+ with:
53
+ name: dist
54
+ path: dist/
55
+
56
+ - name: Create GitHub Release
57
+ uses: softprops/action-gh-release@v2
58
+ with:
59
+ files: dist/*
60
+ generate_release_notes: true
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ venv/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .DS_Store
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Isman Kurniawan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,120 @@
1
+ Metadata-Version: 2.5
2
+ Name: ikn-library
3
+ Version: 0.1.0
4
+ Summary: Nature-inspired metaheuristic algorithms for feature selection and parameter optimization
5
+ Project-URL: Homepage, https://github.com/ismankrn/ikn-library
6
+ Author-email: Isman Kurniawan <isman.krn@gmail.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: ant-colony-optimization,feature-selection,hyperparameter-optimization,metaheuristic,optimization
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
14
+ Requires-Python: >=3.9
15
+ Requires-Dist: numpy>=1.24
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest>=7.0; extra == 'dev'
18
+ Requires-Dist: ruff; extra == 'dev'
19
+ Requires-Dist: scikit-learn>=1.3; extra == 'dev'
20
+ Provides-Extra: ml
21
+ Requires-Dist: scikit-learn>=1.3; extra == 'ml'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # ikn-library
25
+
26
+ Nature-inspired metaheuristic algorithms for continuous optimization, feature
27
+ selection, and parameter optimization — focusing on algorithms not yet
28
+ available in [NiaPy](https://github.com/NiaOrg/NiaPy), starting with
29
+ **Ant Colony Optimization for continuous domains (ACO-R)**.
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install ikn-library
35
+ ```
36
+
37
+ Or from source (development mode):
38
+
39
+ ```bash
40
+ pip install -e ".[dev]"
41
+ ```
42
+
43
+ ## Quick start
44
+
45
+ ```python
46
+ from ikn_library import Task
47
+ from ikn_library.problems import Sphere
48
+ from ikn_library.algorithms import AntColonyOptimization
49
+
50
+ task = Task(problem=Sphere(dimension=10), max_evals=10000)
51
+ algo = AntColonyOptimization(population_size=30, archive_size=50, seed=42)
52
+ best_x, best_fitness = algo.run(task)
53
+
54
+ print("Best fitness:", best_fitness)
55
+ print("Best solution:", best_x)
56
+ ```
57
+
58
+ ## Custom problems
59
+
60
+ Subclass `Problem` and implement `_evaluate` — for example, a
61
+ cross-validation score for hyperparameter optimization:
62
+
63
+ ```python
64
+ import numpy as np
65
+ from ikn_library.problems import Problem
66
+
67
+ class MyProblem(Problem):
68
+ def __init__(self, dimension=10):
69
+ super().__init__(dimension, lower=-10.0, upper=10.0)
70
+
71
+ def _evaluate(self, x):
72
+ return float(np.sum(np.abs(x)))
73
+ ```
74
+
75
+ Use `OptimizationType.MAXIMIZATION` in the `Task` when higher is better
76
+ (e.g. accuracy).
77
+
78
+ ## Feature selection
79
+
80
+ Wrapper-based feature selection with a scikit-learn estimator
81
+ (`pip install ikn-library[ml]`):
82
+
83
+ ```python
84
+ from sklearn.datasets import load_breast_cancer
85
+
86
+ from ikn_library import Task
87
+ from ikn_library.problems import FeatureSelectionProblem
88
+ from ikn_library.algorithms import BinaryAntColonyOptimization
89
+
90
+ X, y = load_breast_cancer(return_X_y=True)
91
+ problem = FeatureSelectionProblem(X, y, cv=5, alpha=0.99)
92
+ task = Task(problem=problem, max_evals=1000)
93
+ algo = BinaryAntColonyOptimization(population_size=20, seed=42)
94
+ best_x, best_fitness = algo.run(task)
95
+
96
+ print("Selected features:", problem.selected_features(best_x))
97
+ ```
98
+
99
+ The fitness balances the cross-validated score against the subset size:
100
+ `alpha * (1 - cv_score) + (1 - alpha) * n_selected / n_features`.
101
+
102
+ ## Algorithms
103
+
104
+ | Algorithm | Class | Domain | Reference |
105
+ |---|---|---|---|
106
+ | Ant Colony Optimization (ACO-R) | `AntColonyOptimization` | continuous | Socha & Dorigo, EJOR 185(3), 2008 |
107
+ | Binary Ant Colony Optimization | `BinaryAntColonyOptimization` | binary / subsets | hyper-cube pheromone update |
108
+
109
+ More algorithms are planned.
110
+
111
+ ## Development
112
+
113
+ ```bash
114
+ pip install -e ".[dev]"
115
+ pytest
116
+ ```
117
+
118
+ ## License
119
+
120
+ MIT
@@ -0,0 +1,97 @@
1
+ # ikn-library
2
+
3
+ Nature-inspired metaheuristic algorithms for continuous optimization, feature
4
+ selection, and parameter optimization — focusing on algorithms not yet
5
+ available in [NiaPy](https://github.com/NiaOrg/NiaPy), starting with
6
+ **Ant Colony Optimization for continuous domains (ACO-R)**.
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ pip install ikn-library
12
+ ```
13
+
14
+ Or from source (development mode):
15
+
16
+ ```bash
17
+ pip install -e ".[dev]"
18
+ ```
19
+
20
+ ## Quick start
21
+
22
+ ```python
23
+ from ikn_library import Task
24
+ from ikn_library.problems import Sphere
25
+ from ikn_library.algorithms import AntColonyOptimization
26
+
27
+ task = Task(problem=Sphere(dimension=10), max_evals=10000)
28
+ algo = AntColonyOptimization(population_size=30, archive_size=50, seed=42)
29
+ best_x, best_fitness = algo.run(task)
30
+
31
+ print("Best fitness:", best_fitness)
32
+ print("Best solution:", best_x)
33
+ ```
34
+
35
+ ## Custom problems
36
+
37
+ Subclass `Problem` and implement `_evaluate` — for example, a
38
+ cross-validation score for hyperparameter optimization:
39
+
40
+ ```python
41
+ import numpy as np
42
+ from ikn_library.problems import Problem
43
+
44
+ class MyProblem(Problem):
45
+ def __init__(self, dimension=10):
46
+ super().__init__(dimension, lower=-10.0, upper=10.0)
47
+
48
+ def _evaluate(self, x):
49
+ return float(np.sum(np.abs(x)))
50
+ ```
51
+
52
+ Use `OptimizationType.MAXIMIZATION` in the `Task` when higher is better
53
+ (e.g. accuracy).
54
+
55
+ ## Feature selection
56
+
57
+ Wrapper-based feature selection with a scikit-learn estimator
58
+ (`pip install ikn-library[ml]`):
59
+
60
+ ```python
61
+ from sklearn.datasets import load_breast_cancer
62
+
63
+ from ikn_library import Task
64
+ from ikn_library.problems import FeatureSelectionProblem
65
+ from ikn_library.algorithms import BinaryAntColonyOptimization
66
+
67
+ X, y = load_breast_cancer(return_X_y=True)
68
+ problem = FeatureSelectionProblem(X, y, cv=5, alpha=0.99)
69
+ task = Task(problem=problem, max_evals=1000)
70
+ algo = BinaryAntColonyOptimization(population_size=20, seed=42)
71
+ best_x, best_fitness = algo.run(task)
72
+
73
+ print("Selected features:", problem.selected_features(best_x))
74
+ ```
75
+
76
+ The fitness balances the cross-validated score against the subset size:
77
+ `alpha * (1 - cv_score) + (1 - alpha) * n_selected / n_features`.
78
+
79
+ ## Algorithms
80
+
81
+ | Algorithm | Class | Domain | Reference |
82
+ |---|---|---|---|
83
+ | Ant Colony Optimization (ACO-R) | `AntColonyOptimization` | continuous | Socha & Dorigo, EJOR 185(3), 2008 |
84
+ | Binary Ant Colony Optimization | `BinaryAntColonyOptimization` | binary / subsets | hyper-cube pheromone update |
85
+
86
+ More algorithms are planned.
87
+
88
+ ## Development
89
+
90
+ ```bash
91
+ pip install -e ".[dev]"
92
+ pytest
93
+ ```
94
+
95
+ ## License
96
+
97
+ MIT
@@ -0,0 +1,29 @@
1
+ """Example: feature selection on the breast-cancer dataset with Binary ACO.
2
+
3
+ Requires scikit-learn: pip install ikn-library[ml]
4
+ """
5
+
6
+ from sklearn.datasets import load_breast_cancer
7
+ from sklearn.model_selection import cross_val_score
8
+ from sklearn.neighbors import KNeighborsClassifier
9
+
10
+ from ikn_library import Task
11
+ from ikn_library.problems import FeatureSelectionProblem
12
+ from ikn_library.algorithms import BinaryAntColonyOptimization
13
+
14
+ data = load_breast_cancer()
15
+ X, y = data.data, data.target
16
+ estimator = KNeighborsClassifier(n_neighbors=5)
17
+
18
+ problem = FeatureSelectionProblem(X, y, estimator=estimator, cv=5, alpha=0.99)
19
+ task = Task(problem=problem, max_evals=1000)
20
+ algo = BinaryAntColonyOptimization(population_size=20, evaporation=0.1, seed=42)
21
+ best_x, best_fitness = algo.run(task)
22
+
23
+ selected = problem.selected_features(best_x)
24
+ baseline = cross_val_score(estimator, X, y, cv=5).mean()
25
+ score = cross_val_score(estimator, X[:, selected], y, cv=5).mean()
26
+
27
+ print(f"All {X.shape[1]} features : accuracy = {baseline:.4f}")
28
+ print(f"Selected {len(selected)} features: accuracy = {score:.4f}")
29
+ print("Selected feature names:", list(data.feature_names[selected]))
@@ -0,0 +1,12 @@
1
+ """Example: minimize benchmark functions with Ant Colony Optimization."""
2
+
3
+ from ikn_library import Task
4
+ from ikn_library.problems import Ackley, Rastrigin, Sphere
5
+ from ikn_library.algorithms import AntColonyOptimization
6
+
7
+ for problem_cls in (Sphere, Rastrigin, Ackley):
8
+ task = Task(problem=problem_cls(dimension=10), max_evals=20000)
9
+ algo = AntColonyOptimization(population_size=30, archive_size=50, seed=42)
10
+ best_x, best_fitness = algo.run(task)
11
+ print(f"{problem_cls.__name__:>10}: best fitness = {best_fitness:.6g} "
12
+ f"({task.evals} evals, {task.iters} iters)")
@@ -0,0 +1,47 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "ikn-library"
7
+ version = "0.1.0"
8
+ description = "Nature-inspired metaheuristic algorithms for feature selection and parameter optimization"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ authors = [{ name = "Isman Kurniawan", email = "isman.krn@gmail.com" }]
12
+ requires-python = ">=3.9"
13
+ keywords = [
14
+ "optimization",
15
+ "metaheuristic",
16
+ "ant-colony-optimization",
17
+ "feature-selection",
18
+ "hyperparameter-optimization",
19
+ ]
20
+ classifiers = [
21
+ "Development Status :: 3 - Alpha",
22
+ "Intended Audience :: Science/Research",
23
+ "Programming Language :: Python :: 3",
24
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
25
+ ]
26
+ dependencies = [
27
+ "numpy>=1.24",
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ ml = [
32
+ "scikit-learn>=1.3",
33
+ ]
34
+ dev = [
35
+ "pytest>=7.0",
36
+ "ruff",
37
+ "scikit-learn>=1.3",
38
+ ]
39
+
40
+ [project.urls]
41
+ Homepage = "https://github.com/ismankrn/ikn-library"
42
+
43
+ [tool.hatch.build.targets.wheel]
44
+ packages = ["src/ikn_library"]
45
+
46
+ [tool.pytest.ini_options]
47
+ testpaths = ["tests"]
@@ -0,0 +1,18 @@
1
+ """ikn_library: nature-inspired metaheuristic algorithms.
2
+
3
+ Metaheuristic algorithms for continuous optimization, feature selection,
4
+ and parameter optimization, following a NiaPy-like workflow:
5
+
6
+ >>> from ikn_library import Task
7
+ >>> from ikn_library.problems import Sphere
8
+ >>> from ikn_library.algorithms import AntColonyOptimization
9
+ >>> task = Task(problem=Sphere(dimension=10), max_evals=10000)
10
+ >>> algo = AntColonyOptimization(population_size=30, seed=42)
11
+ >>> best_x, best_fitness = algo.run(task)
12
+ """
13
+
14
+ from ikn_library.task import OptimizationType, Task
15
+
16
+ __version__ = "0.1.0"
17
+
18
+ __all__ = ["OptimizationType", "Task", "__version__"]
@@ -0,0 +1,7 @@
1
+ """Metaheuristic algorithms."""
2
+
3
+ from ikn_library.algorithms.aco import AntColonyOptimization
4
+ from ikn_library.algorithms.algorithm import Algorithm
5
+ from ikn_library.algorithms.binary_aco import BinaryAntColonyOptimization
6
+
7
+ __all__ = ["Algorithm", "AntColonyOptimization", "BinaryAntColonyOptimization"]
@@ -0,0 +1,76 @@
1
+ """Ant Colony Optimization for continuous domains (ACO-R).
2
+
3
+ Reference:
4
+ K. Socha and M. Dorigo, "Ant colony optimization for continuous
5
+ domains," European Journal of Operational Research, 185(3), 2008.
6
+ """
7
+
8
+ import numpy as np
9
+
10
+ from ikn_library.algorithms.algorithm import Algorithm
11
+
12
+
13
+ class AntColonyOptimization(Algorithm):
14
+ """ACO-R: Ant Colony Optimization for continuous search spaces.
15
+
16
+ Keeps an archive of the best solutions found so far. Each ant builds
17
+ a new solution by picking a guide solution from the archive (better
18
+ solutions are picked with higher probability) and sampling each
19
+ coordinate from a Gaussian centered on the guide. The Gaussian width
20
+ shrinks as the archive converges, balancing exploration and
21
+ exploitation — the archive plays the role of the pheromone trail.
22
+
23
+ Args:
24
+ population_size: Number of ants (new solutions) per iteration.
25
+ archive_size: Number of solutions kept in the archive (k).
26
+ intensification: Locality of search (q). Small values focus on
27
+ the best archive solutions; larger values spread the
28
+ selection more evenly.
29
+ evaporation: Speed of convergence (xi). Plays a role similar to
30
+ pheromone evaporation: higher values mean slower convergence.
31
+ seed: Random seed for reproducibility.
32
+ """
33
+
34
+ def __init__(self, population_size=30, archive_size=50,
35
+ intensification=0.1, evaporation=0.85, seed=None):
36
+ super().__init__(population_size=population_size, seed=seed)
37
+ if archive_size < 2:
38
+ raise ValueError("archive_size must be >= 2")
39
+ self.archive_size = int(archive_size)
40
+ self.intensification = float(intensification)
41
+ self.evaporation = float(evaporation)
42
+
43
+ def init_population(self, task):
44
+ archive = self.rng.uniform(
45
+ task.lower, task.upper, (self.archive_size, task.dimension)
46
+ )
47
+ fitness = np.array([task.eval(x) for x in archive])
48
+ order = np.argsort(fitness)
49
+ return archive[order], fitness[order]
50
+
51
+ def _selection_weights(self):
52
+ k, q = self.archive_size, self.intensification
53
+ ranks = np.arange(k)
54
+ weights = np.exp(-(ranks ** 2) / (2.0 * (q * k) ** 2)) / (q * k * np.sqrt(2.0 * np.pi))
55
+ return weights / np.sum(weights)
56
+
57
+ def run_iteration(self, task, state):
58
+ archive, fitness = state
59
+ probabilities = self._selection_weights()
60
+
61
+ ants = np.empty((self.population_size, task.dimension))
62
+ for a in range(self.population_size):
63
+ guide = self.rng.choice(self.archive_size, p=probabilities)
64
+ # Gaussian width per dimension: mean distance from the guide
65
+ # to the rest of the archive, scaled by the evaporation rate.
66
+ sigma = self.evaporation * np.sum(
67
+ np.abs(archive - archive[guide]), axis=0
68
+ ) / (self.archive_size - 1)
69
+ ants[a] = task.repair(self.rng.normal(archive[guide], np.maximum(sigma, 1e-12)))
70
+ ant_fitness = np.array([task.eval(x) for x in ants])
71
+
72
+ # Merge ants into the archive and keep the best archive_size solutions.
73
+ merged = np.vstack([archive, ants])
74
+ merged_fitness = np.concatenate([fitness, ant_fitness])
75
+ order = np.argsort(merged_fitness)[: self.archive_size]
76
+ return merged[order], merged_fitness[order]
@@ -0,0 +1,54 @@
1
+ """Base class for metaheuristic algorithms."""
2
+
3
+ import numpy as np
4
+
5
+
6
+ class Algorithm:
7
+ """Base class for population-based metaheuristic algorithms.
8
+
9
+ Subclasses implement :meth:`init_population` and :meth:`run_iteration`;
10
+ the shared :meth:`run` loop handles the budget and result reporting.
11
+
12
+ Args:
13
+ population_size: Number of individuals in the population.
14
+ seed: Random seed for reproducibility (optional).
15
+ """
16
+
17
+ def __init__(self, population_size=25, seed=None):
18
+ if population_size < 1:
19
+ raise ValueError("population_size must be >= 1")
20
+ self.population_size = int(population_size)
21
+ self.rng = np.random.default_rng(seed)
22
+
23
+ def run(self, task):
24
+ """Run the algorithm on ``task`` until its budget is exhausted.
25
+
26
+ Returns:
27
+ tuple: ``(best_x, best_fitness)``.
28
+ """
29
+ state = self.init_population(task)
30
+ task.next_iter()
31
+ while not task.stopping_condition():
32
+ state = self.run_iteration(task, state)
33
+ task.next_iter()
34
+ return task.result()
35
+
36
+ def init_population(self, task):
37
+ """Create the initial population; return the algorithm state.
38
+
39
+ The default creates ``population_size`` uniform random solutions
40
+ and returns ``(population, fitness)`` arrays.
41
+ """
42
+ population = self.rng.uniform(
43
+ task.lower, task.upper, (self.population_size, task.dimension)
44
+ )
45
+ fitness = np.array([task.eval(x) for x in population])
46
+ return population, fitness
47
+
48
+ def run_iteration(self, task, state):
49
+ """Perform one iteration; receive and return the algorithm state."""
50
+ raise NotImplementedError
51
+
52
+ @property
53
+ def name(self):
54
+ return type(self).__name__
@@ -0,0 +1,73 @@
1
+ """Binary Ant Colony Optimization for subset-selection problems.
2
+
3
+ Each decision variable is a bit (1 = selected, 0 = not selected). A
4
+ pheromone value is maintained per (variable, bit-value) pair; ants build
5
+ bit strings by sampling each bit with a probability proportional to its
6
+ pheromone. Pheromone is updated in the hyper-cube framework: it
7
+ evaporates toward the bit values of the best solution found so far, and
8
+ is clamped to ``[tau_min, tau_max]`` to preserve exploration.
9
+ """
10
+
11
+ import numpy as np
12
+
13
+ from ikn_library.algorithms.algorithm import Algorithm
14
+
15
+
16
+ class BinaryAntColonyOptimization(Algorithm):
17
+ """Binary ACO for feature selection and other subset problems.
18
+
19
+ Solutions are 0/1 vectors, so the wrapped problem must interpret its
20
+ input as a bit mask (e.g.
21
+ :class:`~ikn_library.problems.FeatureSelectionProblem`).
22
+
23
+ Args:
24
+ population_size: Number of ants per iteration.
25
+ evaporation: Pheromone evaporation/learning rate (rho) in (0, 1).
26
+ Higher values converge faster toward the best solution.
27
+ alpha: Pheromone importance exponent.
28
+ tau_min: Lower pheromone limit, keeps every bit reachable.
29
+ tau_max: Upper pheromone limit.
30
+ seed: Random seed for reproducibility.
31
+ """
32
+
33
+ def __init__(self, population_size=30, evaporation=0.1, alpha=1.0,
34
+ tau_min=0.1, tau_max=0.9, seed=None):
35
+ super().__init__(population_size=population_size, seed=seed)
36
+ if not 0.0 < evaporation < 1.0:
37
+ raise ValueError("evaporation must be in (0, 1)")
38
+ if not 0.0 < tau_min < tau_max:
39
+ raise ValueError("require 0 < tau_min < tau_max")
40
+ self.evaporation = float(evaporation)
41
+ self.alpha = float(alpha)
42
+ self.tau_min = float(tau_min)
43
+ self.tau_max = float(tau_max)
44
+
45
+ def _sample_ants(self, task, pheromone):
46
+ weights = pheromone ** self.alpha
47
+ p_one = weights[:, 1] / np.sum(weights, axis=1)
48
+ ants = (self.rng.random((self.population_size, task.dimension)) < p_one).astype(float)
49
+ # An all-zero ant selects nothing and is unevaluable as a subset;
50
+ # repair it by switching one random bit on.
51
+ for ant in ants:
52
+ if not ant.any():
53
+ ant[self.rng.integers(task.dimension)] = 1.0
54
+ return ants
55
+
56
+ def init_population(self, task):
57
+ pheromone = np.full((task.dimension, 2), 0.5)
58
+ ants = self._sample_ants(task, pheromone)
59
+ for ant in ants:
60
+ task.eval(ant)
61
+ return pheromone
62
+
63
+ def run_iteration(self, task, pheromone):
64
+ ants = self._sample_ants(task, pheromone)
65
+ for ant in ants:
66
+ task.eval(ant)
67
+
68
+ # Hyper-cube pheromone update toward the best-so-far solution.
69
+ best_bits = task.best_x.astype(int)
70
+ deposit = np.zeros_like(pheromone)
71
+ deposit[np.arange(task.dimension), best_bits] = 1.0
72
+ pheromone = (1.0 - self.evaporation) * pheromone + self.evaporation * deposit
73
+ return np.clip(pheromone, self.tau_min, self.tau_max)
@@ -0,0 +1,7 @@
1
+ """Optimization problems: base class, benchmarks, and feature selection."""
2
+
3
+ from ikn_library.problems.benchmarks import Ackley, Rastrigin, Sphere
4
+ from ikn_library.problems.feature_selection import FeatureSelectionProblem
5
+ from ikn_library.problems.problem import Problem
6
+
7
+ __all__ = ["Ackley", "FeatureSelectionProblem", "Problem", "Rastrigin", "Sphere"]
@@ -0,0 +1,41 @@
1
+ """Standard benchmark functions for testing algorithms.
2
+
3
+ All benchmarks are minimization problems with a known global optimum of 0.
4
+ """
5
+
6
+ import numpy as np
7
+
8
+ from ikn_library.problems.problem import Problem
9
+
10
+
11
+ class Sphere(Problem):
12
+ """Sphere function: ``f(x) = sum(x_i^2)``. Optimum ``f(0) = 0``."""
13
+
14
+ def __init__(self, dimension=10, lower=-5.12, upper=5.12):
15
+ super().__init__(dimension, lower, upper)
16
+
17
+ def _evaluate(self, x):
18
+ return np.sum(x ** 2)
19
+
20
+
21
+ class Rastrigin(Problem):
22
+ """Rastrigin function, highly multimodal. Optimum ``f(0) = 0``."""
23
+
24
+ def __init__(self, dimension=10, lower=-5.12, upper=5.12):
25
+ super().__init__(dimension, lower, upper)
26
+
27
+ def _evaluate(self, x):
28
+ return 10.0 * self.dimension + np.sum(x ** 2 - 10.0 * np.cos(2.0 * np.pi * x))
29
+
30
+
31
+ class Ackley(Problem):
32
+ """Ackley function, multimodal with a nearly flat outer region. Optimum ``f(0) = 0``."""
33
+
34
+ def __init__(self, dimension=10, lower=-32.768, upper=32.768):
35
+ super().__init__(dimension, lower, upper)
36
+
37
+ def _evaluate(self, x):
38
+ n = self.dimension
39
+ term1 = -20.0 * np.exp(-0.2 * np.sqrt(np.sum(x ** 2) / n))
40
+ term2 = -np.exp(np.sum(np.cos(2.0 * np.pi * x)) / n)
41
+ return term1 + term2 + 20.0 + np.e
@@ -0,0 +1,86 @@
1
+ """Wrapper-based feature selection as an optimization problem."""
2
+
3
+ import numpy as np
4
+
5
+ from ikn_library.problems.problem import Problem
6
+
7
+
8
+ class FeatureSelectionProblem(Problem):
9
+ """Wrapper feature selection: pick the feature subset that maximizes
10
+ a cross-validated model score while keeping the subset small.
11
+
12
+ Solutions are vectors in ``[0, 1]``; entries above ``threshold`` mark
13
+ selected features, so both binary algorithms (which emit 0/1 bits)
14
+ and continuous algorithms can optimize this problem.
15
+
16
+ The fitness (minimized) is::
17
+
18
+ alpha * (1 - cv_score) + (1 - alpha) * n_selected / n_features
19
+
20
+ Requires scikit-learn (``pip install ikn-library[ml]``).
21
+
22
+ Args:
23
+ X: Feature matrix of shape ``(n_samples, n_features)``.
24
+ y: Target vector of shape ``(n_samples,)``.
25
+ estimator: A scikit-learn estimator. Defaults to
26
+ ``KNeighborsClassifier(n_neighbors=5)``, a common choice in
27
+ wrapper feature-selection studies.
28
+ cv: Number of cross-validation folds.
29
+ scoring: scikit-learn scoring name (e.g. ``"accuracy"``, ``"f1"``).
30
+ alpha: Trade-off between score quality and subset size, in [0, 1].
31
+ Values near 1 prioritize the model score.
32
+ threshold: Cut-off above which a variable counts as selected.
33
+ """
34
+
35
+ def __init__(self, X, y, estimator=None, cv=5, scoring="accuracy",
36
+ alpha=0.99, threshold=0.5):
37
+ try:
38
+ from sklearn.model_selection import cross_val_score # noqa: F401
39
+ except ImportError as exc:
40
+ raise ImportError(
41
+ "FeatureSelectionProblem requires scikit-learn; "
42
+ "install it with: pip install ikn-library[ml]"
43
+ ) from exc
44
+
45
+ self.X = np.asarray(X)
46
+ self.y = np.asarray(y)
47
+ if self.X.ndim != 2:
48
+ raise ValueError("X must be 2-dimensional (n_samples, n_features)")
49
+ if len(self.X) != len(self.y):
50
+ raise ValueError("X and y must have the same number of samples")
51
+ if not 0.0 <= alpha <= 1.0:
52
+ raise ValueError("alpha must be in [0, 1]")
53
+
54
+ super().__init__(dimension=self.X.shape[1], lower=0.0, upper=1.0)
55
+
56
+ if estimator is None:
57
+ from sklearn.neighbors import KNeighborsClassifier
58
+ estimator = KNeighborsClassifier(n_neighbors=5)
59
+ self.estimator = estimator
60
+ self.cv = cv
61
+ self.scoring = scoring
62
+ self.alpha = float(alpha)
63
+ self.threshold = float(threshold)
64
+
65
+ def feature_mask(self, x):
66
+ """Boolean mask of selected features for a solution vector."""
67
+ return np.asarray(x, dtype=float) > self.threshold
68
+
69
+ def selected_features(self, x):
70
+ """Indices of the features selected by a solution vector."""
71
+ return np.flatnonzero(self.feature_mask(x))
72
+
73
+ def _evaluate(self, x):
74
+ from sklearn.base import clone
75
+ from sklearn.model_selection import cross_val_score
76
+
77
+ mask = self.feature_mask(x)
78
+ n_selected = int(np.sum(mask))
79
+ if n_selected == 0:
80
+ return 1.0 # worst possible fitness: nothing selected
81
+ score = cross_val_score(
82
+ clone(self.estimator), self.X[:, mask], self.y,
83
+ cv=self.cv, scoring=self.scoring,
84
+ ).mean()
85
+ return (self.alpha * (1.0 - score)
86
+ + (1.0 - self.alpha) * n_selected / self.dimension)
@@ -0,0 +1,52 @@
1
+ """Base class for optimization problems."""
2
+
3
+ import numpy as np
4
+
5
+
6
+ class Problem:
7
+ """An optimization problem defined on a box-constrained search space.
8
+
9
+ Subclass this and implement :meth:`_evaluate` to define a custom
10
+ problem (e.g. a machine-learning objective for parameter optimization
11
+ or a wrapper objective for feature selection).
12
+
13
+ Args:
14
+ dimension: Number of decision variables.
15
+ lower: Lower bound(s) of the search space. Scalar or array of
16
+ shape ``(dimension,)``.
17
+ upper: Upper bound(s) of the search space. Scalar or array of
18
+ shape ``(dimension,)``.
19
+
20
+ Example:
21
+ >>> class MyProblem(Problem):
22
+ ... def __init__(self, dimension):
23
+ ... super().__init__(dimension, lower=-10.0, upper=10.0)
24
+ ... def _evaluate(self, x):
25
+ ... return float(np.sum(x ** 2))
26
+ """
27
+
28
+ def __init__(self, dimension, lower=-1.0, upper=1.0):
29
+ if dimension < 1:
30
+ raise ValueError("dimension must be >= 1")
31
+ self.dimension = int(dimension)
32
+ self.lower = np.full(self.dimension, lower, dtype=float) if np.isscalar(lower) else np.asarray(lower, dtype=float)
33
+ self.upper = np.full(self.dimension, upper, dtype=float) if np.isscalar(upper) else np.asarray(upper, dtype=float)
34
+ if self.lower.shape != (self.dimension,) or self.upper.shape != (self.dimension,):
35
+ raise ValueError("lower/upper must be scalars or arrays of shape (dimension,)")
36
+ if np.any(self.lower >= self.upper):
37
+ raise ValueError("each lower bound must be strictly less than its upper bound")
38
+
39
+ def evaluate(self, x):
40
+ """Evaluate a solution vector and return its fitness as ``float``."""
41
+ x = np.asarray(x, dtype=float)
42
+ if x.shape != (self.dimension,):
43
+ raise ValueError(f"expected solution of shape ({self.dimension},), got {x.shape}")
44
+ return float(self._evaluate(x))
45
+
46
+ def _evaluate(self, x):
47
+ """Compute the objective value for solution ``x``. Must be overridden."""
48
+ raise NotImplementedError
49
+
50
+ @property
51
+ def name(self):
52
+ return type(self).__name__
@@ -0,0 +1,92 @@
1
+ """Task: wraps a Problem with a stopping condition and bookkeeping."""
2
+
3
+ from enum import Enum
4
+
5
+ import numpy as np
6
+
7
+
8
+ class OptimizationType(Enum):
9
+ MINIMIZATION = 1
10
+ MAXIMIZATION = -1
11
+
12
+
13
+ class Task:
14
+ """An optimization run: a problem plus a budget and progress tracking.
15
+
16
+ The task counts evaluations, tracks the best solution found so far,
17
+ and records the convergence history. Algorithms should call
18
+ :meth:`eval` for every candidate solution and check :meth:`stopping_condition`.
19
+
20
+ Args:
21
+ problem: The :class:`~ikn_library.problems.Problem` to optimize.
22
+ max_evals: Stop after this many fitness evaluations (optional).
23
+ max_iters: Stop after this many iterations (optional). The
24
+ algorithm must call :meth:`next_iter` once per iteration.
25
+ optimization_type: Minimize (default) or maximize.
26
+
27
+ At least one of ``max_evals`` / ``max_iters`` must be given.
28
+ """
29
+
30
+ def __init__(self, problem, max_evals=None, max_iters=None,
31
+ optimization_type=OptimizationType.MINIMIZATION):
32
+ if max_evals is None and max_iters is None:
33
+ raise ValueError("provide max_evals and/or max_iters")
34
+ self.problem = problem
35
+ self.max_evals = np.inf if max_evals is None else int(max_evals)
36
+ self.max_iters = np.inf if max_iters is None else int(max_iters)
37
+ self.optimization_type = optimization_type
38
+
39
+ self.evals = 0
40
+ self.iters = 0
41
+ self.best_x = None
42
+ self.best_fitness = np.inf
43
+ self.convergence = [] # best internal fitness after each iteration
44
+
45
+ @property
46
+ def dimension(self):
47
+ return self.problem.dimension
48
+
49
+ @property
50
+ def lower(self):
51
+ return self.problem.lower
52
+
53
+ @property
54
+ def upper(self):
55
+ return self.problem.upper
56
+
57
+ def repair(self, x):
58
+ """Clip a solution back into the search-space bounds."""
59
+ return np.clip(x, self.lower, self.upper)
60
+
61
+ def eval(self, x):
62
+ """Evaluate ``x``, update counters and the best-so-far solution.
63
+
64
+ Returns the fitness in *internal* form (maximization problems are
65
+ negated so that algorithms can always minimize).
66
+ """
67
+ if self.stopping_condition():
68
+ return np.inf
69
+ fitness = self.problem.evaluate(x) * self.optimization_type.value
70
+ self.evals += 1
71
+ if fitness < self.best_fitness:
72
+ self.best_fitness = fitness
73
+ self.best_x = np.array(x, dtype=float)
74
+ return fitness
75
+
76
+ def next_iter(self):
77
+ """Advance the iteration counter and record convergence."""
78
+ self.iters += 1
79
+ self.convergence.append(self.best_fitness)
80
+
81
+ def stopping_condition(self):
82
+ """True when the evaluation or iteration budget is exhausted."""
83
+ return self.evals >= self.max_evals or self.iters >= self.max_iters
84
+
85
+ def result(self):
86
+ """Return ``(best_x, best_fitness)`` in the problem's original sense."""
87
+ return self.best_x, self.best_fitness * self.optimization_type.value
88
+
89
+ def convergence_data(self):
90
+ """Convergence history as ``(iterations, best_fitness_values)``."""
91
+ values = np.array(self.convergence) * self.optimization_type.value
92
+ return np.arange(1, len(values) + 1), values
@@ -0,0 +1,54 @@
1
+ import numpy as np
2
+ import pytest
3
+
4
+ from ikn_library import Task
5
+ from ikn_library.algorithms import AntColonyOptimization
6
+ from ikn_library.problems import Rastrigin, Sphere
7
+
8
+
9
+ def test_aco_converges_on_sphere():
10
+ task = Task(problem=Sphere(dimension=5), max_evals=10000)
11
+ algo = AntColonyOptimization(population_size=30, archive_size=50, seed=42)
12
+ best_x, best_fitness = algo.run(task)
13
+ assert best_fitness < 1e-3
14
+ assert best_x.shape == (5,)
15
+
16
+
17
+ def test_aco_improves_on_rastrigin():
18
+ problem = Rastrigin(dimension=5)
19
+ task = Task(problem=problem, max_evals=10000)
20
+ algo = AntColonyOptimization(population_size=30, archive_size=50, seed=1)
21
+ _, best_fitness = algo.run(task)
22
+ random_baseline = min(
23
+ problem.evaluate(x)
24
+ for x in np.random.default_rng(1).uniform(-5.12, 5.12, (100, 5))
25
+ )
26
+ assert best_fitness < random_baseline
27
+
28
+
29
+ def test_aco_respects_bounds():
30
+ task = Task(problem=Sphere(dimension=3), max_evals=2000)
31
+ algo = AntColonyOptimization(population_size=10, archive_size=20, seed=7)
32
+ best_x, _ = algo.run(task)
33
+ assert np.all(best_x >= task.lower) and np.all(best_x <= task.upper)
34
+
35
+
36
+ def test_aco_is_reproducible_with_seed():
37
+ results = []
38
+ for _ in range(2):
39
+ task = Task(problem=Sphere(dimension=4), max_evals=3000)
40
+ algo = AntColonyOptimization(population_size=20, seed=123)
41
+ results.append(algo.run(task))
42
+ np.testing.assert_allclose(results[0][0], results[1][0])
43
+ assert results[0][1] == results[1][1]
44
+
45
+
46
+ def test_aco_respects_eval_budget():
47
+ task = Task(problem=Sphere(dimension=3), max_evals=500)
48
+ AntColonyOptimization(population_size=10, seed=0).run(task)
49
+ assert task.evals <= 500
50
+
51
+
52
+ def test_invalid_archive_size():
53
+ with pytest.raises(ValueError):
54
+ AntColonyOptimization(archive_size=1)
@@ -0,0 +1,74 @@
1
+ import numpy as np
2
+ import pytest
3
+
4
+ from ikn_library import Task
5
+ from ikn_library.algorithms import BinaryAntColonyOptimization
6
+ from ikn_library.problems import Problem
7
+
8
+
9
+ class SubsetMatch(Problem):
10
+ """Minimize the Hamming distance to a known target bit mask."""
11
+
12
+ def __init__(self, target):
13
+ self.target = np.asarray(target, dtype=float)
14
+ super().__init__(dimension=len(self.target), lower=0.0, upper=1.0)
15
+
16
+ def _evaluate(self, x):
17
+ return float(np.sum((x > 0.5) != (self.target > 0.5)))
18
+
19
+
20
+ def test_binary_aco_finds_target_subset():
21
+ target = np.array([1, 0, 1, 1, 0, 0, 0, 1, 0, 0])
22
+ task = Task(problem=SubsetMatch(target), max_evals=6000)
23
+ algo = BinaryAntColonyOptimization(population_size=20, seed=42)
24
+ best_x, best_fitness = algo.run(task)
25
+ assert best_fitness == 0.0
26
+ np.testing.assert_array_equal(best_x, target)
27
+
28
+
29
+ def test_binary_aco_emits_only_bits():
30
+ target = np.ones(6)
31
+ task = Task(problem=SubsetMatch(target), max_evals=500)
32
+ best_x, _ = BinaryAntColonyOptimization(population_size=10, seed=3).run(task)
33
+ assert set(np.unique(best_x)) <= {0.0, 1.0}
34
+
35
+
36
+ def test_binary_aco_never_selects_empty_subset():
37
+ class CountOnes(Problem):
38
+ def __init__(self):
39
+ super().__init__(dimension=8, lower=0.0, upper=1.0)
40
+
41
+ def _evaluate(self, x):
42
+ assert np.sum(x) >= 1, "empty subset was evaluated"
43
+ return float(np.sum(x))
44
+
45
+ task = Task(problem=CountOnes(), max_evals=2000)
46
+ best_x, _ = BinaryAntColonyOptimization(population_size=15, seed=5).run(task)
47
+ assert np.sum(best_x) >= 1
48
+
49
+
50
+ def test_binary_aco_is_reproducible_with_seed():
51
+ target = np.array([1, 0, 1, 0, 1, 0])
52
+ results = []
53
+ for _ in range(2):
54
+ task = Task(problem=SubsetMatch(target), max_evals=1000)
55
+ algo = BinaryAntColonyOptimization(population_size=10, seed=99)
56
+ results.append(algo.run(task))
57
+ np.testing.assert_array_equal(results[0][0], results[1][0])
58
+ assert results[0][1] == results[1][1]
59
+
60
+
61
+ def test_binary_aco_respects_eval_budget():
62
+ task = Task(problem=SubsetMatch(np.ones(5)), max_evals=300)
63
+ BinaryAntColonyOptimization(population_size=10, seed=0).run(task)
64
+ assert task.evals <= 300
65
+
66
+
67
+ @pytest.mark.parametrize("kwargs", [
68
+ {"evaporation": 0.0},
69
+ {"evaporation": 1.0},
70
+ {"tau_min": 0.5, "tau_max": 0.4},
71
+ ])
72
+ def test_binary_aco_invalid_params(kwargs):
73
+ with pytest.raises(ValueError):
74
+ BinaryAntColonyOptimization(**kwargs)
@@ -0,0 +1,62 @@
1
+ import numpy as np
2
+ import pytest
3
+
4
+ sklearn = pytest.importorskip("sklearn")
5
+
6
+ from sklearn.datasets import make_classification
7
+
8
+ from ikn_library import Task
9
+ from ikn_library.algorithms import BinaryAntColonyOptimization
10
+ from ikn_library.problems import FeatureSelectionProblem
11
+
12
+
13
+ @pytest.fixture(scope="module")
14
+ def dataset():
15
+ return make_classification(
16
+ n_samples=120, n_features=10, n_informative=3, n_redundant=0,
17
+ n_repeated=0, shuffle=False, random_state=42,
18
+ )
19
+
20
+
21
+ def test_selected_features_and_mask(dataset):
22
+ X, y = dataset
23
+ problem = FeatureSelectionProblem(X, y)
24
+ x = np.array([0.9, 0.1, 0.6, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.7])
25
+ np.testing.assert_array_equal(problem.selected_features(x), [0, 2, 9])
26
+ assert problem.feature_mask(x).sum() == 3
27
+
28
+
29
+ def test_empty_subset_gets_worst_fitness(dataset):
30
+ X, y = dataset
31
+ problem = FeatureSelectionProblem(X, y)
32
+ assert problem.evaluate(np.zeros(10)) == 1.0
33
+
34
+
35
+ def test_fitness_in_unit_interval(dataset):
36
+ X, y = dataset
37
+ problem = FeatureSelectionProblem(X, y, cv=3)
38
+ fitness = problem.evaluate(np.ones(10))
39
+ assert 0.0 <= fitness <= 1.0
40
+
41
+
42
+ def test_binary_aco_feature_selection_improves(dataset):
43
+ X, y = dataset
44
+ problem = FeatureSelectionProblem(X, y, cv=3)
45
+ all_features_fitness = problem.evaluate(np.ones(10))
46
+
47
+ task = Task(problem=problem, max_evals=300)
48
+ algo = BinaryAntColonyOptimization(population_size=10, seed=42)
49
+ best_x, best_fitness = algo.run(task)
50
+
51
+ assert best_fitness <= all_features_fitness
52
+ assert len(problem.selected_features(best_x)) >= 1
53
+
54
+
55
+ def test_input_validation(dataset):
56
+ X, y = dataset
57
+ with pytest.raises(ValueError):
58
+ FeatureSelectionProblem(X, y[:-5])
59
+ with pytest.raises(ValueError):
60
+ FeatureSelectionProblem(X, y, alpha=1.5)
61
+ with pytest.raises(ValueError):
62
+ FeatureSelectionProblem(X[0], y)
@@ -0,0 +1,32 @@
1
+ import numpy as np
2
+ import pytest
3
+
4
+ from ikn_library.problems import Ackley, Problem, Rastrigin, Sphere
5
+
6
+
7
+ @pytest.mark.parametrize("cls", [Sphere, Rastrigin, Ackley])
8
+ def test_benchmark_optimum_is_zero(cls):
9
+ problem = cls(dimension=10)
10
+ assert problem.evaluate(np.zeros(10)) == pytest.approx(0.0, abs=1e-9)
11
+
12
+
13
+ def test_evaluate_rejects_wrong_shape():
14
+ problem = Sphere(dimension=5)
15
+ with pytest.raises(ValueError):
16
+ problem.evaluate(np.zeros(3))
17
+
18
+
19
+ def test_custom_problem():
20
+ class Linear(Problem):
21
+ def __init__(self):
22
+ super().__init__(dimension=3, lower=0.0, upper=1.0)
23
+
24
+ def _evaluate(self, x):
25
+ return np.sum(x)
26
+
27
+ assert Linear().evaluate([0.1, 0.2, 0.3]) == pytest.approx(0.6)
28
+
29
+
30
+ def test_invalid_bounds_raise():
31
+ with pytest.raises(ValueError):
32
+ Sphere(dimension=2, lower=1.0, upper=-1.0)
@@ -0,0 +1,44 @@
1
+ import numpy as np
2
+ import pytest
3
+
4
+ from ikn_library import OptimizationType, Task
5
+ from ikn_library.problems import Sphere
6
+
7
+
8
+ def test_requires_a_budget():
9
+ with pytest.raises(ValueError):
10
+ Task(problem=Sphere(dimension=2))
11
+
12
+
13
+ def test_counts_evals_and_tracks_best():
14
+ task = Task(problem=Sphere(dimension=2), max_evals=10)
15
+ task.eval(np.array([1.0, 1.0]))
16
+ task.eval(np.array([0.5, 0.5]))
17
+ assert task.evals == 2
18
+ assert task.best_fitness == pytest.approx(0.5)
19
+ np.testing.assert_allclose(task.best_x, [0.5, 0.5])
20
+
21
+
22
+ def test_stops_at_max_evals():
23
+ task = Task(problem=Sphere(dimension=2), max_evals=3)
24
+ for _ in range(5):
25
+ task.eval(np.array([1.0, 1.0]))
26
+ assert task.evals == 3
27
+ assert task.stopping_condition()
28
+
29
+
30
+ def test_repair_clips_to_bounds():
31
+ task = Task(problem=Sphere(dimension=2), max_evals=10)
32
+ repaired = task.repair(np.array([100.0, -100.0]))
33
+ np.testing.assert_allclose(repaired, [5.12, -5.12])
34
+
35
+
36
+ def test_maximization_result_sign():
37
+ task = Task(
38
+ problem=Sphere(dimension=2),
39
+ max_evals=10,
40
+ optimization_type=OptimizationType.MAXIMIZATION,
41
+ )
42
+ task.eval(np.array([2.0, 0.0]))
43
+ _, best = task.result()
44
+ assert best == pytest.approx(4.0)