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

Replace RandomState with Generator #173

Merged
merged 16 commits into from
Oct 21, 2024
40 changes: 20 additions & 20 deletions alns/ALNS.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@ class _OperatorType(Protocol):
def __call__(
self,
state: State,
rnd_state: rnd.RandomState,
rng: rnd.Generator,
**kwargs,
) -> State:
pass # pragma: no cover
...


class _CallbackType(Protocol):
__name__: str

def __call__(self, state: State, rnd_state: rnd.RandomState, **kwargs):
pass # pragma: no cover
def __call__(self, state: State, rng: rnd.Generator, **kwargs):
...


logger = logging.getLogger(__name__)
Expand All @@ -47,19 +47,19 @@ class ALNS:
callback functions (registered via :meth:`~alns.ALNS.ALNS.on_best`,
:meth:`~alns.ALNS.ALNS.on_better`, :meth:`~alns.ALNS.ALNS.on_accept`,
or :meth:`~alns.ALNS.ALNS.on_reject`) should take a candidate
:class:`~alns.State.State` and :class:`~numpy.random.RandomState` as
:class:`~alns.State.State` and :class:`~numpy.random.Generator` as
arguments. Unlike the operators, no solution should be returned: if
desired, the given candidate solution should be modified in-place
instead. Note that this solution is **not** evaluated again (so a
rejected candidate solution will stay rejected!).

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.
rng
Optional random number generator (RNG). When passed, this generator
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
----------
Expand All @@ -68,8 +68,8 @@ class ALNS:
- 420). Springer.
"""

def __init__(self, rnd_state: rnd.RandomState = rnd.RandomState()):
self._rnd_state = rnd_state
def __init__(self, rng: rnd.Generator = rnd.default_rng()):
self._rng = rng

self._d_ops: Dict[str, _OperatorType] = {}
self._r_ops: Dict[str, _OperatorType] = {}
Expand Down Expand Up @@ -121,7 +121,7 @@ def add_destroy_operator(
op
An operator that, when applied to the current state, returns a new
state reflecting its implemented destroy action. Its second
argument is the random state passed to the ALNS instance.
argument is the RNG passed to the ALNS instance.
name
Optional name argument, naming the operator. When not passed, the
function name is used instead.
Expand All @@ -140,7 +140,7 @@ def add_repair_operator(
op
An operator that, when applied to the destroyed state, returns a
new state reflecting its implemented repair action. Its second
argument is the random state passed to the ALNS instance.
argument is the RNG passed to the ALNS instance.
name
Optional name argument, naming the operator. When not passed, the
function name is used instead.
Expand Down Expand Up @@ -212,16 +212,16 @@ class of vehicle routing problems with backhauls. *European
stats.collect_objective(init_obj)
stats.collect_runtime(time.perf_counter())

while not stop(self._rnd_state, best, curr):
d_idx, r_idx = op_select(self._rnd_state, best, curr)
while not stop(self._rng, best, curr):
d_idx, r_idx = op_select(self._rng, best, curr)

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)
destroyed = d_operator(curr, self._rng, **kwargs)
cand = r_operator(destroyed, self._rng, **kwargs)

best, curr, outcome = self._eval_cand(
accept, best, curr, cand, **kwargs
Expand Down Expand Up @@ -295,7 +295,7 @@ def _eval_cand(
func = self._on_outcome.get(outcome)

if callable(func):
func(cand, self._rnd_state, **kwargs)
func(cand, self._rng, **kwargs)

if outcome == Outcome.BEST:
return cand, cand, outcome
Expand All @@ -317,7 +317,7 @@ def _determine_outcome(
"""
outcome = Outcome.REJECT

if accept(self._rnd_state, best, curr, cand): # accept candidate
if accept(self._rng, best, curr, cand): # accept candidate
outcome = Outcome.ACCEPT

if cand.objective() < curr.objective():
Expand Down
5 changes: 3 additions & 2 deletions alns/accept/AcceptanceCriterion.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Protocol

from numpy.random import RandomState
from numpy.random import Generator

from alns.State import State

Expand All @@ -11,7 +11,7 @@ class AcceptanceCriterion(Protocol):
"""

def __call__(
self, rnd: RandomState, best: State, current: State, candidate: State
self, rng: Generator, best: State, current: State, candidate: State
leonlan marked this conversation as resolved.
Show resolved Hide resolved
) -> bool:
"""
Determines whether to accept the proposed, candidate solution based on
Expand All @@ -33,3 +33,4 @@ def __call__(
bool
Whether to accept the candidate state (True), or not (False).
"""
...
2 changes: 1 addition & 1 deletion alns/accept/AlwaysAccept.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ class AlwaysAccept:
This criterion always accepts the candidate solution.
"""

def __call__(self, rnd, best, current, candidate):
def __call__(self, rng, best, current, candidate):
return True
2 changes: 1 addition & 1 deletion alns/accept/GreatDeluge.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def alpha(self):
def beta(self):
return self._beta

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

Expand Down
2 changes: 1 addition & 1 deletion alns/accept/HillClimbing.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ class HillClimbing:
that result in a worse objective value.
"""

def __call__(self, rnd, best, current, candidate):
def __call__(self, rng, best, current, candidate):
return candidate.objective() <= current.objective()
2 changes: 1 addition & 1 deletion alns/accept/LateAcceptanceHillClimbing.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def greedy(self):
def better_history(self):
return self._better_history

def __call__(self, rnd, best, current, candidate):
def __call__(self, rng, best, current, candidate):
if not self._history:
self._history.append(current.objective())
return candidate.objective() < current.objective()
Expand Down
2 changes: 1 addition & 1 deletion alns/accept/MovingAverageThreshold.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def gamma(self) -> int:
def history(self) -> List[float]:
return list(self._history)

def __call__(self, rnd, best, current, candidate) -> bool:
def __call__(self, rng, best, current, candidate) -> bool:
self._history.append(candidate.objective())
recent_best = min(self._history)
recent_avg = mean(self._history)
Expand Down
2 changes: 1 addition & 1 deletion alns/accept/NonLinearGreatDeluge.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def gamma(self):
def delta(self):
return self._delta

def __call__(self, rnd, best, current, candidate):
def __call__(self, rng, best, current, candidate):
if self._threshold is None:
if best.objective() == 0:
raise ValueError("Initial solution cannot have zero value.")
Expand Down
4 changes: 2 additions & 2 deletions alns/accept/RandomAccept.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,12 @@ def step(self) -> float:
def method(self) -> str:
return self._method

def __call__(self, rnd, best, current, candidate):
def __call__(self, rng, best, current, candidate):
# Always accept better
res = candidate.objective() < current.objective()

if not res: # maybe accept worse
res = rnd.random() < self._prob
res = rng.random() < self._prob

self._prob = max(
self.end_prob, update(self._prob, self.step, self.method)
Expand Down
2 changes: 1 addition & 1 deletion alns/accept/RecordToRecordTravel.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def step(self) -> float:
def method(self) -> str:
return self._method

def __call__(self, rnd, best, current, candidate):
def __call__(self, rng, best, current, candidate):
# 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
Expand Down
9 changes: 2 additions & 7 deletions alns/accept/SimulatedAnnealing.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def step(self) -> float:
def method(self) -> str:
return self._method

def __call__(self, rnd, best, current, candidate):
def __call__(self, rng, best, current, candidate):
probability = np.exp(
(current.objective() - candidate.objective()) / self._temperature
)
Expand All @@ -110,12 +110,7 @@ def __call__(self, rnd, best, current, candidate):
update(self._temperature, self.step, self.method),
)

# TODO deprecate RandomState in favour of Generator - which uses
# random(), rather than random_sample().
try:
return probability >= rnd.random()
except AttributeError:
return probability >= rnd.random_sample()
return probability >= rng.random()

@classmethod
def autofit(
Expand Down
12 changes: 6 additions & 6 deletions alns/accept/tests/test_great_deluge.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,21 +40,21 @@ def test_accepts_below_threshold():
great_deluge = GreatDeluge(2.01, 0.5)

# Initial threshold is set at 2.01, hence Two should be accepted
assert_(great_deluge(rnd.RandomState(), One(), Zero(), Two()))
assert_(great_deluge(rnd.default_rng(), One(), Zero(), Two()))


def test_rejects_above_threshold():
great_deluge = GreatDeluge(1.01, 0.5)

# Initial threshold is set at 1.01, hence Two should be rejected
assert_(not great_deluge(rnd.RandomState(), One(), Zero(), Two()))
assert_(not great_deluge(rnd.default_rng(), One(), Zero(), Two()))


def test_rejects_equal_threshold():
great_deluge = GreatDeluge(2, 0.5)

# Initial threshold is set at 2, hence Two should be rejected
assert_(not great_deluge(rnd.RandomState(), One(), Zero(), Two()))
assert_(not great_deluge(rnd.default_rng(), One(), Zero(), Two()))


def test_evaluate_consecutive_solutions():
Expand All @@ -65,11 +65,11 @@ def test_evaluate_consecutive_solutions():

# The initial threshold is set at 2*0 = 0, so the first candidate with
# value one is rejected. The threshold is updated to 0.01.
assert_(not great_deluge(rnd.RandomState(), Zero(), Zero(), One()))
assert_(not great_deluge(rnd.default_rng(), Zero(), Zero(), One()))

# The second candidate is below the threshold (0 < 0.01), hence accepted.
# The threshold is updated to 0.0099.
assert_(great_deluge(rnd.RandomState(), Zero(), Zero(), Zero()))
assert_(great_deluge(rnd.default_rng(), Zero(), Zero(), Zero()))

# The third candidate is below the threshold (0 < 0.0099), hence accepted.
assert_(great_deluge(rnd.RandomState(), Zero(), Zero(), Zero()))
assert_(great_deluge(rnd.default_rng(), Zero(), Zero(), Zero()))
6 changes: 3 additions & 3 deletions alns/accept/tests/test_hill_climbing.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ def test_accepts_better():
Tests if the hill climbing method accepts a better solution.
"""
hill_climbing = HillClimbing()
assert_(hill_climbing(rnd.RandomState(), One(), One(), Zero()))
assert_(hill_climbing(rnd.default_rng(), One(), One(), Zero()))


def test_rejects_worse():
"""
Tests if the hill climbing method accepts a worse solution.
"""
hill_climbing = HillClimbing()
assert_(not hill_climbing(rnd.RandomState(), Zero(), Zero(), One()))
assert_(not hill_climbing(rnd.default_rng(), Zero(), Zero(), One()))


def test_accepts_equal():
Expand All @@ -27,4 +27,4 @@ def test_accepts_equal():
same objective value.
"""
hill_climbing = HillClimbing()
assert_(hill_climbing(rnd.RandomState(), Zero(), Zero(), Zero()))
assert_(hill_climbing(rnd.default_rng(), Zero(), Zero(), Zero()))
Loading
Loading