Skip to content

Commit

Permalink
Stopping criteria (#64)
Browse files Browse the repository at this point in the history
* Base class for stopping criteria

* Implemented MaxIterations and passed tests

* Imeplemented MaxRuntime and passed tests

* Refactor MaxIterations and MaxRuntime

* Add start_time and total_runtime fields to Statistics
  • Loading branch information
leonlan authored May 17, 2022
1 parent f478e0c commit 2550a14
Show file tree
Hide file tree
Showing 11 changed files with 363 additions and 50 deletions.
51 changes: 26 additions & 25 deletions alns/ALNS.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from alns.Statistics import Statistics
from alns.criteria import AcceptanceCriterion
from alns.weight_schemes import WeightScheme
from alns.stopping_criteria import StoppingCriterion

# Potential candidate solution consideration outcomes.
_BEST = 0
Expand All @@ -21,7 +22,6 @@


class ALNS:

def __init__(self, rnd_state: rnd.RandomState = rnd.RandomState()):
"""
Implements the adaptive large neighbourhood search (ALNS) algorithm.
Expand Down Expand Up @@ -107,12 +107,14 @@ def add_repair_operator(self, op: _OperatorType, name: str = None):
"""
self._repair_operators[name if name else op.__name__] = op

def iterate(self,
initial_solution: State,
weight_scheme: WeightScheme,
crit: AcceptanceCriterion,
iterations: int = 10_000,
**kwargs) -> Result:
def iterate(
self,
initial_solution: State,
weight_scheme: WeightScheme,
crit: AcceptanceCriterion,
stop: StoppingCriterion,
**kwargs,
) -> Result:
"""
Runs the adaptive large neighbourhood search heuristic [1], using the
previously set destroy and repair operators. The first solution is set
Expand All @@ -129,8 +131,10 @@ def iterate(self,
crit
The acceptance criterion to use for candidate states. See also
the ``alns.criteria`` module for an overview.
iterations
The number of iterations. Default 10_000.
stop
The stopping criterion to use for stopping the iterations.
See also the ``alns.stopping_criteria`` module for an overview.
**kwargs
Optional keyword arguments. These are passed to the operators,
including callbacks.
Expand All @@ -156,18 +160,17 @@ class of vehicle routing problems with backhauls. *European Journal of
Operational Research*, 171: 750–775, 2006.
"""
if len(self.destroy_operators) == 0 or len(self.repair_operators) == 0:
raise ValueError("Missing at least one destroy or repair operator.")

if iterations < 0:
raise ValueError("Negative number of iterations.")
raise ValueError(
"Missing at least one destroy or repair operator."
)

curr = best = initial_solution

stats = Statistics()
stats.collect_objective(initial_solution.objective())
stats.collect_runtime(time.perf_counter())

for iteration in range(iterations):
while not stop(self._rnd_state, best, curr):
d_idx, r_idx = weight_scheme.select_operators(self._rnd_state)

d_name, d_operator = self.destroy_operators[d_idx]
Expand All @@ -176,11 +179,9 @@ class of vehicle routing problems with backhauls. *European Journal of
destroyed = d_operator(curr, self._rnd_state, **kwargs)
cand = r_operator(destroyed, self._rnd_state, **kwargs)

best, curr, s_idx = self._eval_cand(crit,
best,
curr,
cand,
**kwargs)
best, curr, s_idx = self._eval_cand(
crit, best, curr, cand, **kwargs
)

weight_scheme.update_weights(d_idx, r_idx, s_idx)

Expand All @@ -206,12 +207,12 @@ def on_best(self, func: _OperatorType):
self._on_best = func

def _eval_cand(
self,
crit: AcceptanceCriterion,
best: State,
curr: State,
cand: State,
**kwargs
self,
crit: AcceptanceCriterion,
best: State,
curr: State,
cand: State,
**kwargs,
) -> Tuple[State, State, int]:
"""
Considers the candidate solution by comparing it against the best and
Expand Down
14 changes: 14 additions & 0 deletions alns/Statistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@ def objectives(self) -> np.ndarray:
"""
return np.array(self._objectives)

@property
def start_time(self) -> float:
"""
Return the reference start time to compute the runtimes.
"""
return self._runtimes[0]

@property
def total_runtime(self) -> float:
"""
Return the total runtime (in seconds).
"""
return self._runtimes[-1] - self._runtimes[0]

@property
def runtimes(self) -> np.ndarray:
"""
Expand Down
25 changes: 25 additions & 0 deletions alns/stopping_criteria/MaxIterations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from numpy.random import RandomState

from alns.State import State
from alns.stopping_criteria.StoppingCriterion import StoppingCriterion


class MaxIterations(StoppingCriterion):
def __init__(self, max_iterations: int):
"""
Criterion that stops after a maximum number of iterations.
"""
if max_iterations < 0:
raise ValueError("Max iterations must be non-negative.")

self._max_iterations = max_iterations
self._current_iteration = 0

@property
def max_iterations(self) -> int:
return self._max_iterations

def __call__(self, rnd: RandomState, best: State, current: State) -> bool:
self._current_iteration += 1

return self._current_iteration > self.max_iterations
29 changes: 29 additions & 0 deletions alns/stopping_criteria/MaxRuntime.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import time

from typing import Optional
from numpy.random import RandomState

from alns.State import State
from alns.stopping_criteria.StoppingCriterion import StoppingCriterion


class MaxRuntime(StoppingCriterion):
def __init__(self, max_runtime: float):
"""
Criterion that stops after a specified maximum runtime.
"""
if max_runtime < 0:
raise ValueError("Max runtime must be non-negative.")

self._max_runtime = max_runtime
self._start_runtime: Optional[float] = None

@property
def max_runtime(self) -> float:
return self._max_runtime

def __call__(self, rnd: RandomState, best: State, current: State) -> bool:
if self._start_runtime is None:
self._start_runtime = time.perf_counter()

return time.perf_counter () - self._start_runtime > self.max_runtime
31 changes: 31 additions & 0 deletions alns/stopping_criteria/StoppingCriterion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from abc import ABC, abstractmethod

from numpy.random import RandomState

from alns.State import State


class StoppingCriterion(ABC):
"""
Base class from which to implement a stopping criterion.
"""

@abstractmethod
def __call__(self, rnd: RandomState, best: State, current: State) -> bool:
"""
Determines whether to stop based on the implemented stopping criterion.
Parameters
----------
rnd
May be used to draw random numbers from.
best
The best solution state observed so far.
current
The current solution state.
Returns
-------
Whether to stop the iteration (True), or not (False).
"""
return NotImplemented
3 changes: 3 additions & 0 deletions alns/stopping_criteria/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .MaxIterations import MaxIterations
from .MaxRuntime import MaxRuntime
from .StoppingCriterion import StoppingCriterion
Empty file.
52 changes: 52 additions & 0 deletions alns/stopping_criteria/tests/test_max_iterations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import pytest

from numpy.random import RandomState
from numpy.testing import assert_, assert_raises

from alns.stopping_criteria import MaxIterations
from alns.tests.states import Zero


@pytest.mark.parametrize("max_iterations", [-1, -42, -10000])
def test_raise_negative_parameters(max_iterations: int):
"""
Maximum iterations cannot be negative.
"""
with assert_raises(ValueError):
MaxIterations(max_iterations)


@pytest.mark.parametrize("max_iterations", [1, 42, 10000])
def test_does_not_raise(max_iterations: int):
"""
Valid parameters should not raise.
"""
MaxIterations(max_iterations)


@pytest.mark.parametrize("max_iterations", [1, 42, 10000])
def test_max_iterations(max_iterations):
"""
Test if the max iterations parameter is correctly set.
"""
stop = MaxIterations(max_iterations)
assert stop.max_iterations == max_iterations


def test_before_max_iterations():
stop = MaxIterations(100)
rnd = RandomState(0)

for _ in range(100):
assert_(not stop(rnd, Zero(), Zero()))


def test_after_max_iterations():
stop = MaxIterations(100)
rnd = RandomState()

for _ in range(100):
stop(rnd, Zero(), Zero())

for _ in range(100):
assert_(stop(rnd, Zero(), Zero()))
65 changes: 65 additions & 0 deletions alns/stopping_criteria/tests/test_max_runtime.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import time
import pytest

from numpy.random import RandomState
from numpy.testing import assert_, assert_almost_equal, assert_raises

from alns.stopping_criteria import MaxRuntime
from alns.tests.states import Zero


def sleep(duration, get_now=time.perf_counter):
"""
Custom sleep function. Built-in time.sleep function is not precise
and has different performances depending on the OS, see
https://stackoverflow.com/questions/1133857/how-accurate-is-pythons-time-sleep
"""
now = get_now()
end = now + duration
while now < end:
now = get_now()


@pytest.mark.parametrize("max_runtime", [-0.001, -1, -10.1])
def test_raise_negative_parameters(max_runtime: float):
"""
Maximum runtime may not be negative.
"""
with assert_raises(ValueError):
MaxRuntime(max_runtime)


@pytest.mark.parametrize("max_runtime", [0.001, 1, 10.1])
def test_valid_parameters(max_runtime: float):
"""
Does not raise for non-negative parameters.
"""
MaxRuntime(max_runtime)


@pytest.mark.parametrize("max_runtime", [0.01, 0.1, 1])
def test_max_runtime(max_runtime):
"""
Test if the max time parameter is correctly set.
"""
stop = MaxRuntime(max_runtime)
assert_(stop.max_runtime, max_runtime)


@pytest.mark.parametrize("max_runtime", [0.01, 0.05, 0.10])
def test_before_max_runtime(max_runtime):
stop = MaxRuntime(max_runtime)
rnd = RandomState()
for _ in range(100):
assert_(not stop(rnd, Zero(), Zero()))


@pytest.mark.parametrize("max_runtime", [0.01, 0.05, 0.10])
def test_after_max_runtime(max_runtime):
stop = MaxRuntime(max_runtime)
rnd = RandomState()
stop(rnd, Zero(), Zero()) # Trigger the first time measurement
sleep(max_runtime)

for _ in range(100):
assert_(stop(rnd, Zero(), Zero()))
Loading

0 comments on commit 2550a14

Please sign in to comment.