or-tools 0.1.2 → 0.3.0

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: 3d0010ac9fa141621f67cee4eb00ba077a5bafcec58c65202d477c1576ba797e
4
- data.tar.gz: '0328a9b9f0fa296df875e0756b40dc16fdbf756725d5ecc4e3bc23c600245974'
3
+ metadata.gz: 18b9ab384a5aa4b8d27a3415de9ba751085e63b95cbdc431d1fee206b05e1c37
4
+ data.tar.gz: 4b8d29c3f6b5fff8181a817737a818e2c3d7cf6cd0184d82078d4606c2c94740
5
5
  SHA512:
6
- metadata.gz: dd66a479521af3f036ed14f639f957cd6b608efed729a4e7ed1eb5fde749fd779ec471750b93ab5d48e0162116e9865c0763355fd72b610a0b74cf6747dad220
7
- data.tar.gz: 41af8a12b7aa110baae1f62029c4bde6e291ea1b870f9435ba3463a1a9e52d41dc069ef049671a95802d3942e29402f7096709c6a8fd659382a372923e03002a
6
+ metadata.gz: 26e5b155bdd5c0d37aaf13fe26c674ac15ac693933999056a477e340823ced7e86b8565c00c941d4ecdfd35e0b5a4716465e2418bf1e44ca7c1586d171e77515
7
+ data.tar.gz: 1c87b6df5082d9ab61b1ab63d154e563fdd9405fa3dda5fd00df2f6aafc65c7f5f8d251938cab9cba58de04efb7f38b11893701c128833059b8290b3a3dfb0da
@@ -1,3 +1,28 @@
1
+ ## 0.3.0 (2020-07-21)
2
+
3
+ - Updated OR-Tools to 7.7
4
+ - Added `BasicScheduler` class
5
+ - Added `Seating` class
6
+ - Added `TSP` class
7
+ - Added `Sudoku` class
8
+
9
+ ## 0.2.0 (2020-05-22)
10
+
11
+ - No longer need to download the OR-Tools C++ library separately on Mac, Ubuntu 18.04, Ubuntu 16.04, Debian 10, and CentOS 8
12
+
13
+ ## 0.1.5 (2020-04-23)
14
+
15
+ - Added support for OR-Tools 7.6
16
+
17
+ ## 0.1.4 (2020-04-19)
18
+
19
+ - Added support for the Job Shop Problem
20
+
21
+ ## 0.1.3 (2020-03-24)
22
+
23
+ - Added support for more routing problems
24
+ - Added `add_all_different` to `CpModel`
25
+
1
26
  ## 0.1.2 (2020-02-18)
2
27
 
3
28
  - Added support for scheduling
data/README.md CHANGED
@@ -6,37 +6,282 @@
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)
30
267
  - [Solving an Optimization Problem](#solving-an-optimization-problem)
268
+ - [Cryptarithmetic](#cryptarithmetic)
269
+ - [The N-queens Problem](#the-n-queens-problem)
31
270
 
32
- Integer Optimization
271
+ Assignment
33
272
 
34
- - [Mixed-Integer Programming](#mixed-integer-programming)
273
+ - [Assignment](#assignment)
274
+ - [Assignment with Teams](#assignment-with-teams)
35
275
 
36
276
  Routing
37
277
 
38
- - [Traveling Salesperson Problem (TSP)](#traveling-salesperson-problem-tsp)
278
+ - [Traveling Salesperson Problem (TSP)](#traveling-salesperson-problem-tsp-1)
39
279
  - [Vehicle Routing Problem (VRP)](#vehicle-routing-problem-vrp)
280
+ - [Capacity Constraints](#capacity-constraints)
281
+ - [Pickups and Deliveries](#pickups-and-deliveries)
282
+ - [Time Window Constraints](#time-window-constraints)
283
+ - [Resource Constraints](#resource-constraints)
284
+ - [Penalties and Dropping Visits](#penalties-and-dropping-visits)
40
285
  - [Routing Options](#routing-options)
41
286
 
42
287
  Bin Packing
@@ -49,37 +294,32 @@ Network Flows
49
294
 
50
295
  - [Maximum Flows](#maximum-flows)
51
296
  - [Minimum Cost Flows](#minimum-cost-flows)
52
-
53
- Assignment
54
-
55
- - [Assignment](#assignment)
56
- - [Assignment as a Min Cost Problem](#assignment-as-a-min-cost-problem)
57
- - [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)
58
298
 
59
299
  Scheduling
60
300
 
61
301
  - [Employee Scheduling](#employee-scheduling)
302
+ - [The Job Shop Problem](#the-job-shop-problem)
303
+
304
+ Other Examples
305
+
306
+ - [Sudoku](#sudoku-1)
307
+ - [Wedding Seating Chart](#wedding-seating-chart)
308
+ - [Set Partitioning](#set-partitioning)
62
309
 
63
310
  ### The Glop Linear Solver
64
311
 
65
312
  [Guide](https://developers.google.com/optimization/lp/glop)
66
313
 
67
- Declare the solver
68
-
69
314
  ```ruby
315
+ # declare the solver
70
316
  solver = ORTools::Solver.new("LinearProgrammingExample", :glop)
71
- ```
72
-
73
- Create the variables
74
317
 
75
- ```ruby
318
+ # create the variables
76
319
  x = solver.num_var(0, solver.infinity, "x")
77
320
  y = solver.num_var(0, solver.infinity, "y")
78
- ```
79
321
 
80
- Define the constraints
81
-
82
- ```ruby
322
+ # define the constraints
83
323
  constraint0 = solver.constraint(-solver.infinity, 14)
84
324
  constraint0.set_coefficient(x, 1)
85
325
  constraint0.set_coefficient(y, 2)
@@ -91,26 +331,17 @@ constraint1.set_coefficient(y, -1)
91
331
  constraint2 = solver.constraint(-solver.infinity, 2)
92
332
  constraint2.set_coefficient(x, 1)
93
333
  constraint2.set_coefficient(y, -1)
94
- ```
95
-
96
- Define the objective function
97
334
 
98
- ```ruby
335
+ # define the objective function
99
336
  objective = solver.objective
100
337
  objective.set_coefficient(x, 3)
101
338
  objective.set_coefficient(y, 4)
102
339
  objective.set_maximization
103
- ```
104
340
 
105
- Invoke the solver
106
-
107
- ```ruby
341
+ # invoke the solver
108
342
  solver.solve
109
- ```
110
-
111
- Display the solution
112
343
 
113
- ```ruby
344
+ # display the solution
114
345
  opt_solution = 3 * x.solution_value + 4 * y.solution_value
115
346
  puts "Number of variables = #{solver.num_variables}"
116
347
  puts "Number of constraints = #{solver.num_constraints}"
@@ -120,42 +351,75 @@ puts "y = #{y.solution_value}"
120
351
  puts "Optimal objective value = #{opt_solution}"
121
352
  ```
122
353
 
123
- ### CP-SAT Solver
124
-
125
- [Guide](https://developers.google.com/optimization/cp/cp_solver)
354
+ ### Mixed-Integer Programming
126
355
 
127
- Declare the model
356
+ [Guide](https://developers.google.com/optimization/mip/integer_opt)
128
357
 
129
358
  ```ruby
130
- 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
131
398
  ```
132
399
 
133
- Create the variables
400
+ ### CP-SAT Solver
401
+
402
+ [Guide](https://developers.google.com/optimization/cp/cp_solver)
134
403
 
135
404
  ```ruby
405
+ # declare the model
406
+ model = ORTools::CpModel.new
407
+
408
+ # create the variables
136
409
  num_vals = 3
137
410
  x = model.new_int_var(0, num_vals - 1, "x")
138
411
  y = model.new_int_var(0, num_vals - 1, "y")
139
412
  z = model.new_int_var(0, num_vals - 1, "z")
140
- ```
141
413
 
142
- Create the constraint
143
-
144
- ```ruby
414
+ # create the constraint
145
415
  model.add(x != y)
146
- ```
147
-
148
- Call the solver
149
416
 
150
- ```ruby
417
+ # call the solver
151
418
  solver = ORTools::CpSolver.new
152
419
  status = solver.solve(model)
153
- ```
154
420
 
155
- Display the first solution
156
-
157
- ```ruby
158
- if status == :feasible
421
+ # display the first solution
422
+ if status == :optimal
159
423
  puts "x = #{solver.value(x)}"
160
424
  puts "y = #{solver.value(y)}"
161
425
  puts "z = #{solver.value(z)}"
@@ -166,38 +430,25 @@ end
166
430
 
167
431
  [Guide](https://developers.google.com/optimization/cp/integer_opt_cp)
168
432
 
169
- Declare the model
170
-
171
433
  ```ruby
434
+ # declare the model
172
435
  model = ORTools::CpModel.new
173
- ```
174
-
175
- Create the variables
176
436
 
177
- ```ruby
437
+ # create the variables
178
438
  var_upper_bound = [50, 45, 37].max
179
439
  x = model.new_int_var(0, var_upper_bound, "x")
180
440
  y = model.new_int_var(0, var_upper_bound, "y")
181
441
  z = model.new_int_var(0, var_upper_bound, "z")
182
- ```
183
442
 
184
- Define the constraints
185
-
186
- ```ruby
443
+ # define the constraints
187
444
  model.add(x*2 + y*7 + z*3 <= 50)
188
445
  model.add(x*3 - y*5 + z*7 <= 45)
189
446
  model.add(x*5 + y*2 - z*6 <= 37)
190
- ```
191
-
192
- Define the objective function
193
447
 
194
- ```ruby
448
+ # define the objective function
195
449
  model.maximize(x*2 + y*2 + z*3)
196
- ```
197
450
 
198
- Call the solver
199
-
200
- ```ruby
451
+ # call the solver
201
452
  solver = ORTools::CpSolver.new
202
453
  status = solver.solve(model)
203
454
 
@@ -210,75 +461,244 @@ if status == :optimal
210
461
  end
211
462
  ```
212
463
 
213
- ### Mixed-Integer Programming
214
-
215
- [Guide](https://developers.google.com/optimization/mip/integer_opt)
464
+ ### Cryptarithmetic
216
465
 
217
- Declare the MIP solver
466
+ [Guide](https://developers.google.com/optimization/cp/cryptarithmetic)
218
467
 
219
468
  ```ruby
220
- solver = ORTools::Solver.new("simple_mip_program", :cbc)
221
- ```
222
-
223
- Define the variables
469
+ # define the variables
470
+ model = ORTools::CpModel.new
224
471
 
225
- ```ruby
226
- infinity = solver.infinity
227
- x = solver.int_var(0.0, infinity, "x")
228
- y = solver.int_var(0.0, infinity, "y")
472
+ base = 10
229
473
 
230
- puts "Number of variables = #{solver.num_variables}"
231
- ```
474
+ c = model.new_int_var(1, base - 1, "C")
475
+ p = model.new_int_var(0, base - 1, "P")
476
+ i = model.new_int_var(1, base - 1, "I")
477
+ s = model.new_int_var(0, base - 1, "S")
478
+ f = model.new_int_var(1, base - 1, "F")
479
+ u = model.new_int_var(0, base - 1, "U")
480
+ n = model.new_int_var(0, base - 1, "N")
481
+ t = model.new_int_var(1, base - 1, "T")
482
+ r = model.new_int_var(0, base - 1, "R")
483
+ e = model.new_int_var(0, base - 1, "E")
232
484
 
233
- Define the constraints
485
+ letters = [c, p, i, s, f, u, n, t, r, e]
234
486
 
235
- ```ruby
236
- c0 = solver.constraint(-infinity, 17.5)
237
- c0.set_coefficient(x, 1)
238
- c0.set_coefficient(y, 7)
487
+ # define the constraints
488
+ model.add_all_different(letters)
239
489
 
240
- c1 = solver.constraint(-infinity, 3.5)
241
- c1.set_coefficient(x, 1);
242
- c1.set_coefficient(y, 0);
490
+ model.add(c * base + p + i * base + s + f * base * base + u * base +
491
+ n == t * base * base * base + r * base * base + u * base + e)
243
492
 
244
- puts "Number of constraints = #{solver.num_constraints}"
245
- ```
493
+ # define the solution printer
494
+ class VarArraySolutionPrinter < ORTools::CpSolverSolutionCallback
495
+ attr_reader :solution_count
246
496
 
247
- Define the objective
497
+ def initialize(variables)
498
+ super()
499
+ @variables = variables
500
+ @solution_count = 0
501
+ end
248
502
 
249
- ```ruby
250
- objective = solver.objective
251
- objective.set_coefficient(x, 1)
252
- objective.set_coefficient(y, 10)
253
- objective.set_maximization
254
- ```
503
+ def on_solution_callback
504
+ @solution_count += 1
505
+ @variables.each do |v|
506
+ print "%s=%i " % [v.name, value(v)]
507
+ end
508
+ puts
509
+ end
510
+ end
255
511
 
256
- Call the solver
512
+ # invoke the solver
513
+ solver = ORTools::CpSolver.new
514
+ solution_printer = VarArraySolutionPrinter.new(letters)
515
+ status = solver.search_for_all_solutions(model, solution_printer)
257
516
 
258
- ```ruby
259
- status = solver.solve
517
+ puts
518
+ puts "Statistics"
519
+ puts " - status : %s" % status
520
+ puts " - conflicts : %i" % solver.num_conflicts
521
+ puts " - branches : %i" % solver.num_branches
522
+ puts " - wall time : %f s" % solver.wall_time
523
+ puts " - solutions found : %i" % solution_printer.solution_count
260
524
  ```
261
525
 
262
- Display the solution
526
+ ### The N-queens Problem
527
+
528
+ [Guide](https://developers.google.com/optimization/cp/queens)
263
529
 
264
530
  ```ruby
265
- if status == :optimal
266
- puts "Solution:"
267
- puts "Objective value = #{solver.objective.value}"
268
- puts "x = #{x.solution_value}"
269
- puts "y = #{y.solution_value}"
270
- else
271
- puts "The problem does not have an optimal solution."
272
- end
273
- ```
531
+ # declare the model
532
+ board_size = 8
533
+ model = ORTools::CpModel.new
534
+
535
+ # create the variables
536
+ queens = board_size.times.map { |i| model.new_int_var(0, board_size - 1, "x%i" % i) }
537
+
538
+ # create the constraints
539
+ board_size.times do |i|
540
+ diag1 = []
541
+ diag2 = []
542
+ board_size.times do |j|
543
+ q1 = model.new_int_var(0, 2 * board_size, "diag1_%i" % i)
544
+ diag1 << q1
545
+ model.add(q1 == queens[j] + j)
546
+ q2 = model.new_int_var(-board_size, board_size, "diag2_%i" % i)
547
+ diag2 << q2
548
+ model.add(q2 == queens[j] - j)
549
+ end
550
+ model.add_all_different(diag1)
551
+ model.add_all_different(diag2)
552
+ end
553
+
554
+ # create a solution printer
555
+ class SolutionPrinter < ORTools::CpSolverSolutionCallback
556
+ attr_reader :solution_count
557
+
558
+ def initialize(variables)
559
+ super()
560
+ @variables = variables
561
+ @solution_count = 0
562
+ end
563
+
564
+ def on_solution_callback
565
+ @solution_count += 1
566
+ @variables.each do |v|
567
+ print "%s = %i " % [v.name, value(v)]
568
+ end
569
+ puts
570
+ end
571
+ end
572
+
573
+ # call the solver and display the results
574
+ solver = ORTools::CpSolver.new
575
+ solution_printer = SolutionPrinter.new(queens)
576
+ status = solver.search_for_all_solutions(model, solution_printer)
577
+ puts
578
+ puts "Solutions found : %i" % solution_printer.solution_count
579
+ ```
580
+
581
+
582
+ ### Assignment
583
+
584
+ [Guide](https://developers.google.com/optimization/assignment/assignment_example)
585
+
586
+ ```ruby
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
595
+
596
+ # create the solver
597
+ assignment = ORTools::LinearSumAssignment.new
598
+
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
607
+
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
625
+ ```
626
+
627
+ ### Assignment with Teams
628
+
629
+ [Guide](https://developers.google.com/optimization/assignment/assignment_teams)
630
+
631
+ ```ruby
632
+ # create the solver
633
+ solver = ORTools::Solver.new("SolveAssignmentProblemMIP", :cbc)
634
+
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]]
642
+
643
+ team1 = [0, 2, 4]
644
+ team2 = [1, 3, 5]
645
+ team_max = 2
646
+
647
+ # create the variables
648
+ num_workers = cost.length
649
+ num_tasks = cost[1].length
650
+ x = {}
651
+
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
657
+
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
+ ))
662
+
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
667
+
668
+ num_tasks.times do |j|
669
+ solver.add(solver.sum(num_workers.times.map { |i| x[[i, j]] }) == 1)
670
+ end
671
+
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
690
+ end
691
+
692
+ puts
693
+ puts "Time = #{solver.wall_time} milliseconds"
694
+ ```
274
695
 
275
696
  ### Traveling Salesperson Problem (TSP)
276
697
 
277
698
  [Guide](https://developers.google.com/optimization/routing/tsp.html)
278
699
 
279
- Create the data
280
-
281
700
  ```ruby
701
+ # create the data
282
702
  data = {}
283
703
  data[:distance_matrix] = [
284
704
  [0, 2451, 713, 1018, 1631, 1374, 2408, 213, 2571, 875, 1420, 2145, 1972],
@@ -297,11 +717,8 @@ data[:distance_matrix] = [
297
717
  ]
298
718
  data[:num_vehicles] = 1
299
719
  data[:depot] = 0
300
- ```
301
-
302
- Create the distance callback
303
720
 
304
- ```ruby
721
+ # create the distance callback
305
722
  manager = ORTools::RoutingIndexManager.new(data[:distance_matrix].length, data[:num_vehicles], data[:depot])
306
723
  routing = ORTools::RoutingModel.new(manager)
307
724
 
@@ -313,17 +730,11 @@ end
313
730
 
314
731
  transit_callback_index = routing.register_transit_callback(distance_callback)
315
732
  routing.set_arc_cost_evaluator_of_all_vehicles(transit_callback_index)
316
- ```
317
-
318
- Run the solver
319
-
320
- ```ruby
321
- assignment = routing.solve(first_solution_strategy: :path_cheaper_arc)
322
- ```
323
733
 
324
- Print the solution
734
+ # run the solver
735
+ assignment = routing.solve(first_solution_strategy: :path_cheapest_arc)
325
736
 
326
- ```ruby
737
+ # print the solution
327
738
  puts "Objective: #{assignment.objective_value} miles"
328
739
  index = routing.start(0)
329
740
  plan_output = String.new("Route for vehicle 0:\n")
@@ -342,9 +753,8 @@ puts plan_output
342
753
 
343
754
  [Guide](https://developers.google.com/optimization/routing/vrp)
344
755
 
345
- Create the data
346
-
347
756
  ```ruby
757
+ # create the data
348
758
  data = {}
349
759
  data[:distance_matrix] = [
350
760
  [0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662],
@@ -367,11 +777,8 @@ data[:distance_matrix] = [
367
777
  ]
368
778
  data[:num_vehicles] = 4
369
779
  data[:depot] = 0
370
- ```
371
-
372
- Define the distance callback
373
780
 
374
- ```ruby
781
+ # define the distance callback
375
782
  manager = ORTools::RoutingIndexManager.new(data[:distance_matrix].length, data[:num_vehicles], data[:depot])
376
783
  routing = ORTools::RoutingModel.new(manager)
377
784
 
@@ -383,27 +790,193 @@ end
383
790
 
384
791
  transit_callback_index = routing.register_transit_callback(distance_callback)
385
792
  routing.set_arc_cost_evaluator_of_all_vehicles(transit_callback_index)
386
- ```
387
793
 
388
- Add a distance dimension
389
-
390
- ```ruby
794
+ # add a distance dimension
391
795
  dimension_name = "Distance"
392
796
  routing.add_dimension(transit_callback_index, 0, 3000, true, dimension_name)
393
797
  distance_dimension = routing.mutable_dimension(dimension_name)
394
798
  distance_dimension.global_span_cost_coefficient = 100
799
+
800
+ # run the solver
801
+ solution = routing.solve(first_solution_strategy: :path_cheapest_arc)
802
+
803
+ # print the solution
804
+ max_route_distance = 0
805
+ data[:num_vehicles].times do |vehicle_id|
806
+ index = routing.start(vehicle_id)
807
+ plan_output = String.new("Route for vehicle #{vehicle_id}:\n")
808
+ route_distance = 0
809
+ while !routing.end?(index)
810
+ plan_output += " #{manager.index_to_node(index)} -> "
811
+ previous_index = index
812
+ index = solution.value(routing.next_var(index))
813
+ route_distance += routing.arc_cost_for_vehicle(previous_index, index, vehicle_id)
814
+ end
815
+ plan_output += "#{manager.index_to_node(index)}\n"
816
+ plan_output += "Distance of the route: #{route_distance}m\n\n"
817
+ puts plan_output
818
+ max_route_distance = [route_distance, max_route_distance].max
819
+ end
820
+ puts "Maximum of the route distances: #{max_route_distance}m"
395
821
  ```
396
822
 
397
- Run the solver
823
+ ### Capacity Constraints
824
+
825
+ [Guide](https://developers.google.com/optimization/routing/cvrp)
398
826
 
399
827
  ```ruby
828
+ data = {}
829
+ data[:distance_matrix] = [
830
+ [0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662],
831
+ [548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210],
832
+ [776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754],
833
+ [696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358],
834
+ [582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244],
835
+ [274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708],
836
+ [502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480],
837
+ [194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856],
838
+ [308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514],
839
+ [194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468],
840
+ [536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354],
841
+ [502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844],
842
+ [388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730],
843
+ [354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536],
844
+ [468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194],
845
+ [776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798],
846
+ [662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0]
847
+ ]
848
+ data[:demands] = [0, 1, 1, 2, 4, 2, 4, 8, 8, 1, 2, 1, 2, 4, 4, 8, 8]
849
+ data[:vehicle_capacities] = [15, 15, 15, 15]
850
+ data[:num_vehicles] = 4
851
+ data[:depot] = 0
852
+
853
+ manager = ORTools::RoutingIndexManager.new(data[:distance_matrix].size, data[:num_vehicles], data[:depot])
854
+ routing = ORTools::RoutingModel.new(manager)
855
+
856
+ distance_callback = lambda do |from_index, to_index|
857
+ from_node = manager.index_to_node(from_index)
858
+ to_node = manager.index_to_node(to_index)
859
+ data[:distance_matrix][from_node][to_node]
860
+ end
861
+
862
+ transit_callback_index = routing.register_transit_callback(distance_callback)
863
+
864
+ routing.set_arc_cost_evaluator_of_all_vehicles(transit_callback_index)
865
+
866
+ demand_callback = lambda do |from_index|
867
+ from_node = manager.index_to_node(from_index)
868
+ data[:demands][from_node]
869
+ end
870
+
871
+ demand_callback_index = routing.register_unary_transit_callback(demand_callback)
872
+ routing.add_dimension_with_vehicle_capacity(
873
+ demand_callback_index,
874
+ 0, # null capacity slack
875
+ data[:vehicle_capacities], # vehicle maximum capacities
876
+ true, # start cumul to zero
877
+ "Capacity"
878
+ )
879
+
400
880
  solution = routing.solve(first_solution_strategy: :path_cheapest_arc)
881
+
882
+ total_distance = 0
883
+ total_load = 0
884
+ data[:num_vehicles].times do |vehicle_id|
885
+ index = routing.start(vehicle_id)
886
+ plan_output = String.new("Route for vehicle #{vehicle_id}:\n")
887
+ route_distance = 0
888
+ route_load = 0
889
+ while !routing.end?(index)
890
+ node_index = manager.index_to_node(index)
891
+ route_load += data[:demands][node_index]
892
+ plan_output += " #{node_index} Load(#{route_load}) -> "
893
+ previous_index = index
894
+ index = solution.value(routing.next_var(index))
895
+ route_distance += routing.arc_cost_for_vehicle(previous_index, index, vehicle_id)
896
+ end
897
+ plan_output += " #{manager.index_to_node(index)} Load(#{route_load})\n"
898
+ plan_output += "Distance of the route: #{route_distance}m\n"
899
+ plan_output += "Load of the route: #{route_load}\n\n"
900
+ puts plan_output
901
+ total_distance += route_distance
902
+ total_load += route_load
903
+ end
904
+ puts "Total distance of all routes: #{total_distance}m"
905
+ puts "Total load of all routes: #{total_load}"
401
906
  ```
402
907
 
403
- Print the solution
908
+ ### Pickups and Deliveries
909
+
910
+ [Guide](https://developers.google.com/optimization/routing/pickup_delivery)
404
911
 
405
912
  ```ruby
406
- max_route_distance = 0
913
+ data = {}
914
+ data[:distance_matrix] = [
915
+ [0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662],
916
+ [548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210],
917
+ [776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754],
918
+ [696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358],
919
+ [582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244],
920
+ [274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708],
921
+ [502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480],
922
+ [194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856],
923
+ [308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514],
924
+ [194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468],
925
+ [536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354],
926
+ [502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844],
927
+ [388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730],
928
+ [354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536],
929
+ [468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194],
930
+ [776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798],
931
+ [662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0]
932
+ ]
933
+ data[:pickups_deliveries] = [
934
+ [1, 6],
935
+ [2, 10],
936
+ [4, 3],
937
+ [5, 9],
938
+ [7, 8],
939
+ [15, 11],
940
+ [13, 12],
941
+ [16, 14],
942
+ ]
943
+ data[:num_vehicles] = 4
944
+ data[:depot] = 0
945
+
946
+ manager = ORTools::RoutingIndexManager.new(data[:distance_matrix].size, data[:num_vehicles], data[:depot])
947
+ routing = ORTools::RoutingModel.new(manager)
948
+
949
+ distance_callback = lambda do |from_index, to_index|
950
+ from_node = manager.index_to_node(from_index)
951
+ to_node = manager.index_to_node(to_index)
952
+ data[:distance_matrix][from_node][to_node]
953
+ end
954
+
955
+ transit_callback_index = routing.register_transit_callback(distance_callback)
956
+ routing.set_arc_cost_evaluator_of_all_vehicles(transit_callback_index)
957
+
958
+ dimension_name = "Distance"
959
+ routing.add_dimension(
960
+ transit_callback_index,
961
+ 0, # no slack
962
+ 3000, # vehicle maximum travel distance
963
+ true, # start cumul to zero
964
+ dimension_name
965
+ )
966
+ distance_dimension = routing.mutable_dimension(dimension_name)
967
+ distance_dimension.global_span_cost_coefficient = 100
968
+
969
+ data[:pickups_deliveries].each do |request|
970
+ pickup_index = manager.node_to_index(request[0])
971
+ delivery_index = manager.node_to_index(request[1])
972
+ routing.add_pickup_and_delivery(pickup_index, delivery_index)
973
+ routing.solver.add(routing.vehicle_var(pickup_index) == routing.vehicle_var(delivery_index))
974
+ routing.solver.add(distance_dimension.cumul_var(pickup_index) <= distance_dimension.cumul_var(delivery_index))
975
+ end
976
+
977
+ solution = routing.solve(first_solution_strategy: :parallel_cheapest_insertion)
978
+
979
+ total_distance = 0
407
980
  data[:num_vehicles].times do |vehicle_id|
408
981
  index = routing.start(vehicle_id)
409
982
  plan_output = String.new("Route for vehicle #{vehicle_id}:\n")
@@ -417,9 +990,341 @@ data[:num_vehicles].times do |vehicle_id|
417
990
  plan_output += "#{manager.index_to_node(index)}\n"
418
991
  plan_output += "Distance of the route: #{route_distance}m\n\n"
419
992
  puts plan_output
420
- max_route_distance = [route_distance, max_route_distance].max
993
+ total_distance += route_distance
421
994
  end
422
- puts "Maximum of the route distances: #{max_route_distance}m"
995
+ puts "Total Distance of all routes: #{total_distance}m"
996
+ ```
997
+
998
+ ### Time Window Constraints
999
+
1000
+ [Guide](https://developers.google.com/optimization/routing/vrptw)
1001
+
1002
+ ```ruby
1003
+ data = {}
1004
+ data[:time_matrix] = [
1005
+ [0, 6, 9, 8, 7, 3, 6, 2, 3, 2, 6, 6, 4, 4, 5, 9, 7],
1006
+ [6, 0, 8, 3, 2, 6, 8, 4, 8, 8, 13, 7, 5, 8, 12, 10, 14],
1007
+ [9, 8, 0, 11, 10, 6, 3, 9, 5, 8, 4, 15, 14, 13, 9, 18, 9],
1008
+ [8, 3, 11, 0, 1, 7, 10, 6, 10, 10, 14, 6, 7, 9, 14, 6, 16],
1009
+ [7, 2, 10, 1, 0, 6, 9, 4, 8, 9, 13, 4, 6, 8, 12, 8, 14],
1010
+ [3, 6, 6, 7, 6, 0, 2, 3, 2, 2, 7, 9, 7, 7, 6, 12, 8],
1011
+ [6, 8, 3, 10, 9, 2, 0, 6, 2, 5, 4, 12, 10, 10, 6, 15, 5],
1012
+ [2, 4, 9, 6, 4, 3, 6, 0, 4, 4, 8, 5, 4, 3, 7, 8, 10],
1013
+ [3, 8, 5, 10, 8, 2, 2, 4, 0, 3, 4, 9, 8, 7, 3, 13, 6],
1014
+ [2, 8, 8, 10, 9, 2, 5, 4, 3, 0, 4, 6, 5, 4, 3, 9, 5],
1015
+ [6, 13, 4, 14, 13, 7, 4, 8, 4, 4, 0, 10, 9, 8, 4, 13, 4],
1016
+ [6, 7, 15, 6, 4, 9, 12, 5, 9, 6, 10, 0, 1, 3, 7, 3, 10],
1017
+ [4, 5, 14, 7, 6, 7, 10, 4, 8, 5, 9, 1, 0, 2, 6, 4, 8],
1018
+ [4, 8, 13, 9, 8, 7, 10, 3, 7, 4, 8, 3, 2, 0, 4, 5, 6],
1019
+ [5, 12, 9, 14, 12, 6, 6, 7, 3, 3, 4, 7, 6, 4, 0, 9, 2],
1020
+ [9, 10, 18, 6, 8, 12, 15, 8, 13, 9, 13, 3, 4, 5, 9, 0, 9],
1021
+ [7, 14, 9, 16, 14, 8, 5, 10, 6, 5, 4, 10, 8, 6, 2, 9, 0],
1022
+ ]
1023
+ data[:time_windows] = [
1024
+ [0, 5], # depot
1025
+ [7, 12], # 1
1026
+ [10, 15], # 2
1027
+ [16, 18], # 3
1028
+ [10, 13], # 4
1029
+ [0, 5], # 5
1030
+ [5, 10], # 6
1031
+ [0, 4], # 7
1032
+ [5, 10], # 8
1033
+ [0, 3], # 9
1034
+ [10, 16], # 10
1035
+ [10, 15], # 11
1036
+ [0, 5], # 12
1037
+ [5, 10], # 13
1038
+ [7, 8], # 14
1039
+ [10, 15], # 15
1040
+ [11, 15], # 16
1041
+ ]
1042
+ data[:num_vehicles] = 4
1043
+ data[:depot] = 0
1044
+
1045
+ manager = ORTools::RoutingIndexManager.new(data[:time_matrix].size, data[:num_vehicles], data[:depot])
1046
+ routing = ORTools::RoutingModel.new(manager)
1047
+
1048
+ time_callback = lambda do |from_index, to_index|
1049
+ from_node = manager.index_to_node(from_index)
1050
+ to_node = manager.index_to_node(to_index)
1051
+ data[:time_matrix][from_node][to_node]
1052
+ end
1053
+
1054
+ transit_callback_index = routing.register_transit_callback(time_callback)
1055
+ routing.set_arc_cost_evaluator_of_all_vehicles(transit_callback_index)
1056
+ time = "Time"
1057
+ routing.add_dimension(
1058
+ transit_callback_index,
1059
+ 30, # allow waiting time
1060
+ 30, # maximum time per vehicle
1061
+ false, # don't force start cumul to zero
1062
+ time
1063
+ )
1064
+ time_dimension = routing.mutable_dimension(time)
1065
+
1066
+ data[:time_windows].each_with_index do |time_window, location_idx|
1067
+ next if location_idx == 0
1068
+ index = manager.node_to_index(location_idx)
1069
+ time_dimension.cumul_var(index).set_range(time_window[0], time_window[1])
1070
+ end
1071
+
1072
+ data[:num_vehicles].times do |vehicle_id|
1073
+ index = routing.start(vehicle_id)
1074
+ time_dimension.cumul_var(index).set_range(data[:time_windows][0][0], data[:time_windows][0][1])
1075
+ end
1076
+
1077
+ data[:num_vehicles].times do |i|
1078
+ routing.add_variable_minimized_by_finalizer(time_dimension.cumul_var(routing.start(i)))
1079
+ routing.add_variable_minimized_by_finalizer(time_dimension.cumul_var(routing.end(i)))
1080
+ end
1081
+
1082
+ solution = routing.solve(first_solution_strategy: :path_cheapest_arc)
1083
+
1084
+ time_dimension = routing.mutable_dimension("Time")
1085
+ total_time = 0
1086
+ data[:num_vehicles].times do |vehicle_id|
1087
+ index = routing.start(vehicle_id)
1088
+ plan_output = String.new("Route for vehicle #{vehicle_id}:\n")
1089
+ while !routing.end?(index)
1090
+ time_var = time_dimension.cumul_var(index)
1091
+ plan_output += "#{manager.index_to_node(index)} Time(#{solution.min(time_var)},#{solution.max(time_var)}) -> "
1092
+ index = solution.value(routing.next_var(index))
1093
+ end
1094
+ time_var = time_dimension.cumul_var(index)
1095
+ plan_output += "#{manager.index_to_node(index)} Time(#{solution.min(time_var)},#{solution.max(time_var)})\n"
1096
+ plan_output += "Time of the route: #{solution.min(time_var)}min\n\n"
1097
+ puts plan_output
1098
+ total_time += solution.min(time_var)
1099
+ end
1100
+ puts "Total time of all routes: #{total_time}min"
1101
+ ```
1102
+
1103
+ ### Resource Constraints
1104
+
1105
+ [Guide](https://developers.google.com/optimization/routing/cvrptw_resources)
1106
+
1107
+ ```ruby
1108
+ data = {}
1109
+ data[:time_matrix] = [
1110
+ [0, 6, 9, 8, 7, 3, 6, 2, 3, 2, 6, 6, 4, 4, 5, 9, 7],
1111
+ [6, 0, 8, 3, 2, 6, 8, 4, 8, 8, 13, 7, 5, 8, 12, 10, 14],
1112
+ [9, 8, 0, 11, 10, 6, 3, 9, 5, 8, 4, 15, 14, 13, 9, 18, 9],
1113
+ [8, 3, 11, 0, 1, 7, 10, 6, 10, 10, 14, 6, 7, 9, 14, 6, 16],
1114
+ [7, 2, 10, 1, 0, 6, 9, 4, 8, 9, 13, 4, 6, 8, 12, 8, 14],
1115
+ [3, 6, 6, 7, 6, 0, 2, 3, 2, 2, 7, 9, 7, 7, 6, 12, 8],
1116
+ [6, 8, 3, 10, 9, 2, 0, 6, 2, 5, 4, 12, 10, 10, 6, 15, 5],
1117
+ [2, 4, 9, 6, 4, 3, 6, 0, 4, 4, 8, 5, 4, 3, 7, 8, 10],
1118
+ [3, 8, 5, 10, 8, 2, 2, 4, 0, 3, 4, 9, 8, 7, 3, 13, 6],
1119
+ [2, 8, 8, 10, 9, 2, 5, 4, 3, 0, 4, 6, 5, 4, 3, 9, 5],
1120
+ [6, 13, 4, 14, 13, 7, 4, 8, 4, 4, 0, 10, 9, 8, 4, 13, 4],
1121
+ [6, 7, 15, 6, 4, 9, 12, 5, 9, 6, 10, 0, 1, 3, 7, 3, 10],
1122
+ [4, 5, 14, 7, 6, 7, 10, 4, 8, 5, 9, 1, 0, 2, 6, 4, 8],
1123
+ [4, 8, 13, 9, 8, 7, 10, 3, 7, 4, 8, 3, 2, 0, 4, 5, 6],
1124
+ [5, 12, 9, 14, 12, 6, 6, 7, 3, 3, 4, 7, 6, 4, 0, 9, 2],
1125
+ [9, 10, 18, 6, 8, 12, 15, 8, 13, 9, 13, 3, 4, 5, 9, 0, 9],
1126
+ [7, 14, 9, 16, 14, 8, 5, 10, 6, 5, 4, 10, 8, 6, 2, 9, 0]
1127
+ ]
1128
+ data[:time_windows] = [
1129
+ [0, 5], # depot
1130
+ [7, 12], # 1
1131
+ [10, 15], # 2
1132
+ [5, 14], # 3
1133
+ [5, 13], # 4
1134
+ [0, 5], # 5
1135
+ [5, 10], # 6
1136
+ [0, 10], # 7
1137
+ [5, 10], # 8
1138
+ [0, 5], # 9
1139
+ [10, 16], # 10
1140
+ [10, 15], # 11
1141
+ [0, 5], # 12
1142
+ [5, 10], # 13
1143
+ [7, 12], # 14
1144
+ [10, 15], # 15
1145
+ [5, 15], # 16
1146
+ ]
1147
+ data[:num_vehicles] = 4
1148
+ data[:vehicle_load_time] = 5
1149
+ data[:vehicle_unload_time] = 5
1150
+ data[:depot_capacity] = 2
1151
+ data[:depot] = 0
1152
+
1153
+ manager = ORTools::RoutingIndexManager.new(data[:time_matrix].size, data[:num_vehicles], data[:depot])
1154
+ routing = ORTools::RoutingModel.new(manager)
1155
+
1156
+ time_callback = lambda do |from_index, to_index|
1157
+ from_node = manager.index_to_node(from_index)
1158
+ to_node = manager.index_to_node(to_index)
1159
+ data[:time_matrix][from_node][to_node]
1160
+ end
1161
+
1162
+ transit_callback_index = routing.register_transit_callback(time_callback)
1163
+
1164
+ routing.set_arc_cost_evaluator_of_all_vehicles(transit_callback_index)
1165
+
1166
+ time = "Time"
1167
+ routing.add_dimension(
1168
+ transit_callback_index,
1169
+ 60, # allow waiting time
1170
+ 60, # maximum time per vehicle
1171
+ false, # don't force start cumul to zero
1172
+ time
1173
+ )
1174
+ time_dimension = routing.mutable_dimension(time)
1175
+ data[:time_windows].each_with_index do |time_window, location_idx|
1176
+ next if location_idx == 0
1177
+ index = manager.node_to_index(location_idx)
1178
+ time_dimension.cumul_var(index).set_range(time_window[0], time_window[1])
1179
+ end
1180
+
1181
+ data[:num_vehicles].times do |vehicle_id|
1182
+ index = routing.start(vehicle_id)
1183
+ time_dimension.cumul_var(index).set_range(data[:time_windows][0][0], data[:time_windows][0][1])
1184
+ end
1185
+
1186
+ solver = routing.solver
1187
+ intervals = []
1188
+ data[:num_vehicles].times do |i|
1189
+ intervals << solver.fixed_duration_interval_var(
1190
+ time_dimension.cumul_var(routing.start(i)),
1191
+ data[:vehicle_load_time],
1192
+ "depot_interval"
1193
+ )
1194
+ intervals << solver.fixed_duration_interval_var(
1195
+ time_dimension.cumul_var(routing.end(i)),
1196
+ data[:vehicle_unload_time],
1197
+ "depot_interval"
1198
+ )
1199
+ end
1200
+
1201
+ depot_usage = [1] * intervals.size
1202
+ solver.add(solver.cumulative(intervals, depot_usage, data[:depot_capacity], "depot"))
1203
+
1204
+ data[:num_vehicles].times do |i|
1205
+ routing.add_variable_minimized_by_finalizer(time_dimension.cumul_var(routing.start(i)))
1206
+ routing.add_variable_minimized_by_finalizer(time_dimension.cumul_var(routing.end(i)))
1207
+ end
1208
+
1209
+ solution = routing.solve(first_solution_strategy: :path_cheapest_arc)
1210
+
1211
+ time_dimension = routing.mutable_dimension("Time")
1212
+ total_time = 0
1213
+ data[:num_vehicles].times do |vehicle_id|
1214
+ index = routing.start(vehicle_id)
1215
+ plan_output = String.new("Route for vehicle #{vehicle_id}:\n")
1216
+ while !routing.end?(index)
1217
+ time_var = time_dimension.cumul_var(index)
1218
+ plan_output += "#{manager.index_to_node(index)} Time(#{solution.min(time_var)},#{solution.max(time_var)}) -> "
1219
+ index = solution.value(routing.next_var(index))
1220
+ end
1221
+ time_var = time_dimension.cumul_var(index)
1222
+ plan_output += "#{manager.index_to_node(index)} Time(#{solution.min(time_var)},#{solution.max(time_var)})\n"
1223
+ plan_output += "Time of the route: #{solution.min(time_var)}min\n\n"
1224
+ puts plan_output
1225
+ total_time += solution.min(time_var)
1226
+ end
1227
+ puts "Total time of all routes: #{total_time}min"
1228
+ ```
1229
+
1230
+ ### Penalties and Dropping Visits
1231
+
1232
+ [Guide](https://developers.google.com/optimization/routing/penalties)
1233
+
1234
+ ```ruby
1235
+ data = {}
1236
+ data[:distance_matrix] = [
1237
+ [0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662],
1238
+ [548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210],
1239
+ [776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754],
1240
+ [696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358],
1241
+ [582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244],
1242
+ [274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708],
1243
+ [502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480],
1244
+ [194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856],
1245
+ [308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514],
1246
+ [194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468],
1247
+ [536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354],
1248
+ [502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844],
1249
+ [388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730],
1250
+ [354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536],
1251
+ [468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194],
1252
+ [776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798],
1253
+ [662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0]
1254
+ ]
1255
+ data[:demands] = [0, 1, 1, 3, 6, 3, 6, 8, 8, 1, 2, 1, 2, 6, 6, 8, 8]
1256
+ data[:vehicle_capacities] = [15, 15, 15, 15]
1257
+ data[:num_vehicles] = 4
1258
+ data[:depot] = 0
1259
+
1260
+ manager = ORTools::RoutingIndexManager.new(data[:distance_matrix].size, data[:num_vehicles], data[:depot])
1261
+ routing = ORTools::RoutingModel.new(manager)
1262
+
1263
+ distance_callback = lambda do |from_index, to_index|
1264
+ from_node = manager.index_to_node(from_index)
1265
+ to_node = manager.index_to_node(to_index)
1266
+ data[:distance_matrix][from_node][to_node]
1267
+ end
1268
+
1269
+ transit_callback_index = routing.register_transit_callback(distance_callback)
1270
+
1271
+ routing.set_arc_cost_evaluator_of_all_vehicles(transit_callback_index)
1272
+
1273
+ demand_callback = lambda do |from_index|
1274
+ from_node = manager.index_to_node(from_index)
1275
+ data[:demands][from_node]
1276
+ end
1277
+
1278
+ demand_callback_index = routing.register_unary_transit_callback(demand_callback)
1279
+ routing.add_dimension_with_vehicle_capacity(
1280
+ demand_callback_index,
1281
+ 0, # null capacity slack
1282
+ data[:vehicle_capacities], # vehicle maximum capacities
1283
+ true, # start cumul to zero
1284
+ "Capacity"
1285
+ )
1286
+
1287
+ penalty = 1000
1288
+ 1.upto(data[:distance_matrix].size - 1) do |node|
1289
+ routing.add_disjunction([manager.node_to_index(node)], penalty)
1290
+ end
1291
+
1292
+ assignment = routing.solve(first_solution_strategy: :path_cheapest_arc)
1293
+
1294
+ dropped_nodes = String.new("Dropped nodes:")
1295
+ routing.size.times do |node|
1296
+ next if routing.start?(node) || routing.end?(node)
1297
+
1298
+ if assignment.value(routing.next_var(node)) == node
1299
+ dropped_nodes += " #{manager.index_to_node(node)}"
1300
+ end
1301
+ end
1302
+ puts dropped_nodes
1303
+
1304
+ total_distance = 0
1305
+ total_load = 0
1306
+ data[:num_vehicles].times do |vehicle_id|
1307
+ index = routing.start(vehicle_id)
1308
+ plan_output = "Route for vehicle #{vehicle_id}:\n"
1309
+ route_distance = 0
1310
+ route_load = 0
1311
+ while !routing.end?(index)
1312
+ node_index = manager.index_to_node(index)
1313
+ route_load += data[:demands][node_index]
1314
+ plan_output += " #{node_index} Load(#{route_load}) -> "
1315
+ previous_index = index
1316
+ index = assignment.value(routing.next_var(index))
1317
+ route_distance += routing.arc_cost_for_vehicle(previous_index, index, vehicle_id)
1318
+ end
1319
+ plan_output += " #{manager.index_to_node(index)} Load(#{route_load})\n"
1320
+ plan_output += "Distance of the route: #{route_distance}m\n"
1321
+ plan_output += "Load of the route: #{route_load}\n\n"
1322
+ puts plan_output
1323
+ total_distance += route_distance
1324
+ total_load += route_load
1325
+ end
1326
+ puts "Total Distance of all routes: #{total_distance}m"
1327
+ puts "Total Load of all routes: #{total_load}"
423
1328
  ```
424
1329
 
425
1330
  ### Routing Options
@@ -441,9 +1346,8 @@ routing.solve(
441
1346
 
442
1347
  [Guide](https://developers.google.com/optimization/bin/knapsack)
443
1348
 
444
- Create the data
445
-
446
1349
  ```ruby
1350
+ # create the data
447
1351
  values = [
448
1352
  360, 83, 59, 130, 431, 67, 230, 52, 93, 125, 670, 892, 600, 38, 48, 147,
449
1353
  78, 256, 63, 17, 120, 164, 432, 35, 92, 110, 22, 42, 50, 323, 514, 28,
@@ -456,17 +1360,11 @@ weights = [[
456
1360
  3, 86, 66, 31, 65, 0, 79, 20, 65, 52, 13
457
1361
  ]]
458
1362
  capacities = [850]
459
- ```
460
-
461
- Declare the solver
462
1363
 
463
- ```ruby
1364
+ # declare the solver
464
1365
  solver = ORTools::KnapsackSolver.new(:branch_and_bound, "KnapsackExample")
465
- ```
466
1366
 
467
- Call the solver
468
-
469
- ```ruby
1367
+ # call the solver
470
1368
  solver.init(values, weights, capacities)
471
1369
  computed_value = solver.solve
472
1370
 
@@ -490,9 +1388,8 @@ puts "Packed weights: #{packed_weights}"
490
1388
 
491
1389
  [Guide](https://developers.google.com/optimization/bin/multiple_knapsack)
492
1390
 
493
- Create the data
494
-
495
1391
  ```ruby
1392
+ # create the data
496
1393
  data = {}
497
1394
  weights = [48, 30, 42, 36, 36, 48, 42, 42, 36, 24, 30, 30, 42, 36, 36]
498
1395
  values = [10, 30, 25, 50, 35, 30, 15, 40, 30, 35, 45, 10, 20, 30, 25]
@@ -503,28 +1400,21 @@ data[:num_items] = weights.length
503
1400
  num_bins = 5
504
1401
  data[:bins] = (0...num_bins).to_a
505
1402
  data[:bin_capacities] = [100, 100, 100, 100, 100]
506
- ```
507
1403
 
508
- Declare the solver
509
-
510
- ```ruby
1404
+ # declare the solver
511
1405
  solver = ORTools::Solver.new("simple_mip_program", :cbc)
512
- ```
513
-
514
- Create the variables
515
1406
 
516
- ```ruby
1407
+ # create the variables
1408
+ # x[i, j] = 1 if item i is packed in bin j
517
1409
  x = {}
518
1410
  data[:items].each do |i|
519
1411
  data[:bins].each do |j|
520
1412
  x[[i, j]] = solver.int_var(0, 1, "x_%i_%i" % [i, j])
521
1413
  end
522
1414
  end
523
- ```
524
1415
 
525
- Define the constraints
526
-
527
- ```ruby
1416
+ # define the constraints
1417
+ # each item can be in at most one bin
528
1418
  data[:items].each do |i|
529
1419
  sum = ORTools::LinearExpr.new
530
1420
  data[:bins].each do |j|
@@ -533,6 +1423,7 @@ data[:items].each do |i|
533
1423
  solver.add(sum <= 1.0)
534
1424
  end
535
1425
 
1426
+ # the amount packed in each bin cannot exceed its capacity
536
1427
  data[:bins].each do |j|
537
1428
  weight = ORTools::LinearExpr.new
538
1429
  data[:items].each do |i|
@@ -540,11 +1431,8 @@ data[:bins].each do |j|
540
1431
  end
541
1432
  solver.add(weight <= data[:bin_capacities][j])
542
1433
  end
543
- ```
544
-
545
- Define the objective
546
1434
 
547
- ```ruby
1435
+ # define the objective
548
1436
  objective = solver.objective
549
1437
 
550
1438
  data[:items].each do |i|
@@ -553,11 +1441,8 @@ data[:items].each do |i|
553
1441
  end
554
1442
  end
555
1443
  objective.set_maximization
556
- ```
557
1444
 
558
- Call the solver and print the solution
559
-
560
- ```ruby
1445
+ # call the solver and print the solution
561
1446
  status = solver.solve
562
1447
 
563
1448
  if status == :optimal
@@ -589,26 +1474,20 @@ end
589
1474
 
590
1475
  [Guide](https://developers.google.com/optimization/bin/bin_packing)
591
1476
 
592
- Create the data
593
-
594
1477
  ```ruby
1478
+ # create the data
595
1479
  data = {}
596
1480
  weights = [48, 30, 19, 36, 36, 27, 42, 42, 36, 24, 30]
597
1481
  data[:weights] = weights
598
1482
  data[:items] = (0...weights.length).to_a
599
1483
  data[:bins] = data[:items]
600
1484
  data[:bin_capacity] = 100
601
- ```
602
1485
 
603
- Declare the solver
604
-
605
- ```ruby
1486
+ # create the mip solver with the CBC backend
606
1487
  solver = ORTools::Solver.new("simple_mip_program", :cbc)
607
- ```
608
-
609
- Create the variables
610
1488
 
611
- ```ruby
1489
+ # variables
1490
+ # x[i, j] = 1 if item i is packed in bin j
612
1491
  x = {}
613
1492
  data[:items].each do |i|
614
1493
  data[:bins].each do |j|
@@ -616,34 +1495,28 @@ data[:items].each do |i|
616
1495
  end
617
1496
  end
618
1497
 
1498
+ # y[j] = 1 if bin j is used
619
1499
  y = {}
620
1500
  data[:bins].each do |j|
621
1501
  y[j] = solver.int_var(0, 1, "y[%i]" % j)
622
1502
  end
623
- ```
624
1503
 
625
- Define the constraints
626
-
627
- ```ruby
1504
+ # constraints
1505
+ # each item must be in exactly one bin
628
1506
  data[:items].each do |i|
629
1507
  solver.add(solver.sum(data[:bins].map { |j| x[[i, j]] }) == 1)
630
1508
  end
631
1509
 
1510
+ # the amount packed in each bin cannot exceed its capacity
632
1511
  data[:bins].each do |j|
633
1512
  sum = solver.sum(data[:items].map { |i| x[[i, j]] * data[:weights][i] })
634
1513
  solver.add(sum <= y[j] * data[:bin_capacity])
635
1514
  end
636
- ```
637
-
638
- Define the objective
639
1515
 
640
- ```ruby
1516
+ # objective: minimize the number of bins used
641
1517
  solver.minimize(solver.sum(data[:bins].map { |j| y[j] }))
642
- ```
643
1518
 
644
- Call the solver and print the solution
645
-
646
- ```ruby
1519
+ # call the solver and print the solution
647
1520
  if status == :optimal
648
1521
  num_bins = 0
649
1522
  data[:bins].each do |j|
@@ -677,27 +1550,20 @@ end
677
1550
 
678
1551
  [Guide](https://developers.google.com/optimization/flow/maxflow)
679
1552
 
680
- Define the data
681
-
682
1553
  ```ruby
1554
+ # define the data
683
1555
  start_nodes = [0, 0, 0, 1, 1, 2, 2, 3, 3]
684
1556
  end_nodes = [1, 2, 3, 2, 4, 3, 4, 2, 4]
685
1557
  capacities = [20, 30, 10, 40, 30, 10, 20, 5, 20]
686
- ```
687
1558
 
688
- Declare the solver and add the arcs
689
-
690
- ```ruby
1559
+ # declare the solver and add the arcs
691
1560
  max_flow = ORTools::SimpleMaxFlow.new
692
1561
 
693
1562
  start_nodes.length.times do |i|
694
1563
  max_flow.add_arc_with_capacity(start_nodes[i], end_nodes[i], capacities[i])
695
1564
  end
696
- ```
697
-
698
- Invoke the solver and display the results
699
1565
 
700
- ```ruby
1566
+ # invoke the solver and display the results
701
1567
  if max_flow.solve(0, 4) == :optimal
702
1568
  puts "Max flow: #{max_flow.optimal_flow}"
703
1569
  puts
@@ -721,122 +1587,56 @@ end
721
1587
 
722
1588
  [Guide](https://developers.google.com/optimization/flow/mincostflow)
723
1589
 
724
- Define the data
725
-
726
1590
  ```ruby
1591
+ # define the data
727
1592
  start_nodes = [ 0, 0, 1, 1, 1, 2, 2, 3, 4]
728
1593
  end_nodes = [ 1, 2, 2, 3, 4, 3, 4, 4, 2]
729
1594
  capacities = [15, 8, 20, 4, 10, 15, 4, 20, 5]
730
1595
  unit_costs = [ 4, 4, 2, 2, 6, 1, 3, 2, 3]
731
1596
  supplies = [20, 0, 0, -5, -15]
732
- ```
733
-
734
- Declare the solver and add the arcs
735
-
736
- ```ruby
737
- min_cost_flow = ORTools::SimpleMinCostFlow.new
738
-
739
- start_nodes.length.times do |i|
740
- min_cost_flow.add_arc_with_capacity_and_unit_cost(
741
- start_nodes[i], end_nodes[i], capacities[i], unit_costs[i]
742
- )
743
- end
744
-
745
- supplies.length.times do |i|
746
- min_cost_flow.set_node_supply(i, supplies[i])
747
- end
748
- ```
749
1597
 
750
- Invoke the solver and display the results
751
-
752
- ```ruby
753
- if min_cost_flow.solve == :optimal
754
- puts "Minimum cost #{min_cost_flow.optimal_cost}"
755
- puts
756
- puts " Arc Flow / Capacity Cost"
757
- min_cost_flow.num_arcs.times do |i|
758
- cost = min_cost_flow.flow(i) * min_cost_flow.unit_cost(i)
759
- puts "%1s -> %1s %3s / %3s %3s" % [
760
- min_cost_flow.tail(i),
761
- min_cost_flow.head(i),
762
- min_cost_flow.flow(i),
763
- min_cost_flow.capacity(i),
764
- cost
765
- ]
766
- end
767
- else
768
- puts "There was an issue with the min cost flow input."
769
- end
770
- ```
771
-
772
- ## Assignment
773
-
774
- [Guide](https://developers.google.com/optimization/assignment/simple_assignment)
775
-
776
- Create the data
777
-
778
- ```ruby
779
- cost = [[ 90, 76, 75, 70],
780
- [ 35, 85, 55, 65],
781
- [125, 95, 90, 105],
782
- [ 45, 110, 95, 115]]
783
-
784
- rows = cost.length
785
- cols = cost[0].length
786
- ```
787
-
788
- Create the solver
789
-
790
- ```ruby
791
- assignment = ORTools::LinearSumAssignment.new
792
- ```
793
-
794
- Add the costs to the solver
795
-
796
- ```ruby
797
- rows.times do |worker|
798
- cols.times do |task|
799
- if cost[worker][task]
800
- assignment.add_arc_with_cost(worker, task, cost[worker][task])
801
- end
802
- end
1598
+ # declare the solver and add the arcs
1599
+ min_cost_flow = ORTools::SimpleMinCostFlow.new
1600
+
1601
+ start_nodes.length.times do |i|
1602
+ min_cost_flow.add_arc_with_capacity_and_unit_cost(
1603
+ start_nodes[i], end_nodes[i], capacities[i], unit_costs[i]
1604
+ )
803
1605
  end
804
- ```
805
1606
 
806
- Invoke the solver
1607
+ supplies.length.times do |i|
1608
+ min_cost_flow.set_node_supply(i, supplies[i])
1609
+ end
807
1610
 
808
- ```ruby
809
- solve_status = assignment.solve
810
- if solve_status == :optimal
811
- puts "Total cost = #{assignment.optimal_cost}"
1611
+ # invoke the solver and display the results
1612
+ if min_cost_flow.solve == :optimal
1613
+ puts "Minimum cost #{min_cost_flow.optimal_cost}"
812
1614
  puts
813
- assignment.num_nodes.times do |i|
814
- puts "Worker %d assigned to task %d. Cost = %d" % [
815
- i,
816
- assignment.right_mate(i),
817
- assignment.assignment_cost(i)
1615
+ puts " Arc Flow / Capacity Cost"
1616
+ min_cost_flow.num_arcs.times do |i|
1617
+ cost = min_cost_flow.flow(i) * min_cost_flow.unit_cost(i)
1618
+ puts "%1s -> %1s %3s / %3s %3s" % [
1619
+ min_cost_flow.tail(i),
1620
+ min_cost_flow.head(i),
1621
+ min_cost_flow.flow(i),
1622
+ min_cost_flow.capacity(i),
1623
+ cost
818
1624
  ]
819
1625
  end
820
- elsif solve_status == :infeasible
821
- puts "No assignment is possible."
822
- elsif solve_status == :possible_overflow
823
- puts "Some input costs are too large and may cause an integer overflow."
1626
+ else
1627
+ puts "There was an issue with the min cost flow input."
824
1628
  end
825
1629
  ```
826
1630
 
827
- ## Assignment as a Min Cost Problem
1631
+ ### Assignment as a Min Cost Flow Problem
828
1632
 
829
1633
  [Guide](https://developers.google.com/optimization/assignment/assignment_min_cost_flow)
830
1634
 
831
- Create the solver
832
-
833
1635
  ```ruby
1636
+ # create the solver
834
1637
  min_cost_flow = ORTools::SimpleMinCostFlow.new
835
- ```
836
1638
 
837
- Create the data
838
-
839
- ```ruby
1639
+ # create the data
840
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]
841
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]
842
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]
@@ -845,11 +1645,8 @@ supplies = [4, 0, 0, 0, 0, 0, 0, 0, 0, -4]
845
1645
  source = 0
846
1646
  sink = 9
847
1647
  tasks = 4
848
- ```
849
-
850
- Create the graph and constraints
851
1648
 
852
- ```ruby
1649
+ # create the graph and constraints
853
1650
  start_nodes.length.times do |i|
854
1651
  min_cost_flow.add_arc_with_capacity_and_unit_cost(
855
1652
  start_nodes[i], end_nodes[i], capacities[i], costs[i]
@@ -859,11 +1656,8 @@ end
859
1656
  supplies.length.times do |i|
860
1657
  min_cost_flow.set_node_supply(i, supplies[i])
861
1658
  end
862
- ```
863
1659
 
864
- Invoke the solver
865
-
866
- ```ruby
1660
+ # invoke the solver
867
1661
  if min_cost_flow.solve == :optimal
868
1662
  puts "Total cost = #{min_cost_flow.optimal_cost}"
869
1663
  puts
@@ -883,109 +1677,20 @@ else
883
1677
  end
884
1678
  ```
885
1679
 
886
- ## Assignment as a MIP Problem
887
-
888
- [Guide](https://developers.google.com/optimization/assignment/assignment_mip)
889
-
890
- Create the solver
891
-
892
- ```ruby
893
- solver = ORTools::Solver.new("SolveAssignmentProblemMIP", :cbc)
894
- ```
895
-
896
- Create the data
897
-
898
- ```ruby
899
- cost = [[90, 76, 75, 70],
900
- [35, 85, 55, 65],
901
- [125, 95, 90, 105],
902
- [45, 110, 95, 115],
903
- [60, 105, 80, 75],
904
- [45, 65, 110, 95]]
905
-
906
- team1 = [0, 2, 4]
907
- team2 = [1, 3, 5]
908
- team_max = 2
909
- ```
910
-
911
- Create the variables
912
-
913
- ```ruby
914
- num_workers = cost.length
915
- num_tasks = cost[1].length
916
- x = {}
917
-
918
- num_workers.times do |i|
919
- num_tasks.times do |j|
920
- x[[i, j]] = solver.bool_var("x[#{i},#{j}]")
921
- end
922
- end
923
- ```
924
-
925
- Create the objective function
926
-
927
- ```ruby
928
- solver.minimize(solver.sum(
929
- num_workers.times.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] * cost[i][j] } }
930
- ))
931
- ```
932
-
933
- Create the constraints
934
-
935
- ```ruby
936
- num_workers.times do |i|
937
- solver.add(solver.sum(num_tasks.times.map { |j| x[[i, j]] }) <= 1)
938
- end
939
-
940
- num_tasks.times do |j|
941
- solver.add(solver.sum(num_workers.times.map { |i| x[[i, j]] }) == 1)
942
- end
943
-
944
- solver.add(solver.sum(team1.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] } }) <= team_max)
945
- solver.add(solver.sum(team2.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] } }) <= team_max)
946
- ```
947
-
948
- Invoke the solver
949
-
950
- ```ruby
951
- sol = solver.solve
952
-
953
- puts "Total cost = #{solver.objective.value}"
954
- puts
955
- num_workers.times do |i|
956
- num_tasks.times do |j|
957
- if x[[i, j]].solution_value > 0
958
- puts "Worker %d assigned to task %d. Cost = %d" % [
959
- i,
960
- j,
961
- cost[i][j]
962
- ]
963
- end
964
- end
965
- end
966
-
967
- puts
968
- puts "Time = #{solver.wall_time} milliseconds"
969
- ```
970
-
971
- ## Employee Scheduling
1680
+ ### Employee Scheduling
972
1681
 
973
1682
  [Guide](https://developers.google.com/optimization/scheduling/employee_scheduling)
974
1683
 
975
- Define the data
976
-
977
1684
  ```ruby
1685
+ # define the data
978
1686
  num_nurses = 4
979
1687
  num_shifts = 3
980
1688
  num_days = 3
981
1689
  all_nurses = num_nurses.times.to_a
982
1690
  all_shifts = num_shifts.times.to_a
983
1691
  all_days = num_days.times.to_a
984
- ```
985
-
986
- Create the variables
987
1692
 
988
- ```ruby
1693
+ # create the variables
989
1694
  model = ORTools::CpModel.new
990
1695
 
991
1696
  shifts = {}
@@ -996,11 +1701,8 @@ all_nurses.each do |n|
996
1701
  end
997
1702
  end
998
1703
  end
999
- ```
1000
1704
 
1001
- Assign nurses to shifts
1002
-
1003
- ```ruby
1705
+ # assign nurses to shifts
1004
1706
  all_days.each do |d|
1005
1707
  all_shifts.each do |s|
1006
1708
  model.add(model.sum(all_nurses.map { |n| shifts[[n, d, s]] }) == 1)
@@ -1012,11 +1714,8 @@ all_nurses.each do |n|
1012
1714
  model.add(model.sum(all_shifts.map { |s| shifts[[n, d, s]] }) <= 1)
1013
1715
  end
1014
1716
  end
1015
- ```
1016
-
1017
- Assign shifts evenly
1018
1717
 
1019
- ```ruby
1718
+ # assign shifts evenly
1020
1719
  min_shifts_per_nurse = (num_shifts * num_days) / num_nurses
1021
1720
  max_shifts_per_nurse = min_shifts_per_nurse + 1
1022
1721
  all_nurses.each do |n|
@@ -1024,11 +1723,8 @@ all_nurses.each do |n|
1024
1723
  model.add(num_shifts_worked >= min_shifts_per_nurse)
1025
1724
  model.add(num_shifts_worked <= max_shifts_per_nurse)
1026
1725
  end
1027
- ```
1028
1726
 
1029
- Create a printer
1030
-
1031
- ```ruby
1727
+ # create a printer
1032
1728
  class NursesPartialSolutionPrinter < ORTools::CpSolverSolutionCallback
1033
1729
  attr_reader :solution_count
1034
1730
 
@@ -1065,11 +1761,8 @@ class NursesPartialSolutionPrinter < ORTools::CpSolverSolutionCallback
1065
1761
  @solution_count += 1
1066
1762
  end
1067
1763
  end
1068
- ```
1069
-
1070
- Call the solver and display the results
1071
1764
 
1072
- ```ruby
1765
+ # call the solver and display the results
1073
1766
  solver = ORTools::CpSolver.new
1074
1767
  a_few_solutions = 5.times.to_a
1075
1768
  solution_printer = NursesPartialSolutionPrinter.new(
@@ -1085,6 +1778,395 @@ puts " - wall time : %f s" % solver.wall_time
1085
1778
  puts " - solutions found : %i" % solution_printer.solution_count
1086
1779
  ```
1087
1780
 
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
1885
+
1886
+ [Example](https://github.com/google/or-tools/blob/stable/examples/python/sudoku_sat.py)
1887
+
1888
+ ```ruby
1889
+ # create the model
1890
+ model = ORTools::CpModel.new
1891
+
1892
+ cell_size = 3
1893
+ line_size = cell_size**2
1894
+ line = (0...line_size).to_a
1895
+ cell = (0...cell_size).to_a
1896
+
1897
+ initial_grid = [
1898
+ [0, 6, 0, 0, 5, 0, 0, 2, 0],
1899
+ [0, 0, 0, 3, 0, 0, 0, 9, 0],
1900
+ [7, 0, 0, 6, 0, 0, 0, 1, 0],
1901
+ [0, 0, 6, 0, 3, 0, 4, 0, 0],
1902
+ [0, 0, 4, 0, 7, 0, 1, 0, 0],
1903
+ [0, 0, 5, 0, 9, 0, 8, 0, 0],
1904
+ [0, 4, 0, 0, 0, 1, 0, 0, 6],
1905
+ [0, 3, 0, 0, 0, 8, 0, 0, 0],
1906
+ [0, 2, 0, 0, 4, 0, 0, 5, 0]
1907
+ ]
1908
+
1909
+ grid = {}
1910
+ line.each do |i|
1911
+ line.each do |j|
1912
+ grid[[i, j]] = model.new_int_var(1, line_size, "grid %i %i" % [i, j])
1913
+ end
1914
+ end
1915
+
1916
+ # all different on rows
1917
+ line.each do |i|
1918
+ model.add_all_different(line.map { |j| grid[[i, j]] })
1919
+ end
1920
+
1921
+ # all different on columns
1922
+ line.each do |j|
1923
+ model.add_all_different(line.map { |i| grid[[i, j]] })
1924
+ end
1925
+
1926
+ # all different on cells
1927
+ cell.each do |i|
1928
+ cell.each do |j|
1929
+ one_cell = []
1930
+ cell.each do |di|
1931
+ cell.each do |dj|
1932
+ one_cell << grid[[i * cell_size + di, j * cell_size + dj]]
1933
+ end
1934
+ end
1935
+ model.add_all_different(one_cell)
1936
+ end
1937
+ end
1938
+
1939
+ # initial values
1940
+ line.each do |i|
1941
+ line.each do |j|
1942
+ if initial_grid[i][j] != 0
1943
+ model.add(grid[[i, j]] == initial_grid[i][j])
1944
+ end
1945
+ end
1946
+ end
1947
+
1948
+ # solve and print solution
1949
+ solver = ORTools::CpSolver.new
1950
+ status = solver.solve(model)
1951
+ if status == :optimal
1952
+ line.each do |i|
1953
+ p line.map { |j| solver.value(grid[[i, j]]) }
1954
+ end
1955
+ end
1956
+ ```
1957
+
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
2118
+
2119
+ [Example](https://pythonhosted.org/PuLP/CaseStudies/a_set_partitioning_problem.html)
2120
+
2121
+ ```ruby
2122
+ # A set partitioning model of a wedding seating problem
2123
+ # Authors: Stuart Mitchell 2009
2124
+
2125
+ max_tables = 5
2126
+ max_table_size = 4
2127
+ guests = %w(A B C D E F G I J K L M N O P Q R)
2128
+
2129
+ # Find the happiness of the table
2130
+ # by calculating the maximum distance between the letters
2131
+ def happiness(table)
2132
+ (table[0].ord - table[-1].ord).abs
2133
+ end
2134
+
2135
+ # create list of all possible tables
2136
+ possible_tables = []
2137
+ (1..max_table_size).each do |i|
2138
+ possible_tables += guests.combination(i).to_a
2139
+ end
2140
+
2141
+ solver = ORTools::Solver.new("Wedding Seating Model", :cbc)
2142
+
2143
+ # create a binary variable to state that a table setting is used
2144
+ x = {}
2145
+ possible_tables.each do |table|
2146
+ x[table] = solver.int_var(0, 1, "table #{table.join(", ")}")
2147
+ end
2148
+
2149
+ solver.minimize(solver.sum(possible_tables.map { |table| x[table] * happiness(table) }))
2150
+
2151
+ # specify the maximum number of tables
2152
+ solver.add(solver.sum(x.values) <= max_tables)
2153
+
2154
+ # a guest must seated at one and only one table
2155
+ guests.each do |guest|
2156
+ tables_with_guest = possible_tables.select { |table| table.include?(guest) }
2157
+ solver.add(solver.sum(tables_with_guest.map { |table| x[table] }) == 1)
2158
+ end
2159
+
2160
+ status = solver.solve
2161
+
2162
+ puts "The chosen tables are out of a total of %s:" % possible_tables.size
2163
+ possible_tables.each do |table|
2164
+ if x[table].solution_value == 1
2165
+ p table
2166
+ end
2167
+ end
2168
+ ```
2169
+
1088
2170
  ## History
1089
2171
 
1090
2172
  View the [changelog](https://github.com/ankane/or-tools/blob/master/CHANGELOG.md)
@@ -1104,7 +2186,7 @@ To get started with development:
1104
2186
  git clone https://github.com/ankane/or-tools.git
1105
2187
  cd or-tools
1106
2188
  bundle install
1107
- bundle exec rake compile -- --with-or-tools-dir=/path/to/or-tools
2189
+ bundle exec rake compile
1108
2190
  bundle exec rake test
1109
2191
  ```
1110
2192