bioopt 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bioopt/__init__.py +37 -0
- bioopt/adapters/__init__.py +6 -0
- bioopt/adapters/pytorch.py +89 -0
- bioopt/adapters/tensorflow.py +263 -0
- bioopt/base.py +251 -0
- bioopt/swarm/__init__.py +31 -0
- bioopt/swarm/abc.py +147 -0
- bioopt/swarm/aco.py +100 -0
- bioopt/swarm/fa.py +70 -0
- bioopt/swarm/gwo.py +84 -0
- bioopt/swarm/pso.py +123 -0
- bioopt/swarm/woa.py +74 -0
- bioopt/utils.py +230 -0
- bioopt-0.1.0.dist-info/METADATA +283 -0
- bioopt-0.1.0.dist-info/RECORD +18 -0
- bioopt-0.1.0.dist-info/WHEEL +5 -0
- bioopt-0.1.0.dist-info/licenses/LICENSE +201 -0
- bioopt-0.1.0.dist-info/top_level.txt +1 -0
bioopt/__init__.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""bioopt - Bio-inspired optimization algorithms.
|
|
2
|
+
|
|
3
|
+
A collection of nature-inspired optimization algorithms for
|
|
4
|
+
machine learning and scientific computing.
|
|
5
|
+
|
|
6
|
+
Categories:
|
|
7
|
+
- swarm: Swarm intelligence algorithms (PSO, ACO, ABC, GWO, FA, WOA)
|
|
8
|
+
- evolutionary: Evolutionary algorithms (coming soon)
|
|
9
|
+
- physics: Physics-based algorithms (coming soon)
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
>>> from bioopt.swarm import PSO
|
|
13
|
+
>>> pso = PSO(n_agents=30, bounds=[(-5, 5)] * 10)
|
|
14
|
+
>>> best_pos, best_fit = pso.optimize(objective_fn, iterations=100)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
__version__ = "0.1.0"
|
|
18
|
+
__author__ = "Rehan Guha"
|
|
19
|
+
|
|
20
|
+
from bioopt.base import BaseOptimizer, BoundsError, OptimizationError
|
|
21
|
+
from bioopt.utils import (
|
|
22
|
+
BenchmarkFunctions,
|
|
23
|
+
flatten_params,
|
|
24
|
+
make_bounds_uniform,
|
|
25
|
+
unflatten_params,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"__version__",
|
|
30
|
+
"BaseOptimizer",
|
|
31
|
+
"BoundsError",
|
|
32
|
+
"OptimizationError",
|
|
33
|
+
"BenchmarkFunctions",
|
|
34
|
+
"flatten_params",
|
|
35
|
+
"unflatten_params",
|
|
36
|
+
"make_bounds_uniform",
|
|
37
|
+
]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""PyTorch adapter for biopt optimization algorithms."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
import torch
|
|
9
|
+
import torch.nn as nn
|
|
10
|
+
from torch.utils.data import DataLoader
|
|
11
|
+
except ImportError:
|
|
12
|
+
raise ImportError("PyTorch is required. Install with: pip install bioopt[pytorch]")
|
|
13
|
+
|
|
14
|
+
from bioopt.base import BaseOptimizer
|
|
15
|
+
from bioopt.utils import flatten_params, unflatten_params
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PyTorchAdapter:
|
|
19
|
+
"""Adapter for using biopt optimizers with PyTorch models."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, model: nn.Module, device: Optional[str] = None):
|
|
22
|
+
self.model = model
|
|
23
|
+
self.device = device or next(model.parameters()).device
|
|
24
|
+
self.param_shapes: Dict[str, Tuple[int, ...]] = {}
|
|
25
|
+
self.param_names: List[str] = []
|
|
26
|
+
for name, param in model.named_parameters():
|
|
27
|
+
self.param_shapes[name] = param.shape
|
|
28
|
+
self.param_names.append(name)
|
|
29
|
+
self.n_params = sum(int(np.prod(s)) for s in self.param_shapes.values())
|
|
30
|
+
|
|
31
|
+
def flat_to_model(self, flat_weights: np.ndarray) -> None:
|
|
32
|
+
if len(flat_weights) != self.n_params:
|
|
33
|
+
raise ValueError(f"Expected {self.n_params} parameters, got {len(flat_weights)}")
|
|
34
|
+
shapes = [self.param_shapes[name] for name in self.param_names]
|
|
35
|
+
param_list = unflatten_params(flat_weights, shapes)
|
|
36
|
+
with torch.no_grad():
|
|
37
|
+
for i, (name, param) in enumerate(self.model.named_parameters()):
|
|
38
|
+
param.copy_(torch.from_numpy(param_list[i]).to(dtype=param.dtype, device=self.device))
|
|
39
|
+
|
|
40
|
+
def model_to_flat(self) -> np.ndarray:
|
|
41
|
+
params = [p.detach().cpu().numpy() for _, p in self.model.named_parameters()]
|
|
42
|
+
return flatten_params(params)
|
|
43
|
+
|
|
44
|
+
def get_bounds(self, default_low: float = -5.0, default_high: float = 5.0,
|
|
45
|
+
per_param_bounds: Optional[Dict[str, Tuple[float, float]]] = None) -> List[Tuple[float, float]]:
|
|
46
|
+
bounds = []
|
|
47
|
+
for name in self.param_names:
|
|
48
|
+
n_elements = int(np.prod(self.param_shapes[name]))
|
|
49
|
+
if per_param_bounds and name in per_param_bounds:
|
|
50
|
+
low, high = per_param_bounds[name]
|
|
51
|
+
else:
|
|
52
|
+
low, high = default_low, default_high
|
|
53
|
+
bounds.extend([(low, high)] * n_elements)
|
|
54
|
+
return bounds
|
|
55
|
+
|
|
56
|
+
def evaluate(self, flat_weights: np.ndarray, loss_fn: Callable,
|
|
57
|
+
dataloader: Optional[DataLoader] = None,
|
|
58
|
+
inputs: Optional[torch.Tensor] = None,
|
|
59
|
+
targets: Optional[torch.Tensor] = None, **kwargs) -> float:
|
|
60
|
+
self.flat_to_model(flat_weights)
|
|
61
|
+
self.model.eval()
|
|
62
|
+
with torch.no_grad():
|
|
63
|
+
if dataloader is not None:
|
|
64
|
+
total_loss, n_batches = 0.0, 0
|
|
65
|
+
for batch_inputs, batch_targets in dataloader:
|
|
66
|
+
outputs = self.model(batch_inputs.to(self.device))
|
|
67
|
+
loss = loss_fn(outputs, batch_targets.to(self.device), **kwargs)
|
|
68
|
+
total_loss += loss.item() if isinstance(loss, torch.Tensor) else float(loss)
|
|
69
|
+
n_batches += 1
|
|
70
|
+
return total_loss / n_batches if n_batches > 0 else float("inf")
|
|
71
|
+
elif inputs is not None and targets is not None:
|
|
72
|
+
outputs = self.model(inputs.to(self.device))
|
|
73
|
+
loss = loss_fn(outputs, targets.to(self.device), **kwargs)
|
|
74
|
+
return loss.item() if isinstance(loss, torch.Tensor) else float(loss)
|
|
75
|
+
else:
|
|
76
|
+
loss = loss_fn(self.model, dataloader, **kwargs)
|
|
77
|
+
return loss.item() if isinstance(loss, torch.Tensor) else float(loss)
|
|
78
|
+
|
|
79
|
+
def optimize(self, optimizer: BaseOptimizer, loss_fn: Callable,
|
|
80
|
+
dataloader: Optional[DataLoader] = None,
|
|
81
|
+
inputs: Optional[torch.Tensor] = None,
|
|
82
|
+
targets: Optional[torch.Tensor] = None,
|
|
83
|
+
iterations: int = 100, verbose: bool = False,
|
|
84
|
+
callback: Optional[Callable] = None, **kwargs) -> Tuple[np.ndarray, float]:
|
|
85
|
+
def objective_fn(flat_weights: np.ndarray) -> float:
|
|
86
|
+
return self.evaluate(flat_weights, loss_fn, dataloader, inputs, targets)
|
|
87
|
+
best_weights, best_loss = optimizer.optimize(objective_fn, iterations=iterations, verbose=verbose, callback=callback, **kwargs)
|
|
88
|
+
self.flat_to_model(best_weights)
|
|
89
|
+
return best_weights, best_loss
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""TensorFlow adapter for bioopt optimization algorithms.
|
|
2
|
+
|
|
3
|
+
Provides integration with TensorFlow/Keras models by converting model parameters
|
|
4
|
+
to flat vectors and back, enabling gradient-free optimization of neural networks.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
>>> from bioopt.swarm import PSO
|
|
8
|
+
>>> from bioopt.adapters.tensorflow import TensorFlowAdapter
|
|
9
|
+
>>>
|
|
10
|
+
>>> model = tf.keras.Sequential([...])
|
|
11
|
+
>>> adapter = TensorFlowAdapter(model)
|
|
12
|
+
>>>
|
|
13
|
+
>>> pso = PSO(n_agents=30, bounds=adapter.get_bounds())
|
|
14
|
+
>>> best_weights, best_loss = adapter.optimize(
|
|
15
|
+
... pso, loss_fn, dataset, iterations=50
|
|
16
|
+
... )
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
import tensorflow as tf
|
|
25
|
+
from tensorflow import keras
|
|
26
|
+
except ImportError:
|
|
27
|
+
raise ImportError(
|
|
28
|
+
"TensorFlow is required for this module. Install with: pip install bioopt[tensorflow]"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
from bioopt.base import BaseOptimizer
|
|
32
|
+
from bioopt.utils import flatten_params, unflatten_params
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TensorFlowAdapter:
|
|
36
|
+
"""Adapter for using biopt optimizers with TensorFlow/Keras models.
|
|
37
|
+
|
|
38
|
+
Converts TensorFlow model parameters to/from flat numpy arrays
|
|
39
|
+
so that swarm intelligence algorithms can optimize them.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
model : tf.keras.Model
|
|
44
|
+
TensorFlow/Keras model to optimize.
|
|
45
|
+
|
|
46
|
+
Attributes
|
|
47
|
+
----------
|
|
48
|
+
model : tf.keras.Model
|
|
49
|
+
Reference to the model being optimized.
|
|
50
|
+
param_shapes : dict
|
|
51
|
+
Dictionary mapping parameter names to their shapes.
|
|
52
|
+
n_params : int
|
|
53
|
+
Total number of parameters in the model.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, model: keras.Model):
|
|
57
|
+
self.model = model
|
|
58
|
+
self.param_shapes: Dict[str, Tuple[int, ...]] = {}
|
|
59
|
+
self.param_names: List[str] = []
|
|
60
|
+
|
|
61
|
+
for var in model.trainable_variables:
|
|
62
|
+
self.param_shapes[var.name] = var.shape
|
|
63
|
+
self.param_names.append(var.name)
|
|
64
|
+
|
|
65
|
+
self.n_params = sum(int(np.prod(s)) for s in self.param_shapes.values())
|
|
66
|
+
|
|
67
|
+
def get_weights_list(self) -> List[np.ndarray]:
|
|
68
|
+
"""Get model weights as a list of numpy arrays."""
|
|
69
|
+
return [w.numpy() for w in self.model.trainable_variables]
|
|
70
|
+
|
|
71
|
+
def flat_to_model(self, flat_weights: np.ndarray) -> None:
|
|
72
|
+
"""Set model parameters from a flat array.
|
|
73
|
+
|
|
74
|
+
Parameters
|
|
75
|
+
----------
|
|
76
|
+
flat_weights : ndarray of shape (n_params,)
|
|
77
|
+
Flattened parameter vector.
|
|
78
|
+
"""
|
|
79
|
+
if len(flat_weights) != self.n_params:
|
|
80
|
+
raise ValueError(
|
|
81
|
+
f"Expected {self.n_params} parameters, got {len(flat_weights)}"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
shapes = [self.param_shapes[name] for name in self.param_names]
|
|
85
|
+
param_list = unflatten_params(flat_weights, shapes)
|
|
86
|
+
|
|
87
|
+
for i, var in enumerate(self.model.trainable_variables):
|
|
88
|
+
var.assign(tf.constant(param_list[i], dtype=var.dtype))
|
|
89
|
+
|
|
90
|
+
def model_to_flat(self) -> np.ndarray:
|
|
91
|
+
"""Get current model parameters as a flat array.
|
|
92
|
+
|
|
93
|
+
Returns
|
|
94
|
+
-------
|
|
95
|
+
flat : ndarray of shape (n_params,)
|
|
96
|
+
Flattened parameter vector.
|
|
97
|
+
"""
|
|
98
|
+
weights = self.get_weights_list()
|
|
99
|
+
return flatten_params(weights)
|
|
100
|
+
|
|
101
|
+
def get_bounds(
|
|
102
|
+
self,
|
|
103
|
+
default_low: float = -5.0,
|
|
104
|
+
default_high: float = 5.0,
|
|
105
|
+
per_param_bounds: Optional[Dict[str, Tuple[float, float]]] = None,
|
|
106
|
+
) -> List[Tuple[float, float]]:
|
|
107
|
+
"""Get bounds for all model parameters.
|
|
108
|
+
|
|
109
|
+
Parameters
|
|
110
|
+
----------
|
|
111
|
+
default_low : float
|
|
112
|
+
Default lower bound.
|
|
113
|
+
default_high : float
|
|
114
|
+
Default upper bound.
|
|
115
|
+
per_param_bounds : dict, optional
|
|
116
|
+
Per-parameter bounds as {name: (low, high)}.
|
|
117
|
+
Parameters not specified use default bounds.
|
|
118
|
+
|
|
119
|
+
Returns
|
|
120
|
+
-------
|
|
121
|
+
bounds : list of tuples
|
|
122
|
+
Bounds for each parameter in flat array.
|
|
123
|
+
"""
|
|
124
|
+
bounds = []
|
|
125
|
+
for name in self.param_names:
|
|
126
|
+
shape = self.param_shapes[name]
|
|
127
|
+
n_elements = int(np.prod(shape))
|
|
128
|
+
|
|
129
|
+
if per_param_bounds and name in per_param_bounds:
|
|
130
|
+
low, high = per_param_bounds[name]
|
|
131
|
+
else:
|
|
132
|
+
low, high = default_low, default_high
|
|
133
|
+
|
|
134
|
+
bounds.extend([(low, high)] * n_elements)
|
|
135
|
+
|
|
136
|
+
return bounds
|
|
137
|
+
|
|
138
|
+
def evaluate(
|
|
139
|
+
self,
|
|
140
|
+
flat_weights: np.ndarray,
|
|
141
|
+
loss_fn: Callable,
|
|
142
|
+
dataset: Optional[tf.data.Dataset] = None,
|
|
143
|
+
inputs: Optional[tf.Tensor] = None,
|
|
144
|
+
targets: Optional[tf.Tensor] = None,
|
|
145
|
+
**kwargs
|
|
146
|
+
) -> float:
|
|
147
|
+
"""Evaluate the model with given weights on a loss function.
|
|
148
|
+
|
|
149
|
+
Parameters
|
|
150
|
+
----------
|
|
151
|
+
flat_weights : ndarray
|
|
152
|
+
Flattened parameter vector.
|
|
153
|
+
loss_fn : callable
|
|
154
|
+
Loss function. Can be:
|
|
155
|
+
- A function that takes (outputs, targets) and returns loss
|
|
156
|
+
- A function that takes (model, dataset) and returns loss
|
|
157
|
+
dataset : tf.data.Dataset, optional
|
|
158
|
+
Dataset for evaluation.
|
|
159
|
+
inputs : tf.Tensor, optional
|
|
160
|
+
Input tensor for direct evaluation.
|
|
161
|
+
targets : tf.Tensor, optional
|
|
162
|
+
Target tensor for direct evaluation.
|
|
163
|
+
**kwargs : dict
|
|
164
|
+
Additional arguments passed to loss_fn.
|
|
165
|
+
|
|
166
|
+
Returns
|
|
167
|
+
-------
|
|
168
|
+
loss : float
|
|
169
|
+
Computed loss value.
|
|
170
|
+
"""
|
|
171
|
+
self.flat_to_model(flat_weights)
|
|
172
|
+
|
|
173
|
+
if dataset is not None:
|
|
174
|
+
total_loss = 0.0
|
|
175
|
+
n_batches = 0
|
|
176
|
+
for batch_inputs, batch_targets in dataset:
|
|
177
|
+
outputs = self.model(batch_inputs, training=False)
|
|
178
|
+
loss = loss_fn(outputs, batch_targets, **kwargs)
|
|
179
|
+
if isinstance(loss, tf.Tensor):
|
|
180
|
+
loss = loss.numpy()
|
|
181
|
+
total_loss += float(loss)
|
|
182
|
+
n_batches += 1
|
|
183
|
+
return total_loss / n_batches if n_batches > 0 else float("inf")
|
|
184
|
+
|
|
185
|
+
elif inputs is not None and targets is not None:
|
|
186
|
+
outputs = self.model(inputs, training=False)
|
|
187
|
+
loss = loss_fn(outputs, targets, **kwargs)
|
|
188
|
+
if isinstance(loss, tf.Tensor):
|
|
189
|
+
loss = loss.numpy()
|
|
190
|
+
return float(loss)
|
|
191
|
+
|
|
192
|
+
else:
|
|
193
|
+
loss = loss_fn(self.model, dataset, **kwargs)
|
|
194
|
+
if isinstance(loss, tf.Tensor):
|
|
195
|
+
loss = loss.numpy()
|
|
196
|
+
return float(loss)
|
|
197
|
+
|
|
198
|
+
def optimize(
|
|
199
|
+
self,
|
|
200
|
+
optimizer: BaseOptimizer,
|
|
201
|
+
loss_fn: Callable,
|
|
202
|
+
dataset: Optional[tf.data.Dataset] = None,
|
|
203
|
+
inputs: Optional[tf.Tensor] = None,
|
|
204
|
+
targets: Optional[tf.Tensor] = None,
|
|
205
|
+
iterations: int = 100,
|
|
206
|
+
verbose: bool = False,
|
|
207
|
+
callback: Optional[Callable] = None,
|
|
208
|
+
**kwargs
|
|
209
|
+
) -> Tuple[np.ndarray, float]:
|
|
210
|
+
"""Run optimizer to find best model weights.
|
|
211
|
+
|
|
212
|
+
Parameters
|
|
213
|
+
----------
|
|
214
|
+
optimizer : BaseOptimizer
|
|
215
|
+
A biopt optimizer (PSO, GWO, etc.).
|
|
216
|
+
loss_fn : callable
|
|
217
|
+
Loss function for evaluation.
|
|
218
|
+
dataset : tf.data.Dataset, optional
|
|
219
|
+
Dataset for evaluation.
|
|
220
|
+
inputs : tf.Tensor, optional
|
|
221
|
+
Input tensor for direct evaluation.
|
|
222
|
+
targets : tf.Tensor, optional
|
|
223
|
+
Target tensor for direct evaluation.
|
|
224
|
+
iterations : int
|
|
225
|
+
Number of optimization iterations.
|
|
226
|
+
verbose : bool
|
|
227
|
+
Print progress.
|
|
228
|
+
callback : callable, optional
|
|
229
|
+
Callback function called with (iteration, best_weights, best_loss).
|
|
230
|
+
**kwargs : dict
|
|
231
|
+
Additional arguments passed to optimizer.optimize().
|
|
232
|
+
|
|
233
|
+
Returns
|
|
234
|
+
-------
|
|
235
|
+
best_weights : ndarray
|
|
236
|
+
Best parameter vector found.
|
|
237
|
+
best_loss : float
|
|
238
|
+
Best loss achieved.
|
|
239
|
+
"""
|
|
240
|
+
def objective_fn(flat_weights: np.ndarray) -> float:
|
|
241
|
+
return self.evaluate(flat_weights, loss_fn, dataset, inputs, targets)
|
|
242
|
+
|
|
243
|
+
best_weights, best_loss = optimizer.optimize(
|
|
244
|
+
objective_fn, iterations=iterations, verbose=verbose, callback=callback, **kwargs
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
self.flat_to_model(best_weights)
|
|
248
|
+
|
|
249
|
+
return best_weights, best_loss
|
|
250
|
+
|
|
251
|
+
def get_weight_statistics(self) -> Dict[str, Any]:
|
|
252
|
+
"""Get statistics about current model weights."""
|
|
253
|
+
stats = {}
|
|
254
|
+
for var in self.model.trainable_variables:
|
|
255
|
+
w = var.numpy()
|
|
256
|
+
stats[var.name] = {
|
|
257
|
+
"mean": float(np.mean(w)),
|
|
258
|
+
"std": float(np.std(w)),
|
|
259
|
+
"min": float(np.min(w)),
|
|
260
|
+
"max": float(np.max(w)),
|
|
261
|
+
"norm": float(np.linalg.norm(w)),
|
|
262
|
+
}
|
|
263
|
+
return stats
|
bioopt/base.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""Base optimizer class for bio-inspired optimization algorithms."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Callable, List, Optional, Tuple, Union
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BoundsError(Exception):
|
|
10
|
+
"""Raised when bounds are invalid."""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class OptimizationError(Exception):
|
|
15
|
+
"""Raised when optimization fails."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BaseOptimizer(ABC):
|
|
20
|
+
"""
|
|
21
|
+
Abstract base class for all bio-inspired optimization algorithms.
|
|
22
|
+
|
|
23
|
+
Provides a common interface with bounds handling, fitness tracking,
|
|
24
|
+
and a standardized optimize() method.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
n_agents : int
|
|
29
|
+
Number of agents/particles/solutions in the population.
|
|
30
|
+
bounds : list of tuple
|
|
31
|
+
Search space bounds as [(min, max), ...] for each dimension.
|
|
32
|
+
seed : int, optional
|
|
33
|
+
Random seed for reproducibility.
|
|
34
|
+
|
|
35
|
+
Attributes
|
|
36
|
+
----------
|
|
37
|
+
best_position : ndarray
|
|
38
|
+
Best position found during optimization.
|
|
39
|
+
best_fitness : float
|
|
40
|
+
Best fitness value found during optimization.
|
|
41
|
+
fitness_history : list
|
|
42
|
+
History of best fitness values per iteration.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
n_agents: int,
|
|
48
|
+
bounds: Union[List[Tuple[float, float]], np.ndarray],
|
|
49
|
+
seed: Optional[int] = None,
|
|
50
|
+
):
|
|
51
|
+
self.n_agents = n_agents
|
|
52
|
+
self.bounds = np.array(bounds, dtype=np.float64)
|
|
53
|
+
self.seed = seed
|
|
54
|
+
self.rng = np.random.RandomState(seed)
|
|
55
|
+
|
|
56
|
+
self.best_position: Optional[np.ndarray] = None
|
|
57
|
+
self.best_fitness: float = np.inf
|
|
58
|
+
self.fitness_history: List[float] = []
|
|
59
|
+
|
|
60
|
+
self._validate_bounds()
|
|
61
|
+
|
|
62
|
+
def _validate_bounds(self):
|
|
63
|
+
"""Validate that bounds are properly formatted."""
|
|
64
|
+
if self.bounds.ndim != 2 or self.bounds.shape[1] != 2:
|
|
65
|
+
raise BoundsError(
|
|
66
|
+
f"Bounds must be 2D with shape (n_dims, 2), got {self.bounds.shape}"
|
|
67
|
+
)
|
|
68
|
+
if np.any(self.bounds[:, 0] >= self.bounds[:, 1]):
|
|
69
|
+
raise BoundsError("All lower bounds must be less than upper bounds.")
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def n_dims(self) -> int:
|
|
73
|
+
"""Number of dimensions in the search space."""
|
|
74
|
+
return self.bounds.shape[0]
|
|
75
|
+
|
|
76
|
+
def initialize_population(self) -> np.ndarray:
|
|
77
|
+
"""
|
|
78
|
+
Initialize population uniformly within bounds.
|
|
79
|
+
|
|
80
|
+
Returns
|
|
81
|
+
-------
|
|
82
|
+
population : ndarray of shape (n_agents, n_dims)
|
|
83
|
+
"""
|
|
84
|
+
return self.rng.uniform(
|
|
85
|
+
self.bounds[:, 0],
|
|
86
|
+
self.bounds[:, 1],
|
|
87
|
+
size=(self.n_agents, self.n_dims)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def clip_to_bounds(self, positions: np.ndarray) -> np.ndarray:
|
|
91
|
+
"""
|
|
92
|
+
Clip positions to stay within bounds.
|
|
93
|
+
|
|
94
|
+
Parameters
|
|
95
|
+
----------
|
|
96
|
+
positions : ndarray of shape (n_agents, n_dims)
|
|
97
|
+
|
|
98
|
+
Returns
|
|
99
|
+
-------
|
|
100
|
+
clipped : ndarray of same shape
|
|
101
|
+
"""
|
|
102
|
+
return np.clip(positions, self.bounds[:, 0], self.bounds[:, 1])
|
|
103
|
+
|
|
104
|
+
def evaluate(
|
|
105
|
+
self,
|
|
106
|
+
positions: np.ndarray,
|
|
107
|
+
objective_fn: Callable[[np.ndarray], float]
|
|
108
|
+
) -> np.ndarray:
|
|
109
|
+
"""
|
|
110
|
+
Evaluate fitness for all positions.
|
|
111
|
+
|
|
112
|
+
Parameters
|
|
113
|
+
----------
|
|
114
|
+
positions : ndarray of shape (n_agents, n_dims)
|
|
115
|
+
objective_fn : callable
|
|
116
|
+
Function that takes a 1D array and returns a scalar fitness value.
|
|
117
|
+
|
|
118
|
+
Returns
|
|
119
|
+
-------
|
|
120
|
+
fitness : ndarray of shape (n_agents,)
|
|
121
|
+
"""
|
|
122
|
+
return np.array([objective_fn(pos) for pos in positions])
|
|
123
|
+
|
|
124
|
+
def update_best(
|
|
125
|
+
self,
|
|
126
|
+
positions: np.ndarray,
|
|
127
|
+
fitness: np.ndarray
|
|
128
|
+
) -> None:
|
|
129
|
+
"""
|
|
130
|
+
Update global best position and fitness.
|
|
131
|
+
|
|
132
|
+
Parameters
|
|
133
|
+
----------
|
|
134
|
+
positions : ndarray of shape (n_agents, n_dims)
|
|
135
|
+
fitness : ndarray of shape (n_agents,)
|
|
136
|
+
"""
|
|
137
|
+
best_idx = np.argmin(fitness)
|
|
138
|
+
if fitness[best_idx] < self.best_fitness:
|
|
139
|
+
self.best_fitness = float(fitness[best_idx])
|
|
140
|
+
self.best_position = positions[best_idx].copy()
|
|
141
|
+
|
|
142
|
+
@abstractmethod
|
|
143
|
+
def step(
|
|
144
|
+
self,
|
|
145
|
+
positions: np.ndarray,
|
|
146
|
+
fitness: np.ndarray,
|
|
147
|
+
iteration: int,
|
|
148
|
+
**kwargs
|
|
149
|
+
) -> np.ndarray:
|
|
150
|
+
"""
|
|
151
|
+
Perform one iteration step of the algorithm.
|
|
152
|
+
|
|
153
|
+
Parameters
|
|
154
|
+
----------
|
|
155
|
+
positions : ndarray of shape (n_agents, n_dims)
|
|
156
|
+
Current positions of all agents.
|
|
157
|
+
fitness : ndarray of shape (n_agents,)
|
|
158
|
+
Current fitness values.
|
|
159
|
+
iteration : int
|
|
160
|
+
Current iteration number.
|
|
161
|
+
**kwargs : dict
|
|
162
|
+
Additional algorithm-specific parameters.
|
|
163
|
+
|
|
164
|
+
Returns
|
|
165
|
+
-------
|
|
166
|
+
new_positions : ndarray of shape (n_agents, n_dims)
|
|
167
|
+
"""
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
def optimize(
|
|
171
|
+
self,
|
|
172
|
+
objective_fn: Callable[[np.ndarray], float],
|
|
173
|
+
iterations: int = 100,
|
|
174
|
+
verbose: bool = False,
|
|
175
|
+
callback: Optional[Callable[[int, np.ndarray, float], None]] = None,
|
|
176
|
+
**kwargs
|
|
177
|
+
) -> Tuple[np.ndarray, float]:
|
|
178
|
+
"""
|
|
179
|
+
Run the optimization.
|
|
180
|
+
|
|
181
|
+
Parameters
|
|
182
|
+
----------
|
|
183
|
+
objective_fn : callable
|
|
184
|
+
The objective function to minimize. Takes a 1D array and returns a scalar.
|
|
185
|
+
iterations : int
|
|
186
|
+
Number of iterations to run.
|
|
187
|
+
verbose : bool
|
|
188
|
+
If True, print progress information.
|
|
189
|
+
callback : callable, optional
|
|
190
|
+
Function called after each iteration with signature
|
|
191
|
+
(iteration, best_position, best_fitness).
|
|
192
|
+
**kwargs : dict
|
|
193
|
+
Additional algorithm-specific parameters.
|
|
194
|
+
|
|
195
|
+
Returns
|
|
196
|
+
-------
|
|
197
|
+
best_position : ndarray
|
|
198
|
+
Best position found.
|
|
199
|
+
best_fitness : float
|
|
200
|
+
Best fitness value found.
|
|
201
|
+
"""
|
|
202
|
+
# Initialize
|
|
203
|
+
positions = self.initialize_population()
|
|
204
|
+
fitness = self.evaluate(positions, objective_fn)
|
|
205
|
+
self.update_best(positions, fitness)
|
|
206
|
+
self.fitness_history = [self.best_fitness]
|
|
207
|
+
|
|
208
|
+
if verbose:
|
|
209
|
+
print(f"Iter 0: Best Fitness = {self.best_fitness:.6e}")
|
|
210
|
+
|
|
211
|
+
if callback is not None:
|
|
212
|
+
callback(0, self.best_position, self.best_fitness)
|
|
213
|
+
|
|
214
|
+
# Main loop
|
|
215
|
+
for i in range(1, iterations + 1):
|
|
216
|
+
positions = self.step(positions, fitness, i, **kwargs)
|
|
217
|
+
positions = self.clip_to_bounds(positions)
|
|
218
|
+
fitness = self.evaluate(positions, objective_fn)
|
|
219
|
+
self.update_best(positions, fitness)
|
|
220
|
+
self.fitness_history.append(self.best_fitness)
|
|
221
|
+
|
|
222
|
+
if verbose:
|
|
223
|
+
print(f"Iter {i}: Best Fitness = {self.best_fitness:.6e}")
|
|
224
|
+
|
|
225
|
+
if callback is not None:
|
|
226
|
+
callback(i, self.best_position, self.best_fitness)
|
|
227
|
+
|
|
228
|
+
return self.best_position, self.best_fitness
|
|
229
|
+
|
|
230
|
+
def reset(self) -> None:
|
|
231
|
+
"""Reset optimizer state for a fresh optimization run."""
|
|
232
|
+
self.best_position = None
|
|
233
|
+
self.best_fitness = np.inf
|
|
234
|
+
self.fitness_history = []
|
|
235
|
+
self.rng = np.random.RandomState(self.seed)
|
|
236
|
+
|
|
237
|
+
def get_state(self) -> dict:
|
|
238
|
+
"""Get optimizer state for checkpointing."""
|
|
239
|
+
return {
|
|
240
|
+
"best_position": self.best_position,
|
|
241
|
+
"best_fitness": self.best_fitness,
|
|
242
|
+
"fitness_history": self.fitness_history,
|
|
243
|
+
"rng_state": self.rng.get_state(),
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
def set_state(self, state: dict) -> None:
|
|
247
|
+
"""Restore optimizer state from checkpoint."""
|
|
248
|
+
self.best_position = state["best_position"]
|
|
249
|
+
self.best_fitness = state["best_fitness"]
|
|
250
|
+
self.fitness_history = state["fitness_history"]
|
|
251
|
+
self.rng.set_state(state["rng_state"])
|
bioopt/swarm/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Swarm intelligence algorithms.
|
|
2
|
+
|
|
3
|
+
Available algorithms:
|
|
4
|
+
- PSO: Particle Swarm Optimization
|
|
5
|
+
- ACO: Ant Colony Optimization (Continuous, using KDE)
|
|
6
|
+
- ABC: Artificial Bee Colony
|
|
7
|
+
- GWO: Grey Wolf Optimizer
|
|
8
|
+
- FA: Firefly Algorithm
|
|
9
|
+
- WOA: Whale Optimization Algorithm
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
>>> from bioopt.swarm import PSO, ACO, GWO
|
|
13
|
+
>>> pso = PSO(n_agents=30, bounds=[(-5, 5)] * 10)
|
|
14
|
+
>>> best_pos, best_fit = pso.optimize(objective_fn, iterations=100)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from bioopt.swarm.pso import PSO
|
|
18
|
+
from bioopt.swarm.aco import ACO
|
|
19
|
+
from bioopt.swarm.abc import ABC
|
|
20
|
+
from bioopt.swarm.gwo import GWO
|
|
21
|
+
from bioopt.swarm.fa import FA
|
|
22
|
+
from bioopt.swarm.woa import WOA
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"PSO",
|
|
26
|
+
"ACO",
|
|
27
|
+
"ABC",
|
|
28
|
+
"GWO",
|
|
29
|
+
"FA",
|
|
30
|
+
"WOA",
|
|
31
|
+
]
|