Quick start
OptFrame Python is built upon OptFrame C++, a framework focused in solving challenging optimization problems, specially by means of metaheuristic techniques. This package abstracts away the C++ complexity and makes it easier for users to model efficient optimization techniques directly on Python.
After installing OptFrame Python (maybe using pip), user can start building a solution to the problem at hand.
Hint
The examples from OptFrame Python project follow the same idea from OptFrame C++. Understanding both can be interesting to gain more confidence on the framework. If feeling brave, take a look at OptFrame C++ on ReadTheDocs
First Example: 0-1 Knapsack Problem and Simulated Annealing
Let’s consider a classic problem: the 0-1 Knapsack Problem (KP).
By: Dake CC BY-SA 2.5
Given a set of items \(I\), the KP consists in selecting some items \(S \subseteq I\), such that the sum of weights \(w_i\) (for each selected item) do not exceed knapsack capacity \(Q\), and profit \(p_i\) of the selected items is maximized.
$$maximize\; \sum_{i \in S} p_i$$ $$\sum_{i \in S} w_i \leq Q$$ $$S \subseteq I$$
Solution and Evaluation Types
Users must first define a Representation, which is a data structure that represents
an element in the Solution Space for the KP. A natural candidate here is a list of booleans,
since we need to decide if we are going to carry each item or not. In Python, an interesting
approach is to use native list or even advanced containers of numpy
for greater performance.
User also needs to specify the data type for the Objective Space, in general a numeric type. The standard API of OptFrame Python adopts float64 or double type, and this API is called API1d.
Hint
There are more advanced APIs, other than API1d, which we will explore in the future.
We declare a XSolution class implemented as
SolutionKP.
Note that XSolution concept requires solution to be printable (through __str__
method)
and fully copiable (through __deepcopy__
method):
1
2class SolutionKP(object):
3 def __init__(self):
4 self.n : int = 0 # number of items in solution
5 self.bag : List[int] = [] # selected items in solution
6 def __str__(self):
7 return f"SolutionKP(n={self.n};bag={self.bag})"
8
Hint
For more advanced APIs, we will need a XESolution pair, containing the solution and evaluation value. In the future we will see more of this.
Problem Context
Users will need to store general problem information (such as profits and weights of items), so a ProblemContextKP can be introduced.
1
2
3class ExampleKP(object):
4 def __init__(self):
5 self.engine = Engine()
6 self.n : int = 0 # number of items
7 self.w : List[float] = [] # item weights
8 self.p : List[float] = [] # item profits
9 self.Q : float = 0.0 # knapsack capacity
10
11 def load(self, filename : str):
12 with open(filename, 'r') as f:
13 lines = f.readlines()
14 self.n = int(lines[0])
15 self.Q = int(lines[1])
16 p_lines = lines[2].split()
17 self.p = [int(i) for i in p_lines]
18 w_lines = lines[3].split()
19 self.w = [int(i) for i in w_lines]
20
21 def __str__(self):
22 return f"ExampleKP(n={self.n};Q={self.Q};w={self.w};p={self.p})"
23
Hint
ProblemContext is a user-defined class that can have any desired format. A ‘load’ function is just a suggestion, but not at all necessary. An OptFrame engine is also suggested to be stored here, since this problem will be available to all components.
Random Constructive
We need to have some initial solution for the search process, so we just proceed in a random manner. For simplicity, we allow infeasible solutions to be generated (as if capacity was infinite). Any function name is acceptable (such as mycallback_constructive), but pay attention to the function parameters (receives problem and returns solution):
1# continuation of ExampleKP class...
2 @staticmethod
3 def generateSolution(problem: 'ExampleKP') -> SolutionKP:
4 import random
5 sol = SolutionKP()
6 sol.n = problem.n
7 sol.bag = [random.randint(0, 1) for _ in range(sol.n)]
8 return sol
9
Hint
User can also define many advanced constructive techniques in a similar manner, such as greedy and greedy randomized approaches.
Evaluator
Now it’s time to define an evaluation (or objective) function. According to the goal of maximizing the profits, we iterate over selected items to accumulate profit and weights. As discussed in constructive section, we allow accumulated weight to surpass knapsack capacity, for infeasible configurations. To discourage that, we introduce negative penalization whenever capacity is exceeded (assuming weight -1000000):
1# continuation of ExampleKP class...
2 @staticmethod
3 def maximize(pKP: 'ExampleKP', sol: SolutionKP) -> float:
4 import numpy as np
5 wsum = np.dot(sol.bag, pKP.w)
6 if wsum > pKP.Q:
7 return -1000.0*(wsum - pKP.Q)
8 return np.dot(sol.bag, pKP.p)
9
10# optional tests...
11assert isinstance(SolutionKP, XSolution) # composition tests
12assert isinstance(ExampleKP, XProblem) # composition tests
13assert isinstance(ExampleKP, XConstructive) # composition tests
14assert isinstance(ExampleKP, XMaximize) # composition tests
We have defined maximize
function,
and later we will define its optimization direction.
Hint
User can also choose to minimize
if dealing with a minimization problem. For multi-objective
problems and Pareto optimization, user should visit Multi-Objective section.
Neighborhood Structure
In order to improve a given solution, several metaheuristics employ Local Search Optimization techniques based on the concept of Neighborhood Structure. Every neighborhood is related to a move operator, which is required (on FCore) to have an undo operation (capable of reverting the effects of the move).
We create a BitFlip
move, that changes the true/false
selection of a given item \(k\).
In this case, the move structure (representation of the move) is just an int
, that represents the flipped item.
Note the three class methods apply, canBeApplied and equals.
1
2from optframe.components import Move
3
4class MoveBitFlip(Move):
5 def __init__(self, _k :int):
6 self.k = _k
7 def apply(self, problemCtx: ExampleKP, sol: SolutionKP) -> 'MoveBitFlip':
8 sol.bag[self.k] = 1 - sol.bag[self.k]
9 return MoveBitFlip(self.k)
10 def canBeApplied(self, problemCtx: ExampleKP, sol: SolutionKP) -> bool:
11 return True
12 def eq(self, problemCtx: ExampleKP, m2: 'MoveBitFlip') -> bool:
13 return self.k == m2.k
14
Now, it’s time to define a neighborhood generator for the move.
OptFrame has three main types of neighborhoods: NS
, NSSeq
and NSEnum
.
In this example, we will use NS
, since it only requires the generation of random moves:
1
2class NSBitFlip(object):
3 @staticmethod
4 def randomMove(pKP: ExampleKP, sol: SolutionKP) -> MoveBitFlip:
5 import random
6 return MoveBitFlip(random.randint(0, pKP.n - 1))
7
8assert isinstance(NSBitFlip, XNS) # composition tests
Hint
It is usually a good idea to start developing over the simplest neighborhood, which is NS
.
Most (non-deterministic) metaheuristics only requires a NS
, as it only requires the generation of random moves.
More advanced neighborhoods based on iterators, such as NSSeq
and NSEnum
are only required for advanced Local Search methods.
Time to Test!
At this point, you can already test many nice metaheuristics and solve your knapsack problem! We use the following code to load a problem instance (see Complete Example after):
1
2# set random seed for system
3# random.seed(10)
4
5# loads problem from filesystem
6pKP = ExampleKP()
7pKP.load('knapsack-example.txt')
8#pKP.n = 5
9#pKP.w = [1, 2, 3, 7, 8]
10#pKP.p = [1, 1, 1, 5, 5]
11#pKP.Q = 10.0
12
13
Hint
It is useful to test every FCore structure independently, so as to develop unit testing for them.
Now we register evaluation and constructive into the engine. Then we test the constructive and evaluator:
1
2# register model components (evaluation function, constructive, ...)
3pKP.engine.setup(pKP)
4ev_idx = 0
5c_idx = 0
6is_idx = 0
7# is_idx = pKP.engine.create_initial_search(ev_idx, c_idx)
8
9# register NS class
10pKP.engine.add_ns_class(pKP, NSBitFlip)
11ns_idx = 0
12
13# make engine silent (loglevel 0)
14pKP.engine.experimental_set_parameter("ENGINE_LOG_LEVEL", "0")
15
16# ======= play a little bit ========
17
18fev = pKP.engine.get_evaluator(ev_idx)
19pKP.engine.print_component(fev)
20
21fc = pKP.engine.get_constructive(c_idx)
22pKP.engine.print_component(fc)
23
24solxx = pKP.engine.fconstructive_gensolution(fc)
25print("solxx:", solxx)
26
27z1 = pKP.engine.fevaluator_evaluate(fev, False, solxx)
28print("evaluation:", z1)
29
30# ====== end playing ======
Now we give an example with of the most well-known metaheuristics: the Simulated Annealing.
It has few parameters, including: initial temperature T0
, cooling factor alpha
, and iterations per temperature iterT
.
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).
1
2# list the required parameters for OptFrame SA ComponentBuilder
3print("engine will list builders for :BasicSA ")
4nbuilders=pKP.engine.list_builders(":BasicSA")
5print("nbuilders =", nbuilders)
6
7# pack NS into a NS list
8list_idx = pKP.engine.create_component_list(
9 "[ OptFrame:NS 0 ]", "OptFrame:NS[]")
10
11# make global search silent (loglevel 0)
12pKP.engine.experimental_set_parameter("COMPONENT_LOG_LEVEL", "0")
13
14# check components!
15print("will invoke check module")
16pKP.engine.check(100, 10, False)
17
18# build Simulated Annealing with alpha=0.98 T0=99999 and IterMax=100
19sa = BasicSimulatedAnnealing(pKP.engine, 0, 0, list_idx, 0.98, 100, 99999)
20print("will invoke Simulated Annealing")
21sout = sa.search(10.0)
22print("Best solution: ", sout.best_s)
23print("Best evaluation: ", sout.best_e)
Complete Example
Warning
We present a complete example below. Note that some small differences may exist due to updates in tutorial, including language details.
Feel free to check OptFrame C++ folder OptFrame/Examples
for other examples on FCore and OptFrame Classic.
Example is divided in two files: KP-fcore-ex.py
and mainKP-fcore-ex.py
.
The file mainKP-fcore-ex.py
aggregates all components for execution.
Hint
This example could be made in a single file, to be even simpler. However, we recommend users to have
a clear separation for the header declaration of FCore components (on KP-fcore-ex.py
)
from the main()
entrypoint (on mainKP-fcore-ex.py
), since unit testing is much simpler when these are decoupled.
mainKP-fcore-ex.py
File 'mainKP-fcore-ex.py' located in 'demo/02_QuickstartKP_SA/'
1# OptFrame Python Demo 0-1 Knapsack Problem + Simulated Annealing
2
3from typing import List
4
5from optframe import *
6from optframe.protocols import *
7
8
9class SolutionKP(object):
10 def __init__(self):
11 self.n : int = 0 # number of items in solution
12 self.bag : List[int] = [] # selected items in solution
13 def __str__(self):
14 return f"SolutionKP(n={self.n};bag={self.bag})"
15
16
17
18class ExampleKP(object):
19 def __init__(self):
20 self.engine = Engine()
21 self.n : int = 0 # number of items
22 self.w : List[float] = [] # item weights
23 self.p : List[float] = [] # item profits
24 self.Q : float = 0.0 # knapsack capacity
25
26 def load(self, filename : str):
27 with open(filename, 'r') as f:
28 lines = f.readlines()
29 self.n = int(lines[0])
30 self.Q = int(lines[1])
31 p_lines = lines[2].split()
32 self.p = [int(i) for i in p_lines]
33 w_lines = lines[3].split()
34 self.w = [int(i) for i in w_lines]
35
36 def __str__(self):
37 return f"ExampleKP(n={self.n};Q={self.Q};w={self.w};p={self.p})"
38
39# continuation of ExampleKP class...
40 @staticmethod
41 def generateSolution(problem: 'ExampleKP') -> SolutionKP:
42 import random
43 sol = SolutionKP()
44 sol.n = problem.n
45 sol.bag = [random.randint(0, 1) for _ in range(sol.n)]
46 return sol
47
48# continuation of ExampleKP class...
49 @staticmethod
50 def maximize(pKP: 'ExampleKP', sol: SolutionKP) -> float:
51 import numpy as np
52 wsum = np.dot(sol.bag, pKP.w)
53 if wsum > pKP.Q:
54 return -1000.0*(wsum - pKP.Q)
55 return np.dot(sol.bag, pKP.p)
56
57# optional tests...
58assert isinstance(SolutionKP, XSolution) # composition tests
59assert isinstance(ExampleKP, XProblem) # composition tests
60assert isinstance(ExampleKP, XConstructive) # composition tests
61assert isinstance(ExampleKP, XMaximize) # composition tests
62
63from optframe.components import Move
64
65class MoveBitFlip(Move):
66 def __init__(self, _k :int):
67 self.k = _k
68 def apply(self, problemCtx: ExampleKP, sol: SolutionKP) -> 'MoveBitFlip':
69 sol.bag[self.k] = 1 - sol.bag[self.k]
70 return MoveBitFlip(self.k)
71 def canBeApplied(self, problemCtx: ExampleKP, sol: SolutionKP) -> bool:
72 return True
73 def eq(self, problemCtx: ExampleKP, m2: 'MoveBitFlip') -> bool:
74 return self.k == m2.k
75
76class NSBitFlip(object):
77 @staticmethod
78 def randomMove(pKP: ExampleKP, sol: SolutionKP) -> MoveBitFlip:
79 import random
80 return MoveBitFlip(random.randint(0, pKP.n - 1))
81
82assert isinstance(NSBitFlip, XNS) # composition tests
83
84# ===========================
85# begins main() python script
86# ===========================
87
88# get SimulatedAnnealing
89from optframe.heuristics import *
90
91# set random seed for system
92# random.seed(10)
93
94# loads problem from filesystem
95pKP = ExampleKP()
96pKP.load('knapsack-example.txt')
97#pKP.n = 5
98#pKP.w = [1, 2, 3, 7, 8]
99#pKP.p = [1, 1, 1, 5, 5]
100#pKP.Q = 10.0
101
102
103
104# register model components (evaluation function, constructive, ...)
105pKP.engine.setup(pKP)
106ev_idx = 0
107c_idx = 0
108is_idx = 0
109# is_idx = pKP.engine.create_initial_search(ev_idx, c_idx)
110
111# register NS class
112pKP.engine.add_ns_class(pKP, NSBitFlip)
113ns_idx = 0
114
115# make engine silent (loglevel 0)
116pKP.engine.experimental_set_parameter("ENGINE_LOG_LEVEL", "0")
117
118# ======= play a little bit ========
119
120fev = pKP.engine.get_evaluator(ev_idx)
121pKP.engine.print_component(fev)
122
123fc = pKP.engine.get_constructive(c_idx)
124pKP.engine.print_component(fc)
125
126solxx = pKP.engine.fconstructive_gensolution(fc)
127print("solxx:", solxx)
128
129z1 = pKP.engine.fevaluator_evaluate(fev, False, solxx)
130print("evaluation:", z1)
131
132# ====== end playing ======
133
134# list the required parameters for OptFrame SA ComponentBuilder
135print("engine will list builders for :BasicSA ")
136nbuilders=pKP.engine.list_builders(":BasicSA")
137print("nbuilders =", nbuilders)
138
139# pack NS into a NS list
140list_idx = pKP.engine.create_component_list(
141 "[ OptFrame:NS 0 ]", "OptFrame:NS[]")
142
143# make global search silent (loglevel 0)
144pKP.engine.experimental_set_parameter("COMPONENT_LOG_LEVEL", "0")
145
146# check components!
147print("will invoke check module")
148pKP.engine.check(100, 10, False)
149
150# build Simulated Annealing with alpha=0.98 T0=99999 and IterMax=100
151sa = BasicSimulatedAnnealing(pKP.engine, 0, 0, list_idx, 0.98, 100, 99999)
152print("will invoke Simulated Annealing")
153sout = sa.search(10.0)
154print("Best solution: ", sout.best_s)
155print("Best evaluation: ", sout.best_e)
156
157# =========================
158# ends main() python script
159# =========================
knapsack-example.txt
File 'knapsack-example.txt' located in 'demo/02_QuickstartKP_SA/'
15
210
31 1 1 5 5
41 2 3 7 8
To run it:
python3 mainKP-fcore-ex.py