Skip to content

Commit

Permalink
Release 3.0.0 (#67)
Browse files Browse the repository at this point in the history
* Module name changes, update notebooks and readme

* Black code formatting

* Add black to GH Actions workflow
  • Loading branch information
N-Wouda authored May 18, 2022
1 parent 2550a14 commit 639a616
Show file tree
Hide file tree
Showing 43 changed files with 670 additions and 556 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/alns.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ jobs:
pip install poetry
poetry install
- name: Run tests
run: |
poetry run pytest
run: poetry run pytest
- name: Black
uses: psf/black@stable
- name: Run static analysis
run: |
poetry run mypy alns
run: poetry run mypy alns
- uses: codecov/codecov-action@v2
deploy:
needs: build
Expand Down
2 changes: 1 addition & 1 deletion LICENSE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
The MIT License (MIT)

Copyright (c) 2019 Niels Wouda
Copyright (c) 2019 Niels Wouda and contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
33 changes: 23 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ showing how the ALNS library may be used. These include:
using a number of different operators and enhancement techniques from the
literature.

Finally, the weight schemes and acceptance criteria notebook gives an overview
of various options available in the `alns` package (explained below). In the
notebook we use these different options to solve a toy 0/1-knapsack problem. The
notebook is a good starting point for when you want to use the different schemes
and criteria yourself. It is available [here][5].
Finally, the features notebook gives an overview of various options available
in the `alns` package (explained below). In the notebook we use these different
options to solve a toy 0/1-knapsack problem. The notebook is a good starting
point for when you want to use different schemes, acceptance or stopping criteria
yourself. It is available [here][5].

## How to use
The `alns` package exposes two classes, `ALNS` and `State`. The first
Expand All @@ -43,7 +43,7 @@ criterion_.
### Weight scheme
The weight scheme determines how to select destroy and repair operators in each
iteration of the ALNS algorithm. Several have already been implemented for you,
in `alns.weight_schemes`:
in `alns.weights`:

- `SimpleWeights`. This weight scheme applies a convex combination of the
existing weight vector, and a reward given for the current candidate
Expand All @@ -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.criteria`:
`alns.accept`:

- `HillClimbing`. The simplest acceptance criterion, hill-climbing solely
accepts solutions improving the objective value.
Expand All @@ -70,8 +70,21 @@ each iteration. An overview of common acceptance criteria is given in
scaled probability is bigger than some random number, using an
updating temperature.

Each acceptance criterion inherits from `AcceptanceCriterion`, which may
be used to write your own.
Each acceptance criterion inherits from `AcceptanceCriterion`, which may be used
to write your own.

### Stoppping criterion
The stopping criterion determines when ALNS should stop iterating. Several
commonly used stopping criteria have already been implemented for you, in
`alns.stop`:

- `MaxIterations`. This stopping criterion stops the heuristic search after a
given number of iterations.
- `MaxRuntime`. This stopping criterion stops the heuristic search after a given
number of seconds.

Each stopping criterion inherits from `StoppingCriterion`, which may be used to
write your own.

## References
- Pisinger, D., and Ropke, S. (2010). Large Neighborhood Search. In M.
Expand All @@ -85,5 +98,5 @@ be used to write your own.
[2]: https://github.com/N-Wouda/ALNS/blob/master/examples/travelling_salesman_problem.ipynb
[3]: https://link.springer.com/article/10.1007%2Fs10732-018-9377-x
[4]: https://github.com/N-Wouda/ALNS/blob/master/examples/cutting_stock_problem.ipynb
[5]: https://github.com/N-Wouda/ALNS/blob/master/examples/weight_schemes_acceptance_criteria.ipynb
[5]: https://github.com/N-Wouda/ALNS/blob/master/examples/alns_features.ipynb
[6]: https://github.com/N-Wouda/ALNS/blob/master/examples/resource_constrained_project_scheduling_problem.ipynb
45 changes: 23 additions & 22 deletions alns/ALNS.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
from alns.Result import Result
from alns.State import State
from alns.Statistics import Statistics
from alns.criteria import AcceptanceCriterion
from alns.weight_schemes import WeightScheme
from alns.stopping_criteria import StoppingCriterion
from alns.accept import AcceptanceCriterion
from alns.stop import StoppingCriterion
from alns.weights import WeightScheme

# Potential candidate solution consideration outcomes.
_BEST = 0
Expand All @@ -22,26 +22,27 @@


class ALNS:
def __init__(self, rnd_state: rnd.RandomState = rnd.RandomState()):
"""
Implements the adaptive large neighbourhood search (ALNS) algorithm.
The implementation optimises for a minimisation problem, as explained
in the text by Pisinger and Røpke (2010).
Parameters
----------
rnd_state
Optional random state to use for random number generation. When
passed, this state is used for operator selection and general
computations requiring random numbers. It is also passed to the
destroy and repair operators, as a second argument.
"""
Implements the adaptive large neighbourhood search (ALNS) algorithm.
The implementation optimises for a minimisation problem, as explained
in the text by Pisinger and Røpke (2010).
Parameters
----------
rnd_state
Optional random state to use for random number generation. When
passed, this state is used for operator selection and general
computations requiring random numbers. It is also passed to the
destroy and repair operators, as a second argument.
References
----------
[1]: Pisinger, D., and Røpke, S. (2010). Large Neighborhood Search. In
M. Gendreau (Ed.), *Handbook of Metaheuristics* (2 ed., pp. 399
- 420). Springer.
"""

References
----------
[1]: Pisinger, D., and Røpke, S. (2010). Large Neighborhood Search. In
M. Gendreau (Ed.), *Handbook of Metaheuristics* (2 ed., pp. 399
- 420). Springer.
"""
def __init__(self, rnd_state: rnd.RandomState = rnd.RandomState()):
self._destroy_operators: Dict[str, _OperatorType] = {}
self._repair_operators: Dict[str, _OperatorType] = {}

Expand Down
72 changes: 40 additions & 32 deletions alns/Result.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,19 @@


class Result:
"""
Stores ALNS results. An instance of this class is returned once the
algorithm completes.
def __init__(self, best: State, statistics: Statistics):
"""
Stores ALNS results. An instance of this class is returned once the
algorithm completes.
Parameters
----------
best
The best state observed during the entire iteration.
statistics
Statistics collected during iteration.
"""

Parameters
----------
best
The best state observed during the entire iteration.
statistics
Statistics collected during iteration.
"""
def __init__(self, best: State, statistics: Statistics):
self._best = best
self._statistics = statistics

Expand All @@ -39,10 +39,12 @@ def statistics(self) -> Statistics:
"""
return self._statistics

def plot_objectives(self,
ax: Optional[Axes] = None,
title: Optional[str] = None,
**kwargs: Dict[str, Any]):
def plot_objectives(
self,
ax: Optional[Axes] = None,
title: Optional[str] = None,
**kwargs: Dict[str, Any]
):
"""
Plots the collected objective values at each iteration.
Expand Down Expand Up @@ -75,11 +77,13 @@ def plot_objectives(self,

plt.draw_if_interactive()

def plot_operator_counts(self,
fig: Optional[Figure] = None,
title: Optional[str] = None,
legend: Optional[List[str]] = None,
**kwargs: Dict[str, Any]):
def plot_operator_counts(
self,
fig: Optional[Figure] = None,
title: Optional[str] = None,
legend: Optional[List[str]] = None,
**kwargs: Dict[str, Any]
):
"""
Plots an overview of the destroy and repair operators' performance.
Expand Down Expand Up @@ -114,17 +118,21 @@ def plot_operator_counts(self,
if legend is None:
legend = ["Best", "Better", "Accepted", "Rejected"]

self._plot_op_counts(d_ax,
self.statistics.destroy_operator_counts,
"Destroy operators",
min(len(legend), 4),
**kwargs)

self._plot_op_counts(r_ax,
self.statistics.repair_operator_counts,
"Repair operators",
min(len(legend), 4),
**kwargs)
self._plot_op_counts(
d_ax,
self.statistics.destroy_operator_counts,
"Destroy operators",
min(len(legend), 4),
**kwargs
)

self._plot_op_counts(
r_ax,
self.statistics.repair_operator_counts,
"Repair operators",
min(len(legend), 4),
**kwargs
)

fig.legend(legend[:4], ncol=len(legend), loc="lower center")

Expand Down Expand Up @@ -155,7 +163,7 @@ def _plot_op_counts(ax, operator_counts, title, num_types, **kwargs):
ax.barh(operator_names, widths, left=starts, height=0.5, **kwargs)

for y, (x, label) in enumerate(zip(starts + widths / 2, widths)):
ax.text(x, y, str(label), ha='center', va='center')
ax.text(x, y, str(label), ha="center", va="center")

ax.set_title(title)
ax.set_xlabel("Iterations where operator resulted in this outcome (#)")
Expand Down
8 changes: 4 additions & 4 deletions alns/Statistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@


class Statistics:
"""
Statistics object that stores some iteration results. Populated by the ALNS
algorithm.
"""

def __init__(self):
"""
Statistics object that stores some iteration results, which is
optionally populated by the ALNS algorithm.
"""
self._objectives = []
self._runtimes = []

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,9 @@ class AcceptanceCriterion(ABC):
"""

@abstractmethod
def __call__(self,
rnd: RandomState,
best: State,
current: State,
candidate: State) -> bool:
def __call__(
self, rnd: RandomState, best: State, current: State, candidate: State
) -> bool:
"""
Determines whether to accept the proposed, candidate solution based on
this acceptance criterion and the other solution states.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from alns.criteria.AcceptanceCriterion import AcceptanceCriterion
from alns.accept.AcceptanceCriterion import AcceptanceCriterion


class HillClimbing(AcceptanceCriterion):
Expand Down
89 changes: 89 additions & 0 deletions alns/accept/RecordToRecordTravel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
from alns.accept.AcceptanceCriterion import AcceptanceCriterion
from alns.accept.update import update


class RecordToRecordTravel(AcceptanceCriterion):
"""
Record-to-record travel, using an updating threshold. The threshold is
updated as,
``threshold = max(end_threshold, threshold - step)`` (linear)
``threshold = max(end_threshold, step * threshold)`` (exponential)
where the initial threshold is set to ``start_threshold``.
Parameters
----------
start_threshold
The initial threshold.
end_threshold
The final threshold.
step
The updating step.
method
The updating method, one of {'linear', 'exponential'}. Default
'linear'.
References
----------
[1]: Santini, A., Ropke, S. & Hvattum, L.M. A comparison of acceptance
criteria for the adaptive large neighbourhood search metaheuristic.
*Journal of Heuristics* (2018) 24 (5): 783–815.
[2]: 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__(
self,
start_threshold: float,
end_threshold: float,
step: float,
method: str = "linear",
):
if start_threshold < 0 or end_threshold < 0 or step < 0:
raise ValueError("Thresholds must be positive.")

if start_threshold < end_threshold:
raise ValueError(
"End threshold must be bigger than start threshold."
)

if method == "exponential" and step > 1:
raise ValueError(
"Exponential updating cannot have explosive step parameter."
)

self._start_threshold = start_threshold
self._end_threshold = end_threshold
self._step = step
self._method = method

self._threshold = start_threshold

@property
def start_threshold(self) -> float:
return self._start_threshold

@property
def end_threshold(self) -> float:
return self._end_threshold

@property
def step(self) -> float:
return self._step

@property
def method(self) -> str:
return self._method

def __call__(self, rnd, best, current, candidate):
# This follows from the paper by Dueck and Scheueur (1990), p. 162.
result = (candidate.objective() - best.objective()) <= self._threshold

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

return result
Loading

0 comments on commit 639a616

Please sign in to comment.