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

Avoid copies if it is possible to undo a move #50

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 20 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
24 changes: 17 additions & 7 deletions simanneal/anneal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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):
Expand All @@ -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

Expand Down