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

Addition of evol.problems #86

Merged
merged 18 commits into from
Nov 30, 2018
Merged
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
12 changes: 12 additions & 0 deletions evol/helpers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,15 @@ def result(*args, **kwargs):
except TypeError:
return func(*args, **{k: v for k, v in kwargs.items() if k in signature(func).parameters})
return result


def rotating_window(arr):
"""rotating_window([1,2,3,4]) -> [(4,1), (1,2), (2,3), (3,4)]"""
for i, city in enumerate(arr):
yield arr[i-1], arr[i]


def sliding_window(arr):
"""sliding_window([1,2,3,4]) -> [(1,2), (2,3), (3,4)]"""
for i, city in enumerate(arr[:-1]):
yield arr[i], arr[i+1]
Empty file added evol/problems/__init__.py
Empty file.
9 changes: 9 additions & 0 deletions evol/problems/functions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""
The `evol.problems.functions` part of the library contains
simple problem instances that do with known math functions.

The functions in here are typically inspired from wikipedia:
https://en.wikipedia.org/wiki/Test_functions_for_optimization
"""

from .variableinput import Rosenbrock, Sphere, Rastrigin
56 changes: 56 additions & 0 deletions evol/problems/functions/variableinput.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import math
from typing import Sequence

from evol.helpers.utils import sliding_window
from evol.problems.problem import Problem


class FunctionProblem(Problem):
def __init__(self, size=2):
self.size = size

def check_solution(self, solution: Sequence[float]) -> Sequence[float]:
if len(solution) > self.size:
raise ValueError(f"{self.__class__.__name__} has size {self.size}, \
got solution of size: {len(solution)}")
return solution

def value(self, solution):
return sum(solution)

def eval_function(self, solution: Sequence[float]) -> float:
self.check_solution(solution)
return self.value(solution)


class Sphere(FunctionProblem):
def value(self, solution: Sequence[float]) -> float:
"""
The optimal value can be found when a sequence of zeros is given.
:param solution: a sequence of x_i values
:return: the value of the Sphere function
"""
return sum([_**2 for _ in solution])


class Rosenbrock(FunctionProblem):
def value(self, solution: Sequence[float]) -> float:
"""
The optimal value can be found when a sequence of ones is given.
:param solution: a sequence of x_i values
:return: the value of the Rosenbrock function
"""
result = 0
for x_i, x_j in sliding_window(solution):
result += 100*(x_j - x_i**2)**2 + (1 - x_i)**2
return result


class Rastrigin(FunctionProblem):
def value(self, solution: Sequence[float]) -> float:
"""
The optimal value can be found when a sequence of zeros is given.
:param solution: a sequence of x_i values
:return: the value of the Rosenbrock function
"""
return (10 * self.size) + sum([_**2 - 10 * math.cos(2*math.pi*_) for _ in solution])
8 changes: 8 additions & 0 deletions evol/problems/problem.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from abc import ABCMeta, abstractmethod


class Problem(metaclass=ABCMeta):

@abstractmethod
def eval_function(self, solution):
raise NotImplementedError
10 changes: 10 additions & 0 deletions evol/problems/routing/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""
The `evol.problems.routing` part of the library contains
simple problem instances that do with routing problems. These
are meant to be used for education and training purposes and
these problems are typically good starting points if you want
to play with the library.
"""

from .tsp import TSPProblem
from .magicsanta import MagicSanta
51 changes: 51 additions & 0 deletions evol/problems/routing/coordinates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
united_states_capitols = [
(32.361538, -86.279118, "Montgomery", "Alabama"),
(58.301935, -134.419740, "Juneau", "Alaska"),
(33.448457, -112.073844, "Phoenix", "Arizona"),
(34.736009, -92.331122, "Little Rock", "Arkansas"),
(38.555605, -121.468926, "Sacramento", "California"),
(39.7391667, -104.984167, "Denver", "Colorado"),
(41.767, -72.677, "Hartford", "Connectic"),
(39.161921, -75.526755, "Dover", "Delaware"),
(30.4518, -84.27277, "Tallahassee", "Florida"),
(33.76, -84.39, "Atlanta", "Georgia"),
(21.30895, -157.826182, "Honolulu", "Hawaii"),
(43.613739, -116.237651, "Boise", "Idaho"),
(39.783250, -89.650373, "Springfield", "Illinois"),
(39.790942, -86.147685, "Indianapolis", "Indiana"),
(41.590939, -93.620866, "Des Moines", "Iowa"),
(39.04, -95.69, "Topeka", "Kansas"),
(38.197274, -84.86311, "Frankfort", "Kentucky"),
(30.45809, -91.140229, "Baton Rouge", "Louisiana"),
(44.323535, -69.765261, "Augusta", "Maine"),
(38.972945, -76.501157, "Annapolis", "Maryland"),
(42.2352, -71.0275, "Boston", "Massachuset"),
(42.7335, -84.5467, "Lansing", "Michigan"),
(44.95, -93.094, "Saint Paul", "Minnesot"),
(32.320, -90.207, "Jackson", "Mississip"),
(38.572954, -92.189283, "Jefferson City", "Missouri"),
(46.595805, -112.027031, "Helana", "Montana"),
(40.809868, -96.675345, "Lincoln", "Nebraska"),
(39.160949, -119.753877, "Carson City", "Nevada"),
(43.220093, -71.549127, "Concord", "Hampshire"),
(40.221741, -74.756138, "Trenton", "Jersey"),
(35.667231, -105.964575, "Santa Fe", "Mexico"),
(42.659829, -73.781339, "Albany", "York"),
(35.771, -78.638, "Raleigh", "Car"),
(48.813343, -100.779004, "Bismarck", "Dakota"),
(39.962245, -83.000647, "Columbus", "Ohio"),
(35.482309, -97.534994, "Oklahoma City", "Oklahoma"),
(44.931109, -123.029159, "Salem", "Oregon"),
(40.269789, -76.875613, "Harrisburg", "Pennsylvania"),
(41.82355, -71.422132, "Providence", "Island"),
(34.000, -81.035, "Columbia", "Car"),
(44.367966, -100.336378, "Pierre", "Dakota"),
(36.165, -86.784, "Nashville", "Tennessee"),
(30.266667, -97.75, "Austin", "Texas"),
(40.7547, -111.892622, "Salt Lake City", "Utah"),
(44.26639, -72.57194, "Montpelier", "Vermont"),
(37.54, -77.46, "Richmond", "Virgini"),
(47.042418, -122.893077, "Olympia", "Washington"),
(38.349497, -81.633294, "Charleston", "Virginia"),
(43.074722, -89.384444, "Madison", "Wisconsin"),
(41.145548, -104.802042, "Cheyenne", "Wyoming")]
67 changes: 67 additions & 0 deletions evol/problems/routing/magicsanta.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import math
from collections import Counter
from itertools import chain
from typing import List, Union

from evol.helpers.utils import sliding_window
from evol.problems.problem import Problem


class MagicSanta(Problem):
def __init__(self, city_coordinates, home_coordinate, gift_weight=None, sleigh_weight=1):
"""
This problem is based on this kaggle competition:
https://www.kaggle.com/c/santas-stolen-sleigh#evaluation.
:param city_coordinates: List of tuples containing city coordinates.
:param home_coordinate: Tuple containing coordinate of home base.
:param gift_weight: Vector of weights per gift associated with cities.
:param sleigh_weight: Weight of the sleight.
"""
self.coordinates = city_coordinates
self.home_coordinate = home_coordinate
self.gift_weight = gift_weight
if gift_weight is None:
self.gift_weight = [1 for _ in city_coordinates]
self.sleigh_weight = sleigh_weight

@staticmethod
def distance(coord_a, coord_b):
return math.sqrt(sum([(z[0] - z[1]) ** 2 for z in zip(coord_a, coord_b)]))

def check_solution(self, solution: List[List[int]]):
"""
Check if the solution for the problem is valid.
:param solution: List of lists containing integers representing visited cities.
:return: None, unless errors are raised.
"""
set_visited = set(chain.from_iterable(solution))
set_problem = set(range(len(self.coordinates)))
if set_visited != set_problem:
missing = set_problem.difference(set_visited)
extra = set_visited.difference(set_problem)
raise ValueError(f"Not all cities are visited! Missing: {missing} Extra: {extra}")
city_counter = Counter(chain.from_iterable(solution))
if max(city_counter.values()) > 1:
double_cities = {key for key, value in city_counter.items() if value > 1}
raise ValueError(f"Multiple occurrences found for cities: {double_cities}")

def eval_function(self, solution: List[List[int]]) -> Union[float, int]:
"""
Calculates the cost of the current solution for the TSP problem.
:param solution: List of integers which refer to cities.
:return:
"""
self.check_solution(solution=solution)
cost = 0
for route in solution:
total_route_weight = sum([self.gift_weight[t] for t in route]) + self.sleigh_weight
distance = self.distance(self.home_coordinate, self.coordinates[route[0]])
cost += distance * total_route_weight
for t1, t2 in sliding_window(route):
total_route_weight -= self.gift_weight[t1]
city1 = self.coordinates[t1]
city2 = self.coordinates[t2]
cost += self.distance(city1, city2) * total_route_weight
last_leg_distance = self.distance(self.coordinates[route[-1]], self.home_coordinate)
cost += self.sleigh_weight * last_leg_distance
return cost
50 changes: 50 additions & 0 deletions evol/problems/routing/tsp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import math
from typing import List, Union

from evol.problems.problem import Problem
from evol.helpers.utils import rotating_window


class TSPProblem(Problem):
def __init__(self, distance_matrix):
self.distance_matrix = distance_matrix

@classmethod
def from_coordinates(cls, coordinates: List[Union[tuple, list]]) -> 'TSPProblem':
"""
Creates a distance matrix from a list of city coordinates.
:param coordinates: An iterable that contains tuples or lists representing a x,y coordinate.
:return: A list of lists containing the distances between cities.
"""
res = [[0 for i in coordinates] for j in coordinates]
for i, coord_i in enumerate(coordinates):
for j, coord_j in enumerate(coordinates):
dist = math.sqrt(sum([(z[0] - z[1])**2 for z in zip(coord_i[:2], coord_j[:2])]))
res[i][j] = dist
res[j][i] = dist
return TSPProblem(distance_matrix=res)

def check_solution(self, solution: List[int]):
"""
Check if the solution for the TSP problem is valid.
:param solution: List of integers which refer to cities.
:return: None, unless errors are raised.
"""
set_solution = set(solution)
set_problem = set(range(len(self.distance_matrix)))
if len(solution) > len(self.distance_matrix):
raise ValueError("Solution is longer than number of towns!")
if set_solution != set_problem:
raise ValueError(f"Not all towns are visited! Am missing {set_problem.difference(set_solution)}")

def eval_function(self, solution: List[int]) -> Union[float, int]:
"""
Calculates the cost of the current solution for the TSP problem.
:param solution: List of integers which refer to cities.
:return:
"""
self.check_solution(solution=solution)
cost = 0
for t1, t2 in rotating_window(solution):
cost += self.distance_matrix[t1][t2]
return cost
10 changes: 10 additions & 0 deletions tests/helpers/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from pytest import mark

from evol.helpers.utils import rotating_window, sliding_window
from evol import Individual, Population
from evol.helpers.pickers import pick_random
from evol.helpers.utils import select_arguments, offspring_generator
Expand Down Expand Up @@ -72,3 +73,12 @@ def test_all_kwargs(self, args, kwargs, result):
def fct(a, b=0, **kwargs):
return a + b + sum(kwargs.values())
assert fct(*args, **kwargs) == result


class TestSimpleUtilFunc:

def test_sliding_window(self):
assert list(sliding_window([1, 2, 3, 4])) == [(1, 2), (2, 3), (3, 4)]

def test_rotating_window(self):
assert list(rotating_window([1, 2, 3, 4])) == [(4, 1), (1, 2), (2, 3), (3, 4)]
22 changes: 22 additions & 0 deletions tests/problems/test_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from evol.problems.functions import Rosenbrock, Sphere, Rastrigin


def test_rosenbrock_optimality():
problem = Rosenbrock(size=2)
assert problem.eval_function((1, 1)) == 0.0
problem = Rosenbrock(size=5)
assert problem.eval_function((1, 1, 1, 1, 1)) == 0.0


def test_sphere_optimality():
problem = Sphere(size=2)
assert problem.eval_function((0, 0)) == 0.0
problem = Sphere(size=5)
assert problem.eval_function((0, 0, 0, 0, 0)) == 0.0


def test_rastrigin_optimality():
problem = Rastrigin(size=2)
assert problem.eval_function((0, 0)) == 0.0
problem = Rastrigin(size=5)
assert problem.eval_function((0, 0, 0, 0, 0)) == 0.0
54 changes: 54 additions & 0 deletions tests/problems/test_santa.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import math

import pytest

from evol.problems.routing import MagicSanta


@pytest.fixture
def base_problem():
return MagicSanta(city_coordinates=[(0, 1), (1, 0), (1, 1)],
home_coordinate=(0, 0),
gift_weight=[0, 0, 0])


@pytest.fixture
def adv_problem():
return MagicSanta(city_coordinates=[(0, 1), (1, 1), (0, 1)],
home_coordinate=(0, 0),
gift_weight=[5, 1, 1],
sleigh_weight=2)


def test_error_raised_wrong_cities(base_problem):
# we want an error if we see too many cities
with pytest.raises(ValueError) as execinfo1:
base_problem.eval_function([[0, 1, 2, 3]])
assert "Extra: {3}" in str(execinfo1.value)
# we want an error if we see too few cities
with pytest.raises(ValueError) as execinfo2:
base_problem.eval_function([[0, 2]])
assert "Missing: {1}" in str(execinfo2.value)
# we want an error if we see multiple occurences of cities
with pytest.raises(ValueError) as execinfo3:
base_problem.eval_function([[0, 2], [0, 1]])
assert "Multiple occurrences found for cities: {0}" in str(execinfo3.value)


def test_base_score_method(base_problem):
assert base_problem.distance((0, 0), (0, 2)) == 2
expected = 1 + math.sqrt(2) + 1 + math.sqrt(2)
assert base_problem.eval_function([[0, 1, 2]]) == pytest.approx(expected)
assert base_problem.eval_function([[2, 1, 0]]) == pytest.approx(expected)
base_problem.sleigh_weight = 2
assert base_problem.eval_function([[2, 1, 0]]) == pytest.approx(2*expected)


def test_sleight_gift_weights(adv_problem):
expected = (2+7) + (2+2) + (2+1) + (2+0)
assert adv_problem.eval_function([[0, 1, 2]]) == pytest.approx(expected)


def test_multiple_routes(adv_problem):
expected = (2 + 6) + (2 + 1) + math.sqrt(2)*(2 + 0) + (2 + 1) + (2 + 0)
assert adv_problem.eval_function([[0, 1], [2]]) == pytest.approx(expected)
Loading