or-tools 0.1.3 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9681fd5e91b33fa5eca9ad0611011a93e6cd7f1edf12fed4e9956d914725738c
4
- data.tar.gz: a44de2a7f7c86cd6b341abc72e2cf11dab73b3ecd9599b8b00664333af837739
3
+ metadata.gz: 116cfa149eef398630f24b3776c9db8e0a1dddb2b17c8e4b04f347f064231c72
4
+ data.tar.gz: 9930c7e37bc26fd88a6c59eac718eb057d82990bc3ccb3bee3f395456baa9318
5
5
  SHA512:
6
- metadata.gz: 68bd5337214d4ef54a6d5fd6750beb493372c9735ab6a1dc40d22cac2e08804d1fcb241f0dfb50f02980d202837405edd9c3ffcaa723a34e291a0a6d194c38a0
7
- data.tar.gz: 0e43f33feaa383093c2b46e620bc2976e27a21441e86be52fd1310c2f2bc13aa0385dd96f9e46b88215384f843c1e94c85c6471fd501ab8ab26a7ad15d4fa252
6
+ metadata.gz: eab3682edbfa9d3a90eb54ffe1697abd9f9e4dcb75ae6e08b08dfea1dcaedce4d773d3d53490d4c9de258d4d0e3b92e38bcb1cb4c5f4f50225e829c566b2c74b
7
+ data.tar.gz: '0514577229f62526c914a52b7164a2dc49b17515cbe798cd177f3a9dadc1da4a9026c45cec77830b08ac2bce44831f072ee18c6e3bdb9db66d958b6ed407d7b2'
@@ -1,3 +1,27 @@
1
+ ## 0.3.1 (2020-07-21)
2
+
3
+ - Reduced gem size
4
+
5
+ ## 0.3.0 (2020-07-21)
6
+
7
+ - Updated OR-Tools to 7.7
8
+ - Added `BasicScheduler` class
9
+ - Added `Seating` class
10
+ - Added `TSP` class
11
+ - Added `Sudoku` class
12
+
13
+ ## 0.2.0 (2020-05-22)
14
+
15
+ - No longer need to download the OR-Tools C++ library separately on Mac, Ubuntu 18.04, Ubuntu 16.04, Debian 10, and CentOS 8
16
+
17
+ ## 0.1.5 (2020-04-23)
18
+
19
+ - Added support for OR-Tools 7.6
20
+
21
+ ## 0.1.4 (2020-04-19)
22
+
23
+ - Added support for the Job Shop Problem
24
+
1
25
  ## 0.1.3 (2020-03-24)
2
26
 
3
27
  - Added support for more routing problems
data/README.md CHANGED
@@ -6,24 +6,261 @@
6
6
 
7
7
  ## Installation
8
8
 
9
- Download the [OR-Tools C++ library](https://developers.google.com/optimization/install/cpp). Then run:
9
+ Add this line to your application’s Gemfile:
10
10
 
11
- ```sh
12
- bundle config build.or-tools --with-or-tools-dir=/path/to/or-tools
11
+ ```ruby
12
+ gem 'or-tools'
13
13
  ```
14
14
 
15
- Add this line to your application’s Gemfile:
15
+ Installation can take a few minutes as OR-Tools downloads and builds.
16
+
17
+ ## Higher Level Interfaces
18
+
19
+ - [Scheduling](#scheduling)
20
+ - [Seating](#seating)
21
+ - [Traveling Salesperson Problem (TSP)](#traveling-salesperson-problem-tsp)
22
+ - [Sudoku](#sudoku)
23
+
24
+ ### Scheduling
25
+
26
+ Specify people and their availabililty
27
+
28
+ ```ruby
29
+ people = [
30
+ {
31
+ availability: [
32
+ {starts_at: Time.parse("2020-01-01 08:00:00"), ends_at: Time.parse("2020-01-01 16:00:00")},
33
+ {starts_at: Time.parse("2020-01-02 08:00:00"), ends_at: Time.parse("2020-01-02 16:00:00")}
34
+ ],
35
+ max_hours: 40 # optional, applies to entire scheduling period
36
+ },
37
+ {
38
+ availability: [
39
+ {starts_at: Time.parse("2020-01-01 08:00:00"), ends_at: Time.parse("2020-01-01 16:00:00")},
40
+ {starts_at: Time.parse("2020-01-03 08:00:00"), ends_at: Time.parse("2020-01-03 16:00:00")}
41
+ ],
42
+ max_hours: 20
43
+ }
44
+ ]
45
+ ```
46
+
47
+ Specify shifts
16
48
 
17
49
  ```ruby
18
- gem 'or-tools'
50
+ shifts = [
51
+ {starts_at: Time.parse("2020-01-01 08:00:00"), ends_at: Time.parse("2020-01-01 16:00:00")},
52
+ {starts_at: Time.parse("2020-01-02 08:00:00"), ends_at: Time.parse("2020-01-02 16:00:00")},
53
+ {starts_at: Time.parse("2020-01-03 08:00:00"), ends_at: Time.parse("2020-01-03 16:00:00")}
54
+ ]
55
+ ```
56
+
57
+ Run the scheduler
58
+
59
+ ```ruby
60
+ scheduler = ORTools::BasicScheduler.new(people: people, shifts: shifts)
61
+ ```
62
+
63
+ The scheduler maximizes the number of assigned hours. A person must be available for the entire shift to be considered for it.
64
+
65
+ Get assignments (returns indexes of people and shifts)
66
+
67
+ ```ruby
68
+ scheduler.assignments
69
+ # [
70
+ # {person: 2, shift: 0},
71
+ # {person: 0, shift: 1},
72
+ # {person: 1, shift: 2}
73
+ # ]
74
+ ```
75
+
76
+ Get assigned hours and total hours
77
+
78
+ ```ruby
79
+ scheduler.assigned_hours
80
+ scheduler.total_hours
81
+ ```
82
+
83
+ Feel free to create an issue if you have a scheduling use case that’s not covered.
84
+
85
+ ### Seating
86
+
87
+ Create a seating chart based on personal connections. Uses [this approach](https://www.improbable.com/news/2012/Optimal-seating-chart.pdf).
88
+
89
+ Specify connections
90
+
91
+ ```ruby
92
+ connections = [
93
+ {people: ["A", "B", "C"], weight: 2},
94
+ {people: ["C", "D", "E", "F"], weight: 1}
95
+ ]
96
+ ```
97
+
98
+ Use different weights to prioritize seating. For a wedding, it may look like:
99
+
100
+ ```ruby
101
+ connections = [
102
+ {people: knows_partner1, weight: 1},
103
+ {people: knows_partner2, weight: 1},
104
+ {people: relationship1, weight: 100},
105
+ {people: relationship2, weight: 100},
106
+ {people: relationship3, weight: 100},
107
+ {people: friend_group1, weight: 10},
108
+ {people: friend_group2, weight: 10},
109
+ # ...
110
+ ]
111
+ ```
112
+
113
+ If two people have multiple connections, weights are added.
114
+
115
+ Specify tables and their capacity
116
+
117
+ ```ruby
118
+ tables = [3, 3]
119
+ ```
120
+
121
+ Assign seats
122
+
123
+ ```ruby
124
+ seating = ORTools::Seating.new(connections: connections, tables: tables)
125
+ ```
126
+
127
+ Each person will have a connection with at least one other person at their table.
128
+
129
+ Get tables
130
+
131
+ ```ruby
132
+ seating.assigned_tables
133
+ ```
134
+
135
+ Get assignments by person
136
+
137
+ ```ruby
138
+ seating.assignments
139
+ ```
140
+
141
+ Get all connections for a person
142
+
143
+ ```ruby
144
+ seating.connections_for(person)
145
+ ```
146
+
147
+ Get connections for a person at their table
148
+
149
+ ```ruby
150
+ seating.connections_for(person, same_table: true)
151
+ ```
152
+
153
+ ### Traveling Salesperson Problem (TSP)
154
+
155
+ Create locations - the first location will be the starting and ending point
156
+
157
+ ```ruby
158
+ locations = [
159
+ {name: "Tokyo", latitude: 35.6762, longitude: 139.6503},
160
+ {name: "Delhi", latitude: 28.7041, longitude: 77.1025},
161
+ {name: "Shanghai", latitude: 31.2304, longitude: 121.4737},
162
+ {name: "São Paulo", latitude: -23.5505, longitude: -46.6333},
163
+ {name: "Mexico City", latitude: 19.4326, longitude: -99.1332},
164
+ {name: "Cairo", latitude: 30.0444, longitude: 31.2357},
165
+ {name: "Mumbai", latitude: 19.0760, longitude: 72.8777},
166
+ {name: "Beijing", latitude: 39.9042, longitude: 116.4074},
167
+ {name: "Dhaka", latitude: 23.8103, longitude: 90.4125},
168
+ {name: "Osaka", latitude: 34.6937, longitude: 135.5023},
169
+ {name: "New York City", latitude: 40.7128, longitude: -74.0060},
170
+ {name: "Karachi", latitude: 24.8607, longitude: 67.0011},
171
+ {name: "Buenos Aires", latitude: -34.6037, longitude: -58.3816}
172
+ ]
173
+ ```
174
+
175
+ Locations can have any fields - only `latitude` and `longitude` are required
176
+
177
+ Get route
178
+
179
+ ```ruby
180
+ tsp = ORTools::TSP.new(locations)
181
+ tsp.route # [{name: "Tokyo", ...}, {name: "Osaka", ...}, ...]
182
+ ```
183
+
184
+ Get distances between locations on route
185
+
186
+ ```ruby
187
+ tsp.distances # [392.441, 1362.926, 1067.31, ...]
188
+ ```
189
+
190
+ Distances are in kilometers - multiply by `0.6214` for miles
191
+
192
+ Get total distance
193
+
194
+ ```ruby
195
+ tsp.total_distance
196
+ ```
197
+
198
+ ### Sudoku
199
+
200
+ Create a puzzle with zeros in empty cells
201
+
202
+ ```ruby
203
+ grid = [
204
+ [0, 6, 0, 0, 5, 0, 0, 2, 0],
205
+ [0, 0, 0, 3, 0, 0, 0, 9, 0],
206
+ [7, 0, 0, 6, 0, 0, 0, 1, 0],
207
+ [0, 0, 6, 0, 3, 0, 4, 0, 0],
208
+ [0, 0, 4, 0, 7, 0, 1, 0, 0],
209
+ [0, 0, 5, 0, 9, 0, 8, 0, 0],
210
+ [0, 4, 0, 0, 0, 1, 0, 0, 6],
211
+ [0, 3, 0, 0, 0, 8, 0, 0, 0],
212
+ [0, 2, 0, 0, 4, 0, 0, 5, 0]
213
+ ]
214
+ sudoku = ORTools::Sudoku.new(grid)
215
+ sudoku.solution
216
+ ```
217
+
218
+ It can also solve more advanced puzzles like [The Miracle](https://www.youtube.com/watch?v=yKf9aUIxdb4)
219
+
220
+ ```ruby
221
+ grid = [
222
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
223
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
224
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
225
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
226
+ [0, 0, 1, 0, 0, 0, 0, 0, 0],
227
+ [0, 0, 0, 0, 0, 0, 2, 0, 0],
228
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
229
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
230
+ [0, 0, 0, 0, 0, 0, 0, 0, 0]
231
+ ]
232
+ sudoku = ORTools::Sudoku.new(grid, anti_knight: true, anti_king: true, non_consecutive: true)
233
+ sudoku.solution
234
+ ```
235
+
236
+ And [this 4-digit puzzle](https://www.youtube.com/watch?v=hAyZ9K2EBF0)
237
+
238
+ ```ruby
239
+ grid = [
240
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
241
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
242
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
243
+ [3, 8, 4, 0, 0, 0, 0, 0, 0],
244
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
245
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
246
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
247
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
248
+ [0, 0, 0, 0, 0, 0, 0, 0, 2]
249
+ ]
250
+ sudoku = ORTools::Sudoku.new(grid, x: true, anti_knight: true, magic_square: true)
251
+ sudoku.solution
19
252
  ```
20
253
 
21
- ## Getting Started
254
+ ## Guides
22
255
 
23
256
  Linear Optimization
24
257
 
25
258
  - [The Glop Linear Solver](#the-glop-linear-solver)
26
259
 
260
+ Integer Optimization
261
+
262
+ - [Mixed-Integer Programming](#mixed-integer-programming)
263
+
27
264
  Constraint Optimization
28
265
 
29
266
  - [CP-SAT Solver](#cp-sat-solver)
@@ -31,13 +268,14 @@ Constraint Optimization
31
268
  - [Cryptarithmetic](#cryptarithmetic)
32
269
  - [The N-queens Problem](#the-n-queens-problem)
33
270
 
34
- Integer Optimization
271
+ Assignment
35
272
 
36
- - [Mixed-Integer Programming](#mixed-integer-programming)
273
+ - [Assignment](#assignment)
274
+ - [Assignment with Teams](#assignment-with-teams)
37
275
 
38
276
  Routing
39
277
 
40
- - [Traveling Salesperson Problem (TSP)](#traveling-salesperson-problem-tsp)
278
+ - [Traveling Salesperson Problem (TSP)](#traveling-salesperson-problem-tsp-1)
41
279
  - [Vehicle Routing Problem (VRP)](#vehicle-routing-problem-vrp)
42
280
  - [Capacity Constraints](#capacity-constraints)
43
281
  - [Pickups and Deliveries](#pickups-and-deliveries)
@@ -56,42 +294,32 @@ Network Flows
56
294
 
57
295
  - [Maximum Flows](#maximum-flows)
58
296
  - [Minimum Cost Flows](#minimum-cost-flows)
59
-
60
- Assignment
61
-
62
- - [Assignment](#assignment)
63
- - [Assignment as a Min Cost Problem](#assignment-as-a-min-cost-problem)
64
- - [Assignment as a MIP Problem](#assignment-as-a-mip-problem)
297
+ - [Assignment as a Min Cost Flow Problem](#assignment-as-a-min-cost-flow-problem)
65
298
 
66
299
  Scheduling
67
300
 
68
301
  - [Employee Scheduling](#employee-scheduling)
302
+ - [The Job Shop Problem](#the-job-shop-problem)
69
303
 
70
304
  Other Examples
71
305
 
72
- - [Sudoku](#sudoku)
306
+ - [Sudoku](#sudoku-1)
307
+ - [Wedding Seating Chart](#wedding-seating-chart)
73
308
  - [Set Partitioning](#set-partitioning)
74
309
 
75
310
  ### The Glop Linear Solver
76
311
 
77
312
  [Guide](https://developers.google.com/optimization/lp/glop)
78
313
 
79
- Declare the solver
80
-
81
314
  ```ruby
315
+ # declare the solver
82
316
  solver = ORTools::Solver.new("LinearProgrammingExample", :glop)
83
- ```
84
-
85
- Create the variables
86
317
 
87
- ```ruby
318
+ # create the variables
88
319
  x = solver.num_var(0, solver.infinity, "x")
89
320
  y = solver.num_var(0, solver.infinity, "y")
90
- ```
91
-
92
- Define the constraints
93
321
 
94
- ```ruby
322
+ # define the constraints
95
323
  constraint0 = solver.constraint(-solver.infinity, 14)
96
324
  constraint0.set_coefficient(x, 1)
97
325
  constraint0.set_coefficient(y, 2)
@@ -103,26 +331,17 @@ constraint1.set_coefficient(y, -1)
103
331
  constraint2 = solver.constraint(-solver.infinity, 2)
104
332
  constraint2.set_coefficient(x, 1)
105
333
  constraint2.set_coefficient(y, -1)
106
- ```
107
-
108
- Define the objective function
109
334
 
110
- ```ruby
335
+ # define the objective function
111
336
  objective = solver.objective
112
337
  objective.set_coefficient(x, 3)
113
338
  objective.set_coefficient(y, 4)
114
339
  objective.set_maximization
115
- ```
116
-
117
- Invoke the solver
118
340
 
119
- ```ruby
341
+ # invoke the solver
120
342
  solver.solve
121
- ```
122
-
123
- Display the solution
124
343
 
125
- ```ruby
344
+ # display the solution
126
345
  opt_solution = 3 * x.solution_value + 4 * y.solution_value
127
346
  puts "Number of variables = #{solver.num_variables}"
128
347
  puts "Number of constraints = #{solver.num_constraints}"
@@ -132,42 +351,75 @@ puts "y = #{y.solution_value}"
132
351
  puts "Optimal objective value = #{opt_solution}"
133
352
  ```
134
353
 
135
- ### CP-SAT Solver
136
-
137
- [Guide](https://developers.google.com/optimization/cp/cp_solver)
354
+ ### Mixed-Integer Programming
138
355
 
139
- Declare the model
356
+ [Guide](https://developers.google.com/optimization/mip/integer_opt)
140
357
 
141
358
  ```ruby
142
- model = ORTools::CpModel.new
359
+ # declare the MIP solver
360
+ solver = ORTools::Solver.new("simple_mip_program", :cbc)
361
+
362
+ # define the variables
363
+ infinity = solver.infinity
364
+ x = solver.int_var(0.0, infinity, "x")
365
+ y = solver.int_var(0.0, infinity, "y")
366
+
367
+ puts "Number of variables = #{solver.num_variables}"
368
+
369
+ # define the constraints
370
+ c0 = solver.constraint(-infinity, 17.5)
371
+ c0.set_coefficient(x, 1)
372
+ c0.set_coefficient(y, 7)
373
+
374
+ c1 = solver.constraint(-infinity, 3.5)
375
+ c1.set_coefficient(x, 1);
376
+ c1.set_coefficient(y, 0);
377
+
378
+ puts "Number of constraints = #{solver.num_constraints}"
379
+
380
+ # define the objective
381
+ objective = solver.objective
382
+ objective.set_coefficient(x, 1)
383
+ objective.set_coefficient(y, 10)
384
+ objective.set_maximization
385
+
386
+ # call the solver
387
+ status = solver.solve
388
+
389
+ # display the solution
390
+ if status == :optimal
391
+ puts "Solution:"
392
+ puts "Objective value = #{solver.objective.value}"
393
+ puts "x = #{x.solution_value}"
394
+ puts "y = #{y.solution_value}"
395
+ else
396
+ puts "The problem does not have an optimal solution."
397
+ end
143
398
  ```
144
399
 
145
- Create the variables
400
+ ### CP-SAT Solver
401
+
402
+ [Guide](https://developers.google.com/optimization/cp/cp_solver)
146
403
 
147
404
  ```ruby
405
+ # declare the model
406
+ model = ORTools::CpModel.new
407
+
408
+ # create the variables
148
409
  num_vals = 3
149
410
  x = model.new_int_var(0, num_vals - 1, "x")
150
411
  y = model.new_int_var(0, num_vals - 1, "y")
151
412
  z = model.new_int_var(0, num_vals - 1, "z")
152
- ```
153
413
 
154
- Create the constraint
155
-
156
- ```ruby
414
+ # create the constraint
157
415
  model.add(x != y)
158
- ```
159
416
 
160
- Call the solver
161
-
162
- ```ruby
417
+ # call the solver
163
418
  solver = ORTools::CpSolver.new
164
419
  status = solver.solve(model)
165
- ```
166
420
 
167
- Display the first solution
168
-
169
- ```ruby
170
- if status == :feasible
421
+ # display the first solution
422
+ if status == :optimal
171
423
  puts "x = #{solver.value(x)}"
172
424
  puts "y = #{solver.value(y)}"
173
425
  puts "z = #{solver.value(z)}"
@@ -178,38 +430,25 @@ end
178
430
 
179
431
  [Guide](https://developers.google.com/optimization/cp/integer_opt_cp)
180
432
 
181
- Declare the model
182
-
183
433
  ```ruby
434
+ # declare the model
184
435
  model = ORTools::CpModel.new
185
- ```
186
-
187
- Create the variables
188
436
 
189
- ```ruby
437
+ # create the variables
190
438
  var_upper_bound = [50, 45, 37].max
191
439
  x = model.new_int_var(0, var_upper_bound, "x")
192
440
  y = model.new_int_var(0, var_upper_bound, "y")
193
441
  z = model.new_int_var(0, var_upper_bound, "z")
194
- ```
195
-
196
- Define the constraints
197
442
 
198
- ```ruby
443
+ # define the constraints
199
444
  model.add(x*2 + y*7 + z*3 <= 50)
200
445
  model.add(x*3 - y*5 + z*7 <= 45)
201
446
  model.add(x*5 + y*2 - z*6 <= 37)
202
- ```
203
-
204
- Define the objective function
205
447
 
206
- ```ruby
448
+ # define the objective function
207
449
  model.maximize(x*2 + y*2 + z*3)
208
- ```
209
-
210
- Call the solver
211
450
 
212
- ```ruby
451
+ # call the solver
213
452
  solver = ORTools::CpSolver.new
214
453
  status = solver.solve(model)
215
454
 
@@ -226,9 +465,8 @@ end
226
465
 
227
466
  [Guide](https://developers.google.com/optimization/cp/cryptarithmetic)
228
467
 
229
- Define the variables
230
-
231
468
  ```ruby
469
+ # define the variables
232
470
  model = ORTools::CpModel.new
233
471
 
234
472
  base = 10
@@ -245,20 +483,14 @@ r = model.new_int_var(0, base - 1, "R")
245
483
  e = model.new_int_var(0, base - 1, "E")
246
484
 
247
485
  letters = [c, p, i, s, f, u, n, t, r, e]
248
- ```
249
486
 
250
- Define the constraints
251
-
252
- ```ruby
487
+ # define the constraints
253
488
  model.add_all_different(letters)
254
489
 
255
490
  model.add(c * base + p + i * base + s + f * base * base + u * base +
256
491
  n == t * base * base * base + r * base * base + u * base + e)
257
- ```
258
492
 
259
- Define the solution printer
260
-
261
- ```ruby
493
+ # define the solution printer
262
494
  class VarArraySolutionPrinter < ORTools::CpSolverSolutionCallback
263
495
  attr_reader :solution_count
264
496
 
@@ -276,11 +508,8 @@ class VarArraySolutionPrinter < ORTools::CpSolverSolutionCallback
276
508
  puts
277
509
  end
278
510
  end
279
- ```
280
511
 
281
- Invoke the solver
282
-
283
- ```ruby
512
+ # invoke the solver
284
513
  solver = ORTools::CpSolver.new
285
514
  solution_printer = VarArraySolutionPrinter.new(letters)
286
515
  status = solver.search_for_all_solutions(model, solution_printer)
@@ -298,22 +527,15 @@ puts " - solutions found : %i" % solution_printer.solution_count
298
527
 
299
528
  [Guide](https://developers.google.com/optimization/cp/queens)
300
529
 
301
- Declare the model
302
-
303
530
  ```ruby
531
+ # declare the model
304
532
  board_size = 8
305
533
  model = ORTools::CpModel.new
306
- ```
307
-
308
- Create the variables
309
534
 
310
- ```ruby
535
+ # create the variables
311
536
  queens = board_size.times.map { |i| model.new_int_var(0, board_size - 1, "x%i" % i) }
312
- ```
313
-
314
- Create the constraints
315
537
 
316
- ```ruby
538
+ # create the constraints
317
539
  board_size.times do |i|
318
540
  diag1 = []
319
541
  diag2 = []
@@ -328,11 +550,8 @@ board_size.times do |i|
328
550
  model.add_all_different(diag1)
329
551
  model.add_all_different(diag2)
330
552
  end
331
- ```
332
-
333
- Create a solution printer
334
553
 
335
- ```ruby
554
+ # create a solution printer
336
555
  class SolutionPrinter < ORTools::CpSolverSolutionCallback
337
556
  attr_reader :solution_count
338
557
 
@@ -350,11 +569,8 @@ class SolutionPrinter < ORTools::CpSolverSolutionCallback
350
569
  puts
351
570
  end
352
571
  end
353
- ```
354
-
355
- Call the solver and display the results
356
572
 
357
- ```ruby
573
+ # call the solver and display the results
358
574
  solver = ORTools::CpSolver.new
359
575
  solution_printer = SolutionPrinter.new(queens)
360
576
  status = solver.search_for_all_solutions(model, solution_printer)
@@ -362,75 +578,127 @@ puts
362
578
  puts "Solutions found : %i" % solution_printer.solution_count
363
579
  ```
364
580
 
365
- ### Mixed-Integer Programming
366
581
 
367
- [Guide](https://developers.google.com/optimization/mip/integer_opt)
582
+ ### Assignment
368
583
 
369
- Declare the MIP solver
584
+ [Guide](https://developers.google.com/optimization/assignment/assignment_example)
370
585
 
371
586
  ```ruby
372
- solver = ORTools::Solver.new("simple_mip_program", :cbc)
373
- ```
587
+ # create the data
588
+ cost = [[ 90, 76, 75, 70],
589
+ [ 35, 85, 55, 65],
590
+ [125, 95, 90, 105],
591
+ [ 45, 110, 95, 115]]
592
+
593
+ rows = cost.length
594
+ cols = cost[0].length
374
595
 
375
- Define the variables
596
+ # create the solver
597
+ assignment = ORTools::LinearSumAssignment.new
376
598
 
377
- ```ruby
378
- infinity = solver.infinity
379
- x = solver.int_var(0.0, infinity, "x")
380
- y = solver.int_var(0.0, infinity, "y")
599
+ # add the costs to the solver
600
+ rows.times do |worker|
601
+ cols.times do |task|
602
+ if cost[worker][task]
603
+ assignment.add_arc_with_cost(worker, task, cost[worker][task])
604
+ end
605
+ end
606
+ end
381
607
 
382
- puts "Number of variables = #{solver.num_variables}"
608
+ # invoke the solver
609
+ solve_status = assignment.solve
610
+ if solve_status == :optimal
611
+ puts "Total cost = #{assignment.optimal_cost}"
612
+ puts
613
+ assignment.num_nodes.times do |i|
614
+ puts "Worker %d assigned to task %d. Cost = %d" % [
615
+ i,
616
+ assignment.right_mate(i),
617
+ assignment.assignment_cost(i)
618
+ ]
619
+ end
620
+ elsif solve_status == :infeasible
621
+ puts "No assignment is possible."
622
+ elsif solve_status == :possible_overflow
623
+ puts "Some input costs are too large and may cause an integer overflow."
624
+ end
383
625
  ```
384
626
 
385
- Define the constraints
627
+ ### Assignment with Teams
628
+
629
+ [Guide](https://developers.google.com/optimization/assignment/assignment_teams)
386
630
 
387
631
  ```ruby
388
- c0 = solver.constraint(-infinity, 17.5)
389
- c0.set_coefficient(x, 1)
390
- c0.set_coefficient(y, 7)
632
+ # create the solver
633
+ solver = ORTools::Solver.new("SolveAssignmentProblemMIP", :cbc)
391
634
 
392
- c1 = solver.constraint(-infinity, 3.5)
393
- c1.set_coefficient(x, 1);
394
- c1.set_coefficient(y, 0);
635
+ # create the data
636
+ cost = [[90, 76, 75, 70],
637
+ [35, 85, 55, 65],
638
+ [125, 95, 90, 105],
639
+ [45, 110, 95, 115],
640
+ [60, 105, 80, 75],
641
+ [45, 65, 110, 95]]
395
642
 
396
- puts "Number of constraints = #{solver.num_constraints}"
397
- ```
643
+ team1 = [0, 2, 4]
644
+ team2 = [1, 3, 5]
645
+ team_max = 2
398
646
 
399
- Define the objective
647
+ # create the variables
648
+ num_workers = cost.length
649
+ num_tasks = cost[1].length
650
+ x = {}
400
651
 
401
- ```ruby
402
- objective = solver.objective
403
- objective.set_coefficient(x, 1)
404
- objective.set_coefficient(y, 10)
405
- objective.set_maximization
406
- ```
652
+ num_workers.times do |i|
653
+ num_tasks.times do |j|
654
+ x[[i, j]] = solver.bool_var("x[#{i},#{j}]")
655
+ end
656
+ end
407
657
 
408
- Call the solver
658
+ # create the objective function
659
+ solver.minimize(solver.sum(
660
+ num_workers.times.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] * cost[i][j] } }
661
+ ))
409
662
 
410
- ```ruby
411
- status = solver.solve
412
- ```
663
+ # create the constraints
664
+ num_workers.times do |i|
665
+ solver.add(solver.sum(num_tasks.times.map { |j| x[[i, j]] }) <= 1)
666
+ end
413
667
 
414
- Display the solution
668
+ num_tasks.times do |j|
669
+ solver.add(solver.sum(num_workers.times.map { |i| x[[i, j]] }) == 1)
670
+ end
415
671
 
416
- ```ruby
417
- if status == :optimal
418
- puts "Solution:"
419
- puts "Objective value = #{solver.objective.value}"
420
- puts "x = #{x.solution_value}"
421
- puts "y = #{y.solution_value}"
422
- else
423
- puts "The problem does not have an optimal solution."
672
+ solver.add(solver.sum(team1.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] } }) <= team_max)
673
+ solver.add(solver.sum(team2.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] } }) <= team_max)
674
+
675
+ # invoke the solver
676
+ sol = solver.solve
677
+
678
+ puts "Total cost = #{solver.objective.value}"
679
+ puts
680
+ num_workers.times do |i|
681
+ num_tasks.times do |j|
682
+ if x[[i, j]].solution_value > 0
683
+ puts "Worker %d assigned to task %d. Cost = %d" % [
684
+ i,
685
+ j,
686
+ cost[i][j]
687
+ ]
688
+ end
689
+ end
424
690
  end
691
+
692
+ puts
693
+ puts "Time = #{solver.wall_time} milliseconds"
425
694
  ```
426
695
 
427
696
  ### Traveling Salesperson Problem (TSP)
428
697
 
429
698
  [Guide](https://developers.google.com/optimization/routing/tsp.html)
430
699
 
431
- Create the data
432
-
433
700
  ```ruby
701
+ # create the data
434
702
  data = {}
435
703
  data[:distance_matrix] = [
436
704
  [0, 2451, 713, 1018, 1631, 1374, 2408, 213, 2571, 875, 1420, 2145, 1972],
@@ -449,11 +717,8 @@ data[:distance_matrix] = [
449
717
  ]
450
718
  data[:num_vehicles] = 1
451
719
  data[:depot] = 0
452
- ```
453
-
454
- Create the distance callback
455
720
 
456
- ```ruby
721
+ # create the distance callback
457
722
  manager = ORTools::RoutingIndexManager.new(data[:distance_matrix].length, data[:num_vehicles], data[:depot])
458
723
  routing = ORTools::RoutingModel.new(manager)
459
724
 
@@ -465,17 +730,11 @@ end
465
730
 
466
731
  transit_callback_index = routing.register_transit_callback(distance_callback)
467
732
  routing.set_arc_cost_evaluator_of_all_vehicles(transit_callback_index)
468
- ```
469
-
470
- Run the solver
471
-
472
- ```ruby
473
- assignment = routing.solve(first_solution_strategy: :path_cheaper_arc)
474
- ```
475
733
 
476
- Print the solution
734
+ # run the solver
735
+ assignment = routing.solve(first_solution_strategy: :path_cheapest_arc)
477
736
 
478
- ```ruby
737
+ # print the solution
479
738
  puts "Objective: #{assignment.objective_value} miles"
480
739
  index = routing.start(0)
481
740
  plan_output = String.new("Route for vehicle 0:\n")
@@ -494,9 +753,8 @@ puts plan_output
494
753
 
495
754
  [Guide](https://developers.google.com/optimization/routing/vrp)
496
755
 
497
- Create the data
498
-
499
756
  ```ruby
757
+ # create the data
500
758
  data = {}
501
759
  data[:distance_matrix] = [
502
760
  [0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662],
@@ -519,11 +777,8 @@ data[:distance_matrix] = [
519
777
  ]
520
778
  data[:num_vehicles] = 4
521
779
  data[:depot] = 0
522
- ```
523
780
 
524
- Define the distance callback
525
-
526
- ```ruby
781
+ # define the distance callback
527
782
  manager = ORTools::RoutingIndexManager.new(data[:distance_matrix].length, data[:num_vehicles], data[:depot])
528
783
  routing = ORTools::RoutingModel.new(manager)
529
784
 
@@ -535,26 +790,17 @@ end
535
790
 
536
791
  transit_callback_index = routing.register_transit_callback(distance_callback)
537
792
  routing.set_arc_cost_evaluator_of_all_vehicles(transit_callback_index)
538
- ```
539
793
 
540
- Add a distance dimension
541
-
542
- ```ruby
794
+ # add a distance dimension
543
795
  dimension_name = "Distance"
544
796
  routing.add_dimension(transit_callback_index, 0, 3000, true, dimension_name)
545
797
  distance_dimension = routing.mutable_dimension(dimension_name)
546
798
  distance_dimension.global_span_cost_coefficient = 100
547
- ```
548
799
 
549
- Run the solver
550
-
551
- ```ruby
800
+ # run the solver
552
801
  solution = routing.solve(first_solution_strategy: :path_cheapest_arc)
553
- ```
554
802
 
555
- Print the solution
556
-
557
- ```ruby
803
+ # print the solution
558
804
  max_route_distance = 0
559
805
  data[:num_vehicles].times do |vehicle_id|
560
806
  index = routing.start(vehicle_id)
@@ -698,7 +944,6 @@ data[:num_vehicles] = 4
698
944
  data[:depot] = 0
699
945
 
700
946
  manager = ORTools::RoutingIndexManager.new(data[:distance_matrix].size, data[:num_vehicles], data[:depot])
701
-
702
947
  routing = ORTools::RoutingModel.new(manager)
703
948
 
704
949
  distance_callback = lambda do |from_index, to_index|
@@ -906,7 +1151,6 @@ data[:depot_capacity] = 2
906
1151
  data[:depot] = 0
907
1152
 
908
1153
  manager = ORTools::RoutingIndexManager.new(data[:time_matrix].size, data[:num_vehicles], data[:depot])
909
-
910
1154
  routing = ORTools::RoutingModel.new(manager)
911
1155
 
912
1156
  time_callback = lambda do |from_index, to_index|
@@ -1014,7 +1258,6 @@ data[:num_vehicles] = 4
1014
1258
  data[:depot] = 0
1015
1259
 
1016
1260
  manager = ORTools::RoutingIndexManager.new(data[:distance_matrix].size, data[:num_vehicles], data[:depot])
1017
-
1018
1261
  routing = ORTools::RoutingModel.new(manager)
1019
1262
 
1020
1263
  distance_callback = lambda do |from_index, to_index|
@@ -1103,9 +1346,8 @@ routing.solve(
1103
1346
 
1104
1347
  [Guide](https://developers.google.com/optimization/bin/knapsack)
1105
1348
 
1106
- Create the data
1107
-
1108
1349
  ```ruby
1350
+ # create the data
1109
1351
  values = [
1110
1352
  360, 83, 59, 130, 431, 67, 230, 52, 93, 125, 670, 892, 600, 38, 48, 147,
1111
1353
  78, 256, 63, 17, 120, 164, 432, 35, 92, 110, 22, 42, 50, 323, 514, 28,
@@ -1118,17 +1360,11 @@ weights = [[
1118
1360
  3, 86, 66, 31, 65, 0, 79, 20, 65, 52, 13
1119
1361
  ]]
1120
1362
  capacities = [850]
1121
- ```
1122
-
1123
- Declare the solver
1124
1363
 
1125
- ```ruby
1364
+ # declare the solver
1126
1365
  solver = ORTools::KnapsackSolver.new(:branch_and_bound, "KnapsackExample")
1127
- ```
1128
-
1129
- Call the solver
1130
1366
 
1131
- ```ruby
1367
+ # call the solver
1132
1368
  solver.init(values, weights, capacities)
1133
1369
  computed_value = solver.solve
1134
1370
 
@@ -1152,9 +1388,8 @@ puts "Packed weights: #{packed_weights}"
1152
1388
 
1153
1389
  [Guide](https://developers.google.com/optimization/bin/multiple_knapsack)
1154
1390
 
1155
- Create the data
1156
-
1157
1391
  ```ruby
1392
+ # create the data
1158
1393
  data = {}
1159
1394
  weights = [48, 30, 42, 36, 36, 48, 42, 42, 36, 24, 30, 30, 42, 36, 36]
1160
1395
  values = [10, 30, 25, 50, 35, 30, 15, 40, 30, 35, 45, 10, 20, 30, 25]
@@ -1165,28 +1400,21 @@ data[:num_items] = weights.length
1165
1400
  num_bins = 5
1166
1401
  data[:bins] = (0...num_bins).to_a
1167
1402
  data[:bin_capacities] = [100, 100, 100, 100, 100]
1168
- ```
1169
-
1170
- Declare the solver
1171
1403
 
1172
- ```ruby
1404
+ # declare the solver
1173
1405
  solver = ORTools::Solver.new("simple_mip_program", :cbc)
1174
- ```
1175
-
1176
- Create the variables
1177
1406
 
1178
- ```ruby
1407
+ # create the variables
1408
+ # x[i, j] = 1 if item i is packed in bin j
1179
1409
  x = {}
1180
1410
  data[:items].each do |i|
1181
1411
  data[:bins].each do |j|
1182
1412
  x[[i, j]] = solver.int_var(0, 1, "x_%i_%i" % [i, j])
1183
1413
  end
1184
1414
  end
1185
- ```
1186
1415
 
1187
- Define the constraints
1188
-
1189
- ```ruby
1416
+ # define the constraints
1417
+ # each item can be in at most one bin
1190
1418
  data[:items].each do |i|
1191
1419
  sum = ORTools::LinearExpr.new
1192
1420
  data[:bins].each do |j|
@@ -1195,6 +1423,7 @@ data[:items].each do |i|
1195
1423
  solver.add(sum <= 1.0)
1196
1424
  end
1197
1425
 
1426
+ # the amount packed in each bin cannot exceed its capacity
1198
1427
  data[:bins].each do |j|
1199
1428
  weight = ORTools::LinearExpr.new
1200
1429
  data[:items].each do |i|
@@ -1202,11 +1431,8 @@ data[:bins].each do |j|
1202
1431
  end
1203
1432
  solver.add(weight <= data[:bin_capacities][j])
1204
1433
  end
1205
- ```
1206
-
1207
- Define the objective
1208
1434
 
1209
- ```ruby
1435
+ # define the objective
1210
1436
  objective = solver.objective
1211
1437
 
1212
1438
  data[:items].each do |i|
@@ -1215,11 +1441,8 @@ data[:items].each do |i|
1215
1441
  end
1216
1442
  end
1217
1443
  objective.set_maximization
1218
- ```
1219
1444
 
1220
- Call the solver and print the solution
1221
-
1222
- ```ruby
1445
+ # call the solver and print the solution
1223
1446
  status = solver.solve
1224
1447
 
1225
1448
  if status == :optimal
@@ -1251,26 +1474,20 @@ end
1251
1474
 
1252
1475
  [Guide](https://developers.google.com/optimization/bin/bin_packing)
1253
1476
 
1254
- Create the data
1255
-
1256
1477
  ```ruby
1478
+ # create the data
1257
1479
  data = {}
1258
1480
  weights = [48, 30, 19, 36, 36, 27, 42, 42, 36, 24, 30]
1259
1481
  data[:weights] = weights
1260
1482
  data[:items] = (0...weights.length).to_a
1261
1483
  data[:bins] = data[:items]
1262
1484
  data[:bin_capacity] = 100
1263
- ```
1264
1485
 
1265
- Declare the solver
1266
-
1267
- ```ruby
1486
+ # create the mip solver with the CBC backend
1268
1487
  solver = ORTools::Solver.new("simple_mip_program", :cbc)
1269
- ```
1270
-
1271
- Create the variables
1272
1488
 
1273
- ```ruby
1489
+ # variables
1490
+ # x[i, j] = 1 if item i is packed in bin j
1274
1491
  x = {}
1275
1492
  data[:items].each do |i|
1276
1493
  data[:bins].each do |j|
@@ -1278,34 +1495,28 @@ data[:items].each do |i|
1278
1495
  end
1279
1496
  end
1280
1497
 
1498
+ # y[j] = 1 if bin j is used
1281
1499
  y = {}
1282
1500
  data[:bins].each do |j|
1283
1501
  y[j] = solver.int_var(0, 1, "y[%i]" % j)
1284
1502
  end
1285
- ```
1286
1503
 
1287
- Define the constraints
1288
-
1289
- ```ruby
1504
+ # constraints
1505
+ # each item must be in exactly one bin
1290
1506
  data[:items].each do |i|
1291
1507
  solver.add(solver.sum(data[:bins].map { |j| x[[i, j]] }) == 1)
1292
1508
  end
1293
1509
 
1510
+ # the amount packed in each bin cannot exceed its capacity
1294
1511
  data[:bins].each do |j|
1295
1512
  sum = solver.sum(data[:items].map { |i| x[[i, j]] * data[:weights][i] })
1296
1513
  solver.add(sum <= y[j] * data[:bin_capacity])
1297
1514
  end
1298
- ```
1299
-
1300
- Define the objective
1301
1515
 
1302
- ```ruby
1516
+ # objective: minimize the number of bins used
1303
1517
  solver.minimize(solver.sum(data[:bins].map { |j| y[j] }))
1304
- ```
1305
1518
 
1306
- Call the solver and print the solution
1307
-
1308
- ```ruby
1519
+ # call the solver and print the solution
1309
1520
  if status == :optimal
1310
1521
  num_bins = 0
1311
1522
  data[:bins].each do |j|
@@ -1339,27 +1550,20 @@ end
1339
1550
 
1340
1551
  [Guide](https://developers.google.com/optimization/flow/maxflow)
1341
1552
 
1342
- Define the data
1343
-
1344
1553
  ```ruby
1554
+ # define the data
1345
1555
  start_nodes = [0, 0, 0, 1, 1, 2, 2, 3, 3]
1346
1556
  end_nodes = [1, 2, 3, 2, 4, 3, 4, 2, 4]
1347
1557
  capacities = [20, 30, 10, 40, 30, 10, 20, 5, 20]
1348
- ```
1349
1558
 
1350
- Declare the solver and add the arcs
1351
-
1352
- ```ruby
1559
+ # declare the solver and add the arcs
1353
1560
  max_flow = ORTools::SimpleMaxFlow.new
1354
1561
 
1355
1562
  start_nodes.length.times do |i|
1356
1563
  max_flow.add_arc_with_capacity(start_nodes[i], end_nodes[i], capacities[i])
1357
1564
  end
1358
- ```
1359
-
1360
- Invoke the solver and display the results
1361
1565
 
1362
- ```ruby
1566
+ # invoke the solver and display the results
1363
1567
  if max_flow.solve(0, 4) == :optimal
1364
1568
  puts "Max flow: #{max_flow.optimal_flow}"
1365
1569
  puts
@@ -1383,19 +1587,15 @@ end
1383
1587
 
1384
1588
  [Guide](https://developers.google.com/optimization/flow/mincostflow)
1385
1589
 
1386
- Define the data
1387
-
1388
1590
  ```ruby
1591
+ # define the data
1389
1592
  start_nodes = [ 0, 0, 1, 1, 1, 2, 2, 3, 4]
1390
1593
  end_nodes = [ 1, 2, 2, 3, 4, 3, 4, 4, 2]
1391
1594
  capacities = [15, 8, 20, 4, 10, 15, 4, 20, 5]
1392
1595
  unit_costs = [ 4, 4, 2, 2, 6, 1, 3, 2, 3]
1393
1596
  supplies = [20, 0, 0, -5, -15]
1394
- ```
1395
-
1396
- Declare the solver and add the arcs
1397
1597
 
1398
- ```ruby
1598
+ # declare the solver and add the arcs
1399
1599
  min_cost_flow = ORTools::SimpleMinCostFlow.new
1400
1600
 
1401
1601
  start_nodes.length.times do |i|
@@ -1407,11 +1607,8 @@ end
1407
1607
  supplies.length.times do |i|
1408
1608
  min_cost_flow.set_node_supply(i, supplies[i])
1409
1609
  end
1410
- ```
1411
1610
 
1412
- Invoke the solver and display the results
1413
-
1414
- ```ruby
1611
+ # invoke the solver and display the results
1415
1612
  if min_cost_flow.solve == :optimal
1416
1613
  puts "Minimum cost #{min_cost_flow.optimal_cost}"
1417
1614
  puts
@@ -1431,74 +1628,15 @@ else
1431
1628
  end
1432
1629
  ```
1433
1630
 
1434
- ## Assignment
1435
-
1436
- [Guide](https://developers.google.com/optimization/assignment/simple_assignment)
1437
-
1438
- Create the data
1439
-
1440
- ```ruby
1441
- cost = [[ 90, 76, 75, 70],
1442
- [ 35, 85, 55, 65],
1443
- [125, 95, 90, 105],
1444
- [ 45, 110, 95, 115]]
1445
-
1446
- rows = cost.length
1447
- cols = cost[0].length
1448
- ```
1449
-
1450
- Create the solver
1451
-
1452
- ```ruby
1453
- assignment = ORTools::LinearSumAssignment.new
1454
- ```
1455
-
1456
- Add the costs to the solver
1457
-
1458
- ```ruby
1459
- rows.times do |worker|
1460
- cols.times do |task|
1461
- if cost[worker][task]
1462
- assignment.add_arc_with_cost(worker, task, cost[worker][task])
1463
- end
1464
- end
1465
- end
1466
- ```
1467
-
1468
- Invoke the solver
1469
-
1470
- ```ruby
1471
- solve_status = assignment.solve
1472
- if solve_status == :optimal
1473
- puts "Total cost = #{assignment.optimal_cost}"
1474
- puts
1475
- assignment.num_nodes.times do |i|
1476
- puts "Worker %d assigned to task %d. Cost = %d" % [
1477
- i,
1478
- assignment.right_mate(i),
1479
- assignment.assignment_cost(i)
1480
- ]
1481
- end
1482
- elsif solve_status == :infeasible
1483
- puts "No assignment is possible."
1484
- elsif solve_status == :possible_overflow
1485
- puts "Some input costs are too large and may cause an integer overflow."
1486
- end
1487
- ```
1488
-
1489
- ## Assignment as a Min Cost Problem
1631
+ ### Assignment as a Min Cost Flow Problem
1490
1632
 
1491
1633
  [Guide](https://developers.google.com/optimization/assignment/assignment_min_cost_flow)
1492
1634
 
1493
- Create the solver
1494
-
1495
1635
  ```ruby
1636
+ # create the solver
1496
1637
  min_cost_flow = ORTools::SimpleMinCostFlow.new
1497
- ```
1498
-
1499
- Create the data
1500
1638
 
1501
- ```ruby
1639
+ # create the data
1502
1640
  start_nodes = [0, 0, 0, 0] + [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4] + [5, 6, 7, 8]
1503
1641
  end_nodes = [1, 2, 3, 4] + [5, 6, 7, 8, 5, 6, 7, 8, 5, 6, 7, 8, 5, 6, 7, 8] + [9, 9, 9, 9]
1504
1642
  capacities = [1, 1, 1, 1] + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + [1, 1, 1, 1]
@@ -1507,11 +1645,8 @@ supplies = [4, 0, 0, 0, 0, 0, 0, 0, 0, -4]
1507
1645
  source = 0
1508
1646
  sink = 9
1509
1647
  tasks = 4
1510
- ```
1511
1648
 
1512
- Create the graph and constraints
1513
-
1514
- ```ruby
1649
+ # create the graph and constraints
1515
1650
  start_nodes.length.times do |i|
1516
1651
  min_cost_flow.add_arc_with_capacity_and_unit_cost(
1517
1652
  start_nodes[i], end_nodes[i], capacities[i], costs[i]
@@ -1521,11 +1656,8 @@ end
1521
1656
  supplies.length.times do |i|
1522
1657
  min_cost_flow.set_node_supply(i, supplies[i])
1523
1658
  end
1524
- ```
1525
-
1526
- Invoke the solver
1527
1659
 
1528
- ```ruby
1660
+ # invoke the solver
1529
1661
  if min_cost_flow.solve == :optimal
1530
1662
  puts "Total cost = #{min_cost_flow.optimal_cost}"
1531
1663
  puts
@@ -1545,109 +1677,20 @@ else
1545
1677
  end
1546
1678
  ```
1547
1679
 
1548
- ## Assignment as a MIP Problem
1549
-
1550
- [Guide](https://developers.google.com/optimization/assignment/assignment_mip)
1551
-
1552
- Create the solver
1553
-
1554
- ```ruby
1555
- solver = ORTools::Solver.new("SolveAssignmentProblemMIP", :cbc)
1556
- ```
1557
-
1558
- Create the data
1559
-
1560
- ```ruby
1561
- cost = [[90, 76, 75, 70],
1562
- [35, 85, 55, 65],
1563
- [125, 95, 90, 105],
1564
- [45, 110, 95, 115],
1565
- [60, 105, 80, 75],
1566
- [45, 65, 110, 95]]
1567
-
1568
- team1 = [0, 2, 4]
1569
- team2 = [1, 3, 5]
1570
- team_max = 2
1571
- ```
1572
-
1573
- Create the variables
1574
-
1575
- ```ruby
1576
- num_workers = cost.length
1577
- num_tasks = cost[1].length
1578
- x = {}
1579
-
1580
- num_workers.times do |i|
1581
- num_tasks.times do |j|
1582
- x[[i, j]] = solver.bool_var("x[#{i},#{j}]")
1583
- end
1584
- end
1585
- ```
1586
-
1587
- Create the objective function
1588
-
1589
- ```ruby
1590
- solver.minimize(solver.sum(
1591
- num_workers.times.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] * cost[i][j] } }
1592
- ))
1593
- ```
1594
-
1595
- Create the constraints
1596
-
1597
- ```ruby
1598
- num_workers.times do |i|
1599
- solver.add(solver.sum(num_tasks.times.map { |j| x[[i, j]] }) <= 1)
1600
- end
1601
-
1602
- num_tasks.times do |j|
1603
- solver.add(solver.sum(num_workers.times.map { |i| x[[i, j]] }) == 1)
1604
- end
1605
-
1606
- solver.add(solver.sum(team1.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] } }) <= team_max)
1607
- solver.add(solver.sum(team2.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] } }) <= team_max)
1608
- ```
1609
-
1610
- Invoke the solver
1611
-
1612
- ```ruby
1613
- sol = solver.solve
1614
-
1615
- puts "Total cost = #{solver.objective.value}"
1616
- puts
1617
- num_workers.times do |i|
1618
- num_tasks.times do |j|
1619
- if x[[i, j]].solution_value > 0
1620
- puts "Worker %d assigned to task %d. Cost = %d" % [
1621
- i,
1622
- j,
1623
- cost[i][j]
1624
- ]
1625
- end
1626
- end
1627
- end
1628
-
1629
- puts
1630
- puts "Time = #{solver.wall_time} milliseconds"
1631
- ```
1632
-
1633
- ## Employee Scheduling
1680
+ ### Employee Scheduling
1634
1681
 
1635
1682
  [Guide](https://developers.google.com/optimization/scheduling/employee_scheduling)
1636
1683
 
1637
- Define the data
1638
-
1639
1684
  ```ruby
1685
+ # define the data
1640
1686
  num_nurses = 4
1641
1687
  num_shifts = 3
1642
1688
  num_days = 3
1643
1689
  all_nurses = num_nurses.times.to_a
1644
1690
  all_shifts = num_shifts.times.to_a
1645
1691
  all_days = num_days.times.to_a
1646
- ```
1647
1692
 
1648
- Create the variables
1649
-
1650
- ```ruby
1693
+ # create the variables
1651
1694
  model = ORTools::CpModel.new
1652
1695
 
1653
1696
  shifts = {}
@@ -1658,11 +1701,8 @@ all_nurses.each do |n|
1658
1701
  end
1659
1702
  end
1660
1703
  end
1661
- ```
1662
-
1663
- Assign nurses to shifts
1664
1704
 
1665
- ```ruby
1705
+ # assign nurses to shifts
1666
1706
  all_days.each do |d|
1667
1707
  all_shifts.each do |s|
1668
1708
  model.add(model.sum(all_nurses.map { |n| shifts[[n, d, s]] }) == 1)
@@ -1674,11 +1714,8 @@ all_nurses.each do |n|
1674
1714
  model.add(model.sum(all_shifts.map { |s| shifts[[n, d, s]] }) <= 1)
1675
1715
  end
1676
1716
  end
1677
- ```
1678
1717
 
1679
- Assign shifts evenly
1680
-
1681
- ```ruby
1718
+ # assign shifts evenly
1682
1719
  min_shifts_per_nurse = (num_shifts * num_days) / num_nurses
1683
1720
  max_shifts_per_nurse = min_shifts_per_nurse + 1
1684
1721
  all_nurses.each do |n|
@@ -1686,11 +1723,8 @@ all_nurses.each do |n|
1686
1723
  model.add(num_shifts_worked >= min_shifts_per_nurse)
1687
1724
  model.add(num_shifts_worked <= max_shifts_per_nurse)
1688
1725
  end
1689
- ```
1690
-
1691
- Create a printer
1692
1726
 
1693
- ```ruby
1727
+ # create a printer
1694
1728
  class NursesPartialSolutionPrinter < ORTools::CpSolverSolutionCallback
1695
1729
  attr_reader :solution_count
1696
1730
 
@@ -1727,11 +1761,8 @@ class NursesPartialSolutionPrinter < ORTools::CpSolverSolutionCallback
1727
1761
  @solution_count += 1
1728
1762
  end
1729
1763
  end
1730
- ```
1731
1764
 
1732
- Call the solver and display the results
1733
-
1734
- ```ruby
1765
+ # call the solver and display the results
1735
1766
  solver = ORTools::CpSolver.new
1736
1767
  a_few_solutions = 5.times.to_a
1737
1768
  solution_printer = NursesPartialSolutionPrinter.new(
@@ -1747,7 +1778,110 @@ puts " - wall time : %f s" % solver.wall_time
1747
1778
  puts " - solutions found : %i" % solution_printer.solution_count
1748
1779
  ```
1749
1780
 
1750
- ## Sudoku
1781
+ ### The Job Shop Problem
1782
+
1783
+ [Guide](https://developers.google.com/optimization/scheduling/job_shop)
1784
+
1785
+ ```ruby
1786
+ # create the model
1787
+ model = ORTools::CpModel.new
1788
+
1789
+ jobs_data = [
1790
+ [[0, 3], [1, 2], [2, 2]],
1791
+ [[0, 2], [2, 1], [1, 4]],
1792
+ [[1, 4], [2, 3]]
1793
+ ]
1794
+
1795
+ machines_count = 1 + jobs_data.flat_map { |job| job.map { |task| task[0] } }.max
1796
+ all_machines = machines_count.times.to_a
1797
+
1798
+ # computes horizon dynamically as the sum of all durations
1799
+ horizon = jobs_data.flat_map { |job| job.map { |task| task[1] } }.sum
1800
+
1801
+ # creates job intervals and add to the corresponding machine lists
1802
+ all_tasks = {}
1803
+ machine_to_intervals = Hash.new { |hash, key| hash[key] = [] }
1804
+
1805
+ jobs_data.each_with_index do |job, job_id|
1806
+ job.each_with_index do |task, task_id|
1807
+ machine = task[0]
1808
+ duration = task[1]
1809
+ suffix = "_%i_%i" % [job_id, task_id]
1810
+ start_var = model.new_int_var(0, horizon, "start" + suffix)
1811
+ duration_var = model.new_int_var(duration, duration, "duration" + suffix)
1812
+ end_var = model.new_int_var(0, horizon, "end" + suffix)
1813
+ interval_var = model.new_interval_var(start_var, duration_var, end_var, "interval" + suffix)
1814
+ all_tasks[[job_id, task_id]] = {start: start_var, end: end_var, interval: interval_var}
1815
+ machine_to_intervals[machine] << interval_var
1816
+ end
1817
+ end
1818
+
1819
+ # create and add disjunctive constraints
1820
+ all_machines.each do |machine|
1821
+ model.add_no_overlap(machine_to_intervals[machine])
1822
+ end
1823
+
1824
+ # precedences inside a job
1825
+ jobs_data.each_with_index do |job, job_id|
1826
+ (job.size - 1).times do |task_id|
1827
+ model.add(all_tasks[[job_id, task_id + 1]][:start] >= all_tasks[[job_id, task_id]][:end])
1828
+ end
1829
+ end
1830
+
1831
+ # makespan objective
1832
+ obj_var = model.new_int_var(0, horizon, "makespan")
1833
+ model.add_max_equality(obj_var, jobs_data.map.with_index { |job, job_id| all_tasks[[job_id, job.size - 1]][:end] })
1834
+ model.minimize(obj_var)
1835
+
1836
+ # solve model
1837
+ solver = ORTools::CpSolver.new
1838
+ status = solver.solve(model)
1839
+
1840
+ # create one list of assigned tasks per machine
1841
+ assigned_jobs = Hash.new { |hash, key| hash[key] = [] }
1842
+ jobs_data.each_with_index do |job, job_id|
1843
+ job.each_with_index do |task, task_id|
1844
+ machine = task[0]
1845
+ assigned_jobs[machine] << {
1846
+ start: solver.value(all_tasks[[job_id, task_id]][:start]),
1847
+ job: job_id,
1848
+ index: task_id,
1849
+ duration: task[1]
1850
+ }
1851
+ end
1852
+ end
1853
+
1854
+ # create per machine output lines
1855
+ output = String.new("")
1856
+ all_machines.each do |machine|
1857
+ # sort by starting time
1858
+ assigned_jobs[machine].sort_by! { |v| v[:start] }
1859
+ sol_line_tasks = "Machine #{machine}: "
1860
+ sol_line = " "
1861
+
1862
+ assigned_jobs[machine].each do |assigned_task|
1863
+ name = "job_%i_%i" % [assigned_task[:job], assigned_task[:index]]
1864
+ # add spaces to output to align columns
1865
+ sol_line_tasks += "%-10s" % name
1866
+ start = assigned_task[:start]
1867
+ duration = assigned_task[:duration]
1868
+ sol_tmp = "[%i,%i]" % [start, start + duration]
1869
+ # add spaces to output to align columns
1870
+ sol_line += "%-10s" % sol_tmp
1871
+ end
1872
+
1873
+ sol_line += "\n"
1874
+ sol_line_tasks += "\n"
1875
+ output += sol_line_tasks
1876
+ output += sol_line
1877
+ end
1878
+
1879
+ # finally print the solution found
1880
+ puts "Optimal Schedule Length: %i" % solver.objective_value
1881
+ puts output
1882
+ ```
1883
+
1884
+ ### Sudoku
1751
1885
 
1752
1886
  [Example](https://github.com/google/or-tools/blob/stable/examples/python/sudoku_sat.py)
1753
1887
 
@@ -1814,14 +1948,173 @@ end
1814
1948
  # solve and print solution
1815
1949
  solver = ORTools::CpSolver.new
1816
1950
  status = solver.solve(model)
1817
- if status == :feasible
1951
+ if status == :optimal
1818
1952
  line.each do |i|
1819
1953
  p line.map { |j| solver.value(grid[[i, j]]) }
1820
1954
  end
1821
1955
  end
1822
1956
  ```
1823
1957
 
1824
- ## Set Partitioning
1958
+ ### Wedding Seating Chart
1959
+
1960
+ [Example](https://github.com/google/or-tools/blob/stable/examples/python/wedding_optimal_chart_sat.py)
1961
+
1962
+ ```ruby
1963
+ # From
1964
+ # Meghan L. Bellows and J. D. Luc Peterson
1965
+ # "Finding an optimal seating chart for a wedding"
1966
+ # https://www.improbable.com/news/2012/Optimal-seating-chart.pdf
1967
+ # https://www.improbable.com/2012/02/12/finding-an-optimal-seating-chart-for-a-wedding
1968
+ #
1969
+ # Every year, millions of brides (not to mention their mothers, future
1970
+ # mothers-in-law, and occasionally grooms) struggle with one of the
1971
+ # most daunting tasks during the wedding-planning process: the
1972
+ # seating chart. The guest responses are in, banquet hall is booked,
1973
+ # menu choices have been made. You think the hard parts are over,
1974
+ # but you have yet to embark upon the biggest headache of them all.
1975
+ # In order to make this process easier, we present a mathematical
1976
+ # formulation that models the seating chart problem. This model can
1977
+ # be solved to find the optimal arrangement of guests at tables.
1978
+ # At the very least, it can provide a starting point and hopefully
1979
+ # minimize stress and arguments.
1980
+ #
1981
+ # Adapted from
1982
+ # https://github.com/google/or-tools/blob/stable/examples/python/wedding_optimal_chart_sat.py
1983
+
1984
+ # Easy problem (from the paper)
1985
+ # num_tables = 2
1986
+ # table_capacity = 10
1987
+ # min_known_neighbors = 1
1988
+
1989
+ # Slightly harder problem (also from the paper)
1990
+ num_tables = 5
1991
+ table_capacity = 4
1992
+ min_known_neighbors = 1
1993
+
1994
+ # Connection matrix: who knows who, and how strong
1995
+ # is the relation
1996
+ c = [
1997
+ [1, 50, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
1998
+ [50, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
1999
+ [1, 1, 1, 50, 1, 1, 1, 1, 10, 0, 0, 0, 0, 0, 0, 0, 0],
2000
+ [1, 1, 50, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
2001
+ [1, 1, 1, 1, 1, 50, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
2002
+ [1, 1, 1, 1, 50, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
2003
+ [1, 1, 1, 1, 1, 1, 1, 50, 1, 0, 0, 0, 0, 0, 0, 0, 0],
2004
+ [1, 1, 1, 1, 1, 1, 50, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
2005
+ [1, 1, 10, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
2006
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 50, 1, 1, 1, 1, 1, 1],
2007
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 50, 1, 1, 1, 1, 1, 1, 1],
2008
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1],
2009
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1],
2010
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1],
2011
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1],
2012
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1],
2013
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1]
2014
+ ]
2015
+
2016
+ # Names of the guests. B: Bride side, G: Groom side
2017
+ names = [
2018
+ "Deb (B)", "John (B)", "Martha (B)", "Travis (B)", "Allan (B)",
2019
+ "Lois (B)", "Jayne (B)", "Brad (B)", "Abby (B)", "Mary Helen (G)",
2020
+ "Lee (G)", "Annika (G)", "Carl (G)", "Colin (G)", "Shirley (G)",
2021
+ "DeAnn (G)", "Lori (G)"
2022
+ ]
2023
+
2024
+ num_guests = c.size
2025
+
2026
+ all_tables = num_tables.times.to_a
2027
+ all_guests = num_guests.times.to_a
2028
+
2029
+ # create the cp model
2030
+ model = ORTools::CpModel.new
2031
+
2032
+ # decision variables
2033
+ seats = {}
2034
+ all_tables.each do |t|
2035
+ all_guests.each do |g|
2036
+ seats[[t, g]] = model.new_bool_var("guest %i seats on table %i" % [g, t])
2037
+ end
2038
+ end
2039
+
2040
+ pairs = all_guests.combination(2)
2041
+
2042
+ colocated = {}
2043
+ pairs.each do |g1, g2|
2044
+ colocated[[g1, g2]] = model.new_bool_var("guest %i seats with guest %i" % [g1, g2])
2045
+ end
2046
+
2047
+ same_table = {}
2048
+ pairs.each do |g1, g2|
2049
+ all_tables.each do |t|
2050
+ same_table[[g1, g2, t]] = model.new_bool_var("guest %i seats with guest %i on table %i" % [g1, g2, t])
2051
+ end
2052
+ end
2053
+
2054
+ # Objective
2055
+ model.maximize(model.sum((num_guests - 1).times.flat_map { |g1| (g1 + 1).upto(num_guests - 1).select { |g2| c[g1][g2] > 0 }.map { |g2| colocated[[g1, g2]] * c[g1][g2] } }))
2056
+
2057
+ #
2058
+ # Constraints
2059
+ #
2060
+
2061
+ # Everybody seats at one table.
2062
+ all_guests.each do |g|
2063
+ model.add(model.sum(all_tables.map { |t| seats[[t, g]] }) == 1)
2064
+ end
2065
+
2066
+ # Tables have a max capacity.
2067
+ all_tables.each do |t|
2068
+ model.add(model.sum(all_guests.map { |g| seats[[t, g]] }) <= table_capacity)
2069
+ end
2070
+
2071
+ # Link colocated with seats
2072
+ pairs.each do |g1, g2|
2073
+ all_tables.each do |t|
2074
+ # Link same_table and seats.
2075
+ model.add_bool_or([seats[[t, g1]].not, seats[[t, g2]].not, same_table[[g1, g2, t]]])
2076
+ model.add_implication(same_table[[g1, g2, t]], seats[[t, g1]])
2077
+ model.add_implication(same_table[[g1, g2, t]], seats[[t, g2]])
2078
+ end
2079
+
2080
+ # Link colocated and same_table.
2081
+ model.add(model.sum(all_tables.map { |t| same_table[[g1, g2, t]] }) == colocated[[g1, g2]])
2082
+ end
2083
+
2084
+ # Min known neighbors rule.
2085
+ all_guests.each do |g|
2086
+ model.add(
2087
+ model.sum(
2088
+ (g + 1).upto(num_guests - 1).
2089
+ select { |g2| c[g][g2] > 0 }.
2090
+ product(all_tables).
2091
+ map { |g2, t| same_table[[g, g2, t]] }
2092
+ ) +
2093
+ model.sum(
2094
+ g.times.
2095
+ select { |g1| c[g1][g] > 0 }.
2096
+ product(all_tables).
2097
+ map { |g1, t| same_table[[g1, g, t]] }
2098
+ ) >= min_known_neighbors
2099
+ )
2100
+ end
2101
+
2102
+ # Symmetry breaking. First guest seats on the first table.
2103
+ model.add(seats[[0, 0]] == 1)
2104
+
2105
+ # Solve model
2106
+ solver = ORTools::CpSolver.new
2107
+ solution_printer = WeddingChartPrinter.new(seats, names, num_tables, num_guests)
2108
+ solver.solve_with_solution_callback(model, solution_printer)
2109
+
2110
+ puts "Statistics"
2111
+ puts " - conflicts : %i" % solver.num_conflicts
2112
+ puts " - branches : %i" % solver.num_branches
2113
+ puts " - wall time : %f s" % solver.wall_time
2114
+ puts " - num solutions: %i" % solution_printer.num_solutions
2115
+ ```
2116
+
2117
+ ### Set Partitioning
1825
2118
 
1826
2119
  [Example](https://pythonhosted.org/PuLP/CaseStudies/a_set_partitioning_problem.html)
1827
2120
 
@@ -1893,7 +2186,7 @@ To get started with development:
1893
2186
  git clone https://github.com/ankane/or-tools.git
1894
2187
  cd or-tools
1895
2188
  bundle install
1896
- bundle exec rake compile -- --with-or-tools-dir=/path/to/or-tools
2189
+ bundle exec rake compile
1897
2190
  bundle exec rake test
1898
2191
  ```
1899
2192