4 |
5 |
6 |
7 |
8 | <%= htmlWebpackPlugin.options.title %>
9 |
10 |
11 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/deployment/frontend/src/assets/style.scss:
--------------------------------------------------------------------------------
1 |
2 |
3 | body {
4 | /* font-size: 0.9em; */
5 | background-color: #ecf0f1;
6 | }
7 |
8 | .btn {
9 | padding: 2px 8px;
10 | }
11 |
12 | input {
13 | border: 0px solid #000;
14 | margin: 0;
15 | background: transparent;
16 | width: 100%;
17 | }
18 | table tr td {
19 | border-right: 1px solid #000;
20 | border-bottom: 1px solid #000;
21 | }
22 | table {
23 | background: #fff none repeat scroll 0 0;
24 | border-left: 1px solid #000;
25 | border-top: 1px solid #000;
26 | }
27 | table tr:nth-child(even) {
28 | background: #95a5a6;
29 | }
30 | table tr:nth-child(odd) {
31 | background: #bdc3c7;
32 | }
33 | .active {
34 | font-weight: bold;
35 | }
36 |
37 | .information {
38 | border-radius: 100%;
39 | background: #444;
40 | color: white;
41 | padding: 2px 5px;
42 | font-size: 80%;
43 | font-weight: bold;
44 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Emad Ehsan
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/deployment/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build",
8 | "lint": "vue-cli-service lint"
9 | },
10 | "dependencies": {
11 | "axios": "^0.21.1",
12 | "bootstrap": "^5.1.0",
13 | "bootstrap-vue": "^2.21.2",
14 | "core-js": "^3.6.5",
15 | "d3": "^7.0.1",
16 | "vue": "^3.2.6"
17 | },
18 | "devDependencies": {
19 | "@types/d3": "^7.0.0",
20 | "@vue/cli-plugin-babel": "~4.5.0",
21 | "@vue/cli-plugin-eslint": "~4.5.0",
22 | "@vue/cli-service": "~4.5.0",
23 | "@vue/compiler-sfc": "^3.0.0",
24 | "babel-eslint": "^10.1.0",
25 | "eslint": "^6.7.2",
26 | "eslint-plugin-vue": "^7.0.0"
27 | },
28 | "eslintConfig": {
29 | "root": true,
30 | "env": {
31 | "node": true
32 | },
33 | "extends": [
34 | "plugin:vue/vue3-essential",
35 | "eslint:recommended"
36 | ],
37 | "parserOptions": {
38 | "parser": "babel-eslint"
39 | },
40 | "rules": {}
41 | },
42 | "browserslist": [
43 | "> 1%",
44 | "last 2 versions",
45 | "not dead"
46 | ]
47 | }
48 |
--------------------------------------------------------------------------------
/deployment/server.py:
--------------------------------------------------------------------------------
1 | from flask import Flask, json, request
2 | from flask_cors import CORS, cross_origin
3 |
4 | import stock_cutter # local module
5 |
6 | app = Flask(__name__)
7 | cors = CORS(app)
8 | app.config['CORS_HEADERS'] = 'Content-Type'
9 |
10 | @app.route('/', methods=['GET'])
11 | @cross_origin()
12 | def get_csp():
13 | return 'Cutting Stock Problem'
14 |
15 | '''
16 | route for receving data for 1D problem
17 | '''
18 | @app.route('/stocks_1d', methods=['POST'])
19 | @cross_origin()
20 | def post_stocks_1d():
21 | '''
22 | expects two params to be present
23 | child_rolls:
24 | array of arrays. E.g [ [quantity, width], [quantity, width], ... ]
25 |
26 | parent_rolls:
27 | array of arrays. E.g [ [quantity, width], [quantity, width], ... ]
28 | '''
29 | import stock_cutter_1d
30 |
31 | data = request.json
32 | print('data: ', data)
33 |
34 | child_rolls = data['child_rolls']
35 | parent_rolls = data['parent_rolls']
36 |
37 | '''
38 | it can be
39 | exactCuts: cut exactly as many as specified by user
40 | minWaste: cut some items, more than specified, to avoid waste
41 | '''
42 | cutStyle = data['cutStyle']
43 |
44 | # output = stock_cutter_1d.StockCutter1D(child_rolls, parent_rolls, cutStyle=cutStyle)
45 | output = stock_cutter_1d.StockCutter1D(child_rolls, parent_rolls, large_model=False, cutStyle=cutStyle)
46 |
47 | return output
48 |
49 |
50 |
51 | '''
52 | route for 2D
53 | '''
54 | @app.route('/stocks_2d', methods=['POST'])
55 | @cross_origin()
56 | def post_stocks():
57 | '''
58 | expects two params to be present
59 | child_rects:
60 | array of arrays. Each inner array is like [w, h] i.e. width & height of rectangle
61 |
62 | parent_rects:
63 | array of arrays. Each inner array is like [w, h] i.e. width & height of rectangle
64 | '''
65 | data = request.json
66 | print('data: ', data)
67 |
68 | child_rects = data['child_rects']
69 | parent_rects = data['parent_rects']
70 |
71 | output = stock_cutter.StockCutter(child_rects, parent_rects)
72 |
73 | return output
74 |
75 |
76 |
77 | if __name__ == '__main__':
78 | # app.run()
79 | app.run(threaded=True, port=5000)
80 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Cutting Stock Problem
2 | Cutting Stock Problem (CSP) deals with planning the cutting of items (rods / sheets) from given stock items (which are usually of fixed size).
3 |
4 | ## New to Cutting Stock Problem? Understand Visually
5 |
6 |
7 |
8 |
9 |
10 | This implementation of CSP tries to answer
11 | > How to minimize number of stock items used while cutting customer order
12 |
13 |
14 | while doing so, it also caters
15 | > How to cut the stock for customer orders so that waste is minimum
16 |
17 |
18 | The OR Tools also helps us in calculating the number of possible solutions for your problem. So in addition, we can also compute
19 | > In how many ways can we cut given order from fixed size Stock?
20 |
21 |
22 | ## Quick Usage
23 | This is how CSP Tools looks in action. Click [CSP Tool](https://emadehsan.com/csp/) to use it
24 |
25 |
26 |
27 |
28 | ## Libraries
29 | * [Google OR-Tools](https://developers.google.com/optimization)
30 |
31 | ## Quick Start
32 | Install [Pipenv](https://pipenv.pypa.io/en/latest/), if not already installed
33 | ```sh
34 | $ pip3 install --user pipenv
35 | ```
36 |
37 | Clone this project and install packages
38 | ```sh
39 | $ git clone https://github.com/emadehsan/csp
40 | $ cd csp
41 | $ pipenv install
42 |
43 | # activate env
44 | $ pipenv shell
45 | ```
46 |
47 | ## Run
48 | If you run the `stock_cutter_1d.py` file directly, it runs the example which uses 120 as length of stock Rod and generates some customer rods to cut. You can update these at the end of `stock_cutter_1d.py`.
49 | ```sh
50 | (csp) $ python csp/stock_cutter_1d.py
51 | ```
52 |
53 | Output:
54 |
55 | ```sh
56 | numRollsUsed 5
57 | Status: OPTIMAL
58 | Roll #0: [0.0, [33, 33, 18, 18, 18]]
59 | Roll #1: [2.9999999999999925, [33, 30, 18, 18, 18]]
60 | Roll #2: [5.999999999999993, [30, 30, 18, 18, 18]]
61 | Roll #3: [2.9999999999999987, [33, 33, 33, 18]]
62 | Roll #4: [21.0, [33, 33, 33]]```
63 | ```
64 |
65 | 
66 |
67 |
68 | ### Using input file
69 | If you want to describe your inputs in a file, [infile.txt](./infile.txt) describes the expected format
70 |
71 | ```sh
72 | (csp) $ python3 csp/stock_cutter_1d.py infile.txt
73 | ```
74 |
75 |
76 | ## Thinks to keep in mind
77 | * Works with integers only: IP (Integer Programming) problems working with integers only. If you have some values that have decimal part, you can multiply all of your inputs with some number that will make them integers (or close estimation).
78 | * You cannot specify units: Whether your input is in Inches or Meters, you have to keep a record of that yourself and conversions if any.
79 |
80 |
81 | ## CSP 2D
82 | Code for 2-dimensional Cutting Stock Problem is in [`deployment/stock_cutter.py`](deployment/stock_cutter.py) file. The `deployment` directory also contains code for the API server and deploying it on Heroku.
83 |
84 | ## Resources
85 | The whole code for this project is taken from Serge Kruk's
86 | * [Practical Python AI Projects: Mathematical Models of Optimization Problems with Google OR-Tools](https://amzn.to/3iPceJD)
87 | * [Repository of the code in Serge's book](https://github.com/sgkruk/Apress-AI/)
88 |
--------------------------------------------------------------------------------
/deployment/csp.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 | import collections, json
3 | from ortools.sat.python import cp_model
4 |
5 | # to draw rectangles
6 | import matplotlib.pyplot as plt
7 | import matplotlib.patches as patches
8 |
9 | """
10 | Cutting Stock problem
11 | params
12 | child_rects:
13 | lists of multiple rectangles' coords
14 | e.g.: [ [w, h], [w, h], ...]
15 | parent_rects: rectangle coords
16 | lists of multiple rectangles' coords
17 | e.g.: [ [w, h], [w, h], ...]
18 | """
19 | def StockCutter(child_rects, parent_rects):
20 |
21 | # Create the model
22 | model = cp_model.CpModel()
23 |
24 | # parent rect (to cut from). horizon = [ width, height ] of parent sheet
25 | # for now, parent rectangle is just one
26 | # TODO: to add functionality of cutting from multiple parent sheets, start here:
27 | horizon = parent_rects[0]
28 |
29 | # Named Tuple to store information about created variables
30 | sheet_type = collections.namedtuple('sheet_type', 'x1 y1 x2 y2 x_interval y_interval')
31 |
32 | # Store for all model variables
33 | all_vars = {}
34 |
35 | # sum of to save area of all small rects, to cut from parent rect
36 | total_child_area = 0
37 |
38 | # hold the widths (x) and heights (y) interval vars of each sheet
39 | x_intervals = []
40 | y_intervals = []
41 |
42 | # create model vars and intervals
43 | for rect_id, rect in enumerate(child_rects):
44 | width = rect[0]
45 | height = rect[1]
46 | area = width * height
47 | total_child_area += area
48 | # print(f"Rect: {width}x{height}, Area: {area}")
49 |
50 | suffix = '_%i_%i' % (width, height)
51 |
52 | # interval to represent width. max value can be the width of parent rect
53 | x1_var = model.NewIntVar(0, horizon[0], 'x1' + suffix)
54 | x2_var = model.NewIntVar(0, horizon[0], 'x2' + suffix)
55 | x_interval_var = model.NewIntervalVar(x1_var, width, x2_var, 'x_interval' + suffix)
56 |
57 | # interval to represent height. max value can be the height of parent rect
58 | y1_var = model.NewIntVar(0, horizon[1], 'y1' + suffix)
59 | y2_var = model.NewIntVar(0, horizon[1], 'y2' + suffix)
60 | y_interval_var = model.NewIntervalVar(y1_var, height, y2_var, 'y_interval' + suffix)
61 |
62 | x_intervals.append(x_interval_var)
63 | y_intervals.append(y_interval_var)
64 |
65 | # store the variables for later use
66 | all_vars[rect_id] = sheet_type(
67 | x1=x1_var,
68 | y1=y1_var,
69 | x2=x2_var,
70 | y2=y2_var,
71 | x_interval=x_interval_var,
72 | y_interval=y_interval_var
73 | )
74 |
75 | # add constraint: no over lap of rectangles allowed
76 | model.AddNoOverlap2D(x_intervals, y_intervals)
77 |
78 | # Solve model
79 | solver = cp_model.CpSolver()
80 |
81 | solution_printer = VarArraySolutionPrinter(all_vars)
82 |
83 | # Search for all solutions is only defined on satisfiability problems
84 | status = solver.SearchForAllSolutions(model, solution_printer) # use for satisfiability problem
85 | # status = solver.Solve(model) # use for Optimization Problem
86 |
87 | print('Status:', solver.StatusName(status))
88 | print('Solutions found :', solution_printer.solution_count())
89 |
90 | solutions = solution_printer.get_unique_solutions()
91 |
92 | # call draw methods here, if want to draw with matplotlib
93 |
94 | int_solutions = solutions_to_int(solutions)
95 |
96 | statusName = solver.StatusName(status)
97 | numSolutions = solution_printer.solution_count()
98 | numUniqueSolutions = len(solutions)
99 |
100 | output = {
101 | "statusName": statusName,
102 | "numSolutions": numSolutions,
103 | "numUniqueSolutions": numUniqueSolutions,
104 | "solutions": int_solutions # unique solutions
105 | }
106 |
107 | # return json.dumps(output)
108 |
109 | # draw
110 | for idx, sol in enumerate(solutions):
111 | # sol is string of coordinates of all rectangles in this solution
112 | # format: x1,y1,x2,y2-x1,y1,x2,y2
113 | print('Sol#', idx)
114 | rect_strs = sol.split('-')
115 | rect_coords = [
116 | # [x1,y1,x2,y2],
117 | # [x1,y1,x2,y2],
118 | ]
119 | for rect_str in rect_strs:
120 | coords_str = rect_str.split(',')
121 | coords = [int(c) for c in coords_str]
122 | rect_coords.append(coords)
123 | print('rect_coords')
124 | # print(rect_coords)
125 | drawRectsFromCoords(rect_coords)
126 |
127 | def drawRectsFromCoords(rect_coords):
128 | # draw rectangle
129 | fig,ax = plt.subplots(1)
130 | plt.xlim(0,6) # todo 7
131 | plt.ylim(0,6)
132 | plt.gca().set_aspect('equal', adjustable='box')
133 |
134 | # print coords
135 | coords = []
136 | colors = ['r', 'g', 'b', 'y', 'brown', 'black', 'violet', 'pink', 'gray', 'orange', 'b', 'y']
137 | for idx, coords in enumerate(rect_coords):
138 | x1=coords[0]
139 | y1=coords[1]
140 | x2=coords[2]
141 | y2=coords[3]
142 | # print(f"{x1}, {y1} -> {x2}, {y2}")
143 |
144 | width = abs(x1-x2)
145 | height = abs(y1-y2)
146 | # print(f"Rect#{idx}: {width}x{height}")
147 |
148 | # Create a Rectangle patch
149 | rect_shape = patches.Rectangle((x1,y1), width, height,facecolor=colors[idx])
150 | # Add the patch to the Axes
151 | ax.add_patch(rect_shape)
152 | plt.show()
153 |
154 |
155 |
156 |
157 |
158 | """
159 | TODO complete this, add to git
160 | params:
161 | str_solutions: list of strings. 1 string contains is solution
162 | """
163 | def solutions_to_int(str_solutions):
164 |
165 | # list of solutions, each solution is a list of rectangle coords that look like [x1,y1,x2,y2]
166 | int_solutions = []
167 |
168 | # go over all solutions and convert them to int>list>json
169 | for idx, sol in enumerate(str_solutions):
170 | # sol is string of coordinates of all rectangles in this solution
171 | # format: x1,y1,x2,y2-x1,y1,x2,y2
172 |
173 | rect_strs = sol.split('-')
174 | rect_coords = [
175 | # [x1,y1,x2,y2],
176 | # [x1,y1,x2,y2],
177 | # ...
178 | ]
179 |
180 | # convert each rectangle's coords to int
181 | for rect_str in rect_strs:
182 | coords_str = rect_str.split(',')
183 | coords = [int(c) for c in coords_str]
184 | rect_coords.append(coords)
185 |
186 | # print('rect_coords', rect_coords)
187 |
188 | # call draw methods here, if want to draw individual solutions with matplotlib
189 |
190 | int_solutions.append(rect_coords)
191 |
192 | return int_solutions
193 |
194 |
195 | """
196 | To get all the solutions of the problem, as they come up.
197 | https://developers.google.com/optimization/cp/cp_solver#all_solutions
198 |
199 | The solutions are all unique. But for the child rectangles that have same dimensions,
200 | some solution will be repetitive. Because for the algorithm, they are different solutions,
201 | but because of same size, they are merely permutations of the similar child rectangles -
202 | having other rectangles' positions fixed.
203 |
204 | We want to remove repetitive extra solutions. One way to do this is
205 | 1. Stringify every rectangle coords in a solution
206 | (1,2)->(2,3) becomes "1,2,2,3"
207 |
208 | # here the rectangles are stored as a string: "1,2,2,3" where x1=1, y1=2, x2=2, y2=3
209 |
210 | 2. Put all these string coords into a sorted list. This sorting is important.
211 | Because the rectangles (1,2)->(2,3) and (3,3)->(4,4) are actually same size (1x1) rectangles.
212 | And they can appear in 1st solution as
213 | [(1,2)->(2,3) , (3,3)->(4,4)]
214 | and in the 2nd solution as
215 | [(3,3)->(4,4) , (1,2)->(2,3)]
216 |
217 | but this sorted list of strings will ensure both solutions are represented as
218 |
219 | [..., "1,2,2,3", "3,3,4,4", ...]
220 |
221 | 3. Join the Set of "strings (rectangles)" in to one big string seperated by '-'. For every solution.
222 | So in resulting big strings (solutions), we will have two similar strings (solutions)
223 | that be similar and also contain
224 |
225 | "....1,2,2,3-3,3,4,4-...."
226 |
227 | 4. Now add all these "strings (solutions)" into a Set. this adding to the set
228 | will remove similar strings. And hence duplicate solutions will be removed.
229 |
230 | """
231 | class VarArraySolutionPrinter(cp_model.CpSolverSolutionCallback):
232 |
233 | def __init__(self, variables):
234 | cp_model.CpSolverSolutionCallback.__init__(self)
235 | self.__variables = variables
236 | self.__solution_count = 0
237 |
238 | # hold the calculated solutions
239 | self.__solutions = []
240 | self.__unique_solutions = set()
241 |
242 | def on_solution_callback(self):
243 | self.__solution_count += 1
244 | # print('Sol#: ', self.__solution_count)
245 |
246 | # using list to hold the coordinate strings of rectangles
247 | rect_strs = []
248 |
249 | # extra coordinates of all rectangles for this solution
250 | for rect_id in self.__variables:
251 | rect = self.__variables[rect_id]
252 | x1 = self.Value(rect.x1)
253 | x2 = self.Value(rect.x2)
254 | y1 = self.Value(rect.y1)
255 | y2 = self.Value(rect.y2)
256 |
257 | rect_str = f"{x1},{y1},{x2},{y2}"
258 | # print(rect_str)
259 |
260 | rect_strs.append(rect_str)
261 | # print(f'Rect #{rect_id}: {x1},{y1} -> {x2},{y2}')
262 |
263 | # print(rect_strs)
264 |
265 | # sort the rectangles
266 | rect_strs = sorted(rect_strs)
267 |
268 | # single solution as a string
269 | solution_str = '-'.join(rect_strs)
270 | # print(solution_str)
271 |
272 | # store the solutions
273 | self.__solutions.append(solution_str)
274 | self.__unique_solutions.add(solution_str) # __unique_solutions is a set, so duplicates will get removed
275 |
276 |
277 | def solution_count(self):
278 | return self.__solution_count
279 |
280 | # returns all solutions
281 | def get_solutions(self):
282 | return self.__solutions
283 |
284 | """
285 | returns unique solutions
286 | returns the permutation free list of solution strings
287 | """
288 | def get_unique_solutions(self):
289 | return list(self.__unique_solutions) # __unique_solutions is a Set, convert to list
290 |
291 |
292 |
293 | # for testing
294 | if __name__ == '__main__':
295 |
296 | child_rects = [
297 | # [2, 2],
298 | # [1, 3],
299 | # [4, 3],
300 | # [1, 1],
301 | # [2, 4],
302 |
303 | [3, 3],
304 | [3, 3],
305 | [3, 3],
306 | [3, 3],
307 | ]
308 |
309 | parent_rects = [[6,6]]
310 |
311 | StockCutter(child_rects, parent_rects)
--------------------------------------------------------------------------------
/deployment/stock_cutter.py:
--------------------------------------------------------------------------------
1 | '''
2 | @Author Emad Ehsan
3 | Cutting Stock problem 2D
4 | Not complete.
5 | What's remaining: Finding Optimized solution that minimizes the waste.
6 | '''
7 |
8 | import collections, json
9 | from ortools.sat.python import cp_model
10 |
11 | """
12 | params
13 | child_rects:
14 | lists of multiple rectangles' coords
15 | e.g.: [ [w, h], [w, h], ...]
16 | parent_rects: rectangle coords
17 | lists of multiple rectangles' coords
18 | e.g.: [ [w, h], [w, h], ...]
19 | """
20 | def StockCutter(child_rects, parent_rects, output_json=True):
21 |
22 | # Create the model
23 | model = cp_model.CpModel()
24 |
25 | # parent rect (to cut from). horizon = [ width, height ] of parent sheet
26 | # for now, parent rectangle is just one
27 | # TODO: to add functionality of cutting from multiple parent sheets, start here:
28 | horizon = parent_rects[0]
29 | total_parent_area = horizon[0] * horizon[1] # width x height
30 |
31 | # Named Tuple to store information about created variables
32 | sheet_type = collections.namedtuple('sheet_type', 'x1 y1 x2 y2 x_interval y_interval is_extra')
33 |
34 | # Store for all model variables
35 | all_vars = {}
36 |
37 | # sum of to save area of all small rects, to cut from parent rect
38 | total_child_area = 0
39 |
40 | # hold the widths (x) and heights (y) interval vars of each sheet
41 | x_intervals = []
42 | y_intervals = []
43 |
44 | # create model vars and intervals
45 | for rect_id, rect in enumerate(child_rects):
46 | width = rect[0]
47 | height = rect[1]
48 | area = width * height
49 | total_child_area += area
50 | # print(f"Rect: {width}x{height}, Area: {area}")
51 |
52 | suffix = '_%i_%i' % (width, height)
53 |
54 | # interval to represent width. max value can be the width of parent rect
55 | x1_var = model.NewIntVar(0, horizon[0], 'x1' + suffix)
56 | x2_var = model.NewIntVar(0, horizon[0], 'x2' + suffix)
57 | x_interval_var = model.NewIntervalVar(x1_var, width, x2_var, 'x_interval' + suffix)
58 |
59 | # interval to represent height. max value can be the height of parent rect
60 | y1_var = model.NewIntVar(0, horizon[1], 'y1' + suffix)
61 | y2_var = model.NewIntVar(0, horizon[1], 'y2' + suffix)
62 | y_interval_var = model.NewIntervalVar(y1_var, height, y2_var, 'y_interval' + suffix)
63 |
64 | x_intervals.append(x_interval_var)
65 | y_intervals.append(y_interval_var)
66 |
67 | # store the variables for later use
68 | all_vars[rect_id] = sheet_type(
69 | x1=x1_var,
70 | y1=y1_var,
71 | x2=x2_var,
72 | y2=y2_var,
73 | x_interval=x_interval_var,
74 | y_interval=y_interval_var,
75 | is_extra=False # to keep track of 1x1 custom rects added in next step
76 | )
77 |
78 | # model.Minimize(x1_var)
79 | # model.Minimize(y1_var)
80 |
81 |
82 | # TODO: Minimize (x1,y1) values. So that rectangles are placed at the start
83 | # this reduced the areas wasted by place rectangles in the middle / at the end
84 | # even though the space at the start is available.
85 | # >
86 | # for rect_id in range(len(child_rects)):
87 | # model.Minimize(all_vars[rect_id].x1 + all_vars[rect_id].y1)
88 | # model.Minimize(all_vars[rect_id].x2 + all_vars[rect_id].y2)
89 | # model.Minimize(all_vars[rect_id].x1)
90 | # model.Minimize(all_vars[rect_id].x2)
91 | # model.Minimize(all_vars[rect_id].y1)
92 | # model.Minimize(all_vars[rect_id].y2)
93 |
94 |
95 | '''
96 | FIXME: experiment
97 | Experment: treat the remaining area as small units of 1x1 rectangles. Push these rects to higher x,y.
98 | '''
99 | # leftover_area = total_parent_area - total_child_area
100 | # if leftover_area >= 0:
101 | # '''
102 | # each unit of leftover_area can be represented by 1x1 rectangles.
103 | # For leftover_area = 4 (e.g. 2x2 originally), we can use 4 rects of 1x1. Why? Because
104 | # 1. leftover_area would not always be continous. It is possible it is in the form of two
105 | # separate 2x1 rects or one 2x2 or four rects of 1x1. So we need the simplest version,
106 | # that can cover all types of rects. And it is 1x1
107 | # 2. 1x1 can represent non-adjecent weirdly shaped locations in the parent area that were leftover.
108 | # '''
109 | # num_1x1rects = leftover_area
110 |
111 | # for i in range(num_1x1rects):
112 | # print(f'{i}-th 1x1')
113 | # suffix = '_%i_%i' % (1, 1)
114 |
115 | # # interval to represent width. max value can be the width of parent rect
116 | # x1_var = model.NewIntVar(0, horizon[0], 'x1' + suffix)
117 | # x2_var = model.NewIntVar(0, horizon[0], 'x2' + suffix)
118 | # x_interval_var = model.NewIntervalVar(x1_var, 1, x2_var, 'x_interval' + suffix)
119 |
120 | # # interval to represent height. max value can be the height of parent rect
121 | # y1_var = model.NewIntVar(0, horizon[1], 'y1' + suffix)
122 | # y2_var = model.NewIntVar(0, horizon[1], 'y2' + suffix)
123 | # y_interval_var = model.NewIntervalVar(y1_var, 1, y2_var, 'y_interval' + suffix)
124 |
125 | # x_intervals.append(x_interval_var)
126 | # y_intervals.append(y_interval_var)
127 |
128 | # # store the variables for later use
129 | # all_vars[rect_id] = sheet_type(
130 | # x1=x1_var,
131 | # y1=y1_var,
132 | # x2=x2_var,
133 | # y2=y2_var,
134 | # x_interval=x_interval_var,
135 | # y_interval=y_interval_var,
136 | # is_extra=True
137 | # )
138 | # model.Maximize(x1_var)
139 | # model.Maximize(y1_var)
140 | # else:
141 | # print(f'Problem identified: Area of small rects is larger than parent rect by {leftover_area}')
142 |
143 |
144 |
145 |
146 | # add constraint: no over lap of rectangles allowed
147 | model.AddNoOverlap2D(x_intervals, y_intervals)
148 |
149 | # Solve model
150 | solver = cp_model.CpSolver()
151 |
152 | '''
153 | Search for all solutions is only defined on satisfiability problems
154 | '''
155 | # solution_printer = VarArraySolutionPrinter(all_vars)
156 | # status = solver.SearchForAllSolutions(model, solution_printer) # use for satisfiability problem
157 | # solutions = solution_printer.get_unique_solutions()
158 | # int_solutions = str_solutions_to_int(solutions)
159 | # output = {
160 | # "statusName": solver.StatusName(status),
161 | # "numSolutions": solution_printer.solution_count(),
162 | # "numUniqueSolutions": len(solutions),
163 | # "solutions": int_solutions # unique solutions
164 | # }
165 |
166 | '''
167 | for single solution
168 | '''
169 | status = solver.Solve(model) # use for Optimization Problem
170 | singleSolution = getSingleSolution(solver, all_vars)
171 | int_solutions = [singleSolution] # convert to array
172 | output = {
173 | "statusName": solver.StatusName(status),
174 | "numSolutions": '1',
175 | "numUniqueSolutions": '1',
176 | "solutions": int_solutions # unique solutions
177 | }
178 |
179 |
180 | print('Time:', solver.WallTime())
181 | print('Status:', output['statusName'])
182 | print('Solutions found :', output['numSolutions'])
183 | print('Unique solutions: ', output['numUniqueSolutions'])
184 |
185 | if output_json:
186 | return json.dumps(output)
187 | else:
188 | return int_solutions # integer representation of solutions
189 |
190 | '''
191 | This method is used to extract the single solution from the solver.
192 | Because in the case where VarArraySolutionPrinter is not used, the answers are not
193 | yet extracted from the solver. Use this method to extract the solver.
194 | '''
195 | def getSingleSolution(solver, all_vars):
196 | solution = []
197 | # extra coordinates of all rectangles for this solution
198 | for rect_id in all_vars:
199 | rect = all_vars[rect_id]
200 | x1 = solver.Value(rect.x1)
201 | x2 = solver.Value(rect.x2)
202 | y1 = solver.Value(rect.y1)
203 | y2 = solver.Value(rect.y2)
204 |
205 | # rect_str = f"{x1},{y1},{x2},{y2}"
206 | coords = [x1, y1, x2, y2];
207 | # print(rect_str)
208 |
209 | solution.append(coords)
210 | # print(f'Rect #{rect_id}: {x1},{y1} -> {x2},{y2}')
211 |
212 | # print(rect_strs)
213 | # sort the rectangles
214 | # rect_strs = sorted(rect_strs)
215 | # single solution as a string
216 | # solution_str = '-'.join(rect_strs)
217 | return solution
218 |
219 |
220 | """
221 | converts from string format to integer values. String format, in previous step, was used
222 | to exclude duplicates.
223 | params:
224 | str_solutions: list of strings. 1 string contains is solution
225 | """
226 | def str_solutions_to_int(str_solutions):
227 |
228 | # list of solutions, each solution is a list of rectangle coords that look like [x1,y1,x2,y2]
229 | int_solutions = []
230 |
231 | # go over all solutions and convert them to int>list>json
232 | for idx, sol in enumerate(str_solutions):
233 | # sol is string of coordinates of all rectangles in this solution
234 | # format: x1,y1,x2,y2-x1,y1,x2,y2
235 |
236 | rect_strs = sol.split('-')
237 | rect_coords = [
238 | # [x1,y1,x2,y2],
239 | # [x1,y1,x2,y2],
240 | # ...
241 | ]
242 |
243 | # convert each rectangle's coords to int
244 | for rect_str in rect_strs:
245 | coords_str = rect_str.split(',')
246 | coords = [int(c) for c in coords_str]
247 | rect_coords.append(coords)
248 |
249 | # print('rect_coords', rect_coords)
250 |
251 | int_solutions.append(rect_coords)
252 |
253 | return int_solutions
254 |
255 |
256 | """
257 | To get all the solutions of the problem, as they come up.
258 | https://developers.google.com/optimization/cp/cp_solver#all_solutions
259 |
260 | The solutions are all unique. But for the child rectangles that have same dimensions,
261 | some solution will be repetitive. Because for the algorithm, they are different solutions,
262 | but because of same size, they are merely permutations of the similar child rectangles -
263 | having other rectangles' positions fixed.
264 |
265 | We want to remove repetitive extra solutions. One way to do this is
266 | 1. Stringify every rectangle coords in a solution
267 | (1,2)->(2,3) becomes "1,2,2,3"
268 |
269 | # here the rectangles are stored as a string: "1,2,2,3" where x1=1, y1=2, x2=2, y2=3
270 |
271 | 2. Put all these string coords into a sorted list. This sorting is important.
272 | Because the rectangles (1,2)->(2,3) and (3,3)->(4,4) are actually same size (1x1) rectangles.
273 | And they can appear in 1st solution as
274 | [(1,2)->(2,3) , (3,3)->(4,4)]
275 | and in the 2nd solution as
276 | [(3,3)->(4,4) , (1,2)->(2,3)]
277 |
278 | but this sorted list of strings will ensure both solutions are represented as
279 |
280 | [..., "1,2,2,3", "3,3,4,4", ...]
281 |
282 | 3. Join the Set of "strings (rectangles)" in to one big string seperated by '-'. For every solution.
283 | So in resulting big strings (solutions), we will have two similar strings (solutions)
284 | that be similar and also contain
285 |
286 | "....1,2,2,3-3,3,4,4-...."
287 |
288 | 4. Now add all these "strings (solutions)" into a Set. this adding to the set
289 | will remove similar strings. And hence duplicate solutions will be removed.
290 |
291 | """
292 | class VarArraySolutionPrinter(cp_model.CpSolverSolutionCallback):
293 |
294 | def __init__(self, variables):
295 | cp_model.CpSolverSolutionCallback.__init__(self)
296 | self.__variables = variables
297 | self.__solution_count = 0
298 |
299 | # hold the calculated solutions
300 | self.__solutions = []
301 | self.__unique_solutions = set()
302 |
303 | def on_solution_callback(self):
304 | self.__solution_count += 1
305 | # print('Sol#: ', self.__solution_count)
306 |
307 | # using list to hold the coordinate strings of rectangles
308 | rect_strs = []
309 |
310 | # extra coordinates of all rectangles for this solution
311 | for rect_id in self.__variables:
312 | rect = self.__variables[rect_id]
313 | x1 = self.Value(rect.x1)
314 | x2 = self.Value(rect.x2)
315 | y1 = self.Value(rect.y1)
316 | y2 = self.Value(rect.y2)
317 |
318 | rect_str = f"{x1},{y1},{x2},{y2}"
319 | # print(rect_str)
320 |
321 | rect_strs.append(rect_str)
322 | # print(f'Rect #{rect_id}: {x1},{y1} -> {x2},{y2}')
323 |
324 | # print(rect_strs)
325 |
326 | # sort the rectangles
327 | rect_strs = sorted(rect_strs)
328 |
329 | # single solution as a string
330 | solution_str = '-'.join(rect_strs)
331 | # print(solution_str)
332 |
333 | # store the solutions
334 | self.__solutions.append(solution_str)
335 | self.__unique_solutions.add(solution_str) # __unique_solutions is a set, so duplicates will get removed
336 |
337 |
338 | def solution_count(self):
339 | return self.__solution_count
340 |
341 | # returns all solutions
342 | def get_solutions(self):
343 | return self.__solutions
344 |
345 | """
346 | returns unique solutions
347 | returns the permutation free list of solution strings
348 | """
349 | def get_unique_solutions(self):
350 | return list(self.__unique_solutions) # __unique_solutions is a Set, convert to list
351 |
352 |
353 | '''
354 | non-API method. Used for testing and running locally / in a Notebook.
355 | Draws the rectangles
356 | '''
357 |
358 | def drawRectsFromCoords(rect_coords, parent_rects):
359 | import matplotlib.pyplot as plt
360 | import matplotlib.patches as patches
361 |
362 | # TODO: to add support for multiple parent rects, update here
363 | xSize = parent_rects[0][0]
364 | ySize = parent_rects[0][1]
365 |
366 | # draw rectangle
367 | fig,ax = plt.subplots(1)
368 | plt.xlim(0,xSize)
369 | plt.ylim(0,ySize)
370 | plt.gca().set_aspect('equal', adjustable='box')
371 |
372 | # print coords
373 | coords = []
374 | colors = ['r', 'g', 'b', 'y', 'brown', 'black', 'violet', 'pink', 'gray', 'orange', 'b', 'y']
375 | for idx, coords in enumerate(rect_coords):
376 | x1=coords[0]
377 | y1=coords[1]
378 | x2=coords[2]
379 | y2=coords[3]
380 | # print(f"{x1}, {y1} -> {x2}, {y2}")
381 |
382 | width = abs(x1-x2)
383 | height = abs(y1-y2)
384 | # print(f"Rect#{idx}: {width}x{height}")
385 |
386 | # Create a Rectangle patch
387 | rect_shape = patches.Rectangle((x1,y1), width, height,facecolor=colors[idx])
388 | # Add the patch to the Axes
389 | ax.add_patch(rect_shape)
390 | plt.show()
391 |
392 |
393 |
394 |
395 | # for testing
396 | if __name__ == '__main__':
397 |
398 | child_rects = [
399 | # [1, 1],
400 | # [2, 2],
401 | # [1, 3],
402 | # [4, 3],
403 | # [2, 4],
404 | # [2, 2],
405 |
406 | [27, 17],
407 | [27, 17],
408 | [18, 56],
409 |
410 | # [3, 3],
411 | # [3, 3],
412 | # [3, 3],
413 | # [3, 3],
414 | ]
415 |
416 | # parent_rects = [[6,6]]
417 | parent_rects = [[84,72]]
418 |
419 | solutions = StockCutter(child_rects, parent_rects, output_json=False) # get the integer solution
420 |
421 | for sol in solutions:
422 | print(sol)
423 | drawRectsFromCoords(sol, parent_rects)
--------------------------------------------------------------------------------
/csp/stock_cutter_1d.py:
--------------------------------------------------------------------------------
1 | '''
2 | Original Author: Serge Kruk
3 | Original Version: https://github.com/sgkruk/Apress-AI/blob/master/cutting_stock.py
4 |
5 | Updated by: Emad Ehsan
6 | '''
7 | from ortools.linear_solver import pywraplp
8 | from math import ceil
9 | from random import randint
10 | import json
11 | from read_lengths import get_data
12 | import typer
13 | from typing import Optional
14 |
15 | def newSolver(name,integer=False):
16 | return pywraplp.Solver(name,\
17 | pywraplp.Solver.CBC_MIXED_INTEGER_PROGRAMMING \
18 | if integer else \
19 | pywraplp.Solver.GLOP_LINEAR_PROGRAMMING)
20 |
21 | '''
22 | return a printable value
23 | '''
24 | def SolVal(x):
25 | if type(x) is not list:
26 | return 0 if x is None \
27 | else x if isinstance(x,(int,float)) \
28 | else x.SolutionValue() if x.Integer() is False \
29 | else int(x.SolutionValue())
30 | elif type(x) is list:
31 | return [SolVal(e) for e in x]
32 |
33 | def ObjVal(x):
34 | return x.Objective().Value()
35 |
36 |
37 | def gen_data(num_orders):
38 | R=[] # small rolls
39 | # S=0 # seed?
40 | for i in range(num_orders):
41 | R.append([randint(1,12), randint(5,40)])
42 | return R
43 |
44 |
45 | def solve_model(demands, parent_width=100):
46 | '''
47 | demands = [
48 | [1, 3], # [quantity, width]
49 | [3, 5],
50 | ...
51 | ]
52 |
53 | parent_width = integer
54 | '''
55 | num_orders = len(demands)
56 | solver = newSolver('Cutting Stock', True)
57 | k,b = bounds(demands, parent_width)
58 |
59 | # array of boolean declared as int, if y[i] is 1,
60 | # then y[i] Big roll is used, else it was not used
61 | y = [ solver.IntVar(0, 1, f'y_{i}') for i in range(k[1]) ]
62 |
63 | # x[i][j] = 3 means that small-roll width specified by i-th order
64 | # must be cut from j-th order, 3 tmies
65 | x = [[solver.IntVar(0, b[i], f'x_{i}_{j}') for j in range(k[1])] \
66 | for i in range(num_orders)]
67 |
68 | unused_widths = [ solver.NumVar(0, parent_width, f'w_{j}') \
69 | for j in range(k[1]) ]
70 |
71 | # will contain the number of big rolls used
72 | nb = solver.IntVar(k[0], k[1], 'nb')
73 |
74 | # consntraint: demand fullfilment
75 | for i in range(num_orders):
76 | # small rolls from i-th order must be at least as many in quantity
77 | # as specified by the i-th order
78 | solver.Add(sum(x[i][j] for j in range(k[1])) >= demands[i][0])
79 |
80 | # constraint: max size limit
81 | for j in range(k[1]):
82 | # total width of small rolls cut from j-th big roll,
83 | # must not exceed big rolls width
84 | solver.Add( \
85 | sum(demands[i][1]*x[i][j] for i in range(num_orders)) \
86 | <= parent_width*y[j] \
87 | )
88 |
89 | # width of j-th big roll - total width of all orders cut from j-th roll
90 | # must be equal to unused_widths[j]
91 | # So, we are saying that assign unused_widths[j] the remaining width of j'th big roll
92 | solver.Add(parent_width*y[j] - sum(demands[i][1]*x[i][j] for i in range(num_orders)) == unused_widths[j])
93 |
94 | '''
95 | Book Author's note from page 201:
96 | [the following constraint] breaks the symmetry of multiple solutions that are equivalent
97 | for our purposes: any permutation of the rolls. These permutations, and there are K! of
98 | them, cause most solvers to spend an exorbitant time solving. With this constraint, we
99 | tell the solver to prefer those permutations with more cuts in roll j than in roll j + 1.
100 | The reader is encouraged to solve a medium-sized problem with and without this
101 | symmetry-breaking constraint. I have seen problems take 48 hours to solve without the
102 | constraint and 48 minutes with. Of course, for problems that are solved in seconds, the
103 | constraint will not help; it may even hinder. But who cares if a cutting stock instance
104 | solves in two or in three seconds? We care much more about the difference between two
105 | minutes and three hours, which is what this constraint is meant to address
106 | '''
107 | if j < k[1]-1: # k1 = total big rolls
108 | # total small rolls of i-th order cut from j-th big roll must be >=
109 | # totall small rolls of i-th order cut from j+1-th big roll
110 | solver.Add(sum(x[i][j] for i in range(num_orders)) >= sum(x[i][j+1] for i in range(num_orders)))
111 |
112 | # find & assign to nb, the number of big rolls used
113 | solver.Add(nb == solver.Sum(y[j] for j in range(k[1])))
114 |
115 | '''
116 | minimize total big rolls used
117 | let's say we have y = [1, 0, 1]
118 | here, total big rolls used are 2. 0-th and 2nd. 1st one is not used. So we want our model to use the
119 | earlier rolls first. i.e. y = [1, 1, 0].
120 | The trick to do this is to define the cost of using each next roll to be higher. So the model would be
121 | forced to used the initial rolls, when available, instead of the next rolls.
122 |
123 | So instead of Minimize ( Sum of y ) or Minimize( Sum([1,1,0]) )
124 | we Minimize( Sum([1*1, 1*2, 1*3]) )
125 | '''
126 |
127 | '''
128 | Book Author's note from page 201:
129 |
130 | There are alternative objective functions. For example, we could have minimized the sum of the waste. This makes sense, especially if the demand constraint is formulated as an inequality. Then minimizing the sum of waste Chapter 7 advanCed teChniques
131 | will spend more CPU cycles trying to find more efficient patterns that over-satisfy demand. This is especially good if the demand widths recur regularly and storing cut rolls in inventory to satisfy future demand is possible. Note that the running time will grow quickly with such an objective function
132 | '''
133 |
134 | Cost = solver.Sum((j+1)*y[j] for j in range(k[1]))
135 |
136 | solver.Minimize(Cost)
137 |
138 | status = solver.Solve()
139 | numRollsUsed = SolVal(nb)
140 |
141 | return status, \
142 | numRollsUsed, \
143 | rolls(numRollsUsed, SolVal(x), SolVal(unused_widths), demands), \
144 | SolVal(unused_widths), \
145 | solver.WallTime()
146 |
147 | def bounds(demands, parent_width=100):
148 | '''
149 | b = [sum of widths of individual small rolls of each order]
150 | T = local var. stores sum of widths of adjecent small-rolls. When the width reaches 100%, T is set to 0 again.
151 | k = [k0, k1], k0 = minimum big-rolls requierd, k1: number of big rolls that can be consumed / cut from
152 | TT = local var. stores sum of widths of of all small-rolls. At the end, will be used to estimate lower bound of big-rolls
153 | '''
154 | num_orders = len(demands)
155 | b = []
156 | T = 0
157 | k = [0,1]
158 | TT = 0
159 |
160 | for i in range(num_orders):
161 | # q = quantity, w = width; of i-th order
162 | quantity, width = demands[i][0], demands[i][1]
163 | # TODO Verify: why min of quantity, parent_width/width?
164 | # assumes widths to be entered as percentage
165 | # int(round(parent_width/demands[i][1])) will always be >= 1, because widths of small rolls can't exceed parent_width (which is width of big roll)
166 | # b.append( min(demands[i][0], int(round(parent_width / demands[i][1]))) )
167 | b.append( min(quantity, int(round(parent_width / width))) )
168 |
169 | # if total width of this i-th order + previous order's leftover (T) is less than parent_width
170 | # it's fine. Cut it.
171 | if T + quantity*width <= parent_width:
172 | T, TT = T + quantity*width, TT + quantity*width
173 | # else, the width exceeds, so we have to cut only as much as we can cut from parent_width width of the big roll
174 | else:
175 | while quantity:
176 | if T + width <= parent_width:
177 | T, TT, quantity = T + width, TT + width, quantity-1
178 | else:
179 | k[1],T = k[1]+1, 0 # use next roll (k[1] += 1)
180 | k[0] = int(round(TT/parent_width+0.5))
181 |
182 | print('k', k)
183 | print('b', b)
184 |
185 | return k, b
186 |
187 | '''
188 | nb: array of number of rolls to cut, of each order
189 |
190 | w:
191 | demands: [
192 | [quantity, width],
193 | [quantity, width],
194 | [quantity, width],
195 | ]
196 | '''
197 | def rolls(nb, x, w, demands):
198 | consumed_big_rolls = []
199 | num_orders = len(x)
200 | # go over first row (1st order)
201 | # this row contains the list of all the big rolls available, and if this 1st (0-th) order
202 | # is cut from any big roll, that big roll's index would contain a number > 0
203 | for j in range(len(x[0])):
204 | # w[j]: width of j-th big roll
205 | # int(x[i][j]) * [demands[i][1]] width of all i-th order's small rolls that are to be cut from j-th big roll
206 | RR = [ abs(w[j])] + [ int(x[i][j])*[demands[i][1]] for i in range(num_orders) \
207 | if x[i][j] > 0 ] # if i-th order has some cuts from j-th order, x[i][j] would be > 0
208 | consumed_big_rolls.append(RR)
209 |
210 | return consumed_big_rolls
211 |
212 |
213 |
214 | '''
215 | this model starts with some patterns and then optimizes those patterns
216 | '''
217 | def solve_large_model(demands, parent_width=100):
218 | num_orders = len(demands)
219 | iter = 0
220 | patterns = get_initial_patterns(demands)
221 | # print('method#solve_large_model, patterns', patterns)
222 |
223 | # list quantities of orders
224 | quantities = [demands[i][0] for i in range(num_orders)]
225 | print('quantities', quantities)
226 |
227 | while iter < 20:
228 | status, y, l = solve_master(patterns, quantities, parent_width=parent_width)
229 | iter += 1
230 |
231 | # list widths of orders
232 | widths = [demands[i][1] for i in range(num_orders)]
233 | new_pattern, objectiveValue = get_new_pattern(l, widths, parent_width=parent_width)
234 |
235 | # print('method#solve_large_model, new_pattern', new_pattern)
236 | # print('method#solve_large_model, objectiveValue', objectiveValue)
237 |
238 | for i in range(num_orders):
239 | # add i-th cut of new pattern to i-thp pattern
240 | patterns[i].append(new_pattern[i])
241 |
242 | status, y, l = solve_master(patterns, quantities, parent_width=parent_width, integer=True)
243 |
244 | return status, \
245 | patterns, \
246 | y, \
247 | rolls_patterns(patterns, y, demands, parent_width=parent_width)
248 |
249 |
250 | '''
251 | Dantzig-Wolfe decomposition splits the problem into a Master Problem MP and a sub-problem SP.
252 |
253 | The Master Problem: provided a set of patterns, find the best combination satisfying the demand
254 |
255 | C: patterns
256 | b: demand
257 | '''
258 | def solve_master(patterns, quantities, parent_width=100, integer=False):
259 | title = 'Cutting stock master problem'
260 | num_patterns = len(patterns)
261 | n = len(patterns[0])
262 | # print('**num_patterns x n: ', num_patterns, 'x', n)
263 | # print('**patterns recived:')
264 | # for p in patterns:
265 | # print(p)
266 |
267 | constraints = []
268 |
269 | solver = newSolver(title, integer)
270 |
271 | # y is not boolean, it's an integer now (as compared to y in approach used by solve_model)
272 | y = [ solver.IntVar(0, 1000, '') for j in range(n) ] # right bound?
273 | # minimize total big rolls (y) used
274 | Cost = sum(y[j] for j in range(n))
275 | solver.Minimize(Cost)
276 |
277 | # for every pattern
278 | for i in range(num_patterns):
279 | # add constraint that this pattern (demand) must be met
280 | # there are m such constraints, for each pattern
281 | constraints.append(solver.Add( sum(patterns[i][j]*y[j] for j in range(n)) >= quantities[i]) )
282 |
283 | status = solver.Solve()
284 | y = [int(ceil(e.SolutionValue())) for e in y]
285 |
286 | l = [0 if integer else constraints[i].DualValue() for i in range(num_patterns)]
287 | # sl = [0 if integer else constraints[i].name() for i in range(num_patterns)]
288 | # print('sl: ', sl)
289 |
290 | # l = [0 if integer else u[i].Ub() for i in range(m)]
291 | toreturn = status, y, l
292 | # l_to_print = [round(dd, 2) for dd in toreturn[2]]
293 | # print('l: ', len(l_to_print), '->', l_to_print)
294 | # print('l: ', toreturn[2])
295 | return toreturn
296 |
297 | def get_new_pattern(l, w, parent_width=100):
298 | solver = newSolver('Cutting stock sub-problem', True)
299 | n = len(l)
300 | new_pattern = [ solver.IntVar(0, parent_width, '') for i in range(n) ]
301 |
302 | # maximizes the sum of the values times the number of occurrence of that roll in a pattern
303 | Cost = sum( l[i] * new_pattern[i] for i in range(n))
304 | solver.Maximize(Cost)
305 |
306 | # ensuring that the pattern stays within the total width of the large roll
307 | solver.Add( sum( w[i] * new_pattern[i] for i in range(n)) <= parent_width )
308 |
309 | status = solver.Solve()
310 | return SolVal(new_pattern), ObjVal(solver)
311 |
312 |
313 | '''
314 | the initial patterns must be such that they will allow a feasible solution,
315 | one that satisfies all demands.
316 | Considering the already complex model, let’s keep it simple.
317 | Our initial patterns have exactly one roll per pattern, as obviously feasible as inefficient.
318 | '''
319 | def get_initial_patterns(demands):
320 | num_orders = len(demands)
321 | return [[0 if j != i else 1 for j in range(num_orders)]\
322 | for i in range(num_orders)]
323 |
324 | def rolls_patterns(patterns, y, demands, parent_width=100):
325 | R, m, n = [], len(patterns), len(y)
326 |
327 | for j in range(n):
328 | for _ in range(y[j]):
329 | RR = []
330 | for i in range(m):
331 | if patterns[i][j] > 0:
332 | RR.extend( [demands[i][1]] * int(patterns[i][j]) )
333 | used_width = sum(RR)
334 | R.append([parent_width - used_width, RR])
335 |
336 | return R
337 |
338 |
339 | '''
340 | checks if all small roll widths (demands) smaller than parent roll's width
341 | '''
342 | def checkWidths(demands, parent_width):
343 | for quantity, width in demands:
344 | if width > parent_width:
345 | print(f'Small roll width {width} is greater than parent rolls width {parent_width}. Exiting')
346 | return False
347 | return True
348 |
349 |
350 | '''
351 | params
352 | child_rolls:
353 | list of lists, each containing quantity & width of rod / roll to be cut
354 | e.g.: [ [quantity, width], [quantity, width], ...]
355 | parent_rolls:
356 | list of lists, each containing quantity & width of rod / roll to cut from
357 | e.g.: [ [quantity, width], [quantity, width], ...]
358 | '''
359 | def StockCutter1D(child_rolls, parent_rolls, output_json=True, large_model=True):
360 |
361 | # at the moment, only parent one width of parent rolls is supported
362 | # quantity of parent rolls is calculated by algorithm, so user supplied quantity doesn't matter?
363 | # TODO: or we can check and tell the user the user when parent roll quantity is insufficient
364 | parent_width = parent_rolls[0][1]
365 |
366 | if not checkWidths(demands=child_rolls, parent_width=parent_width):
367 | return []
368 |
369 |
370 | print('child_rolls', child_rolls)
371 | print('parent_rolls', parent_rolls)
372 |
373 | if not large_model:
374 | print('Running Small Model...')
375 | status, numRollsUsed, consumed_big_rolls, unused_roll_widths, wall_time = \
376 | solve_model(demands=child_rolls, parent_width=parent_width)
377 |
378 | # convert the format of output of solve_model to be exactly same as solve_large_model
379 | print('consumed_big_rolls before adjustment: ', consumed_big_rolls)
380 | new_consumed_big_rolls = []
381 | for big_roll in consumed_big_rolls:
382 | if len(big_roll) < 2:
383 | # sometimes the solve_model return a solution that contanis an extra [0.0] entry for big roll
384 | consumed_big_rolls.remove(big_roll)
385 | continue
386 | unused_width = big_roll[0]
387 | subrolls = []
388 | for subitem in big_roll[1:]:
389 | if isinstance(subitem, list):
390 | # if it's a list, concatenate with the other lists, to make a single list for this big_roll
391 | subrolls = subrolls + subitem
392 | else:
393 | # if it's an integer, add it to the list
394 | subrolls.append(subitem)
395 | new_consumed_big_rolls.append([unused_width, subrolls])
396 | print('consumed_big_rolls after adjustment: ', new_consumed_big_rolls)
397 | consumed_big_rolls = new_consumed_big_rolls
398 |
399 | else:
400 | print('Running Large Model...');
401 | status, A, y, consumed_big_rolls = solve_large_model(demands=child_rolls, parent_width=parent_width)
402 |
403 | numRollsUsed = len(consumed_big_rolls)
404 | # print('A:', A, '\n')
405 | # print('y:', y, '\n')
406 |
407 |
408 | STATUS_NAME = ['OPTIMAL',
409 | 'FEASIBLE',
410 | 'INFEASIBLE',
411 | 'UNBOUNDED',
412 | 'ABNORMAL',
413 | 'NOT_SOLVED'
414 | ]
415 |
416 | output = {
417 | "statusName": STATUS_NAME[status],
418 | "numSolutions": '1',
419 | "numUniqueSolutions": '1',
420 | "numRollsUsed": numRollsUsed,
421 | "solutions": consumed_big_rolls # unique solutions
422 | }
423 |
424 |
425 | # print('Wall Time:', wall_time)
426 | print('numRollsUsed', numRollsUsed)
427 | print('Status:', output['statusName'])
428 | print('Solutions found :', output['numSolutions'])
429 | print('Unique solutions: ', output['numUniqueSolutions'])
430 |
431 | if output_json:
432 | return json.dumps(output)
433 | else:
434 | return consumed_big_rolls
435 |
436 |
437 | '''
438 | Draws the big rolls on the graph. Each horizontal colored line represents one big roll.
439 | In each big roll (multi-colored horizontal line), each color represents small roll to be cut from it.
440 | If the big roll ends with a black color, that part of the big roll is unused width.
441 |
442 | TODO: Assign each child roll a unique color
443 | '''
444 | def drawGraph(consumed_big_rolls, child_rolls, parent_width):
445 | import matplotlib.pyplot as plt
446 | import matplotlib.patches as patches
447 |
448 | # TODO: to add support for multiple different parent rolls, update here
449 | xSize = parent_width # width of big roll
450 | ySize = 10 * len(consumed_big_rolls) # one big roll will take 10 units vertical space
451 |
452 | # draw rectangle
453 | fig,ax = plt.subplots(1)
454 | plt.xlim(0, xSize)
455 | plt.ylim(0, ySize)
456 | plt.gca().set_aspect('equal', adjustable='box')
457 |
458 | # print coords
459 | coords = []
460 | colors = ['r', 'g', 'b', 'y', 'brown', 'violet', 'pink', 'gray', 'orange', 'b', 'y']
461 | colorDict = {}
462 | i = 0
463 | for quantity, width in child_rolls:
464 | colorDict[width] = colors[i % 11]
465 | i+= 1
466 |
467 | # start plotting each big roll horizontly, from the bottom
468 | y1 = 0
469 | for i, big_roll in enumerate(consumed_big_rolls):
470 | '''
471 | big_roll = [leftover_width, [small_roll_1_1, small_roll_1_2, other_small_roll_2_1]]
472 | '''
473 | unused_width = big_roll[0]
474 | small_rolls = big_roll[1]
475 |
476 | x1 = 0
477 | x2 = 0
478 | y2 = y1 + 8 # the height of each big roll will be 8
479 | for j, small_roll in enumerate(small_rolls):
480 | x2 = x2 + small_roll
481 | print(f"{x1}, {y1} -> {x2}, {y2}")
482 | width = abs(x1-x2)
483 | height = abs(y1-y2)
484 | # print(f"Rect#{idx}: {width}x{height}")
485 | # Create a Rectangle patch
486 | rect_shape = patches.Rectangle((x1,y1), width, height, facecolor=colorDict[small_roll], label=f'{small_roll}')
487 | ax.add_patch(rect_shape) # Add the patch to the Axes
488 | x1 = x2 # x1 for next small roll in same big roll will be x2 of current roll
489 |
490 | # now that all small rolls have been plotted, check if a there is unused width in this big roll
491 | # set the unused width at the end as black colored rectangle
492 | if unused_width > 0:
493 | width = unused_width
494 | rect_shape = patches.Rectangle((x1,y1), width, height, facecolor='black', label='Unused')
495 | ax.add_patch(rect_shape) # Add the patch to the Axes
496 |
497 | y1 += 10 # next big roll will be plotted on top of current, a roll height is 8, so 2 will be margin between rolls
498 |
499 | plt.show()
500 |
501 |
502 | if __name__ == '__main__':
503 |
504 | # child_rolls = [
505 | # [quantity, width],
506 | # ]
507 | app = typer.Typer()
508 |
509 |
510 | def main(infile_name: Optional[str] = typer.Argument(None)):
511 |
512 | if infile_name:
513 | child_rolls = get_data(infile_name)
514 | else:
515 | child_rolls = gen_data(3)
516 | parent_rolls = [[10, 120]] # 10 doesn't matter, itls not used at the moment
517 |
518 | consumed_big_rolls = StockCutter1D(child_rolls, parent_rolls, output_json=False, large_model=False)
519 | typer.echo(f"{consumed_big_rolls}")
520 |
521 |
522 | for idx, roll in enumerate(consumed_big_rolls):
523 | typer.echo(f"Roll #{idx}:{roll}")
524 |
525 | drawGraph(consumed_big_rolls, child_rolls, parent_width=parent_rolls[0][1])
526 |
527 | if __name__ == "__main__":
528 | typer.run(main)
529 |
--------------------------------------------------------------------------------
/deployment/stock_cutter_1d.py:
--------------------------------------------------------------------------------
1 | '''
2 | Original Author: Serge Kruk
3 | Original Version: https://github.com/sgkruk/Apress-AI/blob/master/cutting_stock.py
4 |
5 | Updated by: Emad Ehsan
6 | V2: https://github.com/emadehsan/Apress-AI/blob/master/my-models/custom_cutting_stock.py
7 |
8 | V3 is following:
9 | '''
10 | from ortools.linear_solver import pywraplp
11 | from math import ceil
12 | from random import randint
13 | import json
14 |
15 | def newSolver(name,integer=False):
16 | return pywraplp.Solver(name,\
17 | pywraplp.Solver.CBC_MIXED_INTEGER_PROGRAMMING \
18 | if integer else \
19 | pywraplp.Solver.GLOP_LINEAR_PROGRAMMING)
20 |
21 | '''
22 | return a printable value
23 | '''
24 | def SolVal(x):
25 | if type(x) is not list:
26 | return 0 if x is None \
27 | else x if isinstance(x,(int,float)) \
28 | else x.SolutionValue() if x.Integer() is False \
29 | else int(x.SolutionValue())
30 | elif type(x) is list:
31 | return [SolVal(e) for e in x]
32 |
33 | def ObjVal(x):
34 | return x.Objective().Value()
35 |
36 |
37 | def gen_data(num_orders):
38 | R=[] # small rolls
39 | # S=0 # seed?
40 | for i in range(num_orders):
41 | R.append([randint(1,12), randint(5,40)])
42 | return R
43 |
44 |
45 | def solve_model(demands, parent_width=100, cutStyle='exactCuts'):
46 | ''' demands = [
47 | [1, 3], # [quantity, width]
48 | [3, 5],
49 | ...
50 | ]
51 |
52 | parent_width = integer
53 | '''
54 | num_orders = len(demands)
55 | solver = newSolver('Cutting Stock', True)
56 | k,b = bounds(demands, parent_width)
57 |
58 | # array of boolean declared as int, if y[i] is 1,
59 | # then y[i] Big roll is used, else it was not used
60 | y = [ solver.IntVar(0, 1, f'y_{i}') for i in range(k[1]) ]
61 |
62 | # x[i][j] = 3 means that small-roll width specified by i-th order
63 | # must be cut from j-th order, 3 tmies
64 | x = [[solver.IntVar(0, b[i], f'x_{i}_{j}') for j in range(k[1])] \
65 | for i in range(num_orders)]
66 |
67 | unused_widths = [ solver.NumVar(0, parent_width, f'w_{j}') \
68 | for j in range(k[1]) ]
69 |
70 | # will contain the number of big rolls used
71 | nb = solver.IntVar(k[0], k[1], 'nb')
72 |
73 | # consntraint: demand fullfilment
74 | for i in range(num_orders):
75 | # small rolls from i-th order must be at least as many in quantity
76 | # as specified by the i-th order
77 | if cutStyle == 'minWaste':
78 | solver.Add(sum(x[i][j] for j in range(k[1])) >= demands[i][0])
79 | else:
80 | # probably cutStyle == exactCuts
81 | solver.Add(sum(x[i][j] for j in range(k[1])) == demands[i][0])
82 |
83 | # constraint: max size limit
84 | for j in range(k[1]):
85 | # total width of small rolls cut from j-th big roll,
86 | # must not exceed big rolls width
87 | solver.Add( \
88 | sum(demands[i][1]*x[i][j] for i in range(num_orders)) \
89 | <= parent_width*y[j] \
90 | )
91 |
92 | # width of j-th big roll - total width of all orders cut from j-th roll
93 | # must be equal to unused_widths[j]
94 | # So, we are saying that assign unused_widths[j] the remaining width of j'th big roll
95 | solver.Add(parent_width*y[j] - sum(demands[i][1]*x[i][j] for i in range(num_orders)) == unused_widths[j])
96 |
97 | '''
98 | Book Author's note from page 201:
99 | [the following constraint] breaks the symmetry of multiple solutions that are equivalent
100 | for our purposes: any permutation of the rolls. These permutations, and there are K! of
101 | them, cause most solvers to spend an exorbitant time solving. With this constraint, we
102 | tell the solver to prefer those permutations with more cuts in roll j than in roll j + 1.
103 | The reader is encouraged to solve a medium-sized problem with and without this
104 | symmetry-breaking constraint. I have seen problems take 48 hours to solve without the
105 | constraint and 48 minutes with. Of course, for problems that are solved in seconds, the
106 | constraint will not help; it may even hinder. But who cares if a cutting stock instance
107 | solves in two or in three seconds? We care much more about the difference between two
108 | minutes and three hours, which is what this constraint is meant to address
109 | '''
110 | if j < k[1]-1: # k1 = total big rolls
111 | # total small rolls of i-th order cut from j-th big roll must be >=
112 | # totall small rolls of i-th order cut from j+1-th big roll
113 | solver.Add(sum(x[i][j] for i in range(num_orders)) >= sum(x[i][j+1] for i in range(num_orders)))
114 |
115 | # find & assign to nb, the number of big rolls used
116 | solver.Add(nb == solver.Sum(y[j] for j in range(k[1])))
117 |
118 | '''
119 | minimize total big rolls used
120 | let's say we have y = [1, 0, 1]
121 | here, total big rolls used are 2. 0-th and 2nd. 1st one is not used. So we want our model to use the
122 | earlier rolls first. i.e. y = [1, 1, 0].
123 | The trick to do this is to define the cost of using each next roll to be higher. So the model would be
124 | forced to used the initial rolls, when available, instead of the next rolls.
125 |
126 | So instead of Minimize ( Sum of y ) or Minimize( Sum([1,1,0]) )
127 | we Minimize( Sum([1*1, 1*2, 1*3]) )
128 | '''
129 |
130 | '''
131 | Book Author's note from page 201:
132 |
133 | There are alternative objective functions. For example, we could have minimized the sum of the waste. This makes sense, especially if the demand constraint is formulated as an inequality. Then minimizing the sum of waste Chapter 7 advanCed teChniques
134 | will spend more CPU cycles trying to find more efficient patterns that over-satisfy demand. This is especially good if the demand widths recur regularly and storing cut rolls in inventory to satisfy future demand is possible. Note that the running time will grow quickly with such an objective function
135 | '''
136 |
137 | Cost = solver.Sum((j+1)*y[j] for j in range(k[1]))
138 |
139 | solver.Minimize(Cost)
140 |
141 | status = solver.Solve()
142 | numRollsUsed = SolVal(nb)
143 |
144 | return status, \
145 | numRollsUsed, \
146 | rolls(numRollsUsed, SolVal(x), SolVal(unused_widths), demands), \
147 | SolVal(unused_widths), \
148 | solver.WallTime()
149 |
150 | def bounds(demands, parent_width=100):
151 | '''
152 | b = [sum of widths of individual small rolls of each order]
153 | T = local var. stores sum of widths of adjecent small-rolls. When the width reaches 100%, T is set to 0 again.
154 | k = [k0, k1], k0 = minimum big-rolls requierd, k1: number of big rolls that can be consumed / cut from
155 | TT = local var. stores sum of widths of of all small-rolls. At the end, will be used to estimate lower bound of big-rolls
156 | '''
157 | num_orders = len(demands)
158 | b = []
159 | T = 0
160 | k = [0,1]
161 | TT = 0
162 |
163 | for i in range(num_orders):
164 | # q = quantity, w = width; of i-th order
165 | quantity, width = demands[i][0], demands[i][1]
166 | # TODO Verify: why min of quantity, parent_width/width?
167 | # assumes widths to be entered as percentage
168 | # int(round(parent_width/demands[i][1])) will always be >= 1, because widths of small rolls can't exceed parent_width (which is width of big roll)
169 | # b.append( min(demands[i][0], int(round(parent_width / demands[i][1]))) )
170 | b.append( min(quantity, int(round(parent_width / width))) )
171 |
172 | # if total width of this i-th order + previous order's leftover (T) is less than parent_width
173 | # it's fine. Cut it.
174 | if T + quantity*width <= parent_width:
175 | T, TT = T + quantity*width, TT + quantity*width
176 | # else, the width exceeds, so we have to cut only as much as we can cut from parent_width width of the big roll
177 | else:
178 | while quantity:
179 | if T + width <= parent_width:
180 | T, TT, quantity = T + width, TT + width, quantity-1
181 | else:
182 | k[1],T = k[1]+1, 0 # use next roll (k[1] += 1)
183 | k[0] = int(round(TT/parent_width+0.5))
184 |
185 | print('k', k)
186 | print('b', b)
187 |
188 | return k, b
189 |
190 | '''
191 | nb: array of number of rolls to cut, of each order
192 |
193 | w:
194 | demands: [
195 | [quantity, width],
196 | [quantity, width],
197 | [quantity, width],
198 | ]
199 | '''
200 | def rolls(nb, x, w, demands):
201 | consumed_big_rolls = []
202 | num_orders = len(x)
203 | # go over first row (1st order)
204 | # this row contains the list of all the big rolls available, and if this 1st (0-th) order
205 | # is cut from any big roll, that big roll's index would contain a number > 0
206 | for j in range(len(x[0])):
207 | # w[j]: width of j-th big roll
208 | # int(x[i][j]) * [demands[i][1]] width of all i-th order's small rolls that are to be cut from j-th big roll
209 | RR = [ abs(w[j])] + [ int(x[i][j])*[demands[i][1]] for i in range(num_orders) \
210 | if x[i][j] > 0 ] # if i-th order has some cuts from j-th order, x[i][j] would be > 0
211 | consumed_big_rolls.append(RR)
212 |
213 | return consumed_big_rolls
214 |
215 |
216 |
217 | '''
218 | this model starts with some patterns and then optimizes those patterns
219 | '''
220 | def solve_large_model(demands, parent_width=100, cutStyle='exactCuts'):
221 | num_orders = len(demands)
222 | iter = 0
223 | patterns = get_initial_patterns(demands)
224 | # print('method#solve_large_model, patterns', patterns)
225 |
226 | # list quantities of orders
227 | quantities = [demands[i][0] for i in range(num_orders)]
228 | print('quantities', quantities)
229 |
230 | while iter < 20:
231 | status, y, l = solve_master(patterns, quantities, parent_width=parent_width, cutStyle=cutStyle)
232 | iter += 1
233 |
234 | # list widths of orders
235 | widths = [demands[i][1] for i in range(num_orders)]
236 | new_pattern, objectiveValue = get_new_pattern(l, widths, parent_width=parent_width)
237 |
238 | # print('method#solve_large_model, new_pattern', new_pattern)
239 | # print('method#solve_large_model, objectiveValue', objectiveValue)
240 |
241 | for i in range(num_orders):
242 | # add i-th cut of new pattern to i-thp pattern
243 | patterns[i].append(new_pattern[i])
244 |
245 | status, y, l = solve_master(patterns, quantities, parent_width=parent_width, integer=True, cutStyle=cutStyle)
246 |
247 | return status, \
248 | patterns, \
249 | y, \
250 | rolls_patterns(patterns, y, demands, parent_width=parent_width)
251 |
252 |
253 | '''
254 | Dantzig-Wolfe decomposition splits the problem into a Master Problem MP and a sub-problem SP.
255 |
256 | The Master Problem: provided a set of patterns, find the best combination satisfying the demand
257 |
258 | C: patterns
259 | b: demand
260 | '''
261 | def solve_master(patterns, quantities, parent_width=100, integer=False, cutStyle='exactCuts'):
262 | title = 'Cutting stock master problem'
263 | num_patterns = len(patterns)
264 | n = len(patterns[0])
265 | # print('**num_patterns x n: ', num_patterns, 'x', n)
266 | # print('**patterns recived:')
267 | # for p in patterns:
268 | # print(p)
269 |
270 | constraints = []
271 |
272 | solver = newSolver(title, integer)
273 |
274 | # y is not boolean, it's an integer now (as compared to y in approach used by solve_model)
275 | y = [ solver.IntVar(0, 1000, '') for j in range(n) ] # right bound?
276 | # minimize total big rolls (y) used
277 | Cost = sum(y[j] for j in range(n))
278 | solver.Minimize(Cost)
279 |
280 | # for every pattern
281 | for i in range(num_patterns):
282 | # add constraint that this pattern (demand) must be met
283 | # there are m such constraints, for each pattern
284 |
285 | if cutStyle == 'minWaste':
286 | constraints.append(solver.Add( sum(patterns[i][j]*y[j] for j in range(n)) >= quantities[i]) )
287 | else:
288 | # probably cutStyle == exactCuts
289 | constraints.append(solver.Add( sum(patterns[i][j]*y[j] for j in range(n)) == quantities[i]) )
290 |
291 |
292 | status = solver.Solve()
293 | y = [int(ceil(e.SolutionValue())) for e in y]
294 |
295 | l = [0 if integer else constraints[i].DualValue() for i in range(num_patterns)]
296 | # sl = [0 if integer else constraints[i].name() for i in range(num_patterns)]
297 | # print('sl: ', sl)
298 |
299 | # l = [0 if integer else u[i].Ub() for i in range(m)]
300 | toreturn = status, y, l
301 | # l_to_print = [round(dd, 2) for dd in toreturn[2]]
302 | # print('l: ', len(l_to_print), '->', l_to_print)
303 | # print('l: ', toreturn[2])
304 | return toreturn
305 |
306 |
307 | '''
308 | TODO Make sense of this:
309 | '''
310 | def get_new_pattern(l, w, parent_width=100):
311 | solver = newSolver('Cutting stock sub-problem', True)
312 | n = len(l)
313 | new_pattern = [ solver.IntVar(0, parent_width, '') for i in range(n) ]
314 |
315 | # maximizes the sum of the values times the number of occurrence of that roll in a pattern
316 | Cost = sum( l[i] * new_pattern[i] for i in range(n))
317 | solver.Maximize(Cost)
318 |
319 | # ensuring that the pattern stays within the total width of the large roll
320 | solver.Add( sum( w[i] * new_pattern[i] for i in range(n)) <= parent_width )
321 |
322 | status = solver.Solve()
323 | return SolVal(new_pattern), ObjVal(solver)
324 |
325 |
326 | '''
327 | the initial patterns must be such that they will allow a feasible solution,
328 | one that satisfies all demands.
329 | Considering the already complex model, let’s keep it simple.
330 | Our initial patterns have exactly one roll per pattern, as obviously feasible as inefficient.
331 | '''
332 | def get_initial_patterns(demands):
333 | num_orders = len(demands)
334 | return [[0 if j != i else 1 for j in range(num_orders)]\
335 | for i in range(num_orders)]
336 |
337 | def rolls_patterns(patterns, y, demands, parent_width=100):
338 | R, m, n = [], len(patterns), len(y)
339 |
340 | for j in range(n):
341 | for _ in range(y[j]):
342 | RR = []
343 | for i in range(m):
344 | if patterns[i][j] > 0:
345 | RR.extend( [demands[i][1]] * int(patterns[i][j]) )
346 | used_width = sum(RR)
347 | R.append([parent_width - used_width, RR])
348 |
349 | return R
350 |
351 |
352 | '''
353 | checks if all small roll widths (demands) smaller than parent roll's width
354 | '''
355 | def checkWidths(demands, parent_width):
356 | for quantity, width in demands:
357 | if width > parent_width:
358 | print(f'Small roll width {width} is greater than parent rolls width {parent_width}. Exiting')
359 | return False
360 | return True
361 |
362 |
363 | '''
364 | params
365 | child_rolls:
366 | list of lists, each containing quantity & width of rod / roll to be cut
367 | e.g.: [ [quantity, width], [quantity, width], ...]
368 | parent_rolls:
369 | list of lists, each containing quantity & width of rod / roll to cut from
370 | e.g.: [ [quantity, width], [quantity, width], ...]
371 | cutStyle:
372 | there are two types of cutting style
373 | 1. cut exactly as many items as specified: exactCuts
374 | 2. cut some items more than specified to minimize waste: minWaste
375 | '''
376 | def StockCutter1D(child_rolls, parent_rolls, output_json=True, large_model=True, cutStyle='exactCuts'):
377 |
378 | # at the moment, only parent one width of parent rolls is supported
379 | # quantity of parent rolls is calculated by algorithm, so user supplied quantity doesn't matter?
380 | # TODO: or we can check and tell the user the user when parent roll quantity is insufficient
381 | parent_width = parent_rolls[0][1]
382 |
383 | if not checkWidths(demands=child_rolls, parent_width=parent_width):
384 | return []
385 |
386 |
387 | print('child_rolls', child_rolls)
388 | print('parent_rolls', parent_rolls)
389 |
390 | if not large_model:
391 | print('Running Small Model...')
392 | status, numRollsUsed, consumed_big_rolls, unused_roll_widths, wall_time = \
393 | solve_model(demands=child_rolls, parent_width=parent_width, cutStyle=cutStyle)
394 |
395 | # convert the format of output of solve_model to be exactly same as solve_large_model
396 | print('consumed_big_rolls before adjustment: ', consumed_big_rolls)
397 | new_consumed_big_rolls = []
398 | for big_roll in consumed_big_rolls:
399 | if len(big_roll) < 2:
400 | # sometimes the solve_model return a solution that contanis an extra [0.0] entry for big roll
401 | consumed_big_rolls.remove(big_roll)
402 | continue
403 | unused_width = big_roll[0]
404 | subrolls = []
405 | for subitem in big_roll[1:]:
406 | if isinstance(subitem, list):
407 | # if it's a list, concatenate with the other lists, to make a single list for this big_roll
408 | subrolls = subrolls + subitem
409 | else:
410 | # if it's an integer, add it to the list
411 | subrolls.append(subitem)
412 | new_consumed_big_rolls.append([unused_width, subrolls])
413 | print('consumed_big_rolls after adjustment: ', new_consumed_big_rolls)
414 | consumed_big_rolls = new_consumed_big_rolls
415 |
416 | else:
417 | print('Running Large Model...');
418 | status, A, y, consumed_big_rolls = solve_large_model(demands=child_rolls, parent_width=parent_width, cutStyle=cutStyle)
419 |
420 | numRollsUsed = len(consumed_big_rolls)
421 | # print('A:', A, '\n')
422 | # print('y:', y, '\n')
423 |
424 |
425 | STATUS_NAME = ['OPTIMAL',
426 | 'FEASIBLE',
427 | 'INFEASIBLE',
428 | 'UNBOUNDED',
429 | 'ABNORMAL',
430 | 'NOT_SOLVED'
431 | ]
432 |
433 | output = {
434 | "statusName": STATUS_NAME[status],
435 | "numSolutions": '1',
436 | "numUniqueSolutions": '1',
437 | "numRollsUsed": numRollsUsed,
438 | "solutions": consumed_big_rolls # unique solutions
439 | }
440 |
441 |
442 | # print('Wall Time:', wall_time)
443 | print('numRollsUsed', numRollsUsed)
444 | print('Status:', output['statusName'])
445 | print('Solutions found :', output['numSolutions'])
446 | print('Unique solutions: ', output['numUniqueSolutions'])
447 |
448 | if output_json:
449 | return json.dumps(output)
450 | else:
451 | return consumed_big_rolls
452 |
453 |
454 | '''
455 | Draws the big rolls on the graph. Each horizontal colored line represents one big roll.
456 | In each big roll (multi-colored horizontal line), each color represents small roll to be cut from it.
457 | If the big roll ends with a black color, that part of the big roll is unused width.
458 |
459 | TODO: Assign each child roll a unique color
460 | '''
461 | def drawGraph(consumed_big_rolls, child_rolls, parent_width):
462 | import matplotlib.pyplot as plt
463 | import matplotlib.patches as patches
464 |
465 | # TODO: to add support for multiple different parent rolls, update here
466 | xSize = parent_width # width of big roll
467 | ySize = 10 * len(consumed_big_rolls) # one big roll will take 10 units vertical space
468 |
469 | # draw rectangle
470 | fig,ax = plt.subplots(1)
471 | plt.xlim(0, xSize)
472 | plt.ylim(0, ySize)
473 | plt.gca().set_aspect('equal', adjustable='box')
474 |
475 | # print coords
476 | coords = []
477 | colors = ['r', 'g', 'b', 'y', 'brown', 'violet', 'pink', 'gray', 'orange', 'b', 'y']
478 | colorDict = {}
479 | i = 0
480 | for quantity, width in child_rolls:
481 | colorDict[width] = colors[i % 11]
482 | i+= 1
483 |
484 | # start plotting each big roll horizontly, from the bottom
485 | y1 = 0
486 | for i, big_roll in enumerate(consumed_big_rolls):
487 | '''
488 | big_roll = [leftover_width, [small_roll_1_1, small_roll_1_2, other_small_roll_2_1]]
489 | '''
490 | unused_width = big_roll[0]
491 | small_rolls = big_roll[1]
492 |
493 | x1 = 0
494 | x2 = 0
495 | y2 = y1 + 8 # the height of each big roll will be 8
496 | for j, small_roll in enumerate(small_rolls):
497 | x2 = x2 + small_roll
498 | print(f"{x1}, {y1} -> {x2}, {y2}")
499 | width = abs(x1-x2)
500 | height = abs(y1-y2)
501 | # print(f"Rect#{idx}: {width}x{height}")
502 | # Create a Rectangle patch
503 | rect_shape = patches.Rectangle((x1,y1), width, height, facecolor=colorDict[small_roll], label=f'{small_roll}')
504 | ax.add_patch(rect_shape) # Add the patch to the Axes
505 | x1 = x2 # x1 for next small roll in same big roll will be x2 of current roll
506 |
507 | # now that all small rolls have been plotted, check if a there is unused width in this big roll
508 | # set the unused width at the end as black colored rectangle
509 | if unused_width > 0:
510 | width = unused_width
511 | rect_shape = patches.Rectangle((x1,y1), width, height, facecolor='black', label='Unused')
512 | ax.add_patch(rect_shape) # Add the patch to the Axes
513 |
514 | y1 += 10 # next big roll will be plotted on top of current, a roll height is 8, so 2 will be margin between rolls
515 |
516 | plt.show()
517 |
518 |
519 | if __name__ == '__main__':
520 |
521 | child_rolls = [
522 | # [quantity, width],
523 | # [6, 25],
524 | # [12, 21],
525 | # [7, 26],
526 | # [3, 23],
527 | # [8, 33],
528 | # [2, 15],
529 | # [2, 34],
530 |
531 | # [3, 3],
532 | # [3, 4],
533 |
534 | [3,3],
535 | [3,1],
536 | [2,4],
537 | [2,2]
538 |
539 | # [3,30],
540 | # [2,72],
541 | # [5,50]
542 | ]
543 |
544 | # child_rolls = gen_data(3)
545 | # parent_rolls = [[10, 120]]
546 | # parent_rolls = [[10, 8]]
547 | parent_rolls = [[10, 6]]
548 | # parent_rolls = [[10, 144]]
549 |
550 |
551 | consumed_big_rolls = StockCutter1D(child_rolls, parent_rolls, output_json=False, large_model=False)
552 | print (consumed_big_rolls)
553 |
554 | for idx, roll in enumerate(consumed_big_rolls):
555 | print(f'Roll #{idx}:', roll)
556 |
557 |
558 | drawGraph(consumed_big_rolls, child_rolls, parent_width=parent_rolls[0][1])
--------------------------------------------------------------------------------
/Pipfile.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_meta": {
3 | "hash": {
4 | "sha256": "a3fd796537e543d9025cfd9ce1ab78504af858c04ed185114aebaa40eb83613e"
5 | },
6 | "pipfile-spec": 6,
7 | "requires": {
8 | "python_version": "3.9"
9 | },
10 | "sources": [
11 | {
12 | "name": "pypi",
13 | "url": "https://pypi.org/simple",
14 | "verify_ssl": true
15 | }
16 | ]
17 | },
18 | "default": {
19 | "absl-py": {
20 | "hashes": [
21 | "sha256:5d15f85b8cc859c6245bc9886ba664460ed96a6fee895416caa37d669ee74a9a",
22 | "sha256:f568809938c49abbda89826223c992b630afd23c638160ad7840cfe347710d97"
23 | ],
24 | "markers": "python_version >= '3.6'",
25 | "version": "==1.2.0"
26 | },
27 | "attrs": {
28 | "hashes": [
29 | "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6",
30 | "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"
31 | ],
32 | "markers": "python_version >= '3.5'",
33 | "version": "==22.1.0"
34 | },
35 | "click": {
36 | "hashes": [
37 | "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
38 | "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
39 | ],
40 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
41 | "version": "==7.1.2"
42 | },
43 | "cycler": {
44 | "hashes": [
45 | "sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3",
46 | "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"
47 | ],
48 | "markers": "python_version >= '3.6'",
49 | "version": "==0.11.0"
50 | },
51 | "iniconfig": {
52 | "hashes": [
53 | "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
54 | "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"
55 | ],
56 | "version": "==1.1.1"
57 | },
58 | "kiwisolver": {
59 | "hashes": [
60 | "sha256:02f79693ec433cb4b5f51694e8477ae83b3205768a6fb48ffba60549080e295b",
61 | "sha256:03baab2d6b4a54ddbb43bba1a3a2d1627e82d205c5cf8f4c924dc49284b87166",
62 | "sha256:1041feb4cda8708ce73bb4dcb9ce1ccf49d553bf87c3954bdfa46f0c3f77252c",
63 | "sha256:10ee06759482c78bdb864f4109886dff7b8a56529bc1609d4f1112b93fe6423c",
64 | "sha256:1d1573129aa0fd901076e2bfb4275a35f5b7aa60fbfb984499d661ec950320b0",
65 | "sha256:283dffbf061a4ec60391d51e6155e372a1f7a4f5b15d59c8505339454f8989e4",
66 | "sha256:28bc5b299f48150b5f822ce68624e445040595a4ac3d59251703779836eceff9",
67 | "sha256:2a66fdfb34e05b705620dd567f5a03f239a088d5a3f321e7b6ac3239d22aa286",
68 | "sha256:2e307eb9bd99801f82789b44bb45e9f541961831c7311521b13a6c85afc09767",
69 | "sha256:2e407cb4bd5a13984a6c2c0fe1845e4e41e96f183e5e5cd4d77a857d9693494c",
70 | "sha256:2f5e60fabb7343a836360c4f0919b8cd0d6dbf08ad2ca6b9cf90bf0c76a3c4f6",
71 | "sha256:36dafec3d6d6088d34e2de6b85f9d8e2324eb734162fba59d2ba9ed7a2043d5b",
72 | "sha256:3fe20f63c9ecee44560d0e7f116b3a747a5d7203376abeea292ab3152334d004",
73 | "sha256:41dae968a94b1ef1897cb322b39360a0812661dba7c682aa45098eb8e193dbdf",
74 | "sha256:4bd472dbe5e136f96a4b18f295d159d7f26fd399136f5b17b08c4e5f498cd494",
75 | "sha256:4ea39b0ccc4f5d803e3337dd46bcce60b702be4d86fd0b3d7531ef10fd99a1ac",
76 | "sha256:5853eb494c71e267912275e5586fe281444eb5e722de4e131cddf9d442615626",
77 | "sha256:5bce61af018b0cb2055e0e72e7d65290d822d3feee430b7b8203d8a855e78766",
78 | "sha256:6295ecd49304dcf3bfbfa45d9a081c96509e95f4b9d0eb7ee4ec0530c4a96514",
79 | "sha256:62ac9cc684da4cf1778d07a89bf5f81b35834cb96ca523d3a7fb32509380cbf6",
80 | "sha256:70e7c2e7b750585569564e2e5ca9845acfaa5da56ac46df68414f29fea97be9f",
81 | "sha256:7577c1987baa3adc4b3c62c33bd1118c3ef5c8ddef36f0f2c950ae0b199e100d",
82 | "sha256:75facbe9606748f43428fc91a43edb46c7ff68889b91fa31f53b58894503a191",
83 | "sha256:787518a6789009c159453da4d6b683f468ef7a65bbde796bcea803ccf191058d",
84 | "sha256:78d6601aed50c74e0ef02f4204da1816147a6d3fbdc8b3872d263338a9052c51",
85 | "sha256:7c43e1e1206cd421cd92e6b3280d4385d41d7166b3ed577ac20444b6995a445f",
86 | "sha256:81e38381b782cc7e1e46c4e14cd997ee6040768101aefc8fa3c24a4cc58e98f8",
87 | "sha256:841293b17ad704d70c578f1f0013c890e219952169ce8a24ebc063eecf775454",
88 | "sha256:872b8ca05c40d309ed13eb2e582cab0c5a05e81e987ab9c521bf05ad1d5cf5cb",
89 | "sha256:877272cf6b4b7e94c9614f9b10140e198d2186363728ed0f701c6eee1baec1da",
90 | "sha256:8c808594c88a025d4e322d5bb549282c93c8e1ba71b790f539567932722d7bd8",
91 | "sha256:8ed58b8acf29798b036d347791141767ccf65eee7f26bde03a71c944449e53de",
92 | "sha256:91672bacaa030f92fc2f43b620d7b337fd9a5af28b0d6ed3f77afc43c4a64b5a",
93 | "sha256:968f44fdbf6dd757d12920d63b566eeb4d5b395fd2d00d29d7ef00a00582aac9",
94 | "sha256:9f85003f5dfa867e86d53fac6f7e6f30c045673fa27b603c397753bebadc3008",
95 | "sha256:a553dadda40fef6bfa1456dc4be49b113aa92c2a9a9e8711e955618cd69622e3",
96 | "sha256:a68b62a02953b9841730db7797422f983935aeefceb1679f0fc85cbfbd311c32",
97 | "sha256:abbe9fa13da955feb8202e215c4018f4bb57469b1b78c7a4c5c7b93001699938",
98 | "sha256:ad881edc7ccb9d65b0224f4e4d05a1e85cf62d73aab798943df6d48ab0cd79a1",
99 | "sha256:b1792d939ec70abe76f5054d3f36ed5656021dcad1322d1cc996d4e54165cef9",
100 | "sha256:b428ef021242344340460fa4c9185d0b1f66fbdbfecc6c63eff4b7c29fad429d",
101 | "sha256:b533558eae785e33e8c148a8d9921692a9fe5aa516efbdff8606e7d87b9d5824",
102 | "sha256:ba59c92039ec0a66103b1d5fe588fa546373587a7d68f5c96f743c3396afc04b",
103 | "sha256:bc8d3bd6c72b2dd9decf16ce70e20abcb3274ba01b4e1c96031e0c4067d1e7cd",
104 | "sha256:bc9db8a3efb3e403e4ecc6cd9489ea2bac94244f80c78e27c31dcc00d2790ac2",
105 | "sha256:bf7d9fce9bcc4752ca4a1b80aabd38f6d19009ea5cbda0e0856983cf6d0023f5",
106 | "sha256:c2dbb44c3f7e6c4d3487b31037b1bdbf424d97687c1747ce4ff2895795c9bf69",
107 | "sha256:c79ebe8f3676a4c6630fd3f777f3cfecf9289666c84e775a67d1d358578dc2e3",
108 | "sha256:c97528e64cb9ebeff9701e7938653a9951922f2a38bd847787d4a8e498cc83ae",
109 | "sha256:d0611a0a2a518464c05ddd5a3a1a0e856ccc10e67079bb17f265ad19ab3c7597",
110 | "sha256:d06adcfa62a4431d404c31216f0f8ac97397d799cd53800e9d3efc2fbb3cf14e",
111 | "sha256:d41997519fcba4a1e46eb4a2fe31bc12f0ff957b2b81bac28db24744f333e955",
112 | "sha256:d5b61785a9ce44e5a4b880272baa7cf6c8f48a5180c3e81c59553ba0cb0821ca",
113 | "sha256:da152d8cdcab0e56e4f45eb08b9aea6455845ec83172092f09b0e077ece2cf7a",
114 | "sha256:da7e547706e69e45d95e116e6939488d62174e033b763ab1496b4c29b76fabea",
115 | "sha256:db5283d90da4174865d520e7366801a93777201e91e79bacbac6e6927cbceede",
116 | "sha256:db608a6757adabb32f1cfe6066e39b3706d8c3aa69bbc353a5b61edad36a5cb4",
117 | "sha256:e0ea21f66820452a3f5d1655f8704a60d66ba1191359b96541eaf457710a5fc6",
118 | "sha256:e7da3fec7408813a7cebc9e4ec55afed2d0fd65c4754bc376bf03498d4e92686",
119 | "sha256:e92a513161077b53447160b9bd8f522edfbed4bd9759e4c18ab05d7ef7e49408",
120 | "sha256:ecb1fa0db7bf4cff9dac752abb19505a233c7f16684c5826d1f11ebd9472b871",
121 | "sha256:efda5fc8cc1c61e4f639b8067d118e742b812c930f708e6667a5ce0d13499e29",
122 | "sha256:f0a1dbdb5ecbef0d34eb77e56fcb3e95bbd7e50835d9782a45df81cc46949750",
123 | "sha256:f0a71d85ecdd570ded8ac3d1c0f480842f49a40beb423bb8014539a9f32a5897",
124 | "sha256:f4f270de01dd3e129a72efad823da90cc4d6aafb64c410c9033aba70db9f1ff0",
125 | "sha256:f6cb459eea32a4e2cf18ba5fcece2dbdf496384413bc1bae15583f19e567f3b2",
126 | "sha256:f8ad8285b01b0d4695102546b342b493b3ccc6781fc28c8c6a1bb63e95d22f09",
127 | "sha256:f9f39e2f049db33a908319cf46624a569b36983c7c78318e9726a4cb8923b26c"
128 | ],
129 | "markers": "python_version >= '3.7'",
130 | "version": "==1.4.4"
131 | },
132 | "matplotlib": {
133 | "hashes": [
134 | "sha256:09225edca87a79815822eb7d3be63a83ebd4d9d98d5aa3a15a94f4eee2435954",
135 | "sha256:0caa687fce6174fef9b27d45f8cc57cbc572e04e98c81db8e628b12b563d59a2",
136 | "sha256:27c9393fada62bd0ad7c730562a0fecbd3d5aaa8d9ed80ba7d3ebb8abc4f0453",
137 | "sha256:2c2c5041608cb75c39cbd0ed05256f8a563e144234a524c59d091abbfa7a868f",
138 | "sha256:2d31aff0c8184b05006ad756b9a4dc2a0805e94d28f3abc3187e881b6673b302",
139 | "sha256:3a4c3e9be63adf8e9b305aa58fb3ec40ecc61fd0f8fd3328ce55bc30e7a2aeb0",
140 | "sha256:5111d6d47a0f5b8f3e10af7a79d5e7eb7e73a22825391834734274c4f312a8a0",
141 | "sha256:5ed3d3342698c2b1f3651f8ea6c099b0f196d16ee00e33dc3a6fee8cb01d530a",
142 | "sha256:6ffd2d80d76df2e5f9f0c0140b5af97e3b87dd29852dcdb103ec177d853ec06b",
143 | "sha256:746897fbd72bd462b888c74ed35d812ca76006b04f717cd44698cdfc99aca70d",
144 | "sha256:756ee498b9ba35460e4cbbd73f09018e906daa8537fff61da5b5bf8d5e9de5c7",
145 | "sha256:7ad44f2c74c50567c694ee91c6fa16d67e7c8af6f22c656b80469ad927688457",
146 | "sha256:83e6c895d93fdf93eeff1a21ee96778ba65ef258e5d284160f7c628fee40c38f",
147 | "sha256:9b03722c89a43a61d4d148acfc89ec5bb54cd0fd1539df25b10eb9c5fa6c393a",
148 | "sha256:a4fe54eab2c7129add75154823e6543b10261f9b65b2abe692d68743a4999f8c",
149 | "sha256:b1b60c6476c4cfe9e5cf8ab0d3127476fd3d5f05de0f343a452badaad0e4bdec",
150 | "sha256:b26c472847911f5a7eb49e1c888c31c77c4ddf8023c1545e0e8e0367ba74fb15",
151 | "sha256:b2a5e1f637a92bb6f3526cc54cc8af0401112e81ce5cba6368a1b7908f9e18bc",
152 | "sha256:b7b09c61a91b742cb5460b72efd1fe26ef83c1c704f666e0af0df156b046aada",
153 | "sha256:b8ba2a1dbb4660cb469fe8e1febb5119506059e675180c51396e1723ff9b79d9",
154 | "sha256:c092fc4673260b1446b8578015321081d5db73b94533fe4bf9b69f44e948d174",
155 | "sha256:c586ac1d64432f92857c3cf4478cfb0ece1ae18b740593f8a39f2f0b27c7fda5",
156 | "sha256:d082f77b4ed876ae94a9373f0db96bf8768a7cca6c58fc3038f94e30ffde1880",
157 | "sha256:e71cdd402047e657c1662073e9361106c6981e9621ab8c249388dfc3ec1de07b",
158 | "sha256:eb6b6700ea454bb88333d98601e74928e06f9669c1ea231b4c4c666c1d7701b4"
159 | ],
160 | "index": "pypi",
161 | "version": "==3.3.3"
162 | },
163 | "numpy": {
164 | "hashes": [
165 | "sha256:004f0efcb2fe1c0bd6ae1fcfc69cc8b6bf2407e0f18be308612007a0762b4089",
166 | "sha256:09f6b7bdffe57fc61d869a22f506049825d707b288039d30f26a0d0d8ea05164",
167 | "sha256:0ea3f98a0ffce3f8f57675eb9119f3f4edb81888b6874bc1953f91e0b1d4f440",
168 | "sha256:17c0e467ade9bda685d5ac7f5fa729d8d3e76b23195471adae2d6a6941bd2c18",
169 | "sha256:1f27b5322ac4067e67c8f9378b41c746d8feac8bdd0e0ffede5324667b8a075c",
170 | "sha256:22d43376ee0acd547f3149b9ec12eec2f0ca4a6ab2f61753c5b29bb3e795ac4d",
171 | "sha256:2ad3ec9a748a8943e6eb4358201f7e1c12ede35f510b1a2221b70af4bb64295c",
172 | "sha256:301c00cf5e60e08e04d842fc47df641d4a181e651c7135c50dc2762ffe293dbd",
173 | "sha256:39a664e3d26ea854211867d20ebcc8023257c1800ae89773cbba9f9e97bae036",
174 | "sha256:51bf49c0cd1d52be0a240aa66f3458afc4b95d8993d2d04f0d91fa60c10af6cd",
175 | "sha256:78a63d2df1d947bd9d1b11d35564c2f9e4b57898aae4626638056ec1a231c40c",
176 | "sha256:7cd1328e5bdf0dee621912f5833648e2daca72e3839ec1d6695e91089625f0b4",
177 | "sha256:8355fc10fd33a5a70981a5b8a0de51d10af3688d7a9e4a34fcc8fa0d7467bb7f",
178 | "sha256:8c79d7cf86d049d0c5089231a5bcd31edb03555bd93d81a16870aa98c6cfb79d",
179 | "sha256:91b8d6768a75247026e951dce3b2aac79dc7e78622fc148329135ba189813584",
180 | "sha256:94c15ca4e52671a59219146ff584488907b1f9b3fc232622b47e2cf832e94fb8",
181 | "sha256:98dcbc02e39b1658dc4b4508442a560fe3ca5ca0d989f0df062534e5ca3a5c1a",
182 | "sha256:a64403f634e5ffdcd85e0b12c08f04b3080d3e840aef118721021f9b48fc1460",
183 | "sha256:bc6e8da415f359b578b00bcfb1d08411c96e9a97f9e6c7adada554a0812a6cc6",
184 | "sha256:bdc9febce3e68b697d931941b263c59e0c74e8f18861f4064c1f712562903411",
185 | "sha256:c1ba66c48b19cc9c2975c0d354f24058888cdc674bebadceb3cdc9ec403fb5d1",
186 | "sha256:c9f707b5bb73bf277d812ded9896f9512a43edff72712f31667d0a8c2f8e71ee",
187 | "sha256:d5422d6a1ea9b15577a9432e26608c73a78faf0b9039437b075cf322c92e98e7",
188 | "sha256:e5d5420053bbb3dd64c30e58f9363d7a9c27444c3648e61460c1237f9ec3fa14",
189 | "sha256:e868b0389c5ccfc092031a861d4e158ea164d8b7fdbb10e3b5689b4fc6498df6",
190 | "sha256:efd9d3abe5774404becdb0748178b48a218f1d8c44e0375475732211ea47c67e",
191 | "sha256:f8c02ec3c4c4fcb718fdf89a6c6f709b14949408e8cf2a2be5bfa9c49548fd85",
192 | "sha256:ffcf105ecdd9396e05a8e58e81faaaf34d3f9875f137c7372450baa5d77c9a54"
193 | ],
194 | "markers": "python_version >= '3.8'",
195 | "version": "==1.23.3"
196 | },
197 | "ortools": {
198 | "hashes": [
199 | "sha256:102f01502e087e872fb5d8b6c0bcf02b90898c7ed37c09966618f3fa53749acb",
200 | "sha256:299c5c55a25cb865f215c295d31c439ce0c28562d363556070d5a40a0203346f",
201 | "sha256:542a3f57fe64b3911be7dafb267f1a1bdcbf7bade8fedfa0cfb24d69b5ec8539",
202 | "sha256:57949f00fbcb96612812c4459149d01a3f51770bf42fd17521a173be63507687",
203 | "sha256:75ea6ce057b8663d3e04b29ebd8380a3805fc68577a84c0a99ccd33f6311a9c4",
204 | "sha256:812c23fdee2501cc4319f9b08bfdd9a156c704682831294a97dcb65ccfb0abfd",
205 | "sha256:86e325ed85767d8d4e1ebd6a0e8d7cbba24bbe93cbdebb78cdceef450427c0f9",
206 | "sha256:a6d9797b5842f3b0053c5b05367584dcc7a79f96758c79b17a99a22aeb21050f",
207 | "sha256:a74a6cb0425f5bbbcd724658425b09bd0a71939902c1ff3c670d3cdd23e0229b",
208 | "sha256:abb1f8abf2978e4d836ef52c4c3ef5891043ee0ed850a6d3a93673649c28739c",
209 | "sha256:c0f34a3bfb6900f40ffa10e1f48588ab0241e95a2aeac56b3f33056e8384d941",
210 | "sha256:c3d5cabb2e91852b2bd708040745d12c7a74432227880e7e6c3f0bd2e68ebab9"
211 | ],
212 | "index": "pypi",
213 | "version": "==8.1.8487"
214 | },
215 | "packaging": {
216 | "hashes": [
217 | "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
218 | "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
219 | ],
220 | "markers": "python_version >= '3.6'",
221 | "version": "==21.3"
222 | },
223 | "pillow": {
224 | "hashes": [
225 | "sha256:0030fdbd926fb85844b8b92e2f9449ba89607231d3dd597a21ae72dc7fe26927",
226 | "sha256:030e3460861488e249731c3e7ab59b07c7853838ff3b8e16aac9561bb345da14",
227 | "sha256:0ed2c4ef2451de908c90436d6e8092e13a43992f1860275b4d8082667fbb2ffc",
228 | "sha256:136659638f61a251e8ed3b331fc6ccd124590eeff539de57c5f80ef3a9594e58",
229 | "sha256:13b725463f32df1bfeacbf3dd197fb358ae8ebcd8c5548faa75126ea425ccb60",
230 | "sha256:1536ad017a9f789430fb6b8be8bf99d2f214c76502becc196c6f2d9a75b01b76",
231 | "sha256:15928f824870535c85dbf949c09d6ae7d3d6ac2d6efec80f3227f73eefba741c",
232 | "sha256:17d4cafe22f050b46d983b71c707162d63d796a1235cdf8b9d7a112e97b15bac",
233 | "sha256:1802f34298f5ba11d55e5bb09c31997dc0c6aed919658dfdf0198a2fe75d5490",
234 | "sha256:1cc1d2451e8a3b4bfdb9caf745b58e6c7a77d2e469159b0d527a4554d73694d1",
235 | "sha256:1fd6f5e3c0e4697fa7eb45b6e93996299f3feee73a3175fa451f49a74d092b9f",
236 | "sha256:254164c57bab4b459f14c64e93df11eff5ded575192c294a0c49270f22c5d93d",
237 | "sha256:2ad0d4df0f5ef2247e27fc790d5c9b5a0af8ade9ba340db4a73bb1a4a3e5fb4f",
238 | "sha256:2c58b24e3a63efd22554c676d81b0e57f80e0a7d3a5874a7e14ce90ec40d3069",
239 | "sha256:2d33a11f601213dcd5718109c09a52c2a1c893e7461f0be2d6febc2879ec2402",
240 | "sha256:336b9036127eab855beec9662ac3ea13a4544a523ae273cbf108b228ecac8437",
241 | "sha256:337a74fd2f291c607d220c793a8135273c4c2ab001b03e601c36766005f36885",
242 | "sha256:37ff6b522a26d0538b753f0b4e8e164fdada12db6c6f00f62145d732d8a3152e",
243 | "sha256:3d1f14f5f691f55e1b47f824ca4fdcb4b19b4323fe43cc7bb105988cad7496be",
244 | "sha256:4134d3f1ba5f15027ff5c04296f13328fecd46921424084516bdb1b2548e66ff",
245 | "sha256:4ad2f835e0ad81d1689f1b7e3fbac7b01bb8777d5a985c8962bedee0cc6d43da",
246 | "sha256:50dff9cc21826d2977ef2d2a205504034e3a4563ca6f5db739b0d1026658e004",
247 | "sha256:510cef4a3f401c246cfd8227b300828715dd055463cdca6176c2e4036df8bd4f",
248 | "sha256:5aed7dde98403cd91d86a1115c78d8145c83078e864c1de1064f52e6feb61b20",
249 | "sha256:69bd1a15d7ba3694631e00df8de65a8cb031911ca11f44929c97fe05eb9b6c1d",
250 | "sha256:6bf088c1ce160f50ea40764f825ec9b72ed9da25346216b91361eef8ad1b8f8c",
251 | "sha256:6e8c66f70fb539301e064f6478d7453e820d8a2c631da948a23384865cd95544",
252 | "sha256:74a04183e6e64930b667d321524e3c5361094bb4af9083db5c301db64cd341f3",
253 | "sha256:75e636fd3e0fb872693f23ccb8a5ff2cd578801251f3a4f6854c6a5d437d3c04",
254 | "sha256:7761afe0126d046974a01e030ae7529ed0ca6a196de3ec6937c11df0df1bc91c",
255 | "sha256:7888310f6214f19ab2b6df90f3f06afa3df7ef7355fc025e78a3044737fab1f5",
256 | "sha256:7b0554af24df2bf96618dac71ddada02420f946be943b181108cac55a7a2dcd4",
257 | "sha256:7c7b502bc34f6e32ba022b4a209638f9e097d7a9098104ae420eb8186217ebbb",
258 | "sha256:808add66ea764ed97d44dda1ac4f2cfec4c1867d9efb16a33d158be79f32b8a4",
259 | "sha256:831e648102c82f152e14c1a0938689dbb22480c548c8d4b8b248b3e50967b88c",
260 | "sha256:93689632949aff41199090eff5474f3990b6823404e45d66a5d44304e9cdc467",
261 | "sha256:96b5e6874431df16aee0c1ba237574cb6dff1dcb173798faa6a9d8b399a05d0e",
262 | "sha256:9a54614049a18a2d6fe156e68e188da02a046a4a93cf24f373bffd977e943421",
263 | "sha256:a138441e95562b3c078746a22f8fca8ff1c22c014f856278bdbdd89ca36cff1b",
264 | "sha256:a647c0d4478b995c5e54615a2e5360ccedd2f85e70ab57fbe817ca613d5e63b8",
265 | "sha256:a9c9bc489f8ab30906d7a85afac4b4944a572a7432e00698a7239f44a44e6efb",
266 | "sha256:ad2277b185ebce47a63f4dc6302e30f05762b688f8dc3de55dbae4651872cdf3",
267 | "sha256:adabc0bce035467fb537ef3e5e74f2847c8af217ee0be0455d4fec8adc0462fc",
268 | "sha256:b6d5e92df2b77665e07ddb2e4dbd6d644b78e4c0d2e9272a852627cdba0d75cf",
269 | "sha256:bc431b065722a5ad1dfb4df354fb9333b7a582a5ee39a90e6ffff688d72f27a1",
270 | "sha256:bdd0de2d64688ecae88dd8935012c4a72681e5df632af903a1dca8c5e7aa871a",
271 | "sha256:c79698d4cd9318d9481d89a77e2d3fcaeff5486be641e60a4b49f3d2ecca4e28",
272 | "sha256:cb6259196a589123d755380b65127ddc60f4c64b21fc3bb46ce3a6ea663659b0",
273 | "sha256:d5b87da55a08acb586bad5c3aa3b86505f559b84f39035b233d5bf844b0834b1",
274 | "sha256:dcd7b9c7139dc8258d164b55696ecd16c04607f1cc33ba7af86613881ffe4ac8",
275 | "sha256:dfe4c1fedfde4e2fbc009d5ad420647f7730d719786388b7de0999bf32c0d9fd",
276 | "sha256:ea98f633d45f7e815db648fd7ff0f19e328302ac36427343e4432c84432e7ff4",
277 | "sha256:ec52c351b35ca269cb1f8069d610fc45c5bd38c3e91f9ab4cbbf0aebc136d9c8",
278 | "sha256:eef7592281f7c174d3d6cbfbb7ee5984a671fcd77e3fc78e973d492e9bf0eb3f",
279 | "sha256:f07f1f00e22b231dd3d9b9208692042e29792d6bd4f6639415d2f23158a80013",
280 | "sha256:f3fac744f9b540148fa7715a435d2283b71f68bfb6d4aae24482a890aed18b59",
281 | "sha256:fa768eff5f9f958270b081bb33581b4b569faabf8774726b283edb06617101dc",
282 | "sha256:fac2d65901fb0fdf20363fbd345c01958a742f2dc62a8dd4495af66e3ff502a4"
283 | ],
284 | "markers": "python_version >= '3.7'",
285 | "version": "==9.2.0"
286 | },
287 | "pluggy": {
288 | "hashes": [
289 | "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
290 | "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
291 | ],
292 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
293 | "version": "==0.13.1"
294 | },
295 | "protobuf": {
296 | "hashes": [
297 | "sha256:07a0bb9cc6114f16a39c866dc28b6e3d96fa4ffb9cc1033057412547e6e75cb9",
298 | "sha256:308173d3e5a3528787bb8c93abea81d5a950bdce62840d9760effc84127fb39c",
299 | "sha256:4143513c766db85b9d7c18dbf8339673c8a290131b2a0fe73855ab20770f72b0",
300 | "sha256:49f88d56a9180dbb7f6199c920f5bb5c1dd0172f672983bb281298d57c2ac8eb",
301 | "sha256:6b1040a5661cd5f6e610cbca9cfaa2a17d60e2bb545309bc1b278bb05be44bdd",
302 | "sha256:77b355c8604fe285536155286b28b0c4cbc57cf81b08d8357bf34829ea982860",
303 | "sha256:7a6cc8842257265bdfd6b74d088b829e44bcac3cca234c5fdd6052730017b9ea",
304 | "sha256:80e6540381080715fddac12690ee42d087d0d17395f8d0078dfd6f1181e7be4c",
305 | "sha256:8f9e60f7d44592c66e7b332b6a7b4b6e8d8b889393c79dbc3a91f815118f8eac",
306 | "sha256:9666da97129138585b26afcb63ad4887f602e169cafe754a8258541c553b8b5d",
307 | "sha256:aa29113ec901281f29d9d27b01193407a98aa9658b8a777b0325e6d97149f5ce",
308 | "sha256:b6cea204865595a92a7b240e4b65bcaaca3ad5d2ce25d9db3756eba06041138e",
309 | "sha256:ba596b9ffb85c909fcfe1b1a23136224ed678af3faf9912d3fa483d5f9813c4e",
310 | "sha256:c7c864148a237f058c739ae7a05a2b403c0dfa4ce7d1f3e5213f352ad52d57c6"
311 | ],
312 | "index": "pypi",
313 | "version": "==4.21.6"
314 | },
315 | "py": {
316 | "hashes": [
317 | "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719",
318 | "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"
319 | ],
320 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
321 | "version": "==1.11.0"
322 | },
323 | "pyparsing": {
324 | "hashes": [
325 | "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb",
326 | "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"
327 | ],
328 | "markers": "python_full_version >= '3.6.8'",
329 | "version": "==3.0.9"
330 | },
331 | "pytest": {
332 | "hashes": [
333 | "sha256:1969f797a1a0dbd8ccf0fecc80262312729afea9c17f1d70ebf85c5e76c6f7c8",
334 | "sha256:66e419b1899bc27346cb2c993e12c5e5e8daba9073c1fbce33b9807abc95c306"
335 | ],
336 | "index": "pypi",
337 | "version": "==6.2.1"
338 | },
339 | "python-dateutil": {
340 | "hashes": [
341 | "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
342 | "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
343 | ],
344 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
345 | "version": "==2.8.2"
346 | },
347 | "six": {
348 | "hashes": [
349 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
350 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
351 | ],
352 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
353 | "version": "==1.16.0"
354 | },
355 | "toml": {
356 | "hashes": [
357 | "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
358 | "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
359 | ],
360 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
361 | "version": "==0.10.2"
362 | },
363 | "typer": {
364 | "hashes": [
365 | "sha256:5455d750122cff96745b0dec87368f56d023725a7ebc9d2e54dd23dc86816303",
366 | "sha256:ba58b920ce851b12a2d790143009fa00ac1d05b3ff3257061ff69dbdfc3d161b"
367 | ],
368 | "index": "pypi",
369 | "version": "==0.3.2"
370 | }
371 | },
372 | "develop": {}
373 | }
374 |
--------------------------------------------------------------------------------
/deployment/frontend/src/components/CspTool.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |