Skip to content

Commit 165461d

Browse files
committed
improved subprocess API with start points
in conjunction with forwarding nullary operator, it now allows versatile seeding of algorithms
1 parent 088101b commit 165461d

File tree

4 files changed

+113
-87
lines changed

4 files changed

+113
-87
lines changed

moptipy/algorithms/so/rls.py

+2-51
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,6 @@
6262
from moptipy.api.algorithm import Algorithm1
6363
from moptipy.api.operators import Op0, Op1
6464
from moptipy.api.process import Process
65-
from moptipy.utils.logger import KeyValueLogSection
66-
from moptipy.utils.types import type_error
6765

6866

6967
# start book
@@ -103,61 +101,14 @@ def solve(self, process: Process) -> None:
103101
if new_f <= best_f: # new_x is not worse than best_x?
104102
best_f = new_f # Store its objective value.
105103
best_x, new_x = new_x, best_x # Swap best and new.
106-
# end book
107104

108-
def __solve_seeded(self, process: Process) -> None:
109-
"""
110-
Apply the RLS to an optimization problem starting from a seed.
105+
# end book
111106

112-
:param process: the black-box process object
113-
"""
114-
# Create records for old and new point in the search space.
115-
best_x = process.create() # record for best-so-far solution
116-
new_x = process.create() # record for new solution
117-
# Obtain the random number generator.
118-
random: Final[Generator] = process.get_random()
119-
120-
# Put function references in variables to save time.
121-
evaluate: Final[Callable] = process.evaluate # the objective
122-
op1: Final[Callable] = self.op1.op1 # the unary operator
123-
should_terminate: Final[Callable] = process.should_terminate
124-
125-
# Start at an existing point in the search space and get its quality.
126-
process.get_copy_of_best_x(best_x) # get the best-so-far solution
127-
best_f: Union[int, float] = process.get_best_f() # get the quality.
128-
129-
while not should_terminate(): # Until we need to quit...
130-
op1(random, new_x, best_x) # new_x = neighbor of best_x
131-
new_f: Union[int, float] = evaluate(new_x)
132-
if new_f <= best_f: # new_x is not worse than best_x?
133-
best_f = new_f # Store its objective value.
134-
best_x, new_x = new_x, best_x # Swap best and new.
135-
136-
def __init__(self, op0: Op0, op1: Op1,
137-
seeded: bool = False) -> None:
107+
def __init__(self, op0: Op0, op1: Op1) -> None:
138108
"""
139109
Create the randomized local search (rls).
140110
141111
:param op0: the nullary search operator
142112
:param op1: the unary search operator
143-
:param seeded: `True` if the algorithm should be run in a seeded
144-
fashion, i.e., expect an existing best solution. `False` if
145-
it should run in the traditional way, starting at a random
146-
solution
147113
"""
148114
super().__init__("rls", op0, op1)
149-
if not isinstance(seeded, bool):
150-
raise type_error(seeded, "seeded", bool)
151-
if seeded:
152-
self.solve = self.__solve_seeded # type: ignore
153-
#: was this algorithm started in its seeded fashion?
154-
self.__seeded: Final[bool] = seeded
155-
156-
def log_parameters_to(self, logger: KeyValueLogSection):
157-
"""
158-
Log the parameters of the algorithm to a logger.
159-
160-
:param logger: the logger for the parameters
161-
"""
162-
super().log_parameters_to(logger)
163-
logger.key_value("seeded", self.__seeded)

moptipy/api/subprocesses.py

+23-1
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,8 @@ def __init__(self, owner: Process, in_and_out_x: Any,
266266
#: the fast call to the owner's register method
267267
self.__register: Final[Callable[[Any, Union[int, float]], None]] \
268268
= owner.register
269+
#: True as long as only the seed has been used
270+
self.__only_seed_used: bool = True
269271

270272
def has_best(self) -> bool:
271273
return True
@@ -277,6 +279,10 @@ def get_best_f(self) -> Union[int, float]:
277279
return self.__best_f
278280

279281
def evaluate(self, x) -> Union[float, int]:
282+
if self.__only_seed_used:
283+
if self.is_equal(x, self.__best_x):
284+
return self.__best_f
285+
self.__only_seed_used = False
280286
self.__fes = fe = self.__fes + 1
281287
f: Final[Union[int, float]] = self.__evaluate(x)
282288
if f < self.__best_f:
@@ -286,8 +292,12 @@ def evaluate(self, x) -> Union[float, int]:
286292
return f
287293

288294
def register(self, x, f: Union[int, float]) -> None:
289-
self.__register(x, f)
295+
if self.__only_seed_used:
296+
if self.is_equal(x, self.__best_x):
297+
return
298+
self.__only_seed_used = False
290299
self.__fes = fe = self.__fes + 1
300+
self.__register(x, f)
291301
if f < self.__best_f:
292302
self.__best_f = f
293303
self.copy(self.__best_x, x)
@@ -311,6 +321,18 @@ def from_starting_point(owner: Process, in_and_out_x: Any,
311321
"""
312322
Create a sub-process searching from one starting point.
313323
324+
This process is especially useful in conjunction with class
325+
:class:`~moptipy.operators.op0_forward.Op0Forward`. This class
326+
allows forwarding the nullary search operator to the function
327+
:meth:`~moptipy.api.process.Process.get_copy_of_best_x`. This way, the
328+
first point that it sampled by a local search can be the point specified
329+
as `in_and_out_x`, which effectively seeds the local search.
330+
331+
To dovetail with chance of seeding, no FEs are counted at the beginning of
332+
the process as long as all points to be evaluated equal to the
333+
`in_and_out_x`. As soon as the first point different from `in_and_out_x`
334+
is evaluated, FE counting starts.
335+
314336
:param owner: the owning process
315337
:param in_and_out_x: the input solution record, which will be
316338
overwritten with the best encountered solution

moptipy/operators/op0_forward.py

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""A nullary operator forwarding to another function."""
2+
from typing import Callable, Any, Optional
3+
4+
import numpy as np
5+
from numpy.random import Generator
6+
7+
from moptipy.api.operators import Op0
8+
from moptipy.utils.types import type_error
9+
10+
11+
class Op0Forward(Op0):
12+
"""Get the forwarder function."""
13+
14+
def __init__(self):
15+
"""Initialize this operator."""
16+
#: the internal blueprint for filling permutations
17+
self.__call: Optional[Callable[[Any], None]] = None
18+
19+
def op0(self, random: Generator, dest: np.ndarray) -> None:
20+
"""Forward the call."""
21+
self.__call(dest)
22+
23+
def forward_to(self, call: Callable[[Any], None]) -> None:
24+
"""
25+
Set the `Callable` to forward all calls from :meth:`op0` to.
26+
27+
:param call: the `Callable` to which all calls to :meth:`op0` should
28+
be delegated to.
29+
"""
30+
if not callable(call):
31+
raise type_error(call, "call", call=True)
32+
self.__call = call
33+
34+
def stop_forwarding(self) -> None:
35+
"""Stop forwarding the call."""
36+
self.__call = None
37+
38+
def __str__(self) -> str:
39+
"""
40+
Get the name of this operator.
41+
42+
:return: "forward"
43+
"""
44+
return "forward"

tests/api/test_subprocess.py

+44-35
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from moptipy.operators.bitstrings.op1_flip1 import Op1Flip1
3030
from moptipy.operators.bitstrings.op1_m_over_n_flip import Op1MoverNflip
3131
from moptipy.operators.bitstrings.op2_uniform import Op2Uniform
32+
from moptipy.operators.op0_forward import Op0Forward
3233
from moptipy.spaces.bitstrings import BitStrings
3334
from moptipy.utils.types import type_name_of
3435

@@ -39,7 +40,8 @@ class MyAlgorithm(Algorithm2):
3940
def __init__(self) -> None:
4041
super().__init__("dummy", Op0Random(), Op1Flip1(), Op2Uniform())
4142
self.ea = EA(self.op0, self.op1, self.op2, 10, 10, 0.3)
42-
self.rls = RLS(self.op0, self.op1, seeded=True)
43+
self.fwd = Op0Forward()
44+
self.rls = RLS(self.fwd, self.op1)
4345

4446
def solve(self, process: Process) -> None:
4547
"""Apply an EA for 100 FEs, followed by RLS."""
@@ -73,21 +75,25 @@ def solve(self, process: Process) -> None:
7375

7476
fnew2: Union[int, float]
7577
fes2: int
76-
with for_fes(from_starting_point(process, x1, fnew), 100) as z:
77-
assert str(z) == f"forFEs_100_fromStart_{process}"
78-
assert z.has_best()
79-
assert z.get_best_f() == fnew
80-
assert z.get_consumed_fes() == 0
81-
self.rls.solve(z)
82-
fnew2 = z.get_best_f()
83-
fes2 = z.get_consumed_fes()
84-
assert fes2 > 0
85-
assert (fnew2 == 0) or (fes2 == 100)
86-
assert (fnew2 >= 0) and (fes2 <= 100)
87-
assert fnew2 <= fnew
78+
with from_starting_point(process, x1, fnew) as z1:
79+
assert str(z1) == f"fromStart_{process}"
80+
with for_fes(z1, 100) as z:
81+
self.fwd.forward_to(z.get_copy_of_best_x)
82+
assert str(z) == f"forFEs_100_fromStart_{process}"
83+
assert z.has_best()
84+
assert z.get_best_f() == fnew
85+
assert z.get_consumed_fes() == 0
86+
self.rls.solve(z)
87+
self.fwd.stop_forwarding()
88+
fnew2 = z.get_best_f()
89+
fes2 = z.get_consumed_fes()
90+
assert fes2 > 0
91+
assert (fnew2 == 0) or (fes2 == 100)
92+
assert (fnew2 >= 0) and (fes2 <= 100)
93+
assert fnew2 <= fnew
8894

8995
allfes = process.get_consumed_fes()
90-
assert allfes == 1 + fes + fes2
96+
assert allfes == fes + fes2
9197
assert process.get_best_f() == fnew2
9298
if fnew2 > 0:
9399
assert process.evaluate(x1) == fnew2
@@ -105,8 +111,8 @@ def test_from_start_for_fes():
105111
with exp.execute() as p:
106112
assert p.has_best()
107113
assert p.get_best_f() >= 0
108-
assert (p.get_best_f() == 0) or (p.get_consumed_fes() == 202)
109-
assert (p.get_best_f() >= 0) and (p.get_consumed_fes() <= 202)
114+
assert (p.get_best_f() == 0) or (p.get_consumed_fes() == 200)
115+
assert (p.get_best_f() >= 0) and (p.get_consumed_fes() <= 200)
110116

111117

112118
class MyAlgorithm2(Algorithm2):
@@ -161,6 +167,7 @@ def test_without_should_terminate():
161167

162168
class _OneMaxRegAlgo(Algorithm0):
163169
"""The one-max algorithm."""
170+
164171
def __init__(self, op0: Op0Random, f: OneMax):
165172
"""Initialize."""
166173
super().__init__("om", op0)
@@ -231,11 +238,11 @@ def test_for_fes_process_no_ss_no_log_reg_norm():
231238
algorithm2: Algorithm = _OneMaxRegAlgo(Op0Random(), objective)
232239
algorithm: __RegisterForFEs = __RegisterForFEs(algorithm1, algorithm2)
233240

234-
with Execution()\
235-
.set_solution_space(space)\
236-
.set_objective(objective)\
237-
.set_algorithm(algorithm)\
238-
.set_max_fes(100)\
241+
with Execution() \
242+
.set_solution_space(space) \
243+
.set_objective(objective) \
244+
.set_algorithm(algorithm) \
245+
.set_max_fes(100) \
239246
.execute() as process:
240247
assert type_name_of(process) \
241248
== "moptipy.api._process_no_ss._ProcessNoSS"
@@ -256,6 +263,7 @@ def test_for_fes_process_no_ss_no_log_reg_norm():
256263

257264
class _MOAlgoForFEs(MOAlgorithm, Algorithm0):
258265
"""The algorithm for multi-objective optimization."""
266+
259267
def __init__(self, op0: Op0Random):
260268
"""Initialize."""
261269
Algorithm0.__init__(self, "om", op0)
@@ -299,14 +307,14 @@ def test_for_fes_mo_process_no_ss_no_log():
299307
algorithm: Algorithm = _MOAlgoForFEs(Op0Random())
300308
ams = int(random.integers(2, 5))
301309

302-
with MOExecution()\
303-
.set_solution_space(space)\
304-
.set_objective(problem)\
305-
.set_archive_pruner(pruner)\
306-
.set_archive_max_size(ams)\
307-
.set_archive_pruning_limit(int(ams + random.integers(0, 3)))\
308-
.set_algorithm(algorithm)\
309-
.set_max_fes(100)\
310+
with MOExecution() \
311+
.set_solution_space(space) \
312+
.set_objective(problem) \
313+
.set_archive_pruner(pruner) \
314+
.set_archive_max_size(ams) \
315+
.set_archive_pruning_limit(int(ams + random.integers(0, 3))) \
316+
.set_algorithm(algorithm) \
317+
.set_max_fes(100) \
310318
.execute() as process:
311319
assert isinstance(process, MOProcess)
312320
assert type_name_of(process) \
@@ -332,6 +340,7 @@ def test_for_fes_mo_process_no_ss_no_log():
332340

333341
class _MOWithoutShouldTerminate(MOAlgorithm, Algorithm0):
334342
"""The algorithm for multi-objective optimization."""
343+
335344
def __init__(self, op0: Op0Random):
336345
"""Initialize."""
337346
Algorithm0.__init__(self, "om", op0)
@@ -370,12 +379,12 @@ def test_without_should_terminate_mo_process_no_ss_no_log():
370379
algorithm: Algorithm = _MOWithoutShouldTerminate(Op0Random())
371380
ams = int(random.integers(2, 5))
372381

373-
with MOExecution()\
374-
.set_solution_space(space)\
375-
.set_objective(problem)\
376-
.set_archive_pruning_limit(int(ams + random.integers(0, 3)))\
377-
.set_algorithm(algorithm)\
378-
.set_max_fes(100)\
382+
with MOExecution() \
383+
.set_solution_space(space) \
384+
.set_objective(problem) \
385+
.set_archive_pruning_limit(int(ams + random.integers(0, 3))) \
386+
.set_algorithm(algorithm) \
387+
.set_max_fes(100) \
379388
.execute() as process:
380389
assert isinstance(process, MOProcess)
381390
assert type_name_of(process) \

0 commit comments

Comments
 (0)