diff --git a/README.md b/README.md index 26a647e..790b482 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,20 @@ them into the constructor like so The last line (calling `__init__` on the super class) is critical. +## Implementation Details + +The simulated annealing algorithm requires that we track states (current, previous, best), which means we need to copy `self.state` frequently. + +Copying an object in Python is not always straightforward or performant. The standard library provides a `copy.deepcopy()` method to copy arbitrary python objects but it is very expensive. Certain objects can be copied by more efficient means: lists can be sliced and dictionaries can use their own .copy method, etc. + +In order to facilitate flexibility, you can specify the `copy_strategy` attribute +which defines one of: +* `deepcopy`: uses `copy.deepcopy(object)` +* `slice`: uses `object[:]` +* `method`: uses `object.copy()` + +If you want to implement your own custom copy mechanism, override the `copy_state` method. + ## Optimizations For some problems the `energy` function is prohibitively expensive to calculate @@ -149,19 +163,12 @@ energy from the previous state, this approach will save you a call to value and sometimes return `None`, depending on the type of modification it makes to the state and the complexity of calculting a delta. -## Implementation Details - -The simulated annealing algorithm requires that we track states (current, previous, best), which means we need to copy `self.state` frequently. - -Copying an object in Python is not always straightforward or performant. The standard library provides a `copy.deepcopy()` method to copy arbitrary python objects but it is very expensive. Certain objects can be copied by more efficient means: lists can be sliced and dictionaries can use their own .copy method, etc. - -In order to facilitate flexibility, you can specify the `copy_strategy` attribute -which defines one of: -* `deepcopy`: uses `copy.deepcopy(object)` -* `slice`: uses `object[:]` -* `method`: uses `object.copy()` - -If you want to implement your own custom copy mechanism, override the `copy_state` method. +Another optimization relates to avoiding copying the state for every move; +for some problems it is possible to easily undo the last move. For +such problems your class can implement an `undo_move(self)` method, which changes +`self.state` back to the state it was in before the last `move(self)` method call. +Using this method almost all the copying can be avoided, which may significantly +speed up the annealing process if the copies themselves take a lot of time. ## Notes diff --git a/simanneal/anneal.py b/simanneal/anneal.py index 3b70d81..d40e07c 100644 --- a/simanneal/anneal.py +++ b/simanneal/anneal.py @@ -58,6 +58,8 @@ def __init__(self, initial_state=None, load_state=None): raise ValueError('No valid values supplied for neither \ initial_state nor load_state') + self.supports_undo_move = hasattr(self, 'undo_move') + signal.signal(signal.SIGINT, self.set_user_exit) def save_state(self, fname=None): @@ -115,6 +117,15 @@ def copy_state(self, state): 'the self.copy_strategy "%s"' % self.copy_strategy) + def restore_state(self, state): + """Restore the state to a provided state either by making a copy, + or by undoing the last move via self.undo_move method if it exists + """ + if self.supports_undo_move: + self.undo_move() + else: + self.state = self.copy_state(state) + def update(self, *args, **kwargs): """Wrapper for internal update. @@ -188,7 +199,7 @@ def anneal(self): # Note initial state T = self.Tmax E = self.energy() - prevState = self.copy_state(self.state) + prevState = self.copy_state(self.state) if not self.supports_undo_move else None prevEnergy = E self.best_state = self.copy_state(self.state) self.best_energy = E @@ -209,15 +220,14 @@ def anneal(self): E += dE trials += 1 if dE > 0.0 and math.exp(-dE / T) < random.random(): - # Restore previous state - self.state = self.copy_state(prevState) + self.restore_state(prevState) E = prevEnergy else: # Accept new state and compare to best state accepts += 1 if dE < 0.0: improves += 1 - prevState = self.copy_state(self.state) + prevState = self.copy_state(self.state) if not self.supports_undo_move else None prevEnergy = E if E < self.best_energy: self.best_state = self.copy_state(self.state) @@ -246,7 +256,7 @@ def run(T, steps): """Anneals a system at constant temperature and returns the state, energy, rate of acceptance, and rate of improvement.""" E = self.energy() - prevState = self.copy_state(self.state) + prevState = self.copy_state(self.state) if not self.supports_undo_move else None prevEnergy = E accepts, improves = 0, 0 for _ in range(steps): @@ -257,13 +267,13 @@ def run(T, steps): else: E = prevEnergy + dE if dE > 0.0 and math.exp(-dE / T) < random.random(): - self.state = self.copy_state(prevState) + self.state = self.restore_state(prevState) E = prevEnergy else: accepts += 1 if dE < 0.0: improves += 1 - prevState = self.copy_state(self.state) + prevState = self.copy_state(self.state) if not self.supports_undo_move else None prevEnergy = E return E, float(accepts) / steps, float(improves) / steps