Skip to content

Commit

Permalink
Version 1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
Fynardo committed Sep 13, 2019
1 parent e64a670 commit e3672b4
Show file tree
Hide file tree
Showing 34 changed files with 730 additions and 230 deletions.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -17,6 +17,7 @@ At this moment, the list of implemented metaheuristics is:

* GRASP
* Simulated Annealing
* Tabu Search


To see the development plans at a glance, all relevant information can be found in the [roadmap](ROADMAP.md).
Expand Down
7 changes: 2 additions & 5 deletions ROADMAP.md
Expand Up @@ -16,18 +16,15 @@ more efficiently.

* GRASP constructive version (basic and multistart approach)
* Simulated Annealing method (basic and multistart approach)
* Tabu Search (basic and multistart approach)


## On development

### Metaheuristic

* Tabu Search

### Techniques

N/A

## Future

### Metaheuristic
Expand All @@ -44,4 +41,4 @@ N/A

All contributions will be greatly appreciated. Keep in mind that there is no need to implement
every metaheuristic ever proposed. However, feel free to propose or implement anything you see
useful or that have served in your work.
useful or that have served in your work.
26 changes: 26 additions & 0 deletions docs/changelog.rst
@@ -0,0 +1,26 @@
.. _changelog:

Change log
----------

Version 1.0.0
^^^^^^^^^^^^^
Released in 13/9/2019.

* Added Tabu Search implementation
* Added Candidate entity to model trajectory solvers
* Added Move entity to model trajectory solvers
* Added neighborhood utilities to decouple solvers
* Solvers now don't need any method overriding thanks to candidates and moves
* Added tests for solvers implemented using TSP example problem
* Moved factory logic to the classes that need it


Version 0.1.0
^^^^^^^^^^^^^
Released in 6/9/2019

* Initial structure for the project.
* Added base solver and multistart approach
* Added GRASP implementation
* Added Simulated Annealing implementation
17 changes: 15 additions & 2 deletions docs/entities.rst
Expand Up @@ -7,16 +7,29 @@ Entities

Instance
--------

.. _entities_instance:

.. automodule:: or_testbed.entities.instance
:members:

Solution
--------

.. _entities_solution:

.. automodule:: or_testbed.entities.solution
:members:

Candidate
---------
.. _entities_candidate:

.. automodule:: or_testbed.entities.candidate
:members:
:private-members:

Neighborhood
------------
.. _entities_neighborhood:

.. automodule:: or_testbed.entities.neighborhood
:members:
7 changes: 0 additions & 7 deletions docs/factories.rst

This file was deleted.

8 changes: 7 additions & 1 deletion docs/index.rst
Expand Up @@ -46,8 +46,14 @@ Utilities
:maxdepth: 2

logging
factories

Changelog
---------

.. toctree::
:maxdepth: 2

changelog


.. Indices and tables
Expand Down
15 changes: 14 additions & 1 deletion docs/solvers.rst
Expand Up @@ -12,7 +12,7 @@ Base Solvers

.. autoclass:: or_testbed.solvers.base.solver.MultiStartSolver

Grasp
GRASP
-----
.. _grasp_solver:

Expand All @@ -34,3 +34,16 @@ Simulated Annealing

.. autoclass:: or_testbed.solvers.simanneal.MultiStartSimAnneal
:members:


Tabu Search
-----------
.. _tabu_search:

.. autoclass:: or_testbed.solvers.tabusearch.TabuSearch
:members:
:private-members:

.. autoclass:: or_testbed.solvers.tabusearch.MultiStartTabuSearch
:members:
:private-members:
101 changes: 41 additions & 60 deletions docs/tutorial.rst
Expand Up @@ -50,11 +50,11 @@ In OR-Testbed, three things are needed to solve a problem:
of a solution and calculating its objective. In TSP, instances will have information about cities and distances.

2. A **Solution**. Solution objects store the obtained solution for some problem, its internal structure depends on the problem itself,
but all solutions in OR-Testbed share share a value called **objective**, that representes the quality of the solution. In TSP this objective
but all solutions in OR-Testbed share share a value called **objective**, that represents the quality of the solution. In TSP this objective
is the cost of the calculated trip.

3. A **Solver**. The solver is the one that does the work, this is, the algorithm itself. Each solver implements one metaheuristic or a variation of
a metaheuristic. In TSP, we will need to implement some methods of the solvers in order to adapt their logic to TSP.
a metaheuristic. In TSP, we will need to implement some methods related to the solvers in order to adapt their logic to TSP.


Defining an instance
Expand Down Expand Up @@ -168,70 +168,56 @@ Where **c(e)** is the cost of candidate **e** (based on the greedy function), **
What this means is that when alpha is 0 only candidates with minimum cost are taken into account (pure greedy approach). On the other hand, when
alpha is 1 all candidates are taken into account (pure randomness approach). What alpha does is to set the confidence we have in our greedy function.

Anyhow, to solve a problem like TSP we must implement some logic (like the greedy function). Basically we need to override the methods
that are not part of the core of GRASP (this happens with every metaheuristic in OR-Testbed). In this case, we must override
``initialize_solution`` (though if you don't need to initialize anything you may pass), ``greedy_function``, ``make_candidates_list``,
``add_candidate`` and ``are_candidates_left``. The code needed to implement GRASP for creating a solution for our TSP is the following

.. code-block:: python
import or_testbed.solvers.grasp as base_grasp
Anyhow, to solve our TSP problem we must implement some other logic. GRASP workflow (as lots of other metaheuristics) is about selecting candidates and making small changes
to solutions in the best trajectory as possible. In OR-Testbed we implement this logic within two entities: candidates and movements.

Lets start with movements. As stated earlier, multiple metaheuristics work by exploring neighborhoods and trying to find paths that eventually may guide them to good solutions.
This neighborhoods are defined by the moves. For example, in GRASP our only move is to add cities to the sequence until all cities are covered.
The neighborhood (the set of candidates to take into account) related to that move are the cities left to be added to the sequence.

class TSPGrasp(base_grasp.GraspConstruct):
def __init__(self, instance, solution_factory, alpha, debug=True, log_file=None):
super().__init__(instance, solution_factory, alpha, debug, log_file)
self.visited = set()
self.remaining = set(self.instance.data.keys())
self.last_visited = self.solution.initial_city
Lets see an example, this could be the GRASP move and candidate definition for TSP:

def _initialize_solution(self):
self.visited.add(self.solution.initial_city)
self.remaining.remove(self.solution.initial_city)
def _greedy_function(self, candidate):
return self.instance.data[self.last_visited][candidate]
def _are_candidates_left(self):
return True if self.remaining else False
.. code-block:: python
def _add_candidate(self, candidate):
self.solution.cities.append(candidate)
self.visited.add(candidate)
self.last_visited = candidate
self.remaining.remove(candidate)
class TSPGraspCandidate(base_candidate.Candidate):
def __init__(self, city):
self.city = city
def _make_candidates_list(self):
return [c for c in self.instance.data[self.last_visited].keys() if c not in self.visited]
def fitness(self, solution, instance):
last_visited = solution.cities[-1]
return instance.data[last_visited][self.city]
class TSPGraspMove(base_move.Move):
@staticmethod
def make_neighborhood(solution, instance):
return [TSPGraspCandidate(city=c) for c in instance.data[solution.cities[-1]].keys() if c not in solution.cities]
At solver initialization, we set some helpful values like visited cities and remaining cities. Note that solvers have access to instance and solution objects.
@staticmethod
def apply(in_candidate, in_solution):
in_solution.cities.append(in_candidate.city)
return in_solution
Initializing the solution is not always needed, but makes sense in this one. The greedy function can be an extremely complicated one, in our case,
is a very naive function, it just takes the distance between the last visited solution and the candidate.
The other three functions are related to the candidates list, first to check if there are candidates left we just check it there's some city
remaining to be visited. To add a candidate we append the candidate to the solution cities sequence and update our values accordingly. Last, to make
the candidates list we take into account all cities not visited (the remaining ones).
Note that all methods are seen as private (that's why their name start with an underscore ``_``), this means that the solver will call the appropriate methods when needed.
Candidates store internal structure and the fitness function, which is the cost of adding the candidate city as the next step on our sequence.
On the other hand, neighborhoods are just a candidates list, in this case, made by the cities not added yet to the solution.
To apply this move, we just append the candidate to the solution.

Executing our solver
^^^^^^^^^^^^^^^^^^^^

All the three needed components are implemented now (solution, instance and solver), that means that there's only one more step, executing it all.
All the needed components are implemented now, that means that there's only one more step, executing it all.

.. code-block:: python
from or_testbed.solvers.factory import make_factory_from
import or_testbed.solvers.grasp as base_grasp
if __name__ == '__main__':
# Instantiate instance
my_tsp = TSPInstance(instance_name, cities)
# Create factory from solution
tsp_solution_factory = make_factory_from(TSPSolution, initial_city='A')
# Instantiate GRASP solver (with parameter alpha = 0.0, greedy approach)
tsp_solver = TSPGrasp(my_tsp, solution_factory=tsp_solution_factory, alpha=0.0)
tsp_solution_factory = TSPSolution.factory(initial_city='A')
# Instantiate GRASP solver (with parameter alpha = 0.0, greedy approach) passing our move.
tsp_solver = base_grasp.GraspConstruct(tsp, alpha=0.0, solution_factory=tsp_solution_factory, grasp_move=TSPGraspMove)
# Run the solver
feasible, solution = tsp_solver.solve()
# Retrieve the cities sequence and the objective value (the cost of the trip)
Expand All @@ -244,20 +230,16 @@ the solution found. In fact, a tuple is returned, first element (``feasible``) i
second element is the solution itself.
Note that the solution is not instantiated directly, what we do is to create a factory around it, but its the same syntax.
What this means is that solvers usually need to be able to create new solutions, so we want to give them a way to do so, thats what
``make_factory_from`` does. The function signature is:

.. autofunction:: or_testbed.solvers.factory.make_factory_from
``factory`` class method does.

So, basically it expects a class reference (``cls``) and the arguments to instantiate that class, in the same way as a normal instantiation.
Once executed we will get a solution for our problem, an easily improvable one to be fair.

Once executed we will get a solution for our problem in basically no time, thats fine, but the solution is easily improvable.

Improving our solver
^^^^^^^^^^^^^^^^^^^^
Improving our solution
^^^^^^^^^^^^^^^^^^^^^^

In our previous example, we solved the problem with alpha being 0.0, this means that there is no randomness, so the greedy function will rule it all.
We could set another value to alpha (like 0.3) so the solver would be able to explore more solutions. That's a fine approximation, but with
randomness involved we usually want to try and stabilize our solutions. This is where **multistart** comes in, this technique lets us run
randomness involved we usually want to try and stabilize our solutions. This is where **multistart** techniques come in, this lets us run
our solvers a number of times and get the best result.

Lets see how we do it with OR-Testbed:
Expand All @@ -270,9 +252,9 @@ Lets see how we do it with OR-Testbed:
# Instantiate instance
my_tsp = TSPInstance(instance_name, cities)
# Make a solution factory as before
tsp_solution_factory = make_factory_from(TSPSolution, initial_city='A')
tsp_solution_factory = TSPSolution.factory(initial_city='A')
# Since we want to execute multiple GRASP instances, we also make a factory from it
tsp_grasp_factory = make_factory_from(TSPGrasp, instance=my_tsp, alpha=0.3, solution_factory=tsp_solution_factory)
tsp_grasp_factory = base_grasp.GraspConstruct.factory(instance=tsp, alpha=0.3, solution_factory=tsp_solution_factory, grasp_move=TSPGraspMove)
# Instantiate our multistart version of GRASP with 25 iterations
tsp_multistart = base_grasp.MultiStartGraspConstruct(iters=25, inner_grasp_factory=tsp_grasp_factory)
# Run the solver
Expand All @@ -281,7 +263,7 @@ Lets see how we do it with OR-Testbed:
print('Salesman will visit: {}'.format(ms_solution.cities))
print('Total cost: {}'.format(ms_solution.get_objective()))
Running a multistart solver is almost the same as running the proper solver, the main difference is that now, for the *inner solver* (TSPGrasp)
Running a multistart solver is almost the same as running the proper solver, the main difference is that now, for the *inner solver* (GRASP)
we don't want an instance, we need a factory, because the multistart solver is going to instantiate it many times. The good thing is that
our solution now is better (the optimal one in fact).

Expand All @@ -300,7 +282,7 @@ We can set that with:

.. code-block:: python
tsp_grasp_factory = make_factory_from(TSPGrasp, instance=my_tsp, alpha=0.3, solution_factory=tsp_solution_factory, debug=False)
tsp_grasp_factory = base_grasp.GraspConstruct.factory(instance=tsp, alpha=0.3, solution_factory=tsp_solution_factory, grasp_move=TSPGraspMove, debug=False)
That's the same line from the example but with the parameter **debug** set to ``False``, that prevents any output to be printed.
Note that we can set debug parameter to ``True`` or ``False`` to any solver.
Expand All @@ -309,8 +291,7 @@ Of course we may not want to see the output but to store it to check later, we c

.. code-block:: python
tsp_grasp_factory = make_factory_from(TSPGrasp, instance=my_tsp, alpha=0.3, solution_factory=tsp_solution_factory, debug=False, log_file='log.txt')
tsp_grasp_factory = base_grasp.GraspConstruct.factory(instance=tsp, alpha=0.3, solution_factory=tsp_solution_factory, grasp_move=TSPGraspMove, debug=False, log_file='log.txt')
That will not print the output but store it to a text file called *log.txt*.

Expand Down
37 changes: 37 additions & 0 deletions examples/tsp/definition.py
Expand Up @@ -7,6 +7,9 @@

import or_testbed.entities.solution as base_solution
import or_testbed.entities.instance as base_instance
import or_testbed.entities.candidate as base_candidate
import or_testbed.entities.move as base_move
import itertools


class TSPInstance(base_instance.Instance):
Expand All @@ -27,5 +30,39 @@ def calculate_objective(self, in_instance):
return sum([in_instance.data[a][b] for a,b in zip(self.cities, self.cities[-1:] + self.cities[:-1])])


class SwapCitiesCandidate(base_candidate.Candidate):
def __init__(self, city1, city2):
self.city1 = city1
self.city2 = city2

def fitness(self, solution, instance):
c1 = solution.cities.index(self.city1)
c2 = solution.cities.index(self.city2)

obj_before = solution.objective

solution.cities[c1], solution.cities[c2] = solution.cities[c2], solution.cities[c1]
obj_after = solution.calculate_objective(instance)
solution.cities[c2], solution.cities[c1] = solution.cities[c1], solution.cities[c2]

retval = obj_before - obj_after
return retval


class SwapCitiesMove(base_move.Move):
@staticmethod
def make_neighborhood(solution, instance):
swaps = itertools.combinations(instance.data.keys(), 2)
return [SwapCitiesCandidate(city1=s[0], city2=s[1]) for s in swaps]

@staticmethod
def apply(in_candidate, in_solution):
first_city = in_solution.cities.index(in_candidate.city1)
second_city = in_solution.cities.index(in_candidate.city2)

in_solution.cities[first_city], in_solution.cities[second_city] = in_solution.cities[second_city], in_solution.cities[first_city]
return in_solution


cities = {'A': {'B': 3, 'C': 5, 'D': 6, 'E': 2}, 'B': {'A': 3, 'C': 25, 'D': 10, 'E': 5}, 'C': {'A': 5, 'B': 25, 'D': 3, 'E': 4}, 'D': {'A': 6, 'B': 10, 'C': 3, 'E': 1}, 'E': {'A': 2, 'B': 5, 'C': 4, 'D': 1}}
tsp = TSPInstance('tsp_example', cities)

0 comments on commit e3672b4

Please sign in to comment.