Quick start (Continued)

We expand the knowledge from Quick Start where the user has learned how to Install OptFrame, and how to compile and test a Simulated Annealing metaheuristic for the classic 0-1 Knapsack Problem (01KP).

We demonstrate how to update this code for other metaheuristics, for the classic Traveling Salesman Problem (TSP).

Second example: Traveling Salesman Problem, Local Search and Random Keys Genetic Algorithm

At this point, we assume the reader is familiarized with the Traveling Salesman Problem… we intend to expand this section in the future with figures and more motivation (for now, sorry, and let’s move on).

TSP Solution definition

We define a TSP solution as a permutations of $N$ cities being visited by a Traveling Salesman. In this representation, each city is represented as a number $0..N-1$, being a solution a vector of N integers (example: [0,2,3,1] means that solution starts from city 0, then follows to city 2, then city 3, then city 1, and finally comes back to city 0). Objective is to find a route that minimizes distance between the $N$ visited cities.

We may define SolutionTSP and its objective value (double by default on API1d, or int on API1i32).

 1
 2class SolutionTSP(object):
 3    def __init__(self):
 4        # number of cities in solution
 5        self.n : int = 0
 6        # visited cities as a list
 7        self.cities : List[int] = []
 8
 9    # MUST provide some printing mechanism
10    def __str__(self):
11        return f"SolutionTSP(n={self.n};cities={self.cities})"
12    

Problem Data

We read a matrix of distances between pairs of cities (considering Euclidean distance), and store in a structure named ProblemContextTSP. Note that we round the result to int, just to allow precise value calculation (but one may use float or double, and then manage the floating-point errors). Other option would be to adopt a strict int API1i32.

 1
 2class ProblemContextTSP(object):
 3    def __init__(self):
 4        # float engine for OptFrame
 5        self.engine = Engine(APILevel.API1d)
 6        # number of cities
 7        self.n = 0
 8        # x coordinates
 9        self.vx = []
10        # y coordinates
11        self.vy = []
12        # distance matrix
13        self.dist = []
14        
15   # Example: "3\n1 10 10\n2 20 20\n3 30 30\n"
16
17    def load(self, filename: str):
18        with open(filename, 'r') as f:
19            lines = f.readlines()
20            self.n = int(lines[0])
21            for i in range(self.n):
22               id_x_y = lines[i+1].split()
23               # ignore id_x_y[0]
24               self.vx.append(int(id_x_y[1]))
25               self.vy.append(int(id_x_y[2]))
26            #
27            self.dist = [[0 for col in range(self.n)] for row in range(self.n)]
28            for i in range(self.n):
29               for j in range(self.n):
30                  self.dist[i][j] = round(self.euclidean(self.vx[i], self.vy[i], self.vx[j], self.vy[j]))
31
32    def euclidean(self, x1, y1, x2, y2):
33        import math
34        return math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2))
35
36    def __str__(self):
37        return f"ProblemContextTSP(n={self.n};vx={self.vx};vy={self.vy};dist={self.dist})"

Finally, we created a function load() to manage de input of data.

Evaluation

Objective calculation can be done by using an evaluator callback, which is compatible to OptFrame Core Evaluator (for single objectives) and GeneralEvaluator (for single and multiple objectives).

 1# continuation of ProblemContextTSP class...
 2    @staticmethod
 3    def minimize(pTSP: 'ProblemContextTSP', s: SolutionTSP) -> float:
 4        assert (s.n == pTSP.n)
 5        assert (len(s.cities) == s.n)
 6        # remember this is an API1d method
 7        f = 0.0
 8        for i in range(pTSP.n-1):
 9          f += pTSP.dist[s.cities[i]][s.cities[i + 1]];
10        f += pTSP.dist[s.cities[int(pTSP.n) - 1]][s.cities[0]];
11        return f

Search methods based on neighborhoods

The next components will depend on the type of search method used, we start with neighborhood-based search techniques.

Random constructive

In a similar manner with Knapsack example (on Quickstart part 1), we define a random solution generator.

 1# continuation of ProblemContextTSP class...
 2    @staticmethod
 3    def generateSolution(problemCtx: 'ProblemContextTSP') -> SolutionTSP:
 4        sol = SolutionTSP()
 5        for i in range(problemCtx.n):
 6            sol.cities.append(i)
 7        random.shuffle(sol.cities)
 8        sol.n = problemCtx.n
 9        return sol
10
11# optional tests...
12assert isinstance(SolutionTSP, XSolution)            # composition tests 
13assert isinstance(ProblemContextTSP,  XProblem)      # composition tests 
14assert isinstance(ProblemContextTSP,  XConstructive) # composition tests    
15assert isinstance(ProblemContextTSP,  XMinimize)     # composition tests

First move operator: Swap

We start with a Move operator capable of exchanging two elements from a given TSP solution.

 1
 2from optframe.components import Move
 3
 4class MoveSwapClass(Move):
 5    def __init__(self, _i: int = 0, _j: int = 0):
 6        self.i = _i
 7        self.j = _j
 8    def __str__(self):
 9        return "MoveSwapClass(i="+str(self.i)+";j="+str(self.j)+")"
10    def apply(self, problemCtx, sol: SolutionTSP) -> 'MoveSwapClass':
11        aux = sol.cities[self.j]
12        sol.cities[self.j] = sol.cities[self.i]
13        sol.cities[self.i] = aux
14        # must create reverse move (j,i)
15        return MoveSwapClass(self.j, self.i)
16    def canBeApplied(self, problemCtx, sol: SolutionTSP) -> bool:
17        return True
18    def eq(self, problemCtx, m2: 'MoveSwapClass') -> bool:
19        return (self.i == m2.i) and (self.j == m2.j)
20
21assert isinstance(MoveSwapClass, XMove)       # composition tests
22assert MoveSwapClass in Move.__subclasses__() # classmethod style

We have four types of neighborhood definitions in OptFrame (NS, NSFind, NSSeq and NSEnum), but two major are NS and NSSeq.

NS works well for random neighborhoods, with no intent of explicitly visiting all possible neighbors (also useful for continuous or infinite neighborhoods). Swap move and NS definition can be seen below.

 1
 2#from optframe.components import NS
 3
 4class NSSwap(object):
 5    @staticmethod
 6    def randomMove(pTSP, sol: SolutionTSP) -> MoveSwapClass:
 7        import random
 8        n = sol.n
 9        i = random.randint(0, n - 1)
10        j = i
11        while  j <= i:
12            i = random.randint(0, n - 1)
13            j = random.randint(0, n - 1)
14        # return MoveSwap(i, j)
15        return MoveSwapClass(i, j)
16    
17#assert NSSwap in NS.__subclasses__()   # optional test

We now define the NSSeq neighborhood with a explicit iterator definition, that requires four operations: first (initializes first valid move), next (skips to next valid move), current (returns current move) and isDone (indicates if no move exists).

 1
 2#from optframe.components import NSSeq
 3from optframe.components import NSIterator
 4
 5# For NSSeq, one must provide a Move Iterator
 6# A Move Iterator has five actions: Init, First, Next, IsDone and Current
 7
 8class IteratorSwap(NSIterator):
 9    def __init__(self, _i: int, _j: int):
10        self.i = _i
11        self.j = _j
12    def first(self, pTSP: ProblemContextTSP):
13        self.i = 0
14        self.j = 1
15    def next(self, pTSP: ProblemContextTSP):
16        if self.j < pTSP.n - 1:
17            self.j = self.j+1
18        else:
19            self.i = self.i + 1
20            self.j = self.i + 1
21    def isDone(self, pTSP: ProblemContextTSP):
22        return self.i >= pTSP.n - 1
23    def current(self, pTSP: ProblemContextTSP):
24        return MoveSwapClass(self.i, self.j)
25    
26assert IteratorSwap in NSIterator.__subclasses__()   # optional test
27    
28class NSSeqSwap(object):
29    @staticmethod
30    def randomMove(pTSP: ProblemContextTSP, sol: SolutionTSP) -> MoveSwapClass:
31        return NSSwap.randomMove(pTSP, sol)  # composition
32    
33    @staticmethod
34    def getIterator(pTSP: ProblemContextTSP, sol: SolutionTSP) -> IteratorSwap:
35        return IteratorSwap(-1, -1)
36
37#assert NSSeqSwap in NSSeq.__subclasses__()   # optional test

Hint

According to groundbreaking ideas from Variable Neighborhood Search community, the user should create multiple neighborhoods, with different ideas in each one, in order to better explore the solution space.

Complete Example for TSP Components

For simplicity, we separate the main TSP components in a file named TSP-fcore.py.

Hint

This example could be made in a multiple files as a package, for the separation of FCore components (on TSP-fcore.py) and the main() entrypoint depending on method used. However, we just merge them into a single file on each application, for simplicity, e.g., mainTSP-fcore-ils.py or mainTSP-fcore-brkga.py.

TSP-fcore.py

File 'TSP-fcore.py' located in 'demo/03_QuickstartTSP_VNS_BRKGA/'

  1# OptFrame Python Demo TSP - Traveling Salesman Problem
  2
  3from typing import List
  4import random
  5
  6from optframe import *
  7from optframe.protocols import *
  8
  9
 10class SolutionTSP(object):
 11    def __init__(self):
 12        # number of cities in solution
 13        self.n : int = 0
 14        # visited cities as a list
 15        self.cities : List[int] = []
 16
 17    # MUST provide some printing mechanism
 18    def __str__(self):
 19        return f"SolutionTSP(n={self.n};cities={self.cities})"
 20    
 21class ProblemContextTSP(object):
 22    def __init__(self):
 23        # float engine for OptFrame
 24        self.engine = Engine(APILevel.API1d)
 25        # number of cities
 26        self.n = 0
 27        # x coordinates
 28        self.vx = []
 29        # y coordinates
 30        self.vy = []
 31        # distance matrix
 32        self.dist = []
 33        
 34   # Example: "3\n1 10 10\n2 20 20\n3 30 30\n"
 35
 36    def load(self, filename: str):
 37        with open(filename, 'r') as f:
 38            lines = f.readlines()
 39            self.n = int(lines[0])
 40            for i in range(self.n):
 41               id_x_y = lines[i+1].split()
 42               # ignore id_x_y[0]
 43               self.vx.append(int(id_x_y[1]))
 44               self.vy.append(int(id_x_y[2]))
 45            #
 46            self.dist = [[0 for col in range(self.n)] for row in range(self.n)]
 47            for i in range(self.n):
 48               for j in range(self.n):
 49                  self.dist[i][j] = round(self.euclidean(self.vx[i], self.vy[i], self.vx[j], self.vy[j]))
 50
 51    def euclidean(self, x1, y1, x2, y2):
 52        import math
 53        return math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2))
 54
 55    def __str__(self):
 56        return f"ProblemContextTSP(n={self.n};vx={self.vx};vy={self.vy};dist={self.dist})"
 57# continuation of ProblemContextTSP class...
 58    @staticmethod
 59    def minimize(pTSP: 'ProblemContextTSP', s: SolutionTSP) -> float:
 60        assert (s.n == pTSP.n)
 61        assert (len(s.cities) == s.n)
 62        # remember this is an API1d method
 63        f = 0.0
 64        for i in range(pTSP.n-1):
 65          f += pTSP.dist[s.cities[i]][s.cities[i + 1]];
 66        f += pTSP.dist[s.cities[int(pTSP.n) - 1]][s.cities[0]];
 67        return f
 68# continuation of ProblemContextTSP class...
 69    @staticmethod
 70    def generateSolution(problemCtx: 'ProblemContextTSP') -> SolutionTSP:
 71        sol = SolutionTSP()
 72        for i in range(problemCtx.n):
 73            sol.cities.append(i)
 74        random.shuffle(sol.cities)
 75        sol.n = problemCtx.n
 76        return sol
 77
 78# optional tests...
 79assert isinstance(SolutionTSP, XSolution)            # composition tests 
 80assert isinstance(ProblemContextTSP,  XProblem)      # composition tests 
 81assert isinstance(ProblemContextTSP,  XConstructive) # composition tests    
 82assert isinstance(ProblemContextTSP,  XMinimize)     # composition tests
 83
 84from optframe.components import Move
 85
 86class MoveSwapClass(Move):
 87    def __init__(self, _i: int = 0, _j: int = 0):
 88        self.i = _i
 89        self.j = _j
 90    def __str__(self):
 91        return "MoveSwapClass(i="+str(self.i)+";j="+str(self.j)+")"
 92    def apply(self, problemCtx, sol: SolutionTSP) -> 'MoveSwapClass':
 93        aux = sol.cities[self.j]
 94        sol.cities[self.j] = sol.cities[self.i]
 95        sol.cities[self.i] = aux
 96        # must create reverse move (j,i)
 97        return MoveSwapClass(self.j, self.i)
 98    def canBeApplied(self, problemCtx, sol: SolutionTSP) -> bool:
 99        return True
100    def eq(self, problemCtx, m2: 'MoveSwapClass') -> bool:
101        return (self.i == m2.i) and (self.j == m2.j)
102
103assert isinstance(MoveSwapClass, XMove)       # composition tests
104assert MoveSwapClass in Move.__subclasses__() # classmethod style
105
106#from optframe.components import NS
107
108class NSSwap(object):
109    @staticmethod
110    def randomMove(pTSP, sol: SolutionTSP) -> MoveSwapClass:
111        import random
112        n = sol.n
113        i = random.randint(0, n - 1)
114        j = i
115        while  j <= i:
116            i = random.randint(0, n - 1)
117            j = random.randint(0, n - 1)
118        # return MoveSwap(i, j)
119        return MoveSwapClass(i, j)
120    
121#assert NSSwap in NS.__subclasses__()   # optional test
122
123#from optframe.components import NSSeq
124from optframe.components import NSIterator
125
126# For NSSeq, one must provide a Move Iterator
127# A Move Iterator has five actions: Init, First, Next, IsDone and Current
128
129class IteratorSwap(NSIterator):
130    def __init__(self, _i: int, _j: int):
131        self.i = _i
132        self.j = _j
133    def first(self, pTSP: ProblemContextTSP):
134        self.i = 0
135        self.j = 1
136    def next(self, pTSP: ProblemContextTSP):
137        if self.j < pTSP.n - 1:
138            self.j = self.j+1
139        else:
140            self.i = self.i + 1
141            self.j = self.i + 1
142    def isDone(self, pTSP: ProblemContextTSP):
143        return self.i >= pTSP.n - 1
144    def current(self, pTSP: ProblemContextTSP):
145        return MoveSwapClass(self.i, self.j)
146    
147assert IteratorSwap in NSIterator.__subclasses__()   # optional test
148    
149class NSSeqSwap(object):
150    @staticmethod
151    def randomMove(pTSP: ProblemContextTSP, sol: SolutionTSP) -> MoveSwapClass:
152        return NSSwap.randomMove(pTSP, sol)  # composition
153    
154    @staticmethod
155    def getIterator(pTSP: ProblemContextTSP, sol: SolutionTSP) -> IteratorSwap:
156        return IteratorSwap(-1, -1)
157
158#assert NSSeqSwap in NSSeq.__subclasses__()   # optional test

Exploring with neighborhood exploration

OptFrame supports several strategies for neighborhood exploration, such as: First Improvement, Best Improvement, Random Selection and Multi Improvement. We can also combine several local search strategies in a multiple strategy called Variable Neighborhood Descent (VND).

Hint

Note that we will use OptFrame Component Builder syntax, to automatically generate a native C++ Component based on a build string. For more details, see the (TODO) Component Builder list on OptFrame C++ ReadTheDocs (TODO).

File 'mainTSP-fcore-ils-part2.py' located in 'demo/03_QuickstartTSP_VNS_BRKGA/'

 1
 2# set random seed for system
 3random.seed(0) # bad generator, just an example..
 4
 5# loads problem from filesystem
 6pTSP = ProblemContextTSP()
 7pTSP.load('tsp-example.txt')
 8print(pTSP)
 9
10# Register Basic Components
11comp_list = pTSP.engine.setup(pTSP)
12print(comp_list)
13
14# get index of new NS
15ns_idx = pTSP.engine.add_ns_class(pTSP, NSSwap)
16print("ns_idx=", ns_idx)
17
18# get index of new NSSeq
19nsseq_idx = pTSP.engine.add_nsseq_class(pTSP, NSSeqSwap)
20print("nsseq_idx=", nsseq_idx)
21
22# ========= play a little bit =========
23
24gev_idx = comp_list[0] # GeneralEvaluator
25ev_idx  = comp_list[1] # Evaluator
26print("evaluator id:", ev_idx)
27
28c_idx = comp_list[2]
29print("c_idx=", c_idx)
30
31is_idx = IdInitialSearch(0)
32print("is_idx=", is_idx)
33
34# test each component
35
36fev = pTSP.engine.get_evaluator(ev_idx)
37pTSP.engine.print_component(fev)
38
39fc = pTSP.engine.get_constructive(c_idx)
40pTSP.engine.print_component(fc)
41
42solxx = pTSP.engine.fconstructive_gensolution(fc)
43print("test solution:", solxx)
44
45z1 = pTSP.engine.fevaluator_evaluate(fev, True, solxx)
46print("test evaluation:", z1)
47
48# some basic tests with moves and iterator
49move = MoveSwapClass(0,1) # swap 0 with 1
50print("move=",move)
51m1 = NSSwap.randomMove(pTSP, solxx)
52print(m1)
53
54print("begin test with iterator")
55it = NSSeqSwap.getIterator(pTSP, solxx)
56it.first(pTSP)
57while not it.isDone(pTSP):
58    m = it.current(pTSP)
59    print(m)
60    it.next(pTSP)
61print("end test with iterator")
62
63# ======== end playing ========
64
65# pack NS into a NS list
66list_idx = pTSP.engine.create_component_list(
67    "[ OptFrame:NS 0 ]", "OptFrame:NS[]")
68print("list_idx=", list_idx)
69
70# print("Listing registered components:")
71# pTSP.engine.list_components("OptFrame:")
72
73# list the required parameters for OptFrame ComponentBuilder
74# print("engine will list builders for OptFrame: ")
75# print(pTSP.engine.list_builders("OptFrame:"))
76# print()
77
78# make next local search component silent (loglevel 0)
79pTSP.engine.experimental_set_parameter("COMPONENT_LOG_LEVEL", "0")
80
81print("building 'BI' neighborhood exploration as local search", flush=True)
82bi = BestImprovement(pTSP.engine, 0, 0)
83ls_idx = bi.get_id()
84print("ls_idx=", ls_idx, flush=True)
85
86print("creating local search list", flush=True)
87list_vnd_idx = pTSP.engine.create_component_list(
88    "[ OptFrame:LocalSearch 0 ]", "OptFrame:LocalSearch[]")
89print("list_vnd_idx=", list_vnd_idx)
90
91
92print("building 'VND' local search")
93vnd = VariableNeighborhoodDescent(pTSP.engine, 0, 0)
94vnd_idx = vnd.get_id()
95print("vnd_idx=", vnd_idx)

If one wants to build a complete metaheuristic, the Iterated Local Search (ILS) or a Variable Neighborhood Search (VNS). The ILS is based on general perturbation concept, so we will use the concept of Levels of Perturbation, that are increased when stuck in a poor quality local optimum. We adopt a perturbation strategy that tries to escape at level p by applying p+2 random moves, e.g., at level 0, 2 random moves are applied, and so on.

 1
 2
 3#####
 4#pTSP.engine.list_components("OptFrame:")
 5
 6ilsl_pert = ILSLevelPertLPlus2(pTSP.engine, 0, 0)
 7pert_idx = ilsl_pert.get_id()
 8print("pert_idx=", pert_idx)
 9
10# pTSP.engine.list_components("OptFrame:")
11
12# make next global search component info (loglevel 3)
13pTSP.engine.experimental_set_parameter("COMPONENT_LOG_LEVEL", "3")
14
15# build Iterated Local Search (ILS) Levels with iterMax=10 maxPert=5
16ilsl = ILSLevels(pTSP.engine, 0, 0, 1, 0, 10, 5)
17print("will start ILS for 3 seconds")
18lout = ilsl.search(3.0)
19print("Best solution: ",   lout.best_s)
20print("Best evaluation: ", lout.best_e)
21
22print("FINISHED")

Complete Example for ILS

We provide the main file for TSP ILS mainTSP-fcore-ils.py.

mainTSP-fcore-ils.py

File 'mainTSP-fcore-ils.py' located in 'demo/03_QuickstartTSP_VNS_BRKGA/'

  1# OptFrame Python Demo TSP - Traveling Salesman Problem
  2
  3from typing import List
  4import random
  5
  6from optframe import *
  7from optframe.protocols import *
  8
  9
 10class SolutionTSP(object):
 11    def __init__(self):
 12        # number of cities in solution
 13        self.n : int = 0
 14        # visited cities as a list
 15        self.cities : List[int] = []
 16
 17    # MUST provide some printing mechanism
 18    def __str__(self):
 19        return f"SolutionTSP(n={self.n};cities={self.cities})"
 20    
 21class ProblemContextTSP(object):
 22    def __init__(self):
 23        # float engine for OptFrame
 24        self.engine = Engine(APILevel.API1d)
 25        # number of cities
 26        self.n = 0
 27        # x coordinates
 28        self.vx = []
 29        # y coordinates
 30        self.vy = []
 31        # distance matrix
 32        self.dist = []
 33        
 34   # Example: "3\n1 10 10\n2 20 20\n3 30 30\n"
 35
 36    def load(self, filename: str):
 37        with open(filename, 'r') as f:
 38            lines = f.readlines()
 39            self.n = int(lines[0])
 40            for i in range(self.n):
 41               id_x_y = lines[i+1].split()
 42               # ignore id_x_y[0]
 43               self.vx.append(int(id_x_y[1]))
 44               self.vy.append(int(id_x_y[2]))
 45            #
 46            self.dist = [[0 for col in range(self.n)] for row in range(self.n)]
 47            for i in range(self.n):
 48               for j in range(self.n):
 49                  self.dist[i][j] = round(self.euclidean(self.vx[i], self.vy[i], self.vx[j], self.vy[j]))
 50
 51    def euclidean(self, x1, y1, x2, y2):
 52        import math
 53        return math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2))
 54
 55    def __str__(self):
 56        return f"ProblemContextTSP(n={self.n};vx={self.vx};vy={self.vy};dist={self.dist})"
 57# continuation of ProblemContextTSP class...
 58    @staticmethod
 59    def minimize(pTSP: 'ProblemContextTSP', s: SolutionTSP) -> float:
 60        assert (s.n == pTSP.n)
 61        assert (len(s.cities) == s.n)
 62        # remember this is an API1d method
 63        f = 0.0
 64        for i in range(pTSP.n-1):
 65          f += pTSP.dist[s.cities[i]][s.cities[i + 1]];
 66        f += pTSP.dist[s.cities[int(pTSP.n) - 1]][s.cities[0]];
 67        return f
 68# continuation of ProblemContextTSP class...
 69    @staticmethod
 70    def generateSolution(problemCtx: 'ProblemContextTSP') -> SolutionTSP:
 71        sol = SolutionTSP()
 72        for i in range(problemCtx.n):
 73            sol.cities.append(i)
 74        random.shuffle(sol.cities)
 75        sol.n = problemCtx.n
 76        return sol
 77
 78# optional tests...
 79assert isinstance(SolutionTSP, XSolution)            # composition tests 
 80assert isinstance(ProblemContextTSP,  XProblem)      # composition tests 
 81assert isinstance(ProblemContextTSP,  XConstructive) # composition tests    
 82assert isinstance(ProblemContextTSP,  XMinimize)     # composition tests
 83
 84from optframe.components import Move
 85
 86class MoveSwapClass(Move):
 87    def __init__(self, _i: int = 0, _j: int = 0):
 88        self.i = _i
 89        self.j = _j
 90    def __str__(self):
 91        return "MoveSwapClass(i="+str(self.i)+";j="+str(self.j)+")"
 92    def apply(self, problemCtx, sol: SolutionTSP) -> 'MoveSwapClass':
 93        aux = sol.cities[self.j]
 94        sol.cities[self.j] = sol.cities[self.i]
 95        sol.cities[self.i] = aux
 96        # must create reverse move (j,i)
 97        return MoveSwapClass(self.j, self.i)
 98    def canBeApplied(self, problemCtx, sol: SolutionTSP) -> bool:
 99        return True
100    def eq(self, problemCtx, m2: 'MoveSwapClass') -> bool:
101        return (self.i == m2.i) and (self.j == m2.j)
102
103assert isinstance(MoveSwapClass, XMove)       # composition tests
104assert MoveSwapClass in Move.__subclasses__() # classmethod style
105
106#from optframe.components import NS
107
108class NSSwap(object):
109    @staticmethod
110    def randomMove(pTSP, sol: SolutionTSP) -> MoveSwapClass:
111        import random
112        n = sol.n
113        i = random.randint(0, n - 1)
114        j = i
115        while  j <= i:
116            i = random.randint(0, n - 1)
117            j = random.randint(0, n - 1)
118        # return MoveSwap(i, j)
119        return MoveSwapClass(i, j)
120    
121#assert NSSwap in NS.__subclasses__()   # optional test
122
123#from optframe.components import NSSeq
124from optframe.components import NSIterator
125
126# For NSSeq, one must provide a Move Iterator
127# A Move Iterator has five actions: Init, First, Next, IsDone and Current
128
129class IteratorSwap(NSIterator):
130    def __init__(self, _i: int, _j: int):
131        self.i = _i
132        self.j = _j
133    def first(self, pTSP: ProblemContextTSP):
134        self.i = 0
135        self.j = 1
136    def next(self, pTSP: ProblemContextTSP):
137        if self.j < pTSP.n - 1:
138            self.j = self.j+1
139        else:
140            self.i = self.i + 1
141            self.j = self.i + 1
142    def isDone(self, pTSP: ProblemContextTSP):
143        return self.i >= pTSP.n - 1
144    def current(self, pTSP: ProblemContextTSP):
145        return MoveSwapClass(self.i, self.j)
146    
147assert IteratorSwap in NSIterator.__subclasses__()   # optional test
148    
149class NSSeqSwap(object):
150    @staticmethod
151    def randomMove(pTSP: ProblemContextTSP, sol: SolutionTSP) -> MoveSwapClass:
152        return NSSwap.randomMove(pTSP, sol)  # composition
153    
154    @staticmethod
155    def getIterator(pTSP: ProblemContextTSP, sol: SolutionTSP) -> IteratorSwap:
156        return IteratorSwap(-1, -1)
157
158#assert NSSeqSwap in NSSeq.__subclasses__()   # optional test
159# ===========================================
160# begins main() python script for TSP ILS/VNS
161# ===========================================
162
163# import ILSLevels and BestImprovement
164from optframe.heuristics import *
165
166# set random seed for system
167random.seed(0) # bad generator, just an example..
168
169# loads problem from filesystem
170pTSP = ProblemContextTSP()
171pTSP.load('tsp-example.txt')
172print(pTSP)
173
174# Register Basic Components
175comp_list = pTSP.engine.setup(pTSP)
176print(comp_list)
177
178# get index of new NS
179ns_idx = pTSP.engine.add_ns_class(pTSP, NSSwap)
180print("ns_idx=", ns_idx)
181
182# get index of new NSSeq
183nsseq_idx = pTSP.engine.add_nsseq_class(pTSP, NSSeqSwap)
184print("nsseq_idx=", nsseq_idx)
185
186# ========= play a little bit =========
187
188gev_idx = comp_list[0] # GeneralEvaluator
189ev_idx  = comp_list[1] # Evaluator
190print("evaluator id:", ev_idx)
191
192c_idx = comp_list[2]
193print("c_idx=", c_idx)
194
195is_idx = IdInitialSearch(0)
196print("is_idx=", is_idx)
197
198# test each component
199
200fev = pTSP.engine.get_evaluator(ev_idx)
201pTSP.engine.print_component(fev)
202
203fc = pTSP.engine.get_constructive(c_idx)
204pTSP.engine.print_component(fc)
205
206solxx = pTSP.engine.fconstructive_gensolution(fc)
207print("test solution:", solxx)
208
209z1 = pTSP.engine.fevaluator_evaluate(fev, True, solxx)
210print("test evaluation:", z1)
211
212# some basic tests with moves and iterator
213move = MoveSwapClass(0,1) # swap 0 with 1
214print("move=",move)
215m1 = NSSwap.randomMove(pTSP, solxx)
216print(m1)
217
218print("begin test with iterator")
219it = NSSeqSwap.getIterator(pTSP, solxx)
220it.first(pTSP)
221while not it.isDone(pTSP):
222    m = it.current(pTSP)
223    print(m)
224    it.next(pTSP)
225print("end test with iterator")
226
227# ======== end playing ========
228
229# pack NS into a NS list
230list_idx = pTSP.engine.create_component_list(
231    "[ OptFrame:NS 0 ]", "OptFrame:NS[]")
232print("list_idx=", list_idx)
233
234# print("Listing registered components:")
235# pTSP.engine.list_components("OptFrame:")
236
237# list the required parameters for OptFrame ComponentBuilder
238# print("engine will list builders for OptFrame: ")
239# print(pTSP.engine.list_builders("OptFrame:"))
240# print()
241
242# make next local search component silent (loglevel 0)
243pTSP.engine.experimental_set_parameter("COMPONENT_LOG_LEVEL", "0")
244
245print("building 'BI' neighborhood exploration as local search", flush=True)
246bi = BestImprovement(pTSP.engine, 0, 0)
247ls_idx = bi.get_id()
248print("ls_idx=", ls_idx, flush=True)
249
250print("creating local search list", flush=True)
251list_vnd_idx = pTSP.engine.create_component_list(
252    "[ OptFrame:LocalSearch 0 ]", "OptFrame:LocalSearch[]")
253print("list_vnd_idx=", list_vnd_idx)
254
255
256print("building 'VND' local search")
257vnd = VariableNeighborhoodDescent(pTSP.engine, 0, 0)
258vnd_idx = vnd.get_id()
259print("vnd_idx=", vnd_idx)
260
261
262#####
263#pTSP.engine.list_components("OptFrame:")
264
265ilsl_pert = ILSLevelPertLPlus2(pTSP.engine, 0, 0)
266pert_idx = ilsl_pert.get_id()
267print("pert_idx=", pert_idx)
268
269# pTSP.engine.list_components("OptFrame:")
270
271# make next global search component info (loglevel 3)
272pTSP.engine.experimental_set_parameter("COMPONENT_LOG_LEVEL", "3")
273
274# build Iterated Local Search (ILS) Levels with iterMax=10 maxPert=5
275ilsl = ILSLevels(pTSP.engine, 0, 0, 1, 0, 10, 5)
276print("will start ILS for 3 seconds")
277lout = ilsl.search(3.0)
278print("Best solution: ",   lout.best_s)
279print("Best evaluation: ", lout.best_e)
280
281print("FINISHED")

To run it:

python3 mainTSP-fcore-ils.py

Search methods based on random keys

We finish with the Biased Random Key Genetic Algorithm (BRKGA), a simple metaheuristic inspired by classic Genetic Algorithm, using the solution representation of $n$ Random Keys, which are $[0,1]^n$ float values.

Random key generation

The BRKGA requires an initial solution generator, which is in this case, $n$ random [0,1] floats. This can be done automatically by the method (since its trivial do generate $n$ [0,1] random numbers), but we choose to demonstrate manually (by inheriting from OptFrame Core class Initial Population).

This is good to tune the degree of randomness (number of random digits) and also the random function used.

 1
 2from optframe.protocols import XConstructiveRK
 3from optframe.core import LibArrayDouble
 4
 5#
 6# random constructive: updates parameter ptr_array_double of type (LibArrayDouble*)
 7#
 8class RKConstructiveTSP(object):
 9    @staticmethod
10    def generateRK(problemCtx: ProblemContextTSP, ptr_array_double : LibArrayDouble) -> int:
11        rkeys = []
12        for i in range(problemCtx.n):
13            key = random.random() # [0,1] uniform
14            rkeys.append(key)
15        #
16        ptr_array_double.contents.size = len(rkeys)
17        ptr_array_double.contents.v = engine.callback_adapter_list_to_vecdouble(rkeys)
18        return len(rkeys)
19

BRKGA decoding

BRKGA also requires a decoder function, that maps this array of random keys into a permutation.

This can be easily done with Functional Core class FDecodeRK, and an interesting approach based on sorting the keys, related to a predefined indexing of each key.

 1
 2from optframe.core import LibArrayDouble
 3from typing import Tuple
 4
 5import ctypes
 6
 7#
 8# decoder function: receives a problem instance and an array of random keys (as LibArrayDouble)
 9#
10
11class DecoderTSP(object):
12    @staticmethod
13    def decodeSolution(pTSP: ProblemContextTSP, array_double : LibArrayDouble) -> SolutionTSP:
14        #
15        sol = SolutionTSP()
16        #
17        lpairs = []
18        for i in range(array_double.size):
19            p = [array_double.v[i], i]
20            lpairs.append(p)
21        #
22        #print("lpairs: ", lpairs)
23        sorted_list = sorted(lpairs)
24        #print("sorted_list: ", sorted_list)
25        #
26        sol.n = pTSP.n
27        sol.cities = []
28        for i in range(array_double.size):
29            sol.cities.append(sorted_list[i][1]) # append index of city in order
30        return sol
31
32    @staticmethod
33    def decodeMinimize(pTSP: ProblemContextTSP, array_double : LibArrayDouble, needsSolution: bool) -> Tuple[SolutionTSP|None, float]:
34        #
35        # print("decodeMinimize! needsSolution="+str(needsSolution), flush=True)
36        sol = DecoderTSP.decodeSolution(pTSP, array_double)
37        #
38        # NOW WILL GET EVALUATION VALUE
39        e = ProblemContextTSP.minimize(pTSP, sol)
40        # FINALLY, WILL RETURN WHAT IS REQUIRED
41        if not needsSolution:
42            return (None, e)
43        else:
44            return (sol, e)
45
46

BRKGA with TSP

We are ready to build a TSP instance with 3 cities with coordinates (10,10), (20,20) and (30,30), and invoke a BRKGA to solve it.

The parameters of BRKGA are: decoding function, initial solution generator, population size, number of iterations, also rates for mutation (randomness), elite (best solutions), preference for elite solutions, and finally, a random generation method.

 1
 2
 3# set random seed for system
 4random.seed(0) # bad generator, just an example..
 5
 6# loads problem from filesystem
 7pTSP = ProblemContextTSP()
 8pTSP.load('tsp-example.txt')
 9
10print("problem=",pTSP)
11
12import optframe
13print(str(optframe.__version__))
14pTSP.engine.welcome()
15
16
17# Register Basic Components
18comp_list = pTSP.engine.setup(pTSP)
19print(comp_list)
20#
21ev_idx = comp_list[1]
22print("evaluator id:", ev_idx)
23
24c_rk_idx = pTSP.engine.add_constructive_rk_class(pTSP, RKConstructiveTSP)
25print("c_rk_idx=", c_rk_idx)
26
27print("")
28dec_rk_idx = pTSP.engine.add_decoder_rk_class(pTSP, DecoderTSP)
29print("dec_rk_idx=", dec_rk_idx)
30
31print("")
32print("WILL CREATE DecoderRandomKeys directly with simultaneous evaluation and optional solution!")
33drk_rk_id = pTSP.engine.add_edecoder_op_rk_class(pTSP, DecoderTSP)
34print("drk_rk_id=", drk_rk_id)
35
36pTSP.engine.list_components("OptFrame:")
37
38#print("")
39#print("WILL CREATE DecoderRandomKeys FROM DecoderRandomKeysNoEvaluation!")
40#drk = DecoderRandomKeys(pTSP.engine, ev_idx, dec_rk_idx)
41#drk_rk_id = drk.get_id()
42#print("drk_rk_id=", drk_rk_id)
43
44# =======================
45# pTSP.engine.experimental_set_parameter("ENGINE_LOG_LEVEL", "4")
46print("")
47print("will start BRKGA for 3 seconds")
48brkga = BRKGA(pTSP.engine, drk_rk_id, c_rk_idx, 30, 1000, 0.4, 0.3, 0.6)
49
50pTSP.engine.list_components("OptFrame:")
51
52lout = brkga.search(3.0)
53print("Best solution: ",   lout.best_s)
54print("Best evaluation: ", lout.best_e)
55

The result from searchOut can be split in two parts, an error code and the returned solution (the same as in Simulated Annealing or any other OptFrame search method).

Complete Example for BRKGA

We provide the main file for TSP BRKGA mainTSP-fcore-brkga.py.

mainTSP-fcore-brkga.cpp

File 'mainTSP-fcore-brkga.py' located in 'demo/03_QuickstartTSP_VNS_BRKGA/'

  1# OptFrame Python Demo TSP - Traveling Salesman Problem
  2
  3from typing import List
  4import random
  5
  6from optframe import *
  7from optframe.protocols import *
  8
  9
 10class SolutionTSP(object):
 11    def __init__(self):
 12        # number of cities in solution
 13        self.n : int = 0
 14        # visited cities as a list
 15        self.cities : List[int] = []
 16
 17    # MUST provide some printing mechanism
 18    def __str__(self):
 19        return f"SolutionTSP(n={self.n};cities={self.cities})"
 20    
 21class ProblemContextTSP(object):
 22    def __init__(self):
 23        # float engine for OptFrame
 24        self.engine = Engine(APILevel.API1d)
 25        # number of cities
 26        self.n = 0
 27        # x coordinates
 28        self.vx = []
 29        # y coordinates
 30        self.vy = []
 31        # distance matrix
 32        self.dist = []
 33        
 34   # Example: "3\n1 10 10\n2 20 20\n3 30 30\n"
 35
 36    def load(self, filename: str):
 37        with open(filename, 'r') as f:
 38            lines = f.readlines()
 39            self.n = int(lines[0])
 40            for i in range(self.n):
 41               id_x_y = lines[i+1].split()
 42               # ignore id_x_y[0]
 43               self.vx.append(int(id_x_y[1]))
 44               self.vy.append(int(id_x_y[2]))
 45            #
 46            self.dist = [[0 for col in range(self.n)] for row in range(self.n)]
 47            for i in range(self.n):
 48               for j in range(self.n):
 49                  self.dist[i][j] = round(self.euclidean(self.vx[i], self.vy[i], self.vx[j], self.vy[j]))
 50
 51    def euclidean(self, x1, y1, x2, y2):
 52        import math
 53        return math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2))
 54
 55    def __str__(self):
 56        return f"ProblemContextTSP(n={self.n};vx={self.vx};vy={self.vy};dist={self.dist})"
 57# continuation of ProblemContextTSP class...
 58    @staticmethod
 59    def minimize(pTSP: 'ProblemContextTSP', s: SolutionTSP) -> float:
 60        assert (s.n == pTSP.n)
 61        assert (len(s.cities) == s.n)
 62        # remember this is an API1d method
 63        f = 0.0
 64        for i in range(pTSP.n-1):
 65          f += pTSP.dist[s.cities[i]][s.cities[i + 1]];
 66        f += pTSP.dist[s.cities[int(pTSP.n) - 1]][s.cities[0]];
 67        return f
 68# continuation of ProblemContextTSP class...
 69    @staticmethod
 70    def generateSolution(problemCtx: 'ProblemContextTSP') -> SolutionTSP:
 71        sol = SolutionTSP()
 72        for i in range(problemCtx.n):
 73            sol.cities.append(i)
 74        random.shuffle(sol.cities)
 75        sol.n = problemCtx.n
 76        return sol
 77
 78# optional tests...
 79assert isinstance(SolutionTSP, XSolution)            # composition tests 
 80assert isinstance(ProblemContextTSP,  XProblem)      # composition tests 
 81assert isinstance(ProblemContextTSP,  XConstructive) # composition tests    
 82assert isinstance(ProblemContextTSP,  XMinimize)     # composition tests
 83
 84from optframe.components import Move
 85
 86class MoveSwapClass(Move):
 87    def __init__(self, _i: int = 0, _j: int = 0):
 88        self.i = _i
 89        self.j = _j
 90    def __str__(self):
 91        return "MoveSwapClass(i="+str(self.i)+";j="+str(self.j)+")"
 92    def apply(self, problemCtx, sol: SolutionTSP) -> 'MoveSwapClass':
 93        aux = sol.cities[self.j]
 94        sol.cities[self.j] = sol.cities[self.i]
 95        sol.cities[self.i] = aux
 96        # must create reverse move (j,i)
 97        return MoveSwapClass(self.j, self.i)
 98    def canBeApplied(self, problemCtx, sol: SolutionTSP) -> bool:
 99        return True
100    def eq(self, problemCtx, m2: 'MoveSwapClass') -> bool:
101        return (self.i == m2.i) and (self.j == m2.j)
102
103assert isinstance(MoveSwapClass, XMove)       # composition tests
104assert MoveSwapClass in Move.__subclasses__() # classmethod style
105
106#from optframe.components import NS
107
108class NSSwap(object):
109    @staticmethod
110    def randomMove(pTSP, sol: SolutionTSP) -> MoveSwapClass:
111        import random
112        n = sol.n
113        i = random.randint(0, n - 1)
114        j = i
115        while  j <= i:
116            i = random.randint(0, n - 1)
117            j = random.randint(0, n - 1)
118        # return MoveSwap(i, j)
119        return MoveSwapClass(i, j)
120    
121#assert NSSwap in NS.__subclasses__()   # optional test
122
123#from optframe.components import NSSeq
124from optframe.components import NSIterator
125
126# For NSSeq, one must provide a Move Iterator
127# A Move Iterator has five actions: Init, First, Next, IsDone and Current
128
129class IteratorSwap(NSIterator):
130    def __init__(self, _i: int, _j: int):
131        self.i = _i
132        self.j = _j
133    def first(self, pTSP: ProblemContextTSP):
134        self.i = 0
135        self.j = 1
136    def next(self, pTSP: ProblemContextTSP):
137        if self.j < pTSP.n - 1:
138            self.j = self.j+1
139        else:
140            self.i = self.i + 1
141            self.j = self.i + 1
142    def isDone(self, pTSP: ProblemContextTSP):
143        return self.i >= pTSP.n - 1
144    def current(self, pTSP: ProblemContextTSP):
145        return MoveSwapClass(self.i, self.j)
146    
147assert IteratorSwap in NSIterator.__subclasses__()   # optional test
148    
149class NSSeqSwap(object):
150    @staticmethod
151    def randomMove(pTSP: ProblemContextTSP, sol: SolutionTSP) -> MoveSwapClass:
152        return NSSwap.randomMove(pTSP, sol)  # composition
153    
154    @staticmethod
155    def getIterator(pTSP: ProblemContextTSP, sol: SolutionTSP) -> IteratorSwap:
156        return IteratorSwap(-1, -1)
157
158#assert NSSeqSwap in NSSeq.__subclasses__()   # optional test
159# =========================================
160# begins main() python script for TSP BRKGA
161# =========================================
162from optframe.heuristics import *
163
164from optframe.protocols import XConstructiveRK
165from optframe.core import LibArrayDouble
166
167#
168# random constructive: updates parameter ptr_array_double of type (LibArrayDouble*)
169#
170class RKConstructiveTSP(object):
171    @staticmethod
172    def generateRK(problemCtx: ProblemContextTSP, ptr_array_double : LibArrayDouble) -> int:
173        rkeys = []
174        for i in range(problemCtx.n):
175            key = random.random() # [0,1] uniform
176            rkeys.append(key)
177        #
178        ptr_array_double.contents.size = len(rkeys)
179        ptr_array_double.contents.v = engine.callback_adapter_list_to_vecdouble(rkeys)
180        return len(rkeys)
181
182
183from optframe.core import LibArrayDouble
184from typing import Tuple
185
186import ctypes
187
188#
189# decoder function: receives a problem instance and an array of random keys (as LibArrayDouble)
190#
191
192class DecoderTSP(object):
193    @staticmethod
194    def decodeSolution(pTSP: ProblemContextTSP, array_double : LibArrayDouble) -> SolutionTSP:
195        #
196        sol = SolutionTSP()
197        #
198        lpairs = []
199        for i in range(array_double.size):
200            p = [array_double.v[i], i]
201            lpairs.append(p)
202        #
203        #print("lpairs: ", lpairs)
204        sorted_list = sorted(lpairs)
205        #print("sorted_list: ", sorted_list)
206        #
207        sol.n = pTSP.n
208        sol.cities = []
209        for i in range(array_double.size):
210            sol.cities.append(sorted_list[i][1]) # append index of city in order
211        return sol
212
213    @staticmethod
214    def decodeMinimize(pTSP: ProblemContextTSP, array_double : LibArrayDouble, needsSolution: bool) -> Tuple[SolutionTSP|None, float]:
215        #
216        # print("decodeMinimize! needsSolution="+str(needsSolution), flush=True)
217        sol = DecoderTSP.decodeSolution(pTSP, array_double)
218        #
219        # NOW WILL GET EVALUATION VALUE
220        e = ProblemContextTSP.minimize(pTSP, sol)
221        # FINALLY, WILL RETURN WHAT IS REQUIRED
222        if not needsSolution:
223            return (None, e)
224        else:
225            return (sol, e)
226
227
228
229
230# set random seed for system
231random.seed(0) # bad generator, just an example..
232
233# loads problem from filesystem
234pTSP = ProblemContextTSP()
235pTSP.load('tsp-example.txt')
236
237print("problem=",pTSP)
238
239import optframe
240print(str(optframe.__version__))
241pTSP.engine.welcome()
242
243
244# Register Basic Components
245comp_list = pTSP.engine.setup(pTSP)
246print(comp_list)
247#
248ev_idx = comp_list[1]
249print("evaluator id:", ev_idx)
250
251c_rk_idx = pTSP.engine.add_constructive_rk_class(pTSP, RKConstructiveTSP)
252print("c_rk_idx=", c_rk_idx)
253
254print("")
255dec_rk_idx = pTSP.engine.add_decoder_rk_class(pTSP, DecoderTSP)
256print("dec_rk_idx=", dec_rk_idx)
257
258print("")
259print("WILL CREATE DecoderRandomKeys directly with simultaneous evaluation and optional solution!")
260drk_rk_id = pTSP.engine.add_edecoder_op_rk_class(pTSP, DecoderTSP)
261print("drk_rk_id=", drk_rk_id)
262
263pTSP.engine.list_components("OptFrame:")
264
265#print("")
266#print("WILL CREATE DecoderRandomKeys FROM DecoderRandomKeysNoEvaluation!")
267#drk = DecoderRandomKeys(pTSP.engine, ev_idx, dec_rk_idx)
268#drk_rk_id = drk.get_id()
269#print("drk_rk_id=", drk_rk_id)
270
271# =======================
272# pTSP.engine.experimental_set_parameter("ENGINE_LOG_LEVEL", "4")
273print("")
274print("will start BRKGA for 3 seconds")
275brkga = BRKGA(pTSP.engine, drk_rk_id, c_rk_idx, 30, 1000, 0.4, 0.3, 0.6)
276
277pTSP.engine.list_components("OptFrame:")
278
279lout = brkga.search(3.0)
280print("Best solution: ",   lout.best_s)
281print("Best evaluation: ", lout.best_e)
282

To run it:

python3 mainTSP-fcore-brkga.py

More Examples

For a other examples, see folder Examples/FCore-BRKGA on OptFrame C++ project.

Warning

Feel free to check folder OptFrame/Examples for other examples on FCore and OptFrame C++.