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).

A knapsack and some items (from wikipedia) 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