ctrl-freak 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.
- ctrl_freak-0.1.0/LICENSE +21 -0
- ctrl_freak-0.1.0/PKG-INFO +238 -0
- ctrl_freak-0.1.0/README.md +210 -0
- ctrl_freak-0.1.0/pyproject.toml +127 -0
- ctrl_freak-0.1.0/src/ctrl_freak/__init__.py +100 -0
- ctrl_freak-0.1.0/src/ctrl_freak/algorithms/__init__.py +10 -0
- ctrl_freak-0.1.0/src/ctrl_freak/algorithms/ga.py +233 -0
- ctrl_freak-0.1.0/src/ctrl_freak/algorithms/nsga2.py +215 -0
- ctrl_freak-0.1.0/src/ctrl_freak/operators/__init__.py +15 -0
- ctrl_freak-0.1.0/src/ctrl_freak/operators/base.py +81 -0
- ctrl_freak-0.1.0/src/ctrl_freak/operators/selection.py +144 -0
- ctrl_freak-0.1.0/src/ctrl_freak/operators/standard.py +275 -0
- ctrl_freak-0.1.0/src/ctrl_freak/population.py +203 -0
- ctrl_freak-0.1.0/src/ctrl_freak/primitives/__init__.py +23 -0
- ctrl_freak-0.1.0/src/ctrl_freak/primitives/pareto.py +222 -0
- ctrl_freak-0.1.0/src/ctrl_freak/protocols.py +186 -0
- ctrl_freak-0.1.0/src/ctrl_freak/py.typed +0 -0
- ctrl_freak-0.1.0/src/ctrl_freak/registry.py +303 -0
- ctrl_freak-0.1.0/src/ctrl_freak/results.py +246 -0
- ctrl_freak-0.1.0/src/ctrl_freak/selection/__init__.py +13 -0
- ctrl_freak-0.1.0/src/ctrl_freak/selection/crowded.py +117 -0
- ctrl_freak-0.1.0/src/ctrl_freak/selection/roulette.py +115 -0
- ctrl_freak-0.1.0/src/ctrl_freak/selection/tournament.py +104 -0
- ctrl_freak-0.1.0/src/ctrl_freak/survival/__init__.py +13 -0
- ctrl_freak-0.1.0/src/ctrl_freak/survival/elitist.py +147 -0
- ctrl_freak-0.1.0/src/ctrl_freak/survival/nsga2.py +140 -0
- ctrl_freak-0.1.0/src/ctrl_freak/survival/truncation.py +104 -0
ctrl_freak-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nicolas Lazaro
|
|
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,238 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ctrl-freak
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pure-numpy genetic algorithm framework for single-objective (GA) and multi-objective (NSGA-II) optimization
|
|
5
|
+
Keywords: genetic-algorithm,evolutionary-algorithm,optimization,multi-objective-optimization,nsga-ii,nsga2,pareto,numpy
|
|
6
|
+
Author: Nicolas Lazaro
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Requires-Dist: numpy>=2.0
|
|
20
|
+
Requires-Dist: joblib>=1.3.0
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Project-URL: Changelog, https://github.com/hydrosolutions/ctrl-freak/blob/main/CHANGELOG.md
|
|
23
|
+
Project-URL: Documentation, https://hydrosolutions.github.io/ctrl-freak/
|
|
24
|
+
Project-URL: Homepage, https://github.com/hydrosolutions/ctrl-freak
|
|
25
|
+
Project-URL: Issues, https://github.com/hydrosolutions/ctrl-freak/issues
|
|
26
|
+
Project-URL: Repository, https://github.com/hydrosolutions/ctrl-freak
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# ctrl-freak
|
|
30
|
+
|
|
31
|
+
An extensible genetic algorithm framework for single and multi-objective optimization, built on pure numpy.
|
|
32
|
+
|
|
33
|
+
## Maintenance Status
|
|
34
|
+
|
|
35
|
+
🟢 **Active Development**
|
|
36
|
+
|
|
37
|
+
This repository is part of an ongoing project and actively maintained.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
uv add ctrl-freak
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Quick Start
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import numpy as np
|
|
51
|
+
from ctrl_freak import nsga2, ga
|
|
52
|
+
|
|
53
|
+
# === Multi-Objective: NSGA-II ===
|
|
54
|
+
def init(rng):
|
|
55
|
+
return rng.uniform(0, 1, size=5)
|
|
56
|
+
|
|
57
|
+
def evaluate_multi(x):
|
|
58
|
+
f1 = x[0]
|
|
59
|
+
f2 = 1 - np.sqrt(x[0]) + x[1:]@x[1:]
|
|
60
|
+
return np.array([f1, f2])
|
|
61
|
+
|
|
62
|
+
def crossover(p1, p2):
|
|
63
|
+
return (p1 + p2) / 2
|
|
64
|
+
|
|
65
|
+
def mutate(x):
|
|
66
|
+
return np.clip(x + np.random.normal(0, 0.1, size=x.shape), 0, 1)
|
|
67
|
+
|
|
68
|
+
result = nsga2(
|
|
69
|
+
init=init,
|
|
70
|
+
evaluate=evaluate_multi,
|
|
71
|
+
crossover=crossover,
|
|
72
|
+
mutate=mutate,
|
|
73
|
+
pop_size=100,
|
|
74
|
+
n_generations=50,
|
|
75
|
+
seed=42,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Extract Pareto front
|
|
79
|
+
pareto_front = result.pareto_front
|
|
80
|
+
print(f"Found {len(pareto_front)} Pareto-optimal solutions")
|
|
81
|
+
|
|
82
|
+
# === Single-Objective: Standard GA ===
|
|
83
|
+
def evaluate_single(x):
|
|
84
|
+
return float(np.sum(x ** 2)) # Sphere function
|
|
85
|
+
|
|
86
|
+
result = ga(
|
|
87
|
+
init=init,
|
|
88
|
+
evaluate=evaluate_single,
|
|
89
|
+
crossover=crossover,
|
|
90
|
+
mutate=mutate,
|
|
91
|
+
pop_size=100,
|
|
92
|
+
n_generations=100,
|
|
93
|
+
seed=42,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
print(f"Best fitness: {result.best[1]:.6f}")
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Documentation
|
|
100
|
+
|
|
101
|
+
- [API Usage Guide](docs/usage.md) — Installation, examples, working with results
|
|
102
|
+
- [User Contracts](docs/contracts.md) — Function signatures and responsibilities
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Design Philosophy
|
|
107
|
+
|
|
108
|
+
- **Pure numpy** for performance
|
|
109
|
+
- **Functional style** with immutable data structures
|
|
110
|
+
- **User thinks about individuals**, framework handles vectorization via `lift()`
|
|
111
|
+
- **Fail fast** with eager validation
|
|
112
|
+
- **Domain agnostic** — framework handles selection pressure, user handles constraints/bounds
|
|
113
|
+
- **Extensible** via pluggable selection and survival strategies
|
|
114
|
+
|
|
115
|
+
## Architecture
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
119
|
+
│ User Domain Layer │
|
|
120
|
+
│ init(), evaluate(), crossover(), mutate() │
|
|
121
|
+
│ (per-individual, user-defined) │
|
|
122
|
+
└─────────────────────────────────────────────────────────────┘
|
|
123
|
+
│ lift()
|
|
124
|
+
▼
|
|
125
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
126
|
+
│ Algorithm Layer │
|
|
127
|
+
│ ┌─────────────┐ ┌─────────────┐ │
|
|
128
|
+
│ │ nsga2() │ │ ga() │ │
|
|
129
|
+
│ │ multi-obj │ │ single-obj │ │
|
|
130
|
+
│ └─────────────┘ └─────────────┘ │
|
|
131
|
+
│ │ │ │
|
|
132
|
+
│ └────────┬─────────┘ │
|
|
133
|
+
│ ▼ │
|
|
134
|
+
│ ┌───────────────────────────────────────────────────────┐ │
|
|
135
|
+
│ │ Pluggable Strategies │ │
|
|
136
|
+
│ │ Selection: crowded, tournament, roulette │ │
|
|
137
|
+
│ │ Survival: nsga2, truncation, elitist │ │
|
|
138
|
+
│ └───────────────────────────────────────────────────────┘ │
|
|
139
|
+
└─────────────────────────────────────────────────────────────┘
|
|
140
|
+
│
|
|
141
|
+
▼
|
|
142
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
143
|
+
│ Primitives (pure functions) │
|
|
144
|
+
│ non_dominated_sort(), crowding_distance(), dominates() │
|
|
145
|
+
└─────────────────────────────────────────────────────────────┘
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## API Reference
|
|
149
|
+
|
|
150
|
+
### Algorithms
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
# Multi-objective optimization
|
|
154
|
+
nsga2(init, evaluate, crossover, mutate, pop_size, n_generations,
|
|
155
|
+
seed=None, callback=None, select='crowded', survive='nsga2',
|
|
156
|
+
n_workers=1) -> NSGA2Result
|
|
157
|
+
|
|
158
|
+
# Single-objective optimization
|
|
159
|
+
ga(init, evaluate, crossover, mutate, pop_size, n_generations,
|
|
160
|
+
seed=None, callback=None, select='tournament', survive='elitist',
|
|
161
|
+
n_workers=1) -> GAResult
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### User Function Contracts
|
|
165
|
+
|
|
166
|
+
| Function | Signature | Description |
|
|
167
|
+
|----------|-----------|-------------|
|
|
168
|
+
| `init` | `(rng) -> (n_vars,)` | Initialize one random individual |
|
|
169
|
+
| `evaluate` | `(n_vars,) -> (n_obj,)` or `float` | Compute objectives (minimization) |
|
|
170
|
+
| `crossover` | `(n_vars,), (n_vars,) -> (n_vars,)` | Combine two parents into one child |
|
|
171
|
+
| `mutate` | `(n_vars,) -> (n_vars,)` | Perturb an individual |
|
|
172
|
+
|
|
173
|
+
### Result Types
|
|
174
|
+
|
|
175
|
+
**NSGA2Result** — Multi-objective optimization result:
|
|
176
|
+
|
|
177
|
+
- `population: Population` — Final population
|
|
178
|
+
- `rank: np.ndarray` — Pareto front ranks `(n,)` where 0 = optimal
|
|
179
|
+
- `crowding_distance: np.ndarray` — Diversity measure `(n,)`
|
|
180
|
+
- `pareto_front: Population` — Property returning rank-0 individuals
|
|
181
|
+
- `generations: int` — Generations completed
|
|
182
|
+
- `evaluations: int` — Total evaluations
|
|
183
|
+
|
|
184
|
+
**GAResult** — Single-objective optimization result:
|
|
185
|
+
|
|
186
|
+
- `population: Population` — Final population
|
|
187
|
+
- `fitness: np.ndarray` — Fitness values `(n,)`
|
|
188
|
+
- `best: tuple[np.ndarray, float]` — Property returning (best_x, best_fitness)
|
|
189
|
+
- `generations: int` — Generations completed
|
|
190
|
+
- `evaluations: int` — Total evaluations
|
|
191
|
+
|
|
192
|
+
### Data Structures
|
|
193
|
+
|
|
194
|
+
**Population** — Immutable collection of solutions:
|
|
195
|
+
|
|
196
|
+
- `x: np.ndarray` — Decision variables `(n, n_vars)`
|
|
197
|
+
- `objectives: np.ndarray | None` — Objective values `(n, n_obj)`
|
|
198
|
+
|
|
199
|
+
### Selection Strategies
|
|
200
|
+
|
|
201
|
+
| Name | Function | Use Case |
|
|
202
|
+
|------|----------|----------|
|
|
203
|
+
| `'crowded'` | `crowded_tournament()` | NSGA-II (rank + crowding) |
|
|
204
|
+
| `'tournament'` | `fitness_tournament()` | GA (fitness-based) |
|
|
205
|
+
| `'roulette'` | `roulette_wheel()` | GA (fitness-proportionate) |
|
|
206
|
+
|
|
207
|
+
### Survival Strategies
|
|
208
|
+
|
|
209
|
+
| Name | Function | Use Case |
|
|
210
|
+
|------|----------|----------|
|
|
211
|
+
| `'nsga2'` | `nsga2_survival()` | NSGA-II (fronts + crowding) |
|
|
212
|
+
| `'truncation'` | `truncation_survival()` | Keep best k |
|
|
213
|
+
| `'elitist'` | `elitist_survival()` | Preserve elite parents |
|
|
214
|
+
|
|
215
|
+
### Primitives
|
|
216
|
+
|
|
217
|
+
| Function | Description |
|
|
218
|
+
|----------|-------------|
|
|
219
|
+
| `dominates(a, b)` | Check if `a` Pareto-dominates `b` |
|
|
220
|
+
| `dominates_matrix(objectives)` | Pairwise dominance matrix |
|
|
221
|
+
| `non_dominated_sort(objectives)` | Assign Pareto front ranks |
|
|
222
|
+
| `crowding_distance(front)` | Compute crowding distances for one front |
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Benchmarks
|
|
227
|
+
|
|
228
|
+
Tested against Pymoo and DEAP on ZDT test problems (100 pop, 250 generations, 10 seeds):
|
|
229
|
+
|
|
230
|
+
| Problem | ctrl-freak | Pymoo | DEAP |
|
|
231
|
+
|---------|------------|-------|------|
|
|
232
|
+
| ZDT1 | 0.8653 ± 0.0011 | 0.8241 ± 0.0255 | **0.8698 ± 0.0002** |
|
|
233
|
+
| ZDT2 | 0.5320 ± 0.0017 | 0.4764 ± 0.0182 | **0.5363 ± 0.0002** |
|
|
234
|
+
| ZDT3 | 1.3224 ± 0.0008 | 1.2836 ± 0.0123 | **1.3275 ± 0.0002** |
|
|
235
|
+
|
|
236
|
+
ctrl-freak matches DEAP-level hypervolume on ZDT1-3 with low variance. See [full benchmark results](benchmarks/README.md).
|
|
237
|
+
|
|
238
|
+
---
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# ctrl-freak
|
|
2
|
+
|
|
3
|
+
An extensible genetic algorithm framework for single and multi-objective optimization, built on pure numpy.
|
|
4
|
+
|
|
5
|
+
## Maintenance Status
|
|
6
|
+
|
|
7
|
+
🟢 **Active Development**
|
|
8
|
+
|
|
9
|
+
This repository is part of an ongoing project and actively maintained.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uv add ctrl-freak
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import numpy as np
|
|
23
|
+
from ctrl_freak import nsga2, ga
|
|
24
|
+
|
|
25
|
+
# === Multi-Objective: NSGA-II ===
|
|
26
|
+
def init(rng):
|
|
27
|
+
return rng.uniform(0, 1, size=5)
|
|
28
|
+
|
|
29
|
+
def evaluate_multi(x):
|
|
30
|
+
f1 = x[0]
|
|
31
|
+
f2 = 1 - np.sqrt(x[0]) + x[1:]@x[1:]
|
|
32
|
+
return np.array([f1, f2])
|
|
33
|
+
|
|
34
|
+
def crossover(p1, p2):
|
|
35
|
+
return (p1 + p2) / 2
|
|
36
|
+
|
|
37
|
+
def mutate(x):
|
|
38
|
+
return np.clip(x + np.random.normal(0, 0.1, size=x.shape), 0, 1)
|
|
39
|
+
|
|
40
|
+
result = nsga2(
|
|
41
|
+
init=init,
|
|
42
|
+
evaluate=evaluate_multi,
|
|
43
|
+
crossover=crossover,
|
|
44
|
+
mutate=mutate,
|
|
45
|
+
pop_size=100,
|
|
46
|
+
n_generations=50,
|
|
47
|
+
seed=42,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Extract Pareto front
|
|
51
|
+
pareto_front = result.pareto_front
|
|
52
|
+
print(f"Found {len(pareto_front)} Pareto-optimal solutions")
|
|
53
|
+
|
|
54
|
+
# === Single-Objective: Standard GA ===
|
|
55
|
+
def evaluate_single(x):
|
|
56
|
+
return float(np.sum(x ** 2)) # Sphere function
|
|
57
|
+
|
|
58
|
+
result = ga(
|
|
59
|
+
init=init,
|
|
60
|
+
evaluate=evaluate_single,
|
|
61
|
+
crossover=crossover,
|
|
62
|
+
mutate=mutate,
|
|
63
|
+
pop_size=100,
|
|
64
|
+
n_generations=100,
|
|
65
|
+
seed=42,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
print(f"Best fitness: {result.best[1]:.6f}")
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Documentation
|
|
72
|
+
|
|
73
|
+
- [API Usage Guide](docs/usage.md) — Installation, examples, working with results
|
|
74
|
+
- [User Contracts](docs/contracts.md) — Function signatures and responsibilities
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Design Philosophy
|
|
79
|
+
|
|
80
|
+
- **Pure numpy** for performance
|
|
81
|
+
- **Functional style** with immutable data structures
|
|
82
|
+
- **User thinks about individuals**, framework handles vectorization via `lift()`
|
|
83
|
+
- **Fail fast** with eager validation
|
|
84
|
+
- **Domain agnostic** — framework handles selection pressure, user handles constraints/bounds
|
|
85
|
+
- **Extensible** via pluggable selection and survival strategies
|
|
86
|
+
|
|
87
|
+
## Architecture
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
91
|
+
│ User Domain Layer │
|
|
92
|
+
│ init(), evaluate(), crossover(), mutate() │
|
|
93
|
+
│ (per-individual, user-defined) │
|
|
94
|
+
└─────────────────────────────────────────────────────────────┘
|
|
95
|
+
│ lift()
|
|
96
|
+
▼
|
|
97
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
98
|
+
│ Algorithm Layer │
|
|
99
|
+
│ ┌─────────────┐ ┌─────────────┐ │
|
|
100
|
+
│ │ nsga2() │ │ ga() │ │
|
|
101
|
+
│ │ multi-obj │ │ single-obj │ │
|
|
102
|
+
│ └─────────────┘ └─────────────┘ │
|
|
103
|
+
│ │ │ │
|
|
104
|
+
│ └────────┬─────────┘ │
|
|
105
|
+
│ ▼ │
|
|
106
|
+
│ ┌───────────────────────────────────────────────────────┐ │
|
|
107
|
+
│ │ Pluggable Strategies │ │
|
|
108
|
+
│ │ Selection: crowded, tournament, roulette │ │
|
|
109
|
+
│ │ Survival: nsga2, truncation, elitist │ │
|
|
110
|
+
│ └───────────────────────────────────────────────────────┘ │
|
|
111
|
+
└─────────────────────────────────────────────────────────────┘
|
|
112
|
+
│
|
|
113
|
+
▼
|
|
114
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
115
|
+
│ Primitives (pure functions) │
|
|
116
|
+
│ non_dominated_sort(), crowding_distance(), dominates() │
|
|
117
|
+
└─────────────────────────────────────────────────────────────┘
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## API Reference
|
|
121
|
+
|
|
122
|
+
### Algorithms
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
# Multi-objective optimization
|
|
126
|
+
nsga2(init, evaluate, crossover, mutate, pop_size, n_generations,
|
|
127
|
+
seed=None, callback=None, select='crowded', survive='nsga2',
|
|
128
|
+
n_workers=1) -> NSGA2Result
|
|
129
|
+
|
|
130
|
+
# Single-objective optimization
|
|
131
|
+
ga(init, evaluate, crossover, mutate, pop_size, n_generations,
|
|
132
|
+
seed=None, callback=None, select='tournament', survive='elitist',
|
|
133
|
+
n_workers=1) -> GAResult
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### User Function Contracts
|
|
137
|
+
|
|
138
|
+
| Function | Signature | Description |
|
|
139
|
+
|----------|-----------|-------------|
|
|
140
|
+
| `init` | `(rng) -> (n_vars,)` | Initialize one random individual |
|
|
141
|
+
| `evaluate` | `(n_vars,) -> (n_obj,)` or `float` | Compute objectives (minimization) |
|
|
142
|
+
| `crossover` | `(n_vars,), (n_vars,) -> (n_vars,)` | Combine two parents into one child |
|
|
143
|
+
| `mutate` | `(n_vars,) -> (n_vars,)` | Perturb an individual |
|
|
144
|
+
|
|
145
|
+
### Result Types
|
|
146
|
+
|
|
147
|
+
**NSGA2Result** — Multi-objective optimization result:
|
|
148
|
+
|
|
149
|
+
- `population: Population` — Final population
|
|
150
|
+
- `rank: np.ndarray` — Pareto front ranks `(n,)` where 0 = optimal
|
|
151
|
+
- `crowding_distance: np.ndarray` — Diversity measure `(n,)`
|
|
152
|
+
- `pareto_front: Population` — Property returning rank-0 individuals
|
|
153
|
+
- `generations: int` — Generations completed
|
|
154
|
+
- `evaluations: int` — Total evaluations
|
|
155
|
+
|
|
156
|
+
**GAResult** — Single-objective optimization result:
|
|
157
|
+
|
|
158
|
+
- `population: Population` — Final population
|
|
159
|
+
- `fitness: np.ndarray` — Fitness values `(n,)`
|
|
160
|
+
- `best: tuple[np.ndarray, float]` — Property returning (best_x, best_fitness)
|
|
161
|
+
- `generations: int` — Generations completed
|
|
162
|
+
- `evaluations: int` — Total evaluations
|
|
163
|
+
|
|
164
|
+
### Data Structures
|
|
165
|
+
|
|
166
|
+
**Population** — Immutable collection of solutions:
|
|
167
|
+
|
|
168
|
+
- `x: np.ndarray` — Decision variables `(n, n_vars)`
|
|
169
|
+
- `objectives: np.ndarray | None` — Objective values `(n, n_obj)`
|
|
170
|
+
|
|
171
|
+
### Selection Strategies
|
|
172
|
+
|
|
173
|
+
| Name | Function | Use Case |
|
|
174
|
+
|------|----------|----------|
|
|
175
|
+
| `'crowded'` | `crowded_tournament()` | NSGA-II (rank + crowding) |
|
|
176
|
+
| `'tournament'` | `fitness_tournament()` | GA (fitness-based) |
|
|
177
|
+
| `'roulette'` | `roulette_wheel()` | GA (fitness-proportionate) |
|
|
178
|
+
|
|
179
|
+
### Survival Strategies
|
|
180
|
+
|
|
181
|
+
| Name | Function | Use Case |
|
|
182
|
+
|------|----------|----------|
|
|
183
|
+
| `'nsga2'` | `nsga2_survival()` | NSGA-II (fronts + crowding) |
|
|
184
|
+
| `'truncation'` | `truncation_survival()` | Keep best k |
|
|
185
|
+
| `'elitist'` | `elitist_survival()` | Preserve elite parents |
|
|
186
|
+
|
|
187
|
+
### Primitives
|
|
188
|
+
|
|
189
|
+
| Function | Description |
|
|
190
|
+
|----------|-------------|
|
|
191
|
+
| `dominates(a, b)` | Check if `a` Pareto-dominates `b` |
|
|
192
|
+
| `dominates_matrix(objectives)` | Pairwise dominance matrix |
|
|
193
|
+
| `non_dominated_sort(objectives)` | Assign Pareto front ranks |
|
|
194
|
+
| `crowding_distance(front)` | Compute crowding distances for one front |
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Benchmarks
|
|
199
|
+
|
|
200
|
+
Tested against Pymoo and DEAP on ZDT test problems (100 pop, 250 generations, 10 seeds):
|
|
201
|
+
|
|
202
|
+
| Problem | ctrl-freak | Pymoo | DEAP |
|
|
203
|
+
|---------|------------|-------|------|
|
|
204
|
+
| ZDT1 | 0.8653 ± 0.0011 | 0.8241 ± 0.0255 | **0.8698 ± 0.0002** |
|
|
205
|
+
| ZDT2 | 0.5320 ± 0.0017 | 0.4764 ± 0.0182 | **0.5363 ± 0.0002** |
|
|
206
|
+
| ZDT3 | 1.3224 ± 0.0008 | 1.2836 ± 0.0123 | **1.3275 ± 0.0002** |
|
|
207
|
+
|
|
208
|
+
ctrl-freak matches DEAP-level hypervolume on ZDT1-3 with low variance. See [full benchmark results](benchmarks/README.md).
|
|
209
|
+
|
|
210
|
+
---
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ctrl-freak"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Pure-numpy genetic algorithm framework for single-objective (GA) and multi-objective (NSGA-II) optimization"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [{ name = "Nicolas Lazaro" }]
|
|
10
|
+
keywords = [
|
|
11
|
+
"genetic-algorithm",
|
|
12
|
+
"evolutionary-algorithm",
|
|
13
|
+
"optimization",
|
|
14
|
+
"multi-objective-optimization",
|
|
15
|
+
"nsga-ii",
|
|
16
|
+
"nsga2",
|
|
17
|
+
"pareto",
|
|
18
|
+
"numpy",
|
|
19
|
+
]
|
|
20
|
+
classifiers = [
|
|
21
|
+
"Development Status :: 4 - Beta",
|
|
22
|
+
"Intended Audience :: Science/Research",
|
|
23
|
+
"Intended Audience :: Developers",
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Programming Language :: Python :: 3.13",
|
|
28
|
+
"Topic :: Scientific/Engineering",
|
|
29
|
+
"Topic :: Scientific/Engineering :: Mathematics",
|
|
30
|
+
"Operating System :: OS Independent",
|
|
31
|
+
]
|
|
32
|
+
dependencies = [
|
|
33
|
+
"numpy>=2.0",
|
|
34
|
+
"joblib>=1.3.0",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/hydrosolutions/ctrl-freak"
|
|
39
|
+
Repository = "https://github.com/hydrosolutions/ctrl-freak"
|
|
40
|
+
Documentation = "https://hydrosolutions.github.io/ctrl-freak/"
|
|
41
|
+
Changelog = "https://github.com/hydrosolutions/ctrl-freak/blob/main/CHANGELOG.md"
|
|
42
|
+
Issues = "https://github.com/hydrosolutions/ctrl-freak/issues"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
[build-system]
|
|
46
|
+
requires = ["uv_build>=0.8.14,<0.9.0"]
|
|
47
|
+
build-backend = "uv_build"
|
|
48
|
+
|
|
49
|
+
[dependency-groups]
|
|
50
|
+
dev = [
|
|
51
|
+
"deap>=1.4.3",
|
|
52
|
+
"pymoo>=0.6.1.6",
|
|
53
|
+
"pytest>=8.3", # tests
|
|
54
|
+
"pytest-cov>=6.2.1",
|
|
55
|
+
"ruff>=0.12", # linter + formatter
|
|
56
|
+
"ty>=0.0.53",
|
|
57
|
+
]
|
|
58
|
+
docs = [
|
|
59
|
+
"mkdocs-material>=9.5",
|
|
60
|
+
"mkdocstrings[python]>=0.26",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
# -----------------------
|
|
64
|
+
# Ruff (lint + format)
|
|
65
|
+
# -----------------------
|
|
66
|
+
[tool.ruff]
|
|
67
|
+
line-length = 120
|
|
68
|
+
target-version = "py311"
|
|
69
|
+
extend-exclude = ["notebooks/", "experiments/"]
|
|
70
|
+
|
|
71
|
+
[tool.ruff.lint]
|
|
72
|
+
# Errors, warnings, naming, imports, modern py, bugbear, comprehensions, simplify
|
|
73
|
+
select = ["E", "F", "W", "N", "I", "UP", "B", "C4", "SIM"]
|
|
74
|
+
ignore = [
|
|
75
|
+
"E501", # let formatter handle wrapping
|
|
76
|
+
"N803", # allow X, Y for ML-style params
|
|
77
|
+
"N806", # allow X, Y for local vars
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
[tool.ruff.format]
|
|
81
|
+
quote-style = "double"
|
|
82
|
+
indent-style = "space"
|
|
83
|
+
|
|
84
|
+
# -----------------------
|
|
85
|
+
# ty (fast static type checker)
|
|
86
|
+
# -----------------------
|
|
87
|
+
[tool.ty]
|
|
88
|
+
[tool.ty.rules]
|
|
89
|
+
possibly-unresolved-reference = "warn"
|
|
90
|
+
|
|
91
|
+
# -----------------------
|
|
92
|
+
# Pytest
|
|
93
|
+
# -----------------------
|
|
94
|
+
[tool.pytest.ini_options]
|
|
95
|
+
pythonpath = ["src"]
|
|
96
|
+
testpaths = ["tests"]
|
|
97
|
+
python_files = ["test_*.py", "*_test.py"]
|
|
98
|
+
python_classes = ["Test*"]
|
|
99
|
+
python_functions = ["test_*"]
|
|
100
|
+
addopts = [
|
|
101
|
+
"--strict-markers",
|
|
102
|
+
"--strict-config",
|
|
103
|
+
"-ra",
|
|
104
|
+
"--ignore=notebooks/",
|
|
105
|
+
"--ignore=experiments/",
|
|
106
|
+
"--cov=ctrl_freak",
|
|
107
|
+
"--cov-report=term-missing",
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
# -----------------------
|
|
111
|
+
# Coverage
|
|
112
|
+
# -----------------------
|
|
113
|
+
[tool.coverage.run]
|
|
114
|
+
source = ["ctrl_freak"]
|
|
115
|
+
branch = false
|
|
116
|
+
|
|
117
|
+
[tool.coverage.report]
|
|
118
|
+
fail_under = 90
|
|
119
|
+
show_missing = true
|
|
120
|
+
exclude_lines = [
|
|
121
|
+
"pragma: no cover",
|
|
122
|
+
"if TYPE_CHECKING:",
|
|
123
|
+
"raise NotImplementedError",
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
[tool.uv.sources]
|
|
127
|
+
# transfer-learning-publication = { git = "https://github.com/CooperBigFoot/transfer-learning-publication.git", rev = "main" }
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""ctrl-freak: Extensible Genetic Algorithm Framework.
|
|
2
|
+
|
|
3
|
+
A pure numpy implementation of genetic algorithms including NSGA-II for
|
|
4
|
+
multi-objective optimization and standard GA for single-objective optimization.
|
|
5
|
+
|
|
6
|
+
Example (multi-objective with NSGA-II):
|
|
7
|
+
>>> from ctrl_freak import nsga2, Population
|
|
8
|
+
>>> import numpy as np
|
|
9
|
+
>>> def init(rng): return rng.uniform(0, 1, size=3)
|
|
10
|
+
>>> def evaluate(x): return np.array([x.sum(), (1 - x).sum()])
|
|
11
|
+
>>> def crossover(p1, p2): return (p1 + p2) / 2
|
|
12
|
+
>>> def mutate(x): return np.clip(x + 0.01, 0, 1)
|
|
13
|
+
>>> result = nsga2(init, evaluate, crossover, mutate, pop_size=10, n_generations=5, seed=42)
|
|
14
|
+
>>> len(result.population)
|
|
15
|
+
10
|
|
16
|
+
|
|
17
|
+
Example (single-objective with GA):
|
|
18
|
+
>>> from ctrl_freak import ga
|
|
19
|
+
>>> import numpy as np
|
|
20
|
+
>>> def init(rng): return rng.uniform(0, 1, size=3)
|
|
21
|
+
>>> def evaluate(x): return x.sum() # Single objective
|
|
22
|
+
>>> def crossover(p1, p2): return (p1 + p2) / 2
|
|
23
|
+
>>> def mutate(x): return np.clip(x + 0.01, 0, 1)
|
|
24
|
+
>>> result = ga(init, evaluate, crossover, mutate, pop_size=10, n_generations=5, seed=42)
|
|
25
|
+
>>> len(result.population)
|
|
26
|
+
10
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from importlib.metadata import version
|
|
30
|
+
|
|
31
|
+
from ctrl_freak.algorithms import ga, nsga2
|
|
32
|
+
from ctrl_freak.operators import (
|
|
33
|
+
create_offspring,
|
|
34
|
+
lift,
|
|
35
|
+
lift_parallel,
|
|
36
|
+
polynomial_mutation,
|
|
37
|
+
sbx_crossover,
|
|
38
|
+
select_parents,
|
|
39
|
+
)
|
|
40
|
+
from ctrl_freak.population import IndividualView, Population
|
|
41
|
+
from ctrl_freak.primitives import (
|
|
42
|
+
crowding_distance,
|
|
43
|
+
dominates,
|
|
44
|
+
dominates_matrix,
|
|
45
|
+
non_dominated_sort,
|
|
46
|
+
)
|
|
47
|
+
from ctrl_freak.protocols import ParentSelector, SurvivorSelector
|
|
48
|
+
from ctrl_freak.registry import (
|
|
49
|
+
SelectionRegistry,
|
|
50
|
+
SurvivalRegistry,
|
|
51
|
+
list_selections,
|
|
52
|
+
list_survivals,
|
|
53
|
+
)
|
|
54
|
+
from ctrl_freak.results import GAResult, NSGA2Result
|
|
55
|
+
from ctrl_freak.selection import crowded_tournament, fitness_tournament, roulette_wheel
|
|
56
|
+
from ctrl_freak.survival import elitist_survival, nsga2_survival, truncation_survival
|
|
57
|
+
|
|
58
|
+
__version__ = version("ctrl-freak")
|
|
59
|
+
|
|
60
|
+
__all__ = [
|
|
61
|
+
# Algorithms
|
|
62
|
+
"nsga2",
|
|
63
|
+
"ga",
|
|
64
|
+
# Selection strategies
|
|
65
|
+
"crowded_tournament",
|
|
66
|
+
"fitness_tournament",
|
|
67
|
+
"roulette_wheel",
|
|
68
|
+
# Survival strategies
|
|
69
|
+
"nsga2_survival",
|
|
70
|
+
"truncation_survival",
|
|
71
|
+
"elitist_survival",
|
|
72
|
+
# Genetic operators
|
|
73
|
+
"lift",
|
|
74
|
+
"lift_parallel",
|
|
75
|
+
"select_parents",
|
|
76
|
+
"create_offspring",
|
|
77
|
+
"sbx_crossover",
|
|
78
|
+
"polynomial_mutation",
|
|
79
|
+
# Primitives
|
|
80
|
+
"dominates",
|
|
81
|
+
"dominates_matrix",
|
|
82
|
+
"non_dominated_sort",
|
|
83
|
+
"crowding_distance",
|
|
84
|
+
# Registry system
|
|
85
|
+
"SelectionRegistry",
|
|
86
|
+
"SurvivalRegistry",
|
|
87
|
+
"list_selections",
|
|
88
|
+
"list_survivals",
|
|
89
|
+
# Protocols
|
|
90
|
+
"ParentSelector",
|
|
91
|
+
"SurvivorSelector",
|
|
92
|
+
# Data structures
|
|
93
|
+
"Population",
|
|
94
|
+
"IndividualView",
|
|
95
|
+
# Result types
|
|
96
|
+
"NSGA2Result",
|
|
97
|
+
"GAResult",
|
|
98
|
+
# Version
|
|
99
|
+
"__version__",
|
|
100
|
+
]
|