Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HillClimbing acceptance criterion #82

Merged
merged 23 commits into from
Jun 22, 2022
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e32a7eb
HillClimbing acceptance criterion
leonlan Jun 5, 2022
1096d3d
Merge branch 'master' into hill-climbing
leonlan Jun 6, 2022
e9baa05
Separate HC (revert to original implementation)
leonlan Jun 6, 2022
8ea1c2c
Add LAHC to init
leonlan Jun 6, 2022
140f055
Merge branch 'master' of github.com:N-Wouda/ALNS into hill-climbing
leonlan Jun 16, 2022
2627ff8
Merge branch 'master' into hill-climbing
leonlan Jun 17, 2022
364bea6
Merge branch 'hill-climbing' of github.com:leonlan/ALNS into hill-cli…
leonlan Jun 17, 2022
22dc171
Implement and tested LAHC
leonlan Jun 17, 2022
f162563
Replace None in tests LAHC
leonlan Jun 17, 2022
448727a
Textual improvements LAHC test
leonlan Jun 17, 2022
bebc837
Change default values LAHC
leonlan Jun 17, 2022
c137063
Add test for update method
leonlan Jun 17, 2022
c3139ed
Add reference to LAHC
leonlan Jun 17, 2022
ff52bc9
refactor __update__, new varnames
leonlan Jun 20, 2022
f2bc32a
test name change, new collect better test
leonlan Jun 20, 2022
78f65ce
Merge branch 'master' of github.com:N-Wouda/ALNS into hill-climbing
leonlan Jun 20, 2022
c24b9eb
new varnames, refactor call, reg_hc added
leonlan Jun 21, 2022
0f98d08
remove unused import, docstring changes
leonlan Jun 21, 2022
a442581
Remove lookback_period typecheck
leonlan Jun 22, 2022
89ec566
Fix assertion test
leonlan Jun 22, 2022
e020ae8
Modify tests for case lookback_period=0
leonlan Jun 22, 2022
3760dff
Small textual improvements
leonlan Jun 22, 2022
a581493
Test docstring textual improvements
leonlan Jun 22, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions alns/accept/LateAcceptanceHillClimbing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from collections import deque

from alns.accept.AcceptanceCriterion import AcceptanceCriterion


class LateAcceptanceHillClimbing(AcceptanceCriterion):
"""
The Late Acceptance Hill Climbing (LAHC) criterion accepts a candidate
solution when it is better than the current solution from a number of
iterations before.

This implementation is based on the description of LAHC in [1].

Parameters
----------
history_length: int
Non-negative integer specifying the maximum number of previous solutions
to be stored. Default: 0 (i.e., no objectives stored).
greedy: bool
Bool indicating whether or not to accept candidate solutions if they are
better than the current solution. Default: False.
collect_better: bool
Bool indicating whether or not to only collect current solutions that
are better than the previous current. Default: False
N-Wouda marked this conversation as resolved.
Show resolved Hide resolved

References
----------
[1]: Burke, E. K., & Bykov, Y. The late acceptance hill-climbing heuristic.
*European Journal of Operational Research* (2017), 258(1), 70-78.
"""

def __init__(
self,
history_length: int = 0,
greedy: bool = False,
collect_better: bool = False,
N-Wouda marked this conversation as resolved.
Show resolved Hide resolved
):
self._history_length = history_length
self._greedy = greedy
self._collect_better = collect_better

if not isinstance(history_length, int) or history_length < 0:
raise ValueError("history_length must be a non-negative integer.")

self._objectives: deque = deque([], maxlen=history_length)
N-Wouda marked this conversation as resolved.
Show resolved Hide resolved

@property
def history_length(self):
return self._history_length

@property
def greedy(self):
return self._greedy

@property
def collect_better(self):
return self._collect_better

def __call__(self, rnd, best, curr, cand):
cand_obj = cand.objective()
curr_obj = curr.objective()

if self._objectives and self._collect_better:
self._objectives.append(min(curr_obj, self._objectives[0]))
else:
self._objectives.append(curr_obj)

res = cand_obj < self._objectives[0]
N-Wouda marked this conversation as resolved.
Show resolved Hide resolved

if not res and self._greedy:
res = cand_obj < curr_obj # Accept if improving

return res
1 change: 1 addition & 0 deletions alns/accept/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from .AcceptanceCriterion import AcceptanceCriterion
from .GreatDeluge import GreatDeluge
from .HillClimbing import HillClimbing
from .LateAcceptanceHillClimbing import LateAcceptanceHillClimbing
from .NonLinearGreatDeluge import NonLinearGreatDeluge
from .RecordToRecordTravel import RecordToRecordTravel
from .SimulatedAnnealing import SimulatedAnnealing
Expand Down
116 changes: 116 additions & 0 deletions alns/accept/tests/test_late_acceptance_hill_climbing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import numpy.random as rnd
from numpy.testing import assert_, assert_equal, assert_raises
from pytest import mark

from alns.accept import LateAcceptanceHillClimbing
from alns.tests.states import Zero, One, Two


@mark.parametrize("history_length", [-0.01, -10, 1.5])
def test_raises_invalid_history_length(history_length):
with assert_raises(ValueError):
LateAcceptanceHillClimbing(history_length=history_length)


@mark.parametrize(
"history_length, greedy, collect_better",
[
(1, True, True),
(10, False, True),
(100, True, False),
(1000, False, False),
],
)
def test_properties(history_length, greedy, collect_better):
lahc = LateAcceptanceHillClimbing(history_length, greedy, collect_better)

assert_equal(lahc.history_length, history_length)
assert_equal(lahc.collect_better, collect_better)
assert_equal(lahc.greedy, greedy)
N-Wouda marked this conversation as resolved.
Show resolved Hide resolved


@mark.parametrize("history_length", [3, 10, 50])
def test_accept(history_length):
"""
Tests if LAHC accepts a solution that is better than the current solution
from history_length iterations ago.
"""
lahc = LateAcceptanceHillClimbing(history_length, False, False)

for _ in range(history_length):
assert_(lahc(rnd.RandomState(), Zero(), Two(), One()))

# The previous current solution history_length ago has value 2, so the
# candidate solution with value 1 should be accepted.
assert_(lahc(rnd.RandomState(), Zero(), Zero(), One()))


@mark.parametrize("history_length", [3, 10, 50])
def test_reject(history_length):
"""
Tests if LAHC rejects a solution that is worse than the current solution
history_length iterations ago.
"""
lahc = LateAcceptanceHillClimbing(history_length, False, False)

for _ in range(history_length):
assert_(lahc(rnd.RandomState(), Zero(), One(), Zero()))

# The compared previous current solution has value 1, so the
# candidate solution with value 1 should be rejected.
assert_(not lahc(rnd.RandomState(), Zero(), Two(), One()))


@mark.parametrize("history_length", [3, 10, 50])
def test_greedy_accept(history_length):
"""
Tests if LAHC criterion with greedy=True accepts a solution that
is better than the current solution despite being worse than the
previous current solution from history_length iterations ago.
"""
lahc = LateAcceptanceHillClimbing(history_length, True, False)

for _ in range(history_length):
assert_(not lahc(rnd.RandomState(), Zero(), One(), Two()))

# The candidate (1) is better than the current (2), hence it is accepted
# despite being worse than the previous current (1).
assert_(lahc(rnd.RandomState(), Zero(), Two(), One()))


def test_collect_better():
"""
Tests if only current solutions are stored that are better than
the compared previous solution when only_better=True.
"""
lahc = LateAcceptanceHillClimbing(1, False, True)

assert_(lahc(rnd.RandomState(), Zero(), One(), Zero()))

# Previous current stays at 1 because 2 it not better
assert_(not lahc(rnd.RandomState(), Zero(), Two(), One()))
assert_(lahc(rnd.RandomState(), Zero(), Two(), Zero()))

# Previous current updates to 0
assert_(not lahc(rnd.RandomState(), Zero(), Zero(), Zero()))


@mark.parametrize("history_length", [3, 10, 50])
def test_collect_better_reject(history_length):
"""
Tests if LAHC criterion with collect_better=True rejects a solution that
is better than the previous current solution from history_length iterations
ago, because that previous current solution was not better than the current
solution from (2 * history_length) iterations ago.
"""
lahc = LateAcceptanceHillClimbing(history_length, False, True)

for _ in range(history_length):
assert_(not lahc(rnd.RandomState(), Zero(), One(), Two()))

for _ in range(history_length):
# The current solutions are not stored because they are worse
# than the previous current solutions
assert_(not lahc(rnd.RandomState(), Zero(), Two(), Two()))

assert_(not lahc(rnd.RandomState(), Zero(), Two(), One()))