Skip to content

Commit

Permalink
Release v4.1.0 (#87)
Browse files Browse the repository at this point in the history
* Update tests; move TA into RRT as a special case

* Bump version

* Update readme

* Update notebooks

* Fix acceptance criteria call signature

* Add flake8 for linting

* Take poetry install step out of deployment
  • Loading branch information
N-Wouda authored Jun 25, 2022
1 parent 078681b commit 6ad1589
Show file tree
Hide file tree
Showing 31 changed files with 438 additions and 482 deletions.
9 changes: 5 additions & 4 deletions .github/workflows/alns.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ jobs:
python -m pip install --upgrade pip
pip install poetry
poetry install
- name: Run tests
- name: Run pytest
run: poetry run pytest
- name: Black
- name: Run black
uses: psf/black@stable
- name: Run static analysis
- name: Run flake8
uses: py-actions/flake8@v2
- name: Run mypy
run: poetry run mypy alns
- uses: codecov/codecov-action@v2
deploy:
Expand All @@ -46,7 +48,6 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install poetry
poetry install
- name: Deploy to PyPI
run: |
poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }}
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ your own.
The acceptance criterion determines the acceptance of a new solution state at
each iteration. An overview of common acceptance criteria is given in
[Santini et al. (2018)][3]. Several have already been implemented for you, in
`alns.accept`:
`alns.accept`. These include:

- `HillClimbing`. The simplest acceptance criterion, hill-climbing solely
accepts solutions improving the objective value.
Expand All @@ -69,6 +69,7 @@ each iteration. An overview of common acceptance criteria is given in
- `SimulatedAnnealing`. This criterion accepts solutions when the
scaled probability is bigger than some random number, using an
updating temperature.
- And many more!

Each acceptance criterion inherits from `AcceptanceCriterion`, which may be used
to write your own.
Expand Down
16 changes: 8 additions & 8 deletions alns/ALNS.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from collections import defaultdict
import logging
import time
from collections import defaultdict
from typing import Callable, Dict, Iterable, List, Optional, Tuple

import numpy as np
Expand Down Expand Up @@ -65,8 +65,8 @@ def destroy_operators(self) -> List[Tuple[str, _OperatorType]]:
Returns
-------
A list of (name, operator) tuples. Their order is the same as the one in
which they were passed to the ALNS instance.
A list of (name, operator) tuples. Their order is the same as the one
in which they were passed to the ALNS instance.
"""
return list(self._d_ops.items())

Expand All @@ -77,8 +77,8 @@ def repair_operators(self) -> List[Tuple[str, _OperatorType]]:
Returns
-------
A list of (name, operator) tuples. Their order is the same as the one in
which they were passed to the ALNS instance.
A list of (name, operator) tuples. Their order is the same as the one
in which they were passed to the ALNS instance.
"""
return list(self._r_ops.items())

Expand All @@ -90,8 +90,8 @@ def add_destroy_operator(self, op: _OperatorType, name: str = None):
----------
op
An operator that, when applied to the current state, returns a new
state reflecting its implemented destroy action. The second argument
is the random state constructed from the passed-in seed.
state reflecting its implemented destroy action. The second
argument is the random state constructed from the passed-in seed.
name
Optional name argument, naming the operator. When not passed, the
function name is used instead.
Expand Down Expand Up @@ -260,7 +260,7 @@ def on_best(self, func: _OperatorType):
Parameters
----------
func
A function that should take a solution State as its first parameter,
A function that should take a solution State as its first argument,
and a numpy RandomState as its second (cf. the operator signature).
It should return a (new) solution State.
"""
Expand Down
12 changes: 6 additions & 6 deletions alns/Result.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,12 @@ def plot_operator_counts(
legend
Optional legend entries. When passed, this should be a list of at
most four strings. The first string describes the number of times
a best solution was found, the second a better, the third a solution
was accepted but did not improve upon the current or global best,
and the fourth the number of times a solution was rejected. If less
than four strings are passed, only the first len(legend) count types
are plotted. When not passed, a sensible default is set and all
counts are shown.
a best solution was found, the second a better, the third a
solution was accepted but did not improve upon the current or
global best, and the fourth the number of times a solution was
rejected. If less than four strings are passed, only the first
len(legend) count types are plotted. When not passed, a sensible
default is set and all counts are shown.
kwargs
Optional arguments passed to each call of ``ax.barh``.
"""
Expand Down
9 changes: 5 additions & 4 deletions alns/accept/GreatDeluge.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class GreatDeluge(AcceptanceCriterion):
The threshold is updated in each iteration as
``threshold = threshold - beta * (threshold - candidate.objective()``
``threshold = threshold - beta * (threshold - candidate.objective())``
The implementation is based on the description of GD in [2].
Expand Down Expand Up @@ -52,12 +52,13 @@ def alpha(self):
def beta(self):
return self._beta

def __call__(self, rnd, best, curr, cand):
def __call__(self, rnd, best, current, candidate):
if self._threshold is None:
self._threshold = self.alpha * best.objective()

res = cand.objective() < self._threshold
diff = self._threshold - candidate.objective()
res = diff > 0

self._threshold -= self.beta * (self._threshold - cand.objective())
self._threshold -= self.beta * diff

return res
21 changes: 10 additions & 11 deletions alns/accept/LateAcceptanceHillClimbing.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@


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
Expand All @@ -15,7 +14,7 @@ class LateAcceptanceHillClimbing(AcceptanceCriterion):
Parameters
----------
lookback_period: int
Nonnegative integer specifying which solution to compare against
Non-negative integer specifying which solution to compare against
for late acceptance. In particular, LAHC compares against the
then-current solution from `lookback_period` iterations ago.
If set to 0, then LAHC reverts to regular hill climbing.
Expand All @@ -25,7 +24,7 @@ class LateAcceptanceHillClimbing(AcceptanceCriterion):
better_history: bool
If set, LAHC uses a history management strategy where current solutions
are stored only if they improve the then-current solution from
`lookback_period` iterations ago. Otherwise the then-current solution
`lookback_period` iterations ago. Otherwise, the then-current solution
is stored again.
References
Expand All @@ -45,7 +44,7 @@ def __init__(
self._better_history = better_history

if lookback_period < 0:
raise ValueError("lookback_period must be a nonnegative integer.")
raise ValueError("lookback_period must be a non-negative integer.")

self._history: deque = deque([], maxlen=lookback_period)

Expand All @@ -61,19 +60,19 @@ def greedy(self):
def better_history(self):
return self._better_history

def __call__(self, rnd, best, curr, cand):
def __call__(self, rnd, best, current, candidate):
if not self._history:
self._history.append(curr.objective())
return cand.objective() < curr.objective()
self._history.append(current.objective())
return candidate.objective() < current.objective()

res = cand.objective() < self._history[0]
res = candidate.objective() < self._history[0]

if not res and self._greedy:
res = cand.objective() < curr.objective()
res = candidate.objective() < current.objective()

if self._better_history:
self._history.append(min(curr.objective(), self._history[0]))
self._history.append(min(current.objective(), self._history[0]))
else:
self._history.append(curr.objective())
self._history.append(current.objective())

return res
33 changes: 17 additions & 16 deletions alns/accept/NonLinearGreatDeluge.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,31 @@

class NonLinearGreatDeluge(GreatDeluge):
"""
The Non Linear Great Deluge (NLGD) criterion accepts solutions if the
candidate solution has a value lower than a threshold (originally called the
water level [1]). The initial threshold is computed as
The Non-Linear Great Deluge (NLGD) criterion accepts solutions if the
candidate solution has a value lower than a threshold (originally called
the water level [1]). The initial threshold is computed as
``threshold = alpha * initial.objective()``
where ``initial`` is the initial solution passed-in to ALNS.
The non-linear GD variant was proposed by [2]. It differs from GD by using
a non-linear updating scheme, see the ``_update`` method for more details.
Moreover, candidate solutions that improve the current solution are always
accepted.
a non-linear updating scheme; see the ``_compute_threshold`` method for
details. Moreover, candidate solutions that improve the current solution
are always accepted.
The implementation is based on the description in [2].
Parameters
----------
alpha
Factor used to compute the initial threshold
Factor used to compute the initial threshold. See [2] for details.
beta
Factor used to update the threshold
Factor used to update the threshold. See [2] for details.
gamma
Factor used to update the threshold
Factor used to update the threshold. See [2] for details.
delta
Factor used to update the threshold
Factor used to update the threshold. See [2] for details.
References
----------
Expand Down Expand Up @@ -61,25 +61,26 @@ def gamma(self):
def delta(self):
return self._delta

def __call__(self, rnd, best, curr, cand):
def __call__(self, rnd, best, current, candidate):
if self._threshold is None:
if best.objective() == 0:
raise ValueError("Initial solution cannot have zero value.")

self._threshold = self._alpha * best.objective()

res = cand.objective() < self._threshold
res = candidate.objective() < self._threshold

if not res:
res = cand.objective() < curr.objective() # Accept if improving
# Accept if improving
res = candidate.objective() < current.objective()

self._threshold = self._update(best, curr, cand)
self._threshold = self._compute_threshold(best, current, candidate)

return res

def _update(self, best, curr, cand):
def _compute_threshold(self, best, curr, cand):
"""
Return the updated threshold value.
Returns the new threshold value.
First, the relative gap between the candidate solution and threshold
is computed. If this relative gap is less than ``beta``, then the
Expand Down
23 changes: 18 additions & 5 deletions alns/accept/RecordToRecordTravel.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
class RecordToRecordTravel(AcceptanceCriterion):
"""
The Record-to-Record Travel (RRT) criterion accepts a candidate solution
if the absolute gap between the candidate and best solution is smaller than
a threshold. The threshold is updated in each iteration as:
if the absolute gap between the candidate and the best or current solution
is smaller than a threshold. The threshold is updated in each iteration as:
``threshold = max(end_threshold, threshold - step)`` (linear)
Expand All @@ -29,6 +29,13 @@ class RecordToRecordTravel(AcceptanceCriterion):
method
The updating method, one of {'linear', 'exponential'}. Default
'linear'.
cmp_best
This parameter determines whether we use default RRT (True), or
threshold accepting (False). By default, `cmp_best` is True, in which
case RRT checks whether the difference between the candidate and best
solution is below the threshold [2]. If `cmp_best` is False, RRT takes
the difference between the candidate and current solution instead. This
yields the behaviour of threshold accepting (TA), see [3] for details.
References
----------
Expand All @@ -38,6 +45,9 @@ class RecordToRecordTravel(AcceptanceCriterion):
[2]: Dueck, G. New optimization heuristics: The great deluge algorithm and
the record-to-record travel. *Journal of Computational Physics* (1993)
104 (1): 86-92.
[3]: Dueck, G., Scheuer, T. Threshold accepting: A general purpose
optimization algorithm appearing superior to simulated annealing.
*Journal of Computational Physics* (1990) 90 (1): 161-175.
"""

def __init__(
Expand All @@ -46,6 +56,7 @@ def __init__(
end_threshold: float,
step: float,
method: str = "linear",
cmp_best: bool = True,
):
if start_threshold < 0 or end_threshold < 0 or step < 0:
raise ValueError("Thresholds and step must be non-negative.")
Expand All @@ -63,6 +74,7 @@ def __init__(
self._end_threshold = end_threshold
self._step = step
self._method = method
self._cmp_best = cmp_best

self._threshold = start_threshold

Expand All @@ -83,14 +95,15 @@ def method(self) -> str:
return self._method

def __call__(self, rnd, best, current, candidate):
# This follows from the paper by Dueck (1993), p. 87.
result = (candidate.objective() - best.objective()) <= self._threshold
# From [2] p. 87 (RRT; best), and [3] p. 162 (TA; current).
baseline = best if self._cmp_best else current
res = candidate.objective() - baseline.objective() <= self._threshold

self._threshold = max(
self.end_threshold, update(self._threshold, self.step, self.method)
)

return result
return res

@classmethod
def autofit(
Expand Down
4 changes: 2 additions & 2 deletions alns/accept/SimulatedAnnealing.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,8 @@ def autofit(
Neighborhood Search Heuristic for the Pickup and Delivery Problem
with Time Windows." _Transportation Science_ 40 (4): 455 - 472.
[2]: Roozbeh et al. 2018. "An Adaptive Large Neighbourhood Search for
asset protection during escaped wildfires." _Computers & Operations
Research_ 97: 125 - 134.
asset protection during escaped wildfires." _Computers &
Operations Research_ 97: 125 - 134.
"""
if not (0 <= worse <= 1):
raise ValueError("worse outside [0, 1] not understood.")
Expand Down
Loading

0 comments on commit 6ad1589

Please sign in to comment.