Skip to content

Commit 246fba3

Browse files
committed
Seed rotations
* Seeds can now be specified at the benchmark level and rotated (the first seed will be used for the first run of every algorithm-problem pair etc.) * `WrappedProblem`s can now be reseeded. Related to #7
1 parent bb808a3 commit 246fba3

File tree

6 files changed

+62
-17
lines changed

6 files changed

+62
-17
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ Changelog
55

66
## New features
77

8+
* Seed rotations: `Benchmark.__init__` now has a `seeds` argument which can
9+
receive a list of seeds. The first seed will be used for all random
10+
generators involved in the first run of every algorithm-problem pair, the
11+
second for all second runs, etc.
812
* When constructing a `WrappedProblem`, the wrapped problem is now deepcopyied
913
by default:
1014
```py
@@ -35,6 +39,9 @@ Changelog
3539

3640
## Breaking changes
3741

42+
* Seeds can no longer be specified in algorithm description dicts (see
43+
`Benchmark.__init__`). Instead, use the `seeds` argument when constructing
44+
benchmarks (see above).
3845
* Class `nmoo.benchmark.Pair` has been replaced by `nmoo.benchmark.PAPair`,
3946
representing a problem-algorithm pair, and `nmoo.benchmark.PARTriple`,
4047
representing a problem-algorithm-(run number) triple. Method

example.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,5 @@ def make_benchmark() -> nmoo.Benchmark:
5656
},
5757
},
5858
n_runs=3,
59+
seeds=[123, 456, 789],
5960
)

nmoo/benchmark.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,11 @@ class Benchmark:
145145
Results of all runs.
146146
"""
147147

148+
_seeds: List[Optional[int]]
149+
"""
150+
List of seeds to use. Must be of length `_n_runs`.
151+
"""
152+
148153
def __init__(
149154
self,
150155
output_dir_path: Union[Path, str],
@@ -154,10 +159,11 @@ def __init__(
154159
dump_histories: bool = True,
155160
performance_indicators: Optional[List[str]] = None,
156161
max_retry: int = -1,
162+
seeds: Optional[List[Optional[int]]] = None,
157163
):
158164
"""
159165
Constructor. The set of problems to be benchmarked is represented by a
160-
dictionary with the following structure::
166+
dictionary with the following structure:
161167
162168
problems = {
163169
<problem_name>: <problem_description>,
@@ -198,9 +204,8 @@ def __init__(
198204
* `evaluator` (optional): an algorithm evaluator object; note that it
199205
is deepcopied for every run of `minimize`;
200206
* `return_least_infeasible` (optional, bool): if the algorithm cannot
201-
find a feasable solution, wether the least infeasable solution
207+
find a feasible solution, wether the least infeasible solution
202208
should still be returned; defaults to `False`;
203-
* `seed` (optional, int): a seed;
204209
* `termination` (optional): a pymoo termination criterion; note that it
205210
is deepcopied for every run of `minimize`;
206211
* `verbose` (optional, bool): wether outputs should be printed during
@@ -250,6 +255,9 @@ def __init__(
250255
In the result dataframe, the corresponding columns will be
251256
named `perf_<name of indicator>`, e.g. `perf_igd`. If left
252257
unspecified, defaults to `["igd"]`.
258+
seeds (Optional[List[Optional[int]]]): List of seeds to use. The
259+
first seed will be used for the first run of every
260+
algorithm-problem pair, etc.
253261
"""
254262
self._output_dir_path = Path(output_dir_path)
255263
self._set_problems(problems)
@@ -263,6 +271,12 @@ def __init__(
263271
self._dump_histories = dump_histories
264272
self._set_performance_indicators(performance_indicators)
265273
self._max_retry = max_retry
274+
if seeds is None:
275+
self._seeds = [None] * n_runs
276+
elif len(seeds) != n_runs:
277+
raise ValueError("Seed list must be of length n_runs")
278+
else:
279+
self._seeds = seeds
266280

267281
def _compute_global_pareto_population(self, pair: PAPair) -> None:
268282
"""
@@ -457,8 +471,11 @@ def _run_par_triple(
457471
triple.algorithm_description.get("evaluator"),
458472
)
459473
try:
474+
seed = self._seeds[triple.n_run - 1]
475+
problem = deepcopy(triple.problem_description["problem"])
476+
problem.reseed(seed)
460477
results = minimize(
461-
deepcopy(triple.problem_description["problem"]),
478+
problem,
462479
triple.algorithm_description["algorithm"],
463480
termination=triple.algorithm_description.get("termination"),
464481
copy_algorithm=True,
@@ -471,7 +488,7 @@ def _run_par_triple(
471488
"return_least_infeasible", False
472489
),
473490
save_history=True,
474-
seed=triple.algorithm_description.get("seed"),
491+
seed=seed,
475492
verbose=triple.algorithm_description.get("verbose", False),
476493
)
477494
except Exception as e: # pylint: disable=broad-except

nmoo/noises/gaussian.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ def __init__(
100100
"Invalid noise specification. Either mean and covariance are "
101101
"both set, or a parameters dict is set."
102102
)
103-
self._generator = np.random.default_rng(seed)
103+
self.reseed(seed)
104104

105105
def _evaluate(self, x, out, *args, **kwargs):
106106
"""
@@ -147,3 +147,8 @@ def _evaluate(self, x, out, *args, **kwargs):
147147
self.add_to_history_x_out(
148148
x, out, **{k + "_noise": v for k, v in noises.items()}
149149
)
150+
151+
def reseed(self, seed: Any) -> None:
152+
self._generator = np.random.default_rng(seed)
153+
if isinstance(self._problem, WrappedProblem):
154+
self._problem.reseed(seed)

nmoo/noises/uniform.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ def __init__(
118118
self._parameters[k].append((-a, a))
119119
except AssertionError as e:
120120
raise ValueError("Invalid noise parameters") from e
121-
self._generator = np.random.default_rng(seed)
121+
self.reseed(seed)
122122

123123
# pylint: disable=duplicate-code
124124
def _evaluate(self, x, out, *args, **kwargs):
@@ -146,3 +146,8 @@ def _evaluate(self, x, out, *args, **kwargs):
146146
self.add_to_history_x_out(
147147
x, out, **{k + "_noise": v for k, v in noises.items()}
148148
)
149+
150+
def reseed(self, seed: Any) -> None:
151+
self._generator = np.random.default_rng(seed)
152+
if isinstance(self._problem, WrappedProblem):
153+
self._problem.reseed(seed)

nmoo/wrapped_problem.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import logging
1616
from copy import deepcopy
1717
from pathlib import Path
18-
from typing import Dict, List, Union
18+
from typing import Any, Dict, List, Union
1919

2020
import numpy as np
2121
from pymoo.core.problem import Problem
@@ -202,6 +202,25 @@ def dump_history(self, path: Union[Path, str], compressed: bool = True):
202202
saver = np.savez_compressed if compressed else np.savez
203203
saver(path, **self._history)
204204

205+
def ground_problem(self) -> Problem:
206+
"""
207+
Recursively goes down the problem wrappers until an actual
208+
`pymoo.Problem` is found, and returns it.
209+
"""
210+
if isinstance(self._problem, WrappedProblem):
211+
return self._problem.ground_problem()
212+
return self._problem
213+
214+
def reseed(self, seed: Any) -> None:
215+
"""
216+
Recursively resets the internal random state of the problem. See the
217+
[numpy
218+
documentation](https://numpy.org/doc/stable/reference/random/generator.html?highlight=default_rng#numpy.random.default_rng)
219+
for details about acceptable seeds.
220+
"""
221+
if isinstance(self._problem, WrappedProblem):
222+
self._problem.reseed(seed)
223+
205224
def start_new_run(self):
206225
"""
207226
In short, it rotates the history of the current problem, and all
@@ -220,15 +239,6 @@ def start_new_run(self):
220239
if isinstance(self._problem, WrappedProblem):
221240
self._problem.start_new_run()
222241

223-
def ground_problem(self) -> Problem:
224-
"""
225-
Recursively goes down the problem wrappers until an actual
226-
`pymoo.Problem` is found, and returns it.
227-
"""
228-
if isinstance(self._problem, WrappedProblem):
229-
return self._problem.ground_problem()
230-
return self._problem
231-
232242
def _evaluate(self, x, out, *args, **kwargs):
233243
"""
234244
Calls the wrapped problems's `_evaluate` method and appends its input

0 commit comments

Comments
 (0)