Skip to content

Commit 639a616

Browse files
authored
Release 3.0.0 (#67)
* Module name changes, update notebooks and readme * Black code formatting * Add black to GH Actions workflow
1 parent 2550a14 commit 639a616

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+670
-556
lines changed

.github/workflows/alns.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ jobs:
2626
pip install poetry
2727
poetry install
2828
- name: Run tests
29-
run: |
30-
poetry run pytest
29+
run: poetry run pytest
30+
- name: Black
31+
uses: psf/black@stable
3132
- name: Run static analysis
32-
run: |
33-
poetry run mypy alns
33+
run: poetry run mypy alns
3434
- uses: codecov/codecov-action@v2
3535
deploy:
3636
needs: build

LICENSE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
The MIT License (MIT)
22

3-
Copyright (c) 2019 Niels Wouda
3+
Copyright (c) 2019 Niels Wouda and contributors
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ showing how the ALNS library may be used. These include:
2525
using a number of different operators and enhancement techniques from the
2626
literature.
2727

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

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

4848
- `SimpleWeights`. This weight scheme applies a convex combination of the
4949
existing weight vector, and a reward given for the current candidate
@@ -60,7 +60,7 @@ your own.
6060
The acceptance criterion determines the acceptance of a new solution state at
6161
each iteration. An overview of common acceptance criteria is given in
6262
[Santini et al. (2018)][3]. Several have already been implemented for you, in
63-
`alns.criteria`:
63+
`alns.accept`:
6464

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

73-
Each acceptance criterion inherits from `AcceptanceCriterion`, which may
74-
be used to write your own.
73+
Each acceptance criterion inherits from `AcceptanceCriterion`, which may be used
74+
to write your own.
75+
76+
### Stoppping criterion
77+
The stopping criterion determines when ALNS should stop iterating. Several
78+
commonly used stopping criteria have already been implemented for you, in
79+
`alns.stop`:
80+
81+
- `MaxIterations`. This stopping criterion stops the heuristic search after a
82+
given number of iterations.
83+
- `MaxRuntime`. This stopping criterion stops the heuristic search after a given
84+
number of seconds.
85+
86+
Each stopping criterion inherits from `StoppingCriterion`, which may be used to
87+
write your own.
7588

7689
## References
7790
- Pisinger, D., and Ropke, S. (2010). Large Neighborhood Search. In M.
@@ -85,5 +98,5 @@ be used to write your own.
8598
[2]: https://github.com/N-Wouda/ALNS/blob/master/examples/travelling_salesman_problem.ipynb
8699
[3]: https://link.springer.com/article/10.1007%2Fs10732-018-9377-x
87100
[4]: https://github.com/N-Wouda/ALNS/blob/master/examples/cutting_stock_problem.ipynb
88-
[5]: https://github.com/N-Wouda/ALNS/blob/master/examples/weight_schemes_acceptance_criteria.ipynb
101+
[5]: https://github.com/N-Wouda/ALNS/blob/master/examples/alns_features.ipynb
89102
[6]: https://github.com/N-Wouda/ALNS/blob/master/examples/resource_constrained_project_scheduling_problem.ipynb

alns/ALNS.py

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
from alns.Result import Result
77
from alns.State import State
88
from alns.Statistics import Statistics
9-
from alns.criteria import AcceptanceCriterion
10-
from alns.weight_schemes import WeightScheme
11-
from alns.stopping_criteria import StoppingCriterion
9+
from alns.accept import AcceptanceCriterion
10+
from alns.stop import StoppingCriterion
11+
from alns.weights import WeightScheme
1212

1313
# Potential candidate solution consideration outcomes.
1414
_BEST = 0
@@ -22,26 +22,27 @@
2222

2323

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

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

alns/Result.py

Lines changed: 40 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,19 @@
99

1010

1111
class Result:
12+
"""
13+
Stores ALNS results. An instance of this class is returned once the
14+
algorithm completes.
1215
13-
def __init__(self, best: State, statistics: Statistics):
14-
"""
15-
Stores ALNS results. An instance of this class is returned once the
16-
algorithm completes.
16+
Parameters
17+
----------
18+
best
19+
The best state observed during the entire iteration.
20+
statistics
21+
Statistics collected during iteration.
22+
"""
1723

18-
Parameters
19-
----------
20-
best
21-
The best state observed during the entire iteration.
22-
statistics
23-
Statistics collected during iteration.
24-
"""
24+
def __init__(self, best: State, statistics: Statistics):
2525
self._best = best
2626
self._statistics = statistics
2727

@@ -39,10 +39,12 @@ def statistics(self) -> Statistics:
3939
"""
4040
return self._statistics
4141

42-
def plot_objectives(self,
43-
ax: Optional[Axes] = None,
44-
title: Optional[str] = None,
45-
**kwargs: Dict[str, Any]):
42+
def plot_objectives(
43+
self,
44+
ax: Optional[Axes] = None,
45+
title: Optional[str] = None,
46+
**kwargs: Dict[str, Any]
47+
):
4648
"""
4749
Plots the collected objective values at each iteration.
4850
@@ -75,11 +77,13 @@ def plot_objectives(self,
7577

7678
plt.draw_if_interactive()
7779

78-
def plot_operator_counts(self,
79-
fig: Optional[Figure] = None,
80-
title: Optional[str] = None,
81-
legend: Optional[List[str]] = None,
82-
**kwargs: Dict[str, Any]):
80+
def plot_operator_counts(
81+
self,
82+
fig: Optional[Figure] = None,
83+
title: Optional[str] = None,
84+
legend: Optional[List[str]] = None,
85+
**kwargs: Dict[str, Any]
86+
):
8387
"""
8488
Plots an overview of the destroy and repair operators' performance.
8589
@@ -114,17 +118,21 @@ def plot_operator_counts(self,
114118
if legend is None:
115119
legend = ["Best", "Better", "Accepted", "Rejected"]
116120

117-
self._plot_op_counts(d_ax,
118-
self.statistics.destroy_operator_counts,
119-
"Destroy operators",
120-
min(len(legend), 4),
121-
**kwargs)
122-
123-
self._plot_op_counts(r_ax,
124-
self.statistics.repair_operator_counts,
125-
"Repair operators",
126-
min(len(legend), 4),
127-
**kwargs)
121+
self._plot_op_counts(
122+
d_ax,
123+
self.statistics.destroy_operator_counts,
124+
"Destroy operators",
125+
min(len(legend), 4),
126+
**kwargs
127+
)
128+
129+
self._plot_op_counts(
130+
r_ax,
131+
self.statistics.repair_operator_counts,
132+
"Repair operators",
133+
min(len(legend), 4),
134+
**kwargs
135+
)
128136

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

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

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

160168
ax.set_title(title)
161169
ax.set_xlabel("Iterations where operator resulted in this outcome (#)")

alns/Statistics.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55

66

77
class Statistics:
8+
"""
9+
Statistics object that stores some iteration results. Populated by the ALNS
10+
algorithm.
11+
"""
812

913
def __init__(self):
10-
"""
11-
Statistics object that stores some iteration results, which is
12-
optionally populated by the ALNS algorithm.
13-
"""
1414
self._objectives = []
1515
self._runtimes = []
1616

alns/criteria/AcceptanceCriterion.py renamed to alns/accept/AcceptanceCriterion.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,9 @@ class AcceptanceCriterion(ABC):
1111
"""
1212

1313
@abstractmethod
14-
def __call__(self,
15-
rnd: RandomState,
16-
best: State,
17-
current: State,
18-
candidate: State) -> bool:
14+
def __call__(
15+
self, rnd: RandomState, best: State, current: State, candidate: State
16+
) -> bool:
1917
"""
2018
Determines whether to accept the proposed, candidate solution based on
2119
this acceptance criterion and the other solution states.

alns/criteria/HillClimbing.py renamed to alns/accept/HillClimbing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from alns.criteria.AcceptanceCriterion import AcceptanceCriterion
1+
from alns.accept.AcceptanceCriterion import AcceptanceCriterion
22

33

44
class HillClimbing(AcceptanceCriterion):

alns/accept/RecordToRecordTravel.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from alns.accept.AcceptanceCriterion import AcceptanceCriterion
2+
from alns.accept.update import update
3+
4+
5+
class RecordToRecordTravel(AcceptanceCriterion):
6+
"""
7+
Record-to-record travel, using an updating threshold. The threshold is
8+
updated as,
9+
10+
``threshold = max(end_threshold, threshold - step)`` (linear)
11+
12+
``threshold = max(end_threshold, step * threshold)`` (exponential)
13+
14+
where the initial threshold is set to ``start_threshold``.
15+
16+
Parameters
17+
----------
18+
start_threshold
19+
The initial threshold.
20+
end_threshold
21+
The final threshold.
22+
step
23+
The updating step.
24+
method
25+
The updating method, one of {'linear', 'exponential'}. Default
26+
'linear'.
27+
28+
References
29+
----------
30+
[1]: Santini, A., Ropke, S. & Hvattum, L.M. A comparison of acceptance
31+
criteria for the adaptive large neighbourhood search metaheuristic.
32+
*Journal of Heuristics* (2018) 24 (5): 783–815.
33+
[2]: Dueck, G., Scheuer, T. Threshold accepting: A general purpose
34+
optimization algorithm appearing superior to simulated annealing.
35+
*Journal of Computational Physics* (1990) 90 (1): 161-175.
36+
"""
37+
38+
def __init__(
39+
self,
40+
start_threshold: float,
41+
end_threshold: float,
42+
step: float,
43+
method: str = "linear",
44+
):
45+
if start_threshold < 0 or end_threshold < 0 or step < 0:
46+
raise ValueError("Thresholds must be positive.")
47+
48+
if start_threshold < end_threshold:
49+
raise ValueError(
50+
"End threshold must be bigger than start threshold."
51+
)
52+
53+
if method == "exponential" and step > 1:
54+
raise ValueError(
55+
"Exponential updating cannot have explosive step parameter."
56+
)
57+
58+
self._start_threshold = start_threshold
59+
self._end_threshold = end_threshold
60+
self._step = step
61+
self._method = method
62+
63+
self._threshold = start_threshold
64+
65+
@property
66+
def start_threshold(self) -> float:
67+
return self._start_threshold
68+
69+
@property
70+
def end_threshold(self) -> float:
71+
return self._end_threshold
72+
73+
@property
74+
def step(self) -> float:
75+
return self._step
76+
77+
@property
78+
def method(self) -> str:
79+
return self._method
80+
81+
def __call__(self, rnd, best, current, candidate):
82+
# This follows from the paper by Dueck and Scheueur (1990), p. 162.
83+
result = (candidate.objective() - best.objective()) <= self._threshold
84+
85+
self._threshold = max(
86+
self.end_threshold, update(self._threshold, self.step, self.method)
87+
)
88+
89+
return result

0 commit comments

Comments
 (0)