diff --git a/alns/accept/RandomWalk.py b/alns/accept/AlwaysAccept.py similarity index 51% rename from alns/accept/RandomWalk.py rename to alns/accept/AlwaysAccept.py index d513118a..126cfd4f 100644 --- a/alns/accept/RandomWalk.py +++ b/alns/accept/AlwaysAccept.py @@ -1,6 +1,6 @@ -class RandomWalk: +class AlwaysAccept: """ - The random walk criterion always accepts the candidate solution. + This criterion always accepts the candidate solution. """ def __call__(self, rnd, best, current, candidate): diff --git a/alns/accept/AdaptiveThreshold.py b/alns/accept/MovingAverageThreshold.py similarity index 86% rename from alns/accept/AdaptiveThreshold.py rename to alns/accept/MovingAverageThreshold.py index 5ed69076..741682c0 100644 --- a/alns/accept/AdaptiveThreshold.py +++ b/alns/accept/MovingAverageThreshold.py @@ -3,11 +3,12 @@ from typing import Deque, List -class AdaptiveThreshold: +class MovingAverageThreshold: """ - The Adaptive Threshold (AT) criterion of [1]. This criterion accepts a - candidate solution if it is better than an adaptive threshold value. The - adaptive threshold is computed as: + The Moving Average Threshold (MAT) criterion of [1]. This criterion accepts + a candidate solution if it is better than a threshold value that is based + on the moving average of the objective values of recently observed + candidate solutions. The threshold is computed as: .. math:: diff --git a/alns/accept/WorseAccept.py b/alns/accept/RandomAccept.py similarity index 89% rename from alns/accept/WorseAccept.py rename to alns/accept/RandomAccept.py index 6c06921b..02117cf9 100644 --- a/alns/accept/WorseAccept.py +++ b/alns/accept/RandomAccept.py @@ -1,11 +1,11 @@ from alns.accept.update import update -class WorseAccept: +class RandomAccept: """ - The Worse Accept criterion accepts a candidate solution if it improves over - the current one, or with a given probability :math:`P` regardless of the - cost. :math:`P` is updated in each iteration as: + The Random Accept criterion accepts a candidate solution if it improves + over the current one, or with a given probability :math:`P` regardless of + the cost. :math:`P` is updated in each iteration as: .. math:: diff --git a/alns/accept/__init__.py b/alns/accept/__init__.py index e3d3e5f6..e455c01f 100644 --- a/alns/accept/__init__.py +++ b/alns/accept/__init__.py @@ -1,10 +1,10 @@ from .AcceptanceCriterion import AcceptanceCriterion -from .AdaptiveThreshold import AdaptiveThreshold +from .AlwaysAccept import AlwaysAccept from .GreatDeluge import GreatDeluge from .HillClimbing import HillClimbing from .LateAcceptanceHillClimbing import LateAcceptanceHillClimbing +from .MovingAverageThreshold import MovingAverageThreshold from .NonLinearGreatDeluge import NonLinearGreatDeluge -from .RandomWalk import RandomWalk +from .RandomAccept import RandomAccept from .RecordToRecordTravel import RecordToRecordTravel from .SimulatedAnnealing import SimulatedAnnealing -from .WorseAccept import WorseAccept diff --git a/alns/accept/tests/test_adaptive_threshold.py b/alns/accept/tests/test_adaptive_threshold.py deleted file mode 100644 index a0c2822b..00000000 --- a/alns/accept/tests/test_adaptive_threshold.py +++ /dev/null @@ -1,124 +0,0 @@ -import numpy.random as rnd -from numpy.testing import assert_, assert_equal, assert_raises -from pytest import mark - -from alns.accept import AdaptiveThreshold -from alns.tests.states import One, Two, VarObj, Zero - - -@mark.parametrize( - "eta, gamma", - [ - (-1, 3), # eta cannot be < 0 - (2, 3), # eta cannot be > 1 - (0.5, -2), # gamma cannot be < 0 - (0.5, 0), # gamma cannot be 0 - ], -) -def test_raise_invalid_parameters(eta, gamma): - with assert_raises(ValueError): - AdaptiveThreshold(eta=eta, gamma=gamma) - - -@mark.parametrize("eta, gamma", [(1, 3), (0.4, 4)]) -def test_no_raise_valid_parameters(eta, gamma): - AdaptiveThreshold(eta=eta, gamma=gamma) - - -@mark.parametrize("eta", [0, 0.01, 0.5, 0.99, 1]) -def test_eta(eta): - adaptive_threshold = AdaptiveThreshold(eta, 3) - assert_equal(adaptive_threshold.eta, eta) - - -@mark.parametrize("gamma", range(1, 10)) -def test_gamma(gamma): - adaptive_threshold = AdaptiveThreshold(0.5, gamma) - assert_equal(adaptive_threshold.gamma, gamma) - - -def test_accepts_below_threshold(): - adaptive_threshold = AdaptiveThreshold(eta=0.5, gamma=4) - adaptive_threshold(rnd.RandomState(), One(), One(), One()) - adaptive_threshold(rnd.RandomState(), One(), One(), Zero()) - - # The threshold is set at 0 + 0.5 * (0.5 - 0) = 0.25 - assert_(adaptive_threshold(rnd.RandomState(), One(), One(), Zero())) - - -def test_rejects_above_threshold(): - adaptive_threshold = AdaptiveThreshold(eta=0.5, gamma=4) - adaptive_threshold(rnd.RandomState(), One(), One(), Two()) - adaptive_threshold(rnd.RandomState(), One(), One(), Zero()) - - # The threshold is set at 0 + 0.5 * (1 - 0) = 0.5 - assert_(not adaptive_threshold(rnd.RandomState(), One(), One(), One())) - - -def test_accepts_equal_threshold(): - accept = AdaptiveThreshold(eta=0.5, gamma=4) - accept(rnd.RandomState(), One(), One(), VarObj(7100)) - accept(rnd.RandomState(), One(), One(), VarObj(7200)) - - # The threshold is set at 7100 + 0.5 * (7140 - 7100) = 7120 - assert_(accept(rnd.RandomState(), One(), One(), VarObj(7120))) - - -def test_accepts_over_gamma_candidates(): - accept = AdaptiveThreshold(eta=0.2, gamma=3) - accept(rnd.RandomState(), One(), One(), VarObj(7100)) - accept(rnd.RandomState(), One(), One(), VarObj(7200)) - accept(rnd.RandomState(), One(), One(), VarObj(7200)) - - # The threshold is set at 7000 + 0.2 * (7133.33 - 7000) = 7013.33 - assert_(accept(rnd.RandomState(), One(), One(), VarObj(7000))) - - -def test_rejects_over_gamma_candidates(): - accept = AdaptiveThreshold(eta=0.2, gamma=3) - - for value in [7100, 7200, 7200, 7000]: - accept(rnd.RandomState(), One(), One(), VarObj(value)) - - # The threshold is set at 7000 + 0.2 * (7100 - 7000) = 7020 - result = accept(rnd.RandomState(), One(), One(), VarObj(7100)) - assert_(not result) - - -def test_evaluate_consecutive_solutions(): - """ - Test if AT correctly accepts and rejects consecutive solutions. - """ - accept = AdaptiveThreshold(eta=0.5, gamma=4) - - # The threshold is set at 7100, hence the solution is accepted. - assert_(accept(rnd.RandomState(), One(), One(), VarObj(7100))) - - # The threshold is set at 7125, hence the solution is accepted. - result = accept(rnd.RandomState(), One(), One(), VarObj(7200)) - assert_(not result) - - # The threshold is set at 7120, hence the solution is accepted. - assert_(accept(rnd.RandomState(), One(), One(), VarObj(7120))) - - -def test_history(): - """ - Test if AT correctly stores the history of the thresholds correctly. - """ - accept = AdaptiveThreshold(eta=0.5, gamma=4) - - accept(rnd.RandomState(), One(), One(), VarObj(7100)) - assert_equal(accept.history, [7100]) - - accept(rnd.RandomState(), One(), One(), VarObj(7200)) - assert_equal(accept.history, [7100, 7200]) - - accept(rnd.RandomState(), One(), One(), VarObj(7120)) - assert_equal(accept.history, [7100, 7200, 7120]) - - accept(rnd.RandomState(), One(), One(), VarObj(7100)) - assert_equal(accept.history, [7100, 7200, 7120, 7100]) - - accept(rnd.RandomState(), One(), One(), VarObj(7200)) - assert_equal(accept.history, [7200, 7120, 7100, 7200]) diff --git a/alns/accept/tests/test_moving_average_threshold.py b/alns/accept/tests/test_moving_average_threshold.py new file mode 100644 index 00000000..36713730 --- /dev/null +++ b/alns/accept/tests/test_moving_average_threshold.py @@ -0,0 +1,124 @@ +import numpy.random as rnd +from numpy.testing import assert_, assert_equal, assert_raises +from pytest import mark + +from alns.accept import MovingAverageThreshold +from alns.tests.states import One, Two, VarObj, Zero + + +@mark.parametrize( + "eta, gamma", + [ + (-1, 3), # eta cannot be < 0 + (2, 3), # eta cannot be > 1 + (0.5, -2), # gamma cannot be < 0 + (0.5, 0), # gamma cannot be 0 + ], +) +def test_raise_invalid_parameters(eta, gamma): + with assert_raises(ValueError): + MovingAverageThreshold(eta=eta, gamma=gamma) + + +@mark.parametrize("eta, gamma", [(1, 3), (0.4, 4)]) +def test_no_raise_valid_parameters(eta, gamma): + MovingAverageThreshold(eta=eta, gamma=gamma) + + +@mark.parametrize("eta", [0, 0.01, 0.5, 0.99, 1]) +def test_eta(eta): + moving_average = MovingAverageThreshold(eta, 3) + assert_equal(moving_average.eta, eta) + + +@mark.parametrize("gamma", range(1, 10)) +def test_gamma(gamma): + moving_average = MovingAverageThreshold(0.5, gamma) + assert_equal(moving_average.gamma, gamma) + + +def test_accepts_below_threshold(): + moving_average = MovingAverageThreshold(eta=0.5, gamma=4) + moving_average(rnd.RandomState(), One(), One(), One()) + moving_average(rnd.RandomState(), One(), One(), Zero()) + + # The threshold is set at 0 + 0.5 * (0.5 - 0) = 0.25 + assert_(moving_average(rnd.RandomState(), One(), One(), Zero())) + + +def test_rejects_above_threshold(): + moving_average = MovingAverageThreshold(eta=0.5, gamma=4) + moving_average(rnd.RandomState(), One(), One(), Two()) + moving_average(rnd.RandomState(), One(), One(), Zero()) + + # The threshold is set at 0 + 0.5 * (1 - 0) = 0.5 + assert_(not moving_average(rnd.RandomState(), One(), One(), One())) + + +def test_accepts_equal_threshold(): + moving_average = MovingAverageThreshold(eta=0.5, gamma=4) + moving_average(rnd.RandomState(), One(), One(), VarObj(7100)) + moving_average(rnd.RandomState(), One(), One(), VarObj(7200)) + + # The threshold is set at 7100 + 0.5 * (7140 - 7100) = 7120 + assert_(moving_average(rnd.RandomState(), One(), One(), VarObj(7120))) + + +def test_accepts_over_gamma_candidates(): + moving_average = MovingAverageThreshold(eta=0.2, gamma=3) + moving_average(rnd.RandomState(), One(), One(), VarObj(7100)) + moving_average(rnd.RandomState(), One(), One(), VarObj(7200)) + moving_average(rnd.RandomState(), One(), One(), VarObj(7200)) + + # The threshold is set at 7000 + 0.2 * (7133.33 - 7000) = 7013.33 + assert_(moving_average(rnd.RandomState(), One(), One(), VarObj(7000))) + + +def test_rejects_over_gamma_candidates(): + moving_average = MovingAverageThreshold(eta=0.2, gamma=3) + + for value in [7100, 7200, 7200, 7000]: + moving_average(rnd.RandomState(), One(), One(), VarObj(value)) + + # The threshold is set at 7000 + 0.2 * (7100 - 7000) = 7020 + result = moving_average(rnd.RandomState(), One(), One(), VarObj(7100)) + assert_(not result) + + +def test_evaluate_consecutive_solutions(): + """ + Test if MAT correctly accepts and rejects consecutive solutions. + """ + moving_average = MovingAverageThreshold(eta=0.5, gamma=4) + + # The threshold is set at 7100, hence the solution is accepted. + assert_(moving_average(rnd.RandomState(), One(), One(), VarObj(7100))) + + # The threshold is set at 7125, hence the solution is accepted. + result = moving_average(rnd.RandomState(), One(), One(), VarObj(7200)) + assert_(not result) + + # The threshold is set at 7120, hence the solution is accepted. + assert_(moving_average(rnd.RandomState(), One(), One(), VarObj(7120))) + + +def test_history(): + """ + Test if MAT correctly stores the history of the thresholds correctly. + """ + moving_average = MovingAverageThreshold(eta=0.5, gamma=4) + + moving_average(rnd.RandomState(), One(), One(), VarObj(7100)) + assert_equal(moving_average.history, [7100]) + + moving_average(rnd.RandomState(), One(), One(), VarObj(7200)) + assert_equal(moving_average.history, [7100, 7200]) + + moving_average(rnd.RandomState(), One(), One(), VarObj(7120)) + assert_equal(moving_average.history, [7100, 7200, 7120]) + + moving_average(rnd.RandomState(), One(), One(), VarObj(7100)) + assert_equal(moving_average.history, [7100, 7200, 7120, 7100]) + + moving_average(rnd.RandomState(), One(), One(), VarObj(7200)) + assert_equal(moving_average.history, [7200, 7120, 7100, 7200]) diff --git a/alns/accept/tests/test_worse_accept.py b/alns/accept/tests/test_random_accept.py similarity index 62% rename from alns/accept/tests/test_worse_accept.py rename to alns/accept/tests/test_random_accept.py index bc2062c4..0bb8bb56 100644 --- a/alns/accept/tests/test_worse_accept.py +++ b/alns/accept/tests/test_random_accept.py @@ -4,7 +4,7 @@ from numpy.testing import assert_, assert_equal, assert_raises from pytest import mark -from alns.accept import WorseAccept +from alns.accept import RandomAccept from alns.tests.states import One, Two, Zero @@ -22,7 +22,7 @@ ) def test_raises_invalid_parameters(start, end, step, method): with assert_raises(ValueError): - WorseAccept(start, end, step, method) + RandomAccept(start, end, step, method) @mark.parametrize( @@ -36,7 +36,7 @@ def test_raises_invalid_parameters(start, end, step, method): ], ) def test_no_raise_valid_parameters(start, end, step, method): - WorseAccept(start, end, step, method) + RandomAccept(start, end, step, method) @mark.parametrize( @@ -53,80 +53,82 @@ def test_properties(start, end, step, method): """ Tests if the properties are correctly set. """ - worse_accept = WorseAccept(start, end, step, method) + random_accept = RandomAccept(start, end, step, method) - assert_equal(worse_accept.start_prob, start) - assert_equal(worse_accept.end_prob, end) - assert_equal(worse_accept.step, step) - assert_equal(worse_accept.method, method) + assert_equal(random_accept.start_prob, start) + assert_equal(random_accept.end_prob, end) + assert_equal(random_accept.step, step) + assert_equal(random_accept.method, method) def test_zero_prob_accepts_better(): """ - Tests if WA with a zero start probability accepts better solutions. + Tests if random accept with a zero start probability accepts better + solutions. """ rnd_vals = [1] rng = Mock(spec_set=rnd.RandomState, random=lambda: rnd_vals.pop(0)) - worse_accept = WorseAccept(0, 0, 0.1) + random_accept = RandomAccept(0, 0, 0.1) - assert_(worse_accept(rng, Zero(), One(), Zero())) - assert_(worse_accept(rng, Zero(), Two(), Zero())) + assert_(random_accept(rng, Zero(), One(), Zero())) + assert_(random_accept(rng, Zero(), Two(), Zero())) def test_zero_prob_never_accept_worse(): """ - Tests if WA with a zero start probability does not accept worse solutions. + Tests if random accept with a zero start probability does not accept worse + solutions. """ - worse_accept = WorseAccept(0, 0, 0, "linear") + random_accept = RandomAccept(0, 0, 0, "linear") - assert_(not worse_accept(rnd.RandomState(), Zero(), One(), One())) - assert_(not worse_accept(rnd.RandomState(), Zero(), Zero(), One())) + assert_(not random_accept(rnd.RandomState(), Zero(), One(), One())) + assert_(not random_accept(rnd.RandomState(), Zero(), Zero(), One())) def test_one_prob_always_accept(): """ - Tests if WA with a fixed probability of 1 leads to always accepting - solutions. + Tests if random accept with a fixed probability of 1 leads to always + accepting solutions. """ - worse_accept = WorseAccept(1, 0, 0, "linear") + random_accept = RandomAccept(1, 0, 0, "linear") for _ in range(100): - assert_(worse_accept(rnd.RandomState(), Zero(), Zero(), One())) + assert_(random_accept(rnd.RandomState(), Zero(), Zero(), One())) def test_linear_consecutive_solutions(): """ - Test if WA with linear updating method correctly accepts and rejects - consecutive solutions. + Test if random accept with linear updating method correctly accepts and + rejects consecutive solutions. """ rnd_vals = [0.9, 0.8, 0.7, 0.6, 0.5, 1] rng = Mock(spec_set=rnd.RandomState, random=lambda: rnd_vals.pop(0)) - worse_accept = WorseAccept(1, 0, 0.1, "linear") + random_accept = RandomAccept(1, 0, 0.1, "linear") # For the first five, the probability is, resp., 1, 0.9, 0.8, 0.7, 0.6 # The random draw is, resp., 0.9, 0.8, 0.7, 0.6, 0.5 so the worsening # solution is still accepted. for _ in range(5): - assert_(worse_accept(rng, Zero(), Zero(), One())) + assert_(random_accept(rng, Zero(), Zero(), One())) # The probability is now 0.5 and the draw is 1, so reject. - assert_(not worse_accept(rng, Zero(), Zero(), One())) + assert_(not random_accept(rng, Zero(), Zero(), One())) def test_exponential_consecutive_solutions(): """ - Test if WA with exponential updating method correctly accepts and rejects - consecutive solutions. + Test if random accept with exponential updating method correctly accepts + and rejects consecutive solutions. """ rnd_vals = [0.5, 0.25, 0.125, 1] rng = Mock(spec_set=rnd.RandomState, random=lambda: rnd_vals.pop(0)) - worse_accept = WorseAccept(1, 0, 0.5, "exponential") + random_accept = RandomAccept(1, 0, 0.5, "exponential") # For the first three, the probability is, resp., 1, 0.5, 0.25 # The random draw is, resp., 0.5, 0.25, 0.125, so the worsening # solution is still accepted. for _ in range(3): - assert_(worse_accept(rng, Zero(), Zero(), One())) + assert_(random_accept(rng, Zero(), Zero(), One())) # The probability is now 0.5 and the draw is 1, so reject. - assert_(not worse_accept(rng, Zero(), Zero(), One())) + assert_(not random_accept(rng, Zero(), Zero(), One())) diff --git a/alns/accept/tests/test_random_walk.py b/alns/accept/tests/test_random_walk.py index eda1e609..c5bd47b3 100644 --- a/alns/accept/tests/test_random_walk.py +++ b/alns/accept/tests/test_random_walk.py @@ -1,30 +1,30 @@ import numpy.random as rnd from numpy.testing import assert_ -from alns.accept import RandomWalk +from alns.accept import AlwaysAccept from alns.tests.states import One, Zero def test_accepts_better(): """ - Tests if the random walk method accepts a better solution. + Tests if the always accept method accepts a better solution. """ - random_walk = RandomWalk() - assert_(random_walk(rnd.RandomState(), One(), One(), Zero())) + always_accept = AlwaysAccept() + assert_(always_accept(rnd.RandomState(), One(), One(), Zero())) def test_accepts_worse(): """ - Tests if the random walk method accepts a worse solution. + Tests if the always accept method accepts a worse solution. """ - random_walk = RandomWalk() - assert_(random_walk(rnd.RandomState(), Zero(), Zero(), One())) + always_accept = AlwaysAccept() + assert_(always_accept(rnd.RandomState(), Zero(), Zero(), One())) def test_accepts_equal(): """ - Tests if the random walk method accepts a solution that results in the + Tests if the always accept method accepts a solution that results in the same objective value. """ - random_walk = RandomWalk() - assert_(random_walk(rnd.RandomState(), Zero(), Zero(), Zero())) + always_accept = AlwaysAccept() + assert_(always_accept(rnd.RandomState(), Zero(), Zero(), Zero())) diff --git a/docs/source/api/accept.rst b/docs/source/api/accept.rst index 4005ec88..e0cb6c13 100644 --- a/docs/source/api/accept.rst +++ b/docs/source/api/accept.rst @@ -13,29 +13,29 @@ All acceptance criteria implement :class:`~alns.accept.AcceptanceCriterion.Accep .. automodule:: alns.accept.AcceptanceCriterion :members: -.. automodule:: alns.accept.AdaptiveThreshold +.. automodule:: alns.accept.AlwaysAccept :members: .. automodule:: alns.accept.GreatDeluge :members: -.. automodule:: alns.accept.NonLinearGreatDeluge - :members: - .. automodule:: alns.accept.HillClimbing :members: .. automodule:: alns.accept.LateAcceptanceHillClimbing :members: -.. automodule:: alns.accept.RandomWalk +.. automodule:: alns.accept.MovingAverageThreshold :members: -.. automodule:: alns.accept.RecordToRecordTravel +.. automodule:: alns.accept.NonLinearGreatDeluge :members: -.. automodule:: alns.accept.SimulatedAnnealing +.. automodule:: alns.accept.RandomAccept :members: -.. automodule:: alns.accept.WorseAccept +.. automodule:: alns.accept.RecordToRecordTravel + :members: + +.. automodule:: alns.accept.SimulatedAnnealing :members: