or-tools 0.2.0 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -0
- data/NOTICE.txt +2 -1
- data/README.md +463 -190
- data/ext/or-tools/ext.cpp +64 -6
- data/ext/or-tools/vendor.rb +11 -8
- data/lib/or-tools.rb +7 -0
- data/lib/or_tools/basic_scheduler.rb +86 -0
- data/lib/or_tools/cp_solver.rb +12 -4
- data/lib/or_tools/routing_index_manager.rb +11 -0
- data/lib/or_tools/seating.rb +115 -0
- data/lib/or_tools/sudoku.rb +132 -0
- data/lib/or_tools/tsp.rb +60 -0
- data/lib/or_tools/version.rb +1 -1
- metadata +12 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dba74899e2a57b086643657e5093f12e67fd4fcdda7e22c2b533911f9991e43a
|
|
4
|
+
data.tar.gz: 963c0b75f55b4b772ead3a8402ec8bb4b90570c23de1c7dc3a437758e8949af2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a5dc8662ec580c97a905817867346615e2fbb187d5b75d6b2b839a86e7385a20b2ed4f9f4c13ed536079c6141cdd0bf95bfad7ea6408d4eb81d8f0ed09e32a68
|
|
7
|
+
data.tar.gz: 195967d1e662ba05751ff31fbec3c96ebac519cd58a343ca551f9ba6c12304542b7f4127a37f88e99cda6b214d42089df7ed9b5a3134c0ec1eb978ea72a95be4
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
## 0.3.4 (2021-01-14)
|
|
2
|
+
|
|
3
|
+
- Added support for time limit for `CpSolver`
|
|
4
|
+
- Added `add_dimension_with_vehicle_transits` and `status` methods to `RoutingModel`
|
|
5
|
+
|
|
6
|
+
## 0.3.3 (2020-10-12)
|
|
7
|
+
|
|
8
|
+
- Added support for start and end points for routing
|
|
9
|
+
|
|
10
|
+
## 0.3.2 (2020-08-04)
|
|
11
|
+
|
|
12
|
+
- Updated OR-Tools to 7.8
|
|
13
|
+
- Added binary installation for Ubuntu 20.04
|
|
14
|
+
|
|
15
|
+
## 0.3.1 (2020-07-21)
|
|
16
|
+
|
|
17
|
+
- Reduced gem size
|
|
18
|
+
|
|
19
|
+
## 0.3.0 (2020-07-21)
|
|
20
|
+
|
|
21
|
+
- Updated OR-Tools to 7.7
|
|
22
|
+
- Added `BasicScheduler` class
|
|
23
|
+
- Added `Seating` class
|
|
24
|
+
- Added `TSP` class
|
|
25
|
+
- Added `Sudoku` class
|
|
26
|
+
|
|
1
27
|
## 0.2.0 (2020-05-22)
|
|
2
28
|
|
|
3
29
|
- No longer need to download the OR-Tools C++ library separately on Mac, Ubuntu 18.04, Ubuntu 16.04, Debian 10, and CentOS 8
|
data/NOTICE.txt
CHANGED
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[OR-Tools](https://github.com/google/or-tools) - operations research tools - for Ruby
|
|
4
4
|
|
|
5
|
-
[](https://github.com/ankane/or-tools/actions)
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -14,26 +14,269 @@ gem 'or-tools'
|
|
|
14
14
|
|
|
15
15
|
Installation can take a few minutes as OR-Tools downloads and builds.
|
|
16
16
|
|
|
17
|
-
##
|
|
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
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
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
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Guides
|
|
18
255
|
|
|
19
256
|
Linear Optimization
|
|
20
257
|
|
|
21
258
|
- [The Glop Linear Solver](#the-glop-linear-solver)
|
|
22
259
|
|
|
260
|
+
Integer Optimization
|
|
261
|
+
|
|
262
|
+
- [Mixed-Integer Programming](#mixed-integer-programming)
|
|
263
|
+
|
|
23
264
|
Constraint Optimization
|
|
24
265
|
|
|
25
266
|
- [CP-SAT Solver](#cp-sat-solver)
|
|
26
267
|
- [Solving an Optimization Problem](#solving-an-optimization-problem)
|
|
27
268
|
- [Cryptarithmetic](#cryptarithmetic)
|
|
28
269
|
- [The N-queens Problem](#the-n-queens-problem)
|
|
270
|
+
- [Setting Solver Limits](#setting-solver-limits) [master]
|
|
29
271
|
|
|
30
|
-
|
|
272
|
+
Assignment
|
|
31
273
|
|
|
32
|
-
- [
|
|
274
|
+
- [Assignment](#assignment)
|
|
275
|
+
- [Assignment with Teams](#assignment-with-teams)
|
|
33
276
|
|
|
34
277
|
Routing
|
|
35
278
|
|
|
36
|
-
- [Traveling Salesperson Problem (TSP)](#traveling-salesperson-problem-tsp)
|
|
279
|
+
- [Traveling Salesperson Problem (TSP)](#traveling-salesperson-problem-tsp-1)
|
|
37
280
|
- [Vehicle Routing Problem (VRP)](#vehicle-routing-problem-vrp)
|
|
38
281
|
- [Capacity Constraints](#capacity-constraints)
|
|
39
282
|
- [Pickups and Deliveries](#pickups-and-deliveries)
|
|
@@ -52,12 +295,7 @@ Network Flows
|
|
|
52
295
|
|
|
53
296
|
- [Maximum Flows](#maximum-flows)
|
|
54
297
|
- [Minimum Cost Flows](#minimum-cost-flows)
|
|
55
|
-
|
|
56
|
-
Assignment
|
|
57
|
-
|
|
58
|
-
- [Assignment](#assignment)
|
|
59
|
-
- [Assignment as a Min Cost Problem](#assignment-as-a-min-cost-problem)
|
|
60
|
-
- [Assignment as a MIP Problem](#assignment-as-a-mip-problem)
|
|
298
|
+
- [Assignment as a Min Cost Flow Problem](#assignment-as-a-min-cost-flow-problem)
|
|
61
299
|
|
|
62
300
|
Scheduling
|
|
63
301
|
|
|
@@ -66,7 +304,7 @@ Scheduling
|
|
|
66
304
|
|
|
67
305
|
Other Examples
|
|
68
306
|
|
|
69
|
-
- [Sudoku](#sudoku)
|
|
307
|
+
- [Sudoku](#sudoku-1)
|
|
70
308
|
- [Wedding Seating Chart](#wedding-seating-chart)
|
|
71
309
|
- [Set Partitioning](#set-partitioning)
|
|
72
310
|
|
|
@@ -114,6 +352,52 @@ puts "y = #{y.solution_value}"
|
|
|
114
352
|
puts "Optimal objective value = #{opt_solution}"
|
|
115
353
|
```
|
|
116
354
|
|
|
355
|
+
### Mixed-Integer Programming
|
|
356
|
+
|
|
357
|
+
[Guide](https://developers.google.com/optimization/mip/integer_opt)
|
|
358
|
+
|
|
359
|
+
```ruby
|
|
360
|
+
# declare the MIP solver
|
|
361
|
+
solver = ORTools::Solver.new("simple_mip_program", :cbc)
|
|
362
|
+
|
|
363
|
+
# define the variables
|
|
364
|
+
infinity = solver.infinity
|
|
365
|
+
x = solver.int_var(0.0, infinity, "x")
|
|
366
|
+
y = solver.int_var(0.0, infinity, "y")
|
|
367
|
+
|
|
368
|
+
puts "Number of variables = #{solver.num_variables}"
|
|
369
|
+
|
|
370
|
+
# define the constraints
|
|
371
|
+
c0 = solver.constraint(-infinity, 17.5)
|
|
372
|
+
c0.set_coefficient(x, 1)
|
|
373
|
+
c0.set_coefficient(y, 7)
|
|
374
|
+
|
|
375
|
+
c1 = solver.constraint(-infinity, 3.5)
|
|
376
|
+
c1.set_coefficient(x, 1);
|
|
377
|
+
c1.set_coefficient(y, 0);
|
|
378
|
+
|
|
379
|
+
puts "Number of constraints = #{solver.num_constraints}"
|
|
380
|
+
|
|
381
|
+
# define the objective
|
|
382
|
+
objective = solver.objective
|
|
383
|
+
objective.set_coefficient(x, 1)
|
|
384
|
+
objective.set_coefficient(y, 10)
|
|
385
|
+
objective.set_maximization
|
|
386
|
+
|
|
387
|
+
# call the solver
|
|
388
|
+
status = solver.solve
|
|
389
|
+
|
|
390
|
+
# display the solution
|
|
391
|
+
if status == :optimal
|
|
392
|
+
puts "Solution:"
|
|
393
|
+
puts "Objective value = #{solver.objective.value}"
|
|
394
|
+
puts "x = #{x.solution_value}"
|
|
395
|
+
puts "y = #{y.solution_value}"
|
|
396
|
+
else
|
|
397
|
+
puts "The problem does not have an optimal solution."
|
|
398
|
+
end
|
|
399
|
+
```
|
|
400
|
+
|
|
117
401
|
### CP-SAT Solver
|
|
118
402
|
|
|
119
403
|
[Guide](https://developers.google.com/optimization/cp/cp_solver)
|
|
@@ -136,7 +420,7 @@ solver = ORTools::CpSolver.new
|
|
|
136
420
|
status = solver.solve(model)
|
|
137
421
|
|
|
138
422
|
# display the first solution
|
|
139
|
-
if status == :
|
|
423
|
+
if status == :optimal
|
|
140
424
|
puts "x = #{solver.value(x)}"
|
|
141
425
|
puts "y = #{solver.value(y)}"
|
|
142
426
|
puts "z = #{solver.value(z)}"
|
|
@@ -295,50 +579,152 @@ puts
|
|
|
295
579
|
puts "Solutions found : %i" % solution_printer.solution_count
|
|
296
580
|
```
|
|
297
581
|
|
|
298
|
-
###
|
|
582
|
+
### Setting Solver Limits
|
|
299
583
|
|
|
300
|
-
[Guide](https://developers.google.com/optimization/
|
|
584
|
+
[Guide](https://developers.google.com/optimization/cp/cp_tasks)
|
|
301
585
|
|
|
302
586
|
```ruby
|
|
303
|
-
#
|
|
304
|
-
|
|
587
|
+
# create the model
|
|
588
|
+
model = ORTools::CpModel.new
|
|
305
589
|
|
|
306
|
-
#
|
|
307
|
-
|
|
308
|
-
x =
|
|
309
|
-
y =
|
|
590
|
+
# create the variables
|
|
591
|
+
num_vals = 3
|
|
592
|
+
x = model.new_int_var(0, num_vals - 1, "x")
|
|
593
|
+
y = model.new_int_var(0, num_vals - 1, "y")
|
|
594
|
+
z = model.new_int_var(0, num_vals - 1, "z")
|
|
310
595
|
|
|
311
|
-
|
|
596
|
+
# add an all-different constraint
|
|
597
|
+
model.add(x != y)
|
|
312
598
|
|
|
313
|
-
#
|
|
314
|
-
|
|
315
|
-
c0.set_coefficient(x, 1)
|
|
316
|
-
c0.set_coefficient(y, 7)
|
|
599
|
+
# create the solver
|
|
600
|
+
solver = ORTools::CpSolver.new
|
|
317
601
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
c1.set_coefficient(y, 0);
|
|
602
|
+
# set a time limit of 10 seconds.
|
|
603
|
+
solver.parameters.max_time_in_seconds = 10
|
|
321
604
|
|
|
322
|
-
|
|
605
|
+
# solve the model
|
|
606
|
+
status = solver.solve(model)
|
|
323
607
|
|
|
324
|
-
#
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
608
|
+
# display the first solution
|
|
609
|
+
if status == :optimal
|
|
610
|
+
puts "x = #{solver.value(x)}"
|
|
611
|
+
puts "y = #{solver.value(y)}"
|
|
612
|
+
puts "z = #{solver.value(z)}"
|
|
613
|
+
end
|
|
614
|
+
```
|
|
329
615
|
|
|
330
|
-
|
|
331
|
-
status = solver.solve
|
|
616
|
+
### Assignment
|
|
332
617
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
618
|
+
[Guide](https://developers.google.com/optimization/assignment/assignment_example)
|
|
619
|
+
|
|
620
|
+
```ruby
|
|
621
|
+
# create the data
|
|
622
|
+
cost = [[ 90, 76, 75, 70],
|
|
623
|
+
[ 35, 85, 55, 65],
|
|
624
|
+
[125, 95, 90, 105],
|
|
625
|
+
[ 45, 110, 95, 115]]
|
|
626
|
+
|
|
627
|
+
rows = cost.length
|
|
628
|
+
cols = cost[0].length
|
|
629
|
+
|
|
630
|
+
# create the solver
|
|
631
|
+
assignment = ORTools::LinearSumAssignment.new
|
|
632
|
+
|
|
633
|
+
# add the costs to the solver
|
|
634
|
+
rows.times do |worker|
|
|
635
|
+
cols.times do |task|
|
|
636
|
+
if cost[worker][task]
|
|
637
|
+
assignment.add_arc_with_cost(worker, task, cost[worker][task])
|
|
638
|
+
end
|
|
639
|
+
end
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
# invoke the solver
|
|
643
|
+
solve_status = assignment.solve
|
|
644
|
+
if solve_status == :optimal
|
|
645
|
+
puts "Total cost = #{assignment.optimal_cost}"
|
|
646
|
+
puts
|
|
647
|
+
assignment.num_nodes.times do |i|
|
|
648
|
+
puts "Worker %d assigned to task %d. Cost = %d" % [
|
|
649
|
+
i,
|
|
650
|
+
assignment.right_mate(i),
|
|
651
|
+
assignment.assignment_cost(i)
|
|
652
|
+
]
|
|
653
|
+
end
|
|
654
|
+
elsif solve_status == :infeasible
|
|
655
|
+
puts "No assignment is possible."
|
|
656
|
+
elsif solve_status == :possible_overflow
|
|
657
|
+
puts "Some input costs are too large and may cause an integer overflow."
|
|
658
|
+
end
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
### Assignment with Teams
|
|
662
|
+
|
|
663
|
+
[Guide](https://developers.google.com/optimization/assignment/assignment_teams)
|
|
664
|
+
|
|
665
|
+
```ruby
|
|
666
|
+
# create the solver
|
|
667
|
+
solver = ORTools::Solver.new("SolveAssignmentProblemMIP", :cbc)
|
|
668
|
+
|
|
669
|
+
# create the data
|
|
670
|
+
cost = [[90, 76, 75, 70],
|
|
671
|
+
[35, 85, 55, 65],
|
|
672
|
+
[125, 95, 90, 105],
|
|
673
|
+
[45, 110, 95, 115],
|
|
674
|
+
[60, 105, 80, 75],
|
|
675
|
+
[45, 65, 110, 95]]
|
|
676
|
+
|
|
677
|
+
team1 = [0, 2, 4]
|
|
678
|
+
team2 = [1, 3, 5]
|
|
679
|
+
team_max = 2
|
|
680
|
+
|
|
681
|
+
# create the variables
|
|
682
|
+
num_workers = cost.length
|
|
683
|
+
num_tasks = cost[1].length
|
|
684
|
+
x = {}
|
|
685
|
+
|
|
686
|
+
num_workers.times do |i|
|
|
687
|
+
num_tasks.times do |j|
|
|
688
|
+
x[[i, j]] = solver.bool_var("x[#{i},#{j}]")
|
|
689
|
+
end
|
|
341
690
|
end
|
|
691
|
+
|
|
692
|
+
# create the objective function
|
|
693
|
+
solver.minimize(solver.sum(
|
|
694
|
+
num_workers.times.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] * cost[i][j] } }
|
|
695
|
+
))
|
|
696
|
+
|
|
697
|
+
# create the constraints
|
|
698
|
+
num_workers.times do |i|
|
|
699
|
+
solver.add(solver.sum(num_tasks.times.map { |j| x[[i, j]] }) <= 1)
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
num_tasks.times do |j|
|
|
703
|
+
solver.add(solver.sum(num_workers.times.map { |i| x[[i, j]] }) == 1)
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
solver.add(solver.sum(team1.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] } }) <= team_max)
|
|
707
|
+
solver.add(solver.sum(team2.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] } }) <= team_max)
|
|
708
|
+
|
|
709
|
+
# invoke the solver
|
|
710
|
+
sol = solver.solve
|
|
711
|
+
|
|
712
|
+
puts "Total cost = #{solver.objective.value}"
|
|
713
|
+
puts
|
|
714
|
+
num_workers.times do |i|
|
|
715
|
+
num_tasks.times do |j|
|
|
716
|
+
if x[[i, j]].solution_value > 0
|
|
717
|
+
puts "Worker %d assigned to task %d. Cost = %d" % [
|
|
718
|
+
i,
|
|
719
|
+
j,
|
|
720
|
+
cost[i][j]
|
|
721
|
+
]
|
|
722
|
+
end
|
|
723
|
+
end
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
puts
|
|
727
|
+
puts "Time = #{solver.wall_time} milliseconds"
|
|
342
728
|
```
|
|
343
729
|
|
|
344
730
|
### Traveling Salesperson Problem (TSP)
|
|
@@ -380,7 +766,7 @@ transit_callback_index = routing.register_transit_callback(distance_callback)
|
|
|
380
766
|
routing.set_arc_cost_evaluator_of_all_vehicles(transit_callback_index)
|
|
381
767
|
|
|
382
768
|
# run the solver
|
|
383
|
-
assignment = routing.solve(first_solution_strategy: :
|
|
769
|
+
assignment = routing.solve(first_solution_strategy: :path_cheapest_arc)
|
|
384
770
|
|
|
385
771
|
# print the solution
|
|
386
772
|
puts "Objective: #{assignment.objective_value} miles"
|
|
@@ -1276,52 +1662,7 @@ else
|
|
|
1276
1662
|
end
|
|
1277
1663
|
```
|
|
1278
1664
|
|
|
1279
|
-
### Assignment
|
|
1280
|
-
|
|
1281
|
-
[Guide](https://developers.google.com/optimization/assignment/simple_assignment)
|
|
1282
|
-
|
|
1283
|
-
```ruby
|
|
1284
|
-
# create the data
|
|
1285
|
-
cost = [[ 90, 76, 75, 70],
|
|
1286
|
-
[ 35, 85, 55, 65],
|
|
1287
|
-
[125, 95, 90, 105],
|
|
1288
|
-
[ 45, 110, 95, 115]]
|
|
1289
|
-
|
|
1290
|
-
rows = cost.length
|
|
1291
|
-
cols = cost[0].length
|
|
1292
|
-
|
|
1293
|
-
# create the solver
|
|
1294
|
-
assignment = ORTools::LinearSumAssignment.new
|
|
1295
|
-
|
|
1296
|
-
# add the costs to the solver
|
|
1297
|
-
rows.times do |worker|
|
|
1298
|
-
cols.times do |task|
|
|
1299
|
-
if cost[worker][task]
|
|
1300
|
-
assignment.add_arc_with_cost(worker, task, cost[worker][task])
|
|
1301
|
-
end
|
|
1302
|
-
end
|
|
1303
|
-
end
|
|
1304
|
-
|
|
1305
|
-
# invoke the solver
|
|
1306
|
-
solve_status = assignment.solve
|
|
1307
|
-
if solve_status == :optimal
|
|
1308
|
-
puts "Total cost = #{assignment.optimal_cost}"
|
|
1309
|
-
puts
|
|
1310
|
-
assignment.num_nodes.times do |i|
|
|
1311
|
-
puts "Worker %d assigned to task %d. Cost = %d" % [
|
|
1312
|
-
i,
|
|
1313
|
-
assignment.right_mate(i),
|
|
1314
|
-
assignment.assignment_cost(i)
|
|
1315
|
-
]
|
|
1316
|
-
end
|
|
1317
|
-
elsif solve_status == :infeasible
|
|
1318
|
-
puts "No assignment is possible."
|
|
1319
|
-
elsif solve_status == :possible_overflow
|
|
1320
|
-
puts "Some input costs are too large and may cause an integer overflow."
|
|
1321
|
-
end
|
|
1322
|
-
```
|
|
1323
|
-
|
|
1324
|
-
### Assignment as a Min Cost Problem
|
|
1665
|
+
### Assignment as a Min Cost Flow Problem
|
|
1325
1666
|
|
|
1326
1667
|
[Guide](https://developers.google.com/optimization/assignment/assignment_min_cost_flow)
|
|
1327
1668
|
|
|
@@ -1370,75 +1711,6 @@ else
|
|
|
1370
1711
|
end
|
|
1371
1712
|
```
|
|
1372
1713
|
|
|
1373
|
-
### Assignment as a MIP Problem
|
|
1374
|
-
|
|
1375
|
-
[Guide](https://developers.google.com/optimization/assignment/assignment_mip)
|
|
1376
|
-
|
|
1377
|
-
```ruby
|
|
1378
|
-
# create the solver
|
|
1379
|
-
solver = ORTools::Solver.new("SolveAssignmentProblemMIP", :cbc)
|
|
1380
|
-
|
|
1381
|
-
# create the data
|
|
1382
|
-
cost = [[90, 76, 75, 70],
|
|
1383
|
-
[35, 85, 55, 65],
|
|
1384
|
-
[125, 95, 90, 105],
|
|
1385
|
-
[45, 110, 95, 115],
|
|
1386
|
-
[60, 105, 80, 75],
|
|
1387
|
-
[45, 65, 110, 95]]
|
|
1388
|
-
|
|
1389
|
-
team1 = [0, 2, 4]
|
|
1390
|
-
team2 = [1, 3, 5]
|
|
1391
|
-
team_max = 2
|
|
1392
|
-
|
|
1393
|
-
# create the variables
|
|
1394
|
-
num_workers = cost.length
|
|
1395
|
-
num_tasks = cost[1].length
|
|
1396
|
-
x = {}
|
|
1397
|
-
|
|
1398
|
-
num_workers.times do |i|
|
|
1399
|
-
num_tasks.times do |j|
|
|
1400
|
-
x[[i, j]] = solver.bool_var("x[#{i},#{j}]")
|
|
1401
|
-
end
|
|
1402
|
-
end
|
|
1403
|
-
|
|
1404
|
-
# create the objective function
|
|
1405
|
-
solver.minimize(solver.sum(
|
|
1406
|
-
num_workers.times.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] * cost[i][j] } }
|
|
1407
|
-
))
|
|
1408
|
-
|
|
1409
|
-
# create the constraints
|
|
1410
|
-
num_workers.times do |i|
|
|
1411
|
-
solver.add(solver.sum(num_tasks.times.map { |j| x[[i, j]] }) <= 1)
|
|
1412
|
-
end
|
|
1413
|
-
|
|
1414
|
-
num_tasks.times do |j|
|
|
1415
|
-
solver.add(solver.sum(num_workers.times.map { |i| x[[i, j]] }) == 1)
|
|
1416
|
-
end
|
|
1417
|
-
|
|
1418
|
-
solver.add(solver.sum(team1.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] } }) <= team_max)
|
|
1419
|
-
solver.add(solver.sum(team2.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] } }) <= team_max)
|
|
1420
|
-
|
|
1421
|
-
# invoke the solver
|
|
1422
|
-
sol = solver.solve
|
|
1423
|
-
|
|
1424
|
-
puts "Total cost = #{solver.objective.value}"
|
|
1425
|
-
puts
|
|
1426
|
-
num_workers.times do |i|
|
|
1427
|
-
num_tasks.times do |j|
|
|
1428
|
-
if x[[i, j]].solution_value > 0
|
|
1429
|
-
puts "Worker %d assigned to task %d. Cost = %d" % [
|
|
1430
|
-
i,
|
|
1431
|
-
j,
|
|
1432
|
-
cost[i][j]
|
|
1433
|
-
]
|
|
1434
|
-
end
|
|
1435
|
-
end
|
|
1436
|
-
end
|
|
1437
|
-
|
|
1438
|
-
puts
|
|
1439
|
-
puts "Time = #{solver.wall_time} milliseconds"
|
|
1440
|
-
```
|
|
1441
|
-
|
|
1442
1714
|
### Employee Scheduling
|
|
1443
1715
|
|
|
1444
1716
|
[Guide](https://developers.google.com/optimization/scheduling/employee_scheduling)
|
|
@@ -1710,7 +1982,7 @@ end
|
|
|
1710
1982
|
# solve and print solution
|
|
1711
1983
|
solver = ORTools::CpSolver.new
|
|
1712
1984
|
status = solver.solve(model)
|
|
1713
|
-
if status == :
|
|
1985
|
+
if status == :optimal
|
|
1714
1986
|
line.each do |i|
|
|
1715
1987
|
p line.map { |j| solver.value(grid[[i, j]]) }
|
|
1716
1988
|
end
|
|
@@ -1725,8 +1997,8 @@ end
|
|
|
1725
1997
|
# From
|
|
1726
1998
|
# Meghan L. Bellows and J. D. Luc Peterson
|
|
1727
1999
|
# "Finding an optimal seating chart for a wedding"
|
|
1728
|
-
#
|
|
1729
|
-
#
|
|
2000
|
+
# https://www.improbable.com/news/2012/Optimal-seating-chart.pdf
|
|
2001
|
+
# https://www.improbable.com/2012/02/12/finding-an-optimal-seating-chart-for-a-wedding
|
|
1730
2002
|
#
|
|
1731
2003
|
# Every year, millions of brides (not to mention their mothers, future
|
|
1732
2004
|
# mothers-in-law, and occasionally grooms) struggle with one of the
|
|
@@ -1799,19 +2071,17 @@ all_tables.each do |t|
|
|
|
1799
2071
|
end
|
|
1800
2072
|
end
|
|
1801
2073
|
|
|
2074
|
+
pairs = all_guests.combination(2)
|
|
2075
|
+
|
|
1802
2076
|
colocated = {}
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
colocated[[g1, g2]] = model.new_bool_var("guest %i seats with guest %i" % [g1, g2])
|
|
1806
|
-
end
|
|
2077
|
+
pairs.each do |g1, g2|
|
|
2078
|
+
colocated[[g1, g2]] = model.new_bool_var("guest %i seats with guest %i" % [g1, g2])
|
|
1807
2079
|
end
|
|
1808
2080
|
|
|
1809
2081
|
same_table = {}
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
same_table[[g1, g2, t]] = model.new_bool_var("guest %i seats with guest %i on table %i" % [g1, g2, t])
|
|
1814
|
-
end
|
|
2082
|
+
pairs.each do |g1, g2|
|
|
2083
|
+
all_tables.each do |t|
|
|
2084
|
+
same_table[[g1, g2, t]] = model.new_bool_var("guest %i seats with guest %i on table %i" % [g1, g2, t])
|
|
1815
2085
|
end
|
|
1816
2086
|
end
|
|
1817
2087
|
|
|
@@ -1833,29 +2103,32 @@ all_tables.each do |t|
|
|
|
1833
2103
|
end
|
|
1834
2104
|
|
|
1835
2105
|
# Link colocated with seats
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
model.add_implication(same_table[[g1, g2, t]], seats[[t, g2]])
|
|
1843
|
-
end
|
|
1844
|
-
|
|
1845
|
-
# Link colocated and same_table.
|
|
1846
|
-
model.add(model.sum(all_tables.map { |t| same_table[[g1, g2, t]] }) == colocated[[g1, g2]])
|
|
2106
|
+
pairs.each do |g1, g2|
|
|
2107
|
+
all_tables.each do |t|
|
|
2108
|
+
# Link same_table and seats.
|
|
2109
|
+
model.add_bool_or([seats[[t, g1]].not, seats[[t, g2]].not, same_table[[g1, g2, t]]])
|
|
2110
|
+
model.add_implication(same_table[[g1, g2, t]], seats[[t, g1]])
|
|
2111
|
+
model.add_implication(same_table[[g1, g2, t]], seats[[t, g2]])
|
|
1847
2112
|
end
|
|
2113
|
+
|
|
2114
|
+
# Link colocated and same_table.
|
|
2115
|
+
model.add(model.sum(all_tables.map { |t| same_table[[g1, g2, t]] }) == colocated[[g1, g2]])
|
|
1848
2116
|
end
|
|
1849
2117
|
|
|
1850
2118
|
# Min known neighbors rule.
|
|
1851
|
-
|
|
2119
|
+
all_guests.each do |g|
|
|
1852
2120
|
model.add(
|
|
1853
2121
|
model.sum(
|
|
1854
|
-
(num_guests - 1).
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
2122
|
+
(g + 1).upto(num_guests - 1).
|
|
2123
|
+
select { |g2| c[g][g2] > 0 }.
|
|
2124
|
+
product(all_tables).
|
|
2125
|
+
map { |g2, t| same_table[[g, g2, t]] }
|
|
2126
|
+
) +
|
|
2127
|
+
model.sum(
|
|
2128
|
+
g.times.
|
|
2129
|
+
select { |g1| c[g1][g] > 0 }.
|
|
2130
|
+
product(all_tables).
|
|
2131
|
+
map { |g1, t| same_table[[g1, g, t]] }
|
|
1859
2132
|
) >= min_known_neighbors
|
|
1860
2133
|
)
|
|
1861
2134
|
end
|