Skip to content

Commit

Permalink
Add logging to ALNS library (#68)
Browse files Browse the repository at this point in the history
* Add logging to ALNS library

* Fix not using new assignment syntax in f-strings

That's only from Py3.8 onwards, and we also support Py3.7
  • Loading branch information
N-Wouda authored May 18, 2022
1 parent 639a616 commit 3761cc4
Show file tree
Hide file tree
Showing 8 changed files with 38 additions and 20 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ If you wish to dive right in, the `examples/` directory contains example noteboo
showing how the ALNS library may be used. These include:

- The travelling salesman problem (TSP), [here][2]. We solve an
instance of 131 cities to within 2.1% of optimality, using simple
instance of 131 cities to within 2% of optimality, using simple
destroy and repair heuristics with a post-processing step.
- The cutting-stock problem (CSP), [here][4]. We solve an instance with
180 beams over 165 distinct sizes to within 1.35% of optimality in
Expand All @@ -37,8 +37,8 @@ may be used to run the ALNS algorithm, the second may be subclassed to
store a solution state - all it requires is to define an `objective`
member function, returning an objective value.

The ALNS algorithm must be supplied with a _weight scheme_ and an _acceptance
criterion_.
The ALNS algorithm must be supplied with a _weight scheme_, an _acceptance
criterion_, and a _stopping criterion_.

### Weight scheme
The weight scheme determines how to select destroy and repair operators in each
Expand Down
21 changes: 17 additions & 4 deletions alns/ALNS.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
import time
from typing import Callable, Dict, List, Optional, Tuple

Expand All @@ -20,6 +21,8 @@
# https://stackoverflow.com/q/61569324/4316405.
_OperatorType = Callable[[State, rnd.RandomState], State]

logger = logging.getLogger(__name__)


class ALNS:
"""
Expand Down Expand Up @@ -90,6 +93,7 @@ def add_destroy_operator(self, op: _OperatorType, name: str = None):
Optional name argument, naming the operator. When not passed, the
function name is used instead.
"""
logger.debug(f"Adding destroy operator {op.__name__}.")
self._destroy_operators[op.__name__ if name is None else name] = op

def add_repair_operator(self, op: _OperatorType, name: str = None):
Expand All @@ -106,6 +110,7 @@ def add_repair_operator(self, op: _OperatorType, name: str = None):
Optional name argument, naming the operator. When not passed, the
function name is used instead.
"""
logger.debug(f"Adding repair operator {op.__name__}.")
self._repair_operators[name if name else op.__name__] = op

def iterate(
Expand Down Expand Up @@ -161,14 +166,15 @@ class of vehicle routing problems with backhauls. *European Journal of
Operational Research*, 171: 750–775, 2006.
"""
if len(self.destroy_operators) == 0 or len(self.repair_operators) == 0:
raise ValueError(
"Missing at least one destroy or repair operator."
)
raise ValueError("Missing destroy or repair operators.")

curr = best = initial_solution
init_obj = initial_solution.objective()

logger.debug(f"Initial solution has objective {init_obj:.2f}.")

stats = Statistics()
stats.collect_objective(initial_solution.objective())
stats.collect_objective(init_obj)
stats.collect_runtime(time.perf_counter())

while not stop(self._rnd_state, best, curr):
Expand All @@ -177,6 +183,8 @@ class of vehicle routing problems with backhauls. *European Journal of
d_name, d_operator = self.destroy_operators[d_idx]
r_name, r_operator = self.repair_operators[r_idx]

logger.debug(f"Selected operators {d_name} and {r_name}.")

destroyed = d_operator(curr, self._rnd_state, **kwargs)
cand = r_operator(destroyed, self._rnd_state, **kwargs)

Expand All @@ -191,6 +199,8 @@ class of vehicle routing problems with backhauls. *European Journal of
stats.collect_repair_operator(r_name, s_idx)
stats.collect_runtime(time.perf_counter())

logger.info(f"Finished iterating in {stats.total_runtime:.2f}s.")

return Result(best, stats)

def on_best(self, func: _OperatorType):
Expand All @@ -205,6 +215,7 @@ def on_best(self, func: _OperatorType):
and a numpy RandomState as its second (cf. the operator signature).
It should return a (new) solution State.
"""
logger.debug(f"Adding on_best callback {func.__name__}.")
self._on_best = func

def _eval_cand(
Expand Down Expand Up @@ -233,6 +244,8 @@ def _eval_cand(
curr = cand

if cand.objective() < best.objective(): # candidate is new best
logger.info(f"New best with objective {cand.objective():.2f}.")

if self._on_best:
cand = self._on_best(cand, self._rnd_state, **kwargs)

Expand Down
8 changes: 2 additions & 6 deletions alns/accept/RecordToRecordTravel.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,10 @@ def __init__(
raise ValueError("Thresholds must be positive.")

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

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

self._start_threshold = start_threshold
self._end_threshold = end_threshold
Expand Down
12 changes: 8 additions & 4 deletions alns/accept/SimulatedAnnealing.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import logging

import numpy as np

from alns.accept.AcceptanceCriterion import AcceptanceCriterion
from alns.accept.update import update

logger = logging.getLogger(__name__)


class SimulatedAnnealing(AcceptanceCriterion):
"""
Expand Down Expand Up @@ -48,13 +52,11 @@ def __init__(

if start_temperature < end_temperature:
raise ValueError(
"Start temperature must be bigger than end temperature."
"start_temperature < end_temperature not understood."
)

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

self._start_temperature = start_temperature
self._end_temperature = end_temperature
Expand Down Expand Up @@ -155,4 +157,6 @@ def autofit(
start_temp = -worse * init_obj / np.log(accept_prob)
step = (1 / start_temp) ** (1 / num_iters)

logger.info(f"Autofit start_temp {start_temp:.2f}, step {step:.2f}.")

return cls(start_temp, 1, step, method="exponential")
2 changes: 1 addition & 1 deletion alns/stop/MaxIterations.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class MaxIterations(StoppingCriterion):

def __init__(self, max_iterations: int):
if max_iterations < 0:
raise ValueError("Max iterations must be non-negative.")
raise ValueError("max_iterations < 0 not understood.")

self._max_iterations = max_iterations
self._current_iteration = 0
Expand Down
2 changes: 1 addition & 1 deletion alns/stop/MaxRuntime.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class MaxRuntime(StoppingCriterion):

def __init__(self, max_runtime: float):
if max_runtime < 0:
raise ValueError("Max runtime must be non-negative.")
raise ValueError("max_runtime < 0 not understood.")

self._max_runtime = max_runtime
self._start_runtime: Optional[float] = None
Expand Down
5 changes: 5 additions & 0 deletions alns/weights/SegmentedWeights.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import logging
from typing import List

import numpy as np

from alns.weights.WeightScheme import WeightScheme

logger = logging.getLogger(__name__)


class SegmentedWeights(WeightScheme):
"""
Expand Down Expand Up @@ -55,6 +58,8 @@ def select_operators(self, rnd_state):
self._iter += 1

if self._iter % self._seg_length == 0:
logger.debug(f"End of segment (#iters = {self._iter}).")

self._d_weights *= self._seg_decay
self._d_weights += (1 - self._seg_decay) * self._d_seg_weights

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "alns"
version = "3.0.0"
version = "3.0.1"
description = "A flexible implementation of the adaptive large neighbourhood search (ALNS) algorithm."
authors = ["Niels Wouda <[email protected]>"]
license = "MIT"
Expand Down

0 comments on commit 3761cc4

Please sign in to comment.