or-tools 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +428 -189
- data/ext/or-tools/ext.cpp +5 -0
- data/ext/or-tools/vendor.rb +7 -7
- data/lib/or-tools.rb +6 -0
- data/lib/or_tools/basic_scheduler.rb +86 -0
- data/lib/or_tools/cp_solver.rb +5 -1
- data/lib/or_tools/ext.bundle +0 -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 +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 18b9ab384a5aa4b8d27a3415de9ba751085e63b95cbdc431d1fee206b05e1c37
|
4
|
+
data.tar.gz: 4b8d29c3f6b5fff8181a817737a818e2c3d7cf6cd0184d82078d4606c2c94740
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 26e5b155bdd5c0d37aaf13fe26c674ac15ac693933999056a477e340823ced7e86b8565c00c941d4ecdfd35e0b5a4716465e2418bf1e44ca7c1586d171e77515
|
7
|
+
data.tar.gz: 1c87b6df5082d9ab61b1ab63d154e563fdd9405fa3dda5fd00df2f6aafc65c7f5f8d251938cab9cba58de04efb7f38b11893701c128833059b8290b3a3dfb0da
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,11 @@
|
|
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
|
+
|
1
9
|
## 0.2.0 (2020-05-22)
|
2
10
|
|
3
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
|
data/README.md
CHANGED
@@ -14,12 +14,253 @@ 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)
|
@@ -27,13 +268,14 @@ Constraint Optimization
|
|
27
268
|
- [Cryptarithmetic](#cryptarithmetic)
|
28
269
|
- [The N-queens Problem](#the-n-queens-problem)
|
29
270
|
|
30
|
-
|
271
|
+
Assignment
|
31
272
|
|
32
|
-
- [
|
273
|
+
- [Assignment](#assignment)
|
274
|
+
- [Assignment with Teams](#assignment-with-teams)
|
33
275
|
|
34
276
|
Routing
|
35
277
|
|
36
|
-
- [Traveling Salesperson Problem (TSP)](#traveling-salesperson-problem-tsp)
|
278
|
+
- [Traveling Salesperson Problem (TSP)](#traveling-salesperson-problem-tsp-1)
|
37
279
|
- [Vehicle Routing Problem (VRP)](#vehicle-routing-problem-vrp)
|
38
280
|
- [Capacity Constraints](#capacity-constraints)
|
39
281
|
- [Pickups and Deliveries](#pickups-and-deliveries)
|
@@ -52,12 +294,7 @@ Network Flows
|
|
52
294
|
|
53
295
|
- [Maximum Flows](#maximum-flows)
|
54
296
|
- [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)
|
297
|
+
- [Assignment as a Min Cost Flow Problem](#assignment-as-a-min-cost-flow-problem)
|
61
298
|
|
62
299
|
Scheduling
|
63
300
|
|
@@ -66,7 +303,7 @@ Scheduling
|
|
66
303
|
|
67
304
|
Other Examples
|
68
305
|
|
69
|
-
- [Sudoku](#sudoku)
|
306
|
+
- [Sudoku](#sudoku-1)
|
70
307
|
- [Wedding Seating Chart](#wedding-seating-chart)
|
71
308
|
- [Set Partitioning](#set-partitioning)
|
72
309
|
|
@@ -114,6 +351,52 @@ puts "y = #{y.solution_value}"
|
|
114
351
|
puts "Optimal objective value = #{opt_solution}"
|
115
352
|
```
|
116
353
|
|
354
|
+
### Mixed-Integer Programming
|
355
|
+
|
356
|
+
[Guide](https://developers.google.com/optimization/mip/integer_opt)
|
357
|
+
|
358
|
+
```ruby
|
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
|
398
|
+
```
|
399
|
+
|
117
400
|
### CP-SAT Solver
|
118
401
|
|
119
402
|
[Guide](https://developers.google.com/optimization/cp/cp_solver)
|
@@ -136,7 +419,7 @@ solver = ORTools::CpSolver.new
|
|
136
419
|
status = solver.solve(model)
|
137
420
|
|
138
421
|
# display the first solution
|
139
|
-
if status == :
|
422
|
+
if status == :optimal
|
140
423
|
puts "x = #{solver.value(x)}"
|
141
424
|
puts "y = #{solver.value(y)}"
|
142
425
|
puts "z = #{solver.value(z)}"
|
@@ -295,50 +578,119 @@ puts
|
|
295
578
|
puts "Solutions found : %i" % solution_printer.solution_count
|
296
579
|
```
|
297
580
|
|
298
|
-
### Mixed-Integer Programming
|
299
581
|
|
300
|
-
|
582
|
+
### Assignment
|
583
|
+
|
584
|
+
[Guide](https://developers.google.com/optimization/assignment/assignment_example)
|
301
585
|
|
302
586
|
```ruby
|
303
|
-
#
|
304
|
-
|
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]]
|
305
592
|
|
306
|
-
|
307
|
-
|
308
|
-
x = solver.int_var(0.0, infinity, "x")
|
309
|
-
y = solver.int_var(0.0, infinity, "y")
|
593
|
+
rows = cost.length
|
594
|
+
cols = cost[0].length
|
310
595
|
|
311
|
-
|
596
|
+
# create the solver
|
597
|
+
assignment = ORTools::LinearSumAssignment.new
|
312
598
|
|
313
|
-
#
|
314
|
-
|
315
|
-
|
316
|
-
|
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
|
317
607
|
|
318
|
-
|
319
|
-
|
320
|
-
|
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
|
+
```
|
321
626
|
|
322
|
-
|
627
|
+
### Assignment with Teams
|
323
628
|
|
324
|
-
|
325
|
-
objective = solver.objective
|
326
|
-
objective.set_coefficient(x, 1)
|
327
|
-
objective.set_coefficient(y, 10)
|
328
|
-
objective.set_maximization
|
629
|
+
[Guide](https://developers.google.com/optimization/assignment/assignment_teams)
|
329
630
|
|
330
|
-
|
331
|
-
|
631
|
+
```ruby
|
632
|
+
# create the solver
|
633
|
+
solver = ORTools::Solver.new("SolveAssignmentProblemMIP", :cbc)
|
332
634
|
|
333
|
-
#
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
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
|
341
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"
|
342
694
|
```
|
343
695
|
|
344
696
|
### Traveling Salesperson Problem (TSP)
|
@@ -380,7 +732,7 @@ transit_callback_index = routing.register_transit_callback(distance_callback)
|
|
380
732
|
routing.set_arc_cost_evaluator_of_all_vehicles(transit_callback_index)
|
381
733
|
|
382
734
|
# run the solver
|
383
|
-
assignment = routing.solve(first_solution_strategy: :
|
735
|
+
assignment = routing.solve(first_solution_strategy: :path_cheapest_arc)
|
384
736
|
|
385
737
|
# print the solution
|
386
738
|
puts "Objective: #{assignment.objective_value} miles"
|
@@ -1276,52 +1628,7 @@ else
|
|
1276
1628
|
end
|
1277
1629
|
```
|
1278
1630
|
|
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
|
1631
|
+
### Assignment as a Min Cost Flow Problem
|
1325
1632
|
|
1326
1633
|
[Guide](https://developers.google.com/optimization/assignment/assignment_min_cost_flow)
|
1327
1634
|
|
@@ -1370,75 +1677,6 @@ else
|
|
1370
1677
|
end
|
1371
1678
|
```
|
1372
1679
|
|
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
1680
|
### Employee Scheduling
|
1443
1681
|
|
1444
1682
|
[Guide](https://developers.google.com/optimization/scheduling/employee_scheduling)
|
@@ -1710,7 +1948,7 @@ end
|
|
1710
1948
|
# solve and print solution
|
1711
1949
|
solver = ORTools::CpSolver.new
|
1712
1950
|
status = solver.solve(model)
|
1713
|
-
if status == :
|
1951
|
+
if status == :optimal
|
1714
1952
|
line.each do |i|
|
1715
1953
|
p line.map { |j| solver.value(grid[[i, j]]) }
|
1716
1954
|
end
|
@@ -1725,8 +1963,8 @@ end
|
|
1725
1963
|
# From
|
1726
1964
|
# Meghan L. Bellows and J. D. Luc Peterson
|
1727
1965
|
# "Finding an optimal seating chart for a wedding"
|
1728
|
-
#
|
1729
|
-
#
|
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
|
1730
1968
|
#
|
1731
1969
|
# Every year, millions of brides (not to mention their mothers, future
|
1732
1970
|
# mothers-in-law, and occasionally grooms) struggle with one of the
|
@@ -1799,19 +2037,17 @@ all_tables.each do |t|
|
|
1799
2037
|
end
|
1800
2038
|
end
|
1801
2039
|
|
2040
|
+
pairs = all_guests.combination(2)
|
2041
|
+
|
1802
2042
|
colocated = {}
|
1803
|
-
|
1804
|
-
|
1805
|
-
colocated[[g1, g2]] = model.new_bool_var("guest %i seats with guest %i" % [g1, g2])
|
1806
|
-
end
|
2043
|
+
pairs.each do |g1, g2|
|
2044
|
+
colocated[[g1, g2]] = model.new_bool_var("guest %i seats with guest %i" % [g1, g2])
|
1807
2045
|
end
|
1808
2046
|
|
1809
2047
|
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
|
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])
|
1815
2051
|
end
|
1816
2052
|
end
|
1817
2053
|
|
@@ -1833,29 +2069,32 @@ all_tables.each do |t|
|
|
1833
2069
|
end
|
1834
2070
|
|
1835
2071
|
# 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]])
|
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]])
|
1847
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]])
|
1848
2082
|
end
|
1849
2083
|
|
1850
2084
|
# Min known neighbors rule.
|
1851
|
-
|
2085
|
+
all_guests.each do |g|
|
1852
2086
|
model.add(
|
1853
2087
|
model.sum(
|
1854
|
-
(num_guests - 1).
|
1855
|
-
|
1856
|
-
|
1857
|
-
|
1858
|
-
|
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]] }
|
1859
2098
|
) >= min_known_neighbors
|
1860
2099
|
)
|
1861
2100
|
end
|
data/ext/or-tools/ext.cpp
CHANGED
@@ -661,6 +661,11 @@ void Init_ext()
|
|
661
661
|
"_solution_integer_value",
|
662
662
|
*[](Object self, CpSolverResponse& response, operations_research::sat::IntVar& x) {
|
663
663
|
return SolutionIntegerValue(response, x);
|
664
|
+
})
|
665
|
+
.define_method(
|
666
|
+
"_solution_boolean_value",
|
667
|
+
*[](Object self, CpSolverResponse& response, operations_research::sat::BoolVar& x) {
|
668
|
+
return SolutionBooleanValue(response, x);
|
664
669
|
});
|
665
670
|
|
666
671
|
define_class_under<CpSolverResponse>(rb_mORTools, "CpSolverResponse")
|
data/ext/or-tools/vendor.rb
CHANGED
@@ -3,26 +3,26 @@ require "fileutils"
|
|
3
3
|
require "net/http"
|
4
4
|
require "tmpdir"
|
5
5
|
|
6
|
-
version = "7.
|
6
|
+
version = "7.7.7810"
|
7
7
|
|
8
8
|
if RbConfig::CONFIG["host_os"] =~ /darwin/i
|
9
|
-
filename = "or-tools_MacOsX-10.15.
|
10
|
-
checksum = "
|
9
|
+
filename = "or-tools_MacOsX-10.15.5_v#{version}.tar.gz"
|
10
|
+
checksum = "764f290f6d916bc366913a37d93e6f83bd7969ad33515ccc1ca390f544d65721"
|
11
11
|
else
|
12
12
|
os = %x[lsb_release -is].chomp rescue nil
|
13
13
|
os_version = %x[lsb_release -rs].chomp rescue nil
|
14
14
|
if os == "Ubuntu" && os_version == "18.04"
|
15
15
|
filename = "or-tools_ubuntu-18.04_v#{version}.tar.gz"
|
16
|
-
checksum = "
|
16
|
+
checksum = "12bdac29144b077b3f9ba602f947e4b9b9ce63ed3df4e325cda1333827edbcf8"
|
17
17
|
elsif os == "Ubuntu" && os_version == "16.04"
|
18
18
|
filename = "or-tools_ubuntu-16.04_v#{version}.tar.gz"
|
19
|
-
checksum = "
|
19
|
+
checksum = "cc696d342b97aa6cf7c62b6ae2cae95dfc665f2483d147c4117fdba434b13a53"
|
20
20
|
elsif os == "Debian" && os_version == "10"
|
21
21
|
filename = "or-tools_debian-10_v#{version}.tar.gz "
|
22
|
-
checksum = "
|
22
|
+
checksum = "3dd0299e9ad8d12fe6d186bfd59e63080c8e9f3c6b0489af9900c389cf7e4224"
|
23
23
|
elsif os == "CentOS" && os_version == "8"
|
24
24
|
filename = "or-tools_centos-8_v#{version}.tar.gz"
|
25
|
-
checksum = "
|
25
|
+
checksum = "1f7d8bce56807c4283374e05024ffac8afd81ff99063217418d02d626cf03088"
|
26
26
|
else
|
27
27
|
# there is a binary download for Windows
|
28
28
|
# however, it's compiled with Visual Studio rather than MinGW (which RubyInstaller uses)
|
data/lib/or-tools.rb
CHANGED
@@ -17,6 +17,12 @@ require "or_tools/sat_int_var"
|
|
17
17
|
require "or_tools/solver"
|
18
18
|
require "or_tools/version"
|
19
19
|
|
20
|
+
# higher level interfaces
|
21
|
+
require "or_tools/basic_scheduler"
|
22
|
+
require "or_tools/seating"
|
23
|
+
require "or_tools/sudoku"
|
24
|
+
require "or_tools/tsp"
|
25
|
+
|
20
26
|
module ORTools
|
21
27
|
class Error < StandardError; end
|
22
28
|
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module ORTools
|
2
|
+
class BasicScheduler
|
3
|
+
attr_reader :assignments, :assigned_hours
|
4
|
+
|
5
|
+
# for time blocks (shifts and availability)
|
6
|
+
# could also use time range (starts_at..ends_at) or starts_at + duration
|
7
|
+
# keep current format for now for flexibility
|
8
|
+
def initialize(people:, shifts:)
|
9
|
+
@shifts = shifts
|
10
|
+
|
11
|
+
model = ORTools::CpModel.new
|
12
|
+
|
13
|
+
# create variables
|
14
|
+
# a person must be available for the entire shift to be considered for it
|
15
|
+
vars = []
|
16
|
+
shifts.each_with_index do |shift, i|
|
17
|
+
people.each_with_index do |person, j|
|
18
|
+
if person[:availability].any? { |a| a[:starts_at] <= shift[:starts_at] && a[:ends_at] >= shift[:ends_at] }
|
19
|
+
vars << {shift: i, person: j, var: model.new_bool_var("{shift: #{i}, person: #{j}}")}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
vars_by_shift = vars.group_by { |v| v[:shift] }
|
25
|
+
vars_by_person = vars.group_by { |v| v[:person] }
|
26
|
+
|
27
|
+
# one person per shift
|
28
|
+
vars_by_shift.each do |j, vs|
|
29
|
+
model.add(model.sum(vs.map { |v| v[:var] }) <= 1)
|
30
|
+
end
|
31
|
+
|
32
|
+
# one shift per day per person
|
33
|
+
# in future, may also want to add option to ensure assigned shifts are N hours apart
|
34
|
+
vars_by_person.each do |j, vs|
|
35
|
+
vs.group_by { |v| shift_dates[v[:shift]] }.each do |_, vs2|
|
36
|
+
model.add(model.sum(vs2.map { |v| v[:var] }) <= 1)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# max hours per person
|
41
|
+
# use seconds since model needs integers
|
42
|
+
vars_by_person.each do |j, vs|
|
43
|
+
max_hours = people[j][:max_hours]
|
44
|
+
if max_hours
|
45
|
+
model.add(model.sum(vs.map { |v| v[:var] * shift_duration[v[:shift]] }) <= max_hours * 3600)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# maximize hours assigned
|
50
|
+
# could also include distance from max hours
|
51
|
+
model.maximize(model.sum(vars.map { |v| v[:var] * shift_duration[v[:shift]] }))
|
52
|
+
|
53
|
+
# solve
|
54
|
+
solver = ORTools::CpSolver.new
|
55
|
+
status = solver.solve(model)
|
56
|
+
raise Error, "No solution found" unless [:feasible, :optimal].include?(status)
|
57
|
+
|
58
|
+
# read solution
|
59
|
+
@assignments = []
|
60
|
+
vars.each do |v|
|
61
|
+
if solver.value(v[:var])
|
62
|
+
@assignments << {
|
63
|
+
person: v[:person],
|
64
|
+
shift: v[:shift]
|
65
|
+
}
|
66
|
+
end
|
67
|
+
end
|
68
|
+
# can calculate manually if objective changes
|
69
|
+
@assigned_hours = solver.objective_value / 3600.0
|
70
|
+
end
|
71
|
+
|
72
|
+
def total_hours
|
73
|
+
@total_hours ||= shift_duration.sum / 3600.0
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def shift_duration
|
79
|
+
@shift_duration ||= @shifts.map { |s| (s[:ends_at] - s[:starts_at]).round }
|
80
|
+
end
|
81
|
+
|
82
|
+
def shift_dates
|
83
|
+
@shift_dates ||= @shifts.map { |s| s[:starts_at].to_date }
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/lib/or_tools/cp_solver.rb
CHANGED
@@ -12,7 +12,11 @@ module ORTools
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def value(var)
|
15
|
-
|
15
|
+
if var.is_a?(BoolVar)
|
16
|
+
_solution_boolean_value(@response, var)
|
17
|
+
else
|
18
|
+
_solution_integer_value(@response, var)
|
19
|
+
end
|
16
20
|
end
|
17
21
|
|
18
22
|
def solve_with_solution_callback(model, observer)
|
Binary file
|
@@ -0,0 +1,115 @@
|
|
1
|
+
module ORTools
|
2
|
+
class Seating
|
3
|
+
attr_reader :assignments, :people, :total_weight
|
4
|
+
|
5
|
+
def initialize(connections:, tables:, min_connections: 1)
|
6
|
+
@people = connections.flat_map { |c| c[:people] }.uniq
|
7
|
+
|
8
|
+
@connections_for = {}
|
9
|
+
@people.each do |person|
|
10
|
+
@connections_for[person] = {}
|
11
|
+
end
|
12
|
+
connections.each do |c|
|
13
|
+
c[:people].each_with_index do |person, i|
|
14
|
+
others = c[:people].dup
|
15
|
+
others.delete_at(i)
|
16
|
+
others.each do |other|
|
17
|
+
@connections_for[person][other] ||= 0
|
18
|
+
@connections_for[person][other] += c[:weight]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
model = ORTools::CpModel.new
|
24
|
+
all_tables = tables.size.times.to_a
|
25
|
+
|
26
|
+
# decision variables
|
27
|
+
seats = {}
|
28
|
+
all_tables.each do |t|
|
29
|
+
people.each do |g|
|
30
|
+
seats[[t, g]] = model.new_bool_var("guest %s seats on table %i" % [g, t])
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
pairs = people.combination(2)
|
35
|
+
|
36
|
+
colocated = {}
|
37
|
+
pairs.each do |g1, g2|
|
38
|
+
colocated[[g1, g2]] = model.new_bool_var("guest %s seats with guest %s" % [g1, g2])
|
39
|
+
end
|
40
|
+
|
41
|
+
same_table = {}
|
42
|
+
pairs.each do |g1, g2|
|
43
|
+
all_tables.each do |t|
|
44
|
+
same_table[[g1, g2, t]] = model.new_bool_var("guest %s seats with guest %s on table %i" % [g1, g2, t])
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# objective
|
49
|
+
objective = []
|
50
|
+
pairs.each do |g1, g2|
|
51
|
+
weight = @connections_for[g1][g2]
|
52
|
+
objective << colocated[[g1, g2]] * weight if weight
|
53
|
+
end
|
54
|
+
model.maximize(model.sum(objective))
|
55
|
+
|
56
|
+
# everybody seats at one table
|
57
|
+
people.each do |g|
|
58
|
+
model.add(model.sum(all_tables.map { |t| seats[[t, g]] }) == 1)
|
59
|
+
end
|
60
|
+
|
61
|
+
# tables have a max capacity
|
62
|
+
all_tables.each do |t|
|
63
|
+
model.add(model.sum(@people.map { |g| seats[[t, g]] }) <= tables[t])
|
64
|
+
end
|
65
|
+
|
66
|
+
# link colocated with seats
|
67
|
+
pairs.each do |g1, g2|
|
68
|
+
all_tables.each do |t|
|
69
|
+
# link same_table and seats
|
70
|
+
model.add_bool_or([seats[[t, g1]].not, seats[[t, g2]].not, same_table[[g1, g2, t]]])
|
71
|
+
model.add_implication(same_table[[g1, g2, t]], seats[[t, g1]])
|
72
|
+
model.add_implication(same_table[[g1, g2, t]], seats[[t, g2]])
|
73
|
+
end
|
74
|
+
|
75
|
+
# link colocated and same_table
|
76
|
+
model.add(model.sum(all_tables.map { |t| same_table[[g1, g2, t]] }) == colocated[[g1, g2]])
|
77
|
+
end
|
78
|
+
|
79
|
+
# min known neighbors rule
|
80
|
+
same_table_by_person = Hash.new { |hash, key| hash[key] = [] }
|
81
|
+
same_table.each do |(g1, g2, t), v|
|
82
|
+
next unless @connections_for[g1][g2]
|
83
|
+
same_table_by_person[g1] << v
|
84
|
+
same_table_by_person[g2] << v
|
85
|
+
end
|
86
|
+
same_table_by_person.each do |_, vars|
|
87
|
+
model.add(model.sum(vars) >= min_connections)
|
88
|
+
end
|
89
|
+
|
90
|
+
# solve
|
91
|
+
solver = ORTools::CpSolver.new
|
92
|
+
status = solver.solve(model)
|
93
|
+
raise Error, "No solution found" unless [:feasible, :optimal].include?(status)
|
94
|
+
|
95
|
+
# read solution
|
96
|
+
@assignments = {}
|
97
|
+
seats.each do |k, v|
|
98
|
+
if solver.value(v)
|
99
|
+
@assignments[k[1]] = k[0]
|
100
|
+
end
|
101
|
+
end
|
102
|
+
@total_weight = solver.objective_value
|
103
|
+
end
|
104
|
+
|
105
|
+
def assigned_tables
|
106
|
+
assignments.group_by { |_, v| v }.map { |k, v| [k, v.map(&:first)] }.sort_by(&:first).map(&:last)
|
107
|
+
end
|
108
|
+
|
109
|
+
def connections_for(person, same_table: false)
|
110
|
+
result = @connections_for[person]
|
111
|
+
result = result.select { |k, _| @assignments[k] == @assignments[person] } if same_table
|
112
|
+
result
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
module ORTools
|
2
|
+
class Sudoku
|
3
|
+
attr_reader :solution
|
4
|
+
|
5
|
+
def initialize(initial_grid, x: false, magic_square: false, anti_knight: false, anti_king: false, non_consecutive: false)
|
6
|
+
raise ArgumentError, "Grid must be 9x9" unless initial_grid.size == 9 && initial_grid.all? { |r| r.size == 9 }
|
7
|
+
raise ArgumentError, "Grid must contain values between 0 and 9" unless initial_grid.flatten(1).all? { |v| (0..9).include?(v) }
|
8
|
+
|
9
|
+
model = ORTools::CpModel.new
|
10
|
+
|
11
|
+
cell_size = 3
|
12
|
+
line_size = cell_size**2
|
13
|
+
line = (0...line_size).to_a
|
14
|
+
cell = (0...cell_size).to_a
|
15
|
+
|
16
|
+
grid = {}
|
17
|
+
line.each do |i|
|
18
|
+
line.each do |j|
|
19
|
+
grid[[i, j]] = model.new_int_var(1, line_size, "grid %i %i" % [i, j])
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
line.each do |i|
|
24
|
+
model.add_all_different(line.map { |j| grid[[i, j]] })
|
25
|
+
end
|
26
|
+
|
27
|
+
line.each do |j|
|
28
|
+
model.add_all_different(line.map { |i| grid[[i, j]] })
|
29
|
+
end
|
30
|
+
|
31
|
+
cell.each do |i|
|
32
|
+
cell.each do |j|
|
33
|
+
one_cell = []
|
34
|
+
cell.each do |di|
|
35
|
+
cell.each do |dj|
|
36
|
+
one_cell << grid[[i * cell_size + di, j * cell_size + dj]]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
model.add_all_different(one_cell)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
line.each do |i|
|
44
|
+
line.each do |j|
|
45
|
+
if initial_grid[i][j] != 0
|
46
|
+
model.add(grid[[i, j]] == initial_grid[i][j])
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
if x
|
52
|
+
model.add_all_different(9.times.map { |i| grid[[i, i]] })
|
53
|
+
model.add_all_different(9.times.map { |i| grid[[i, 8 - i]] })
|
54
|
+
end
|
55
|
+
|
56
|
+
if magic_square
|
57
|
+
magic_sums = []
|
58
|
+
3.times do |i|
|
59
|
+
magic_sums << model.sum(3.times.map { |j| grid[[3 + i, 3 + j]] })
|
60
|
+
magic_sums << model.sum(3.times.map { |j| grid[[3 + j, 3 + i]] })
|
61
|
+
end
|
62
|
+
|
63
|
+
magic_sums << model.sum(3.times.map { |i| grid[[3 + i, 3 + i]] })
|
64
|
+
magic_sums << model.sum(3.times.map { |i| grid[[3 + i, 5 - i]] })
|
65
|
+
|
66
|
+
first_sum = magic_sums.shift
|
67
|
+
magic_sums.each do |magic_sum|
|
68
|
+
model.add(magic_sum == first_sum)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
if anti_knight
|
73
|
+
# add anti-knights rule
|
74
|
+
# for each square, add squares that cannot be feasible
|
75
|
+
moves = [[1, 2], [2, 1], [2, -1], [1, -2], [-1, -2], [-2, -1], [-2, 1], [-1, 2]]
|
76
|
+
9.times do |i|
|
77
|
+
9.times do |j|
|
78
|
+
moves.each do |mi, mj|
|
79
|
+
square = grid[[i + mi, j + mj]]
|
80
|
+
if square
|
81
|
+
model.add(grid[[i, j]] != square)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
if anti_king
|
89
|
+
# add anti-king rule
|
90
|
+
# for each square, add squares that cannot be feasible
|
91
|
+
moves = [[1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-1, -1], [0, -1], [1, -1]]
|
92
|
+
9.times do |i|
|
93
|
+
9.times do |j|
|
94
|
+
moves.each do |mi, mj|
|
95
|
+
square = grid[[i + mi, j + mj]]
|
96
|
+
if square
|
97
|
+
model.add(grid[[i, j]] != square)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
if non_consecutive
|
105
|
+
# add non-consecutive rule
|
106
|
+
# for each square, add squares that cannot be feasible
|
107
|
+
moves = [[1, 0], [0, 1], [-1, 0], [0, -1]]
|
108
|
+
9.times do |i|
|
109
|
+
9.times do |j|
|
110
|
+
moves.each do |mi, mj|
|
111
|
+
square = grid[[i + mi, j + mj]]
|
112
|
+
if square
|
113
|
+
model.add(grid[[i, j]] + 1 != square)
|
114
|
+
model.add(grid[[i, j]] - 1 != square)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
solver = ORTools::CpSolver.new
|
122
|
+
status = solver.solve(model)
|
123
|
+
raise Error, "No solution found" unless [:feasible, :optimal].include?(status)
|
124
|
+
|
125
|
+
solution = []
|
126
|
+
line.each do |i|
|
127
|
+
solution << line.map { |j| solver.value(grid[[i, j]]) }
|
128
|
+
end
|
129
|
+
@solution = solution
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
data/lib/or_tools/tsp.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
module ORTools
|
2
|
+
class TSP
|
3
|
+
attr_reader :route, :route_indexes, :distances, :total_distance
|
4
|
+
|
5
|
+
DISTANCE_SCALE = 1000
|
6
|
+
DEGREES_TO_RADIANS = Math::PI / 180
|
7
|
+
|
8
|
+
def initialize(locations)
|
9
|
+
raise ArgumentError, "Locations must have latitude and longitude" unless locations.all? { |l| l[:latitude] && l[:longitude] }
|
10
|
+
raise ArgumentError, "Latitude must be between -90 and 90" unless locations.all? { |l| l[:latitude] >= -90 && l[:latitude] <= 90 }
|
11
|
+
raise ArgumentError, "Longitude must be between -180 and 180" unless locations.all? { |l| l[:longitude] >= -180 && l[:longitude] <= 180 }
|
12
|
+
raise ArgumentError, "Must be at least two locations" unless locations.size >= 2
|
13
|
+
|
14
|
+
distance_matrix =
|
15
|
+
locations.map do |from|
|
16
|
+
locations.map do |to|
|
17
|
+
# must be integers
|
18
|
+
(distance(from, to) * DISTANCE_SCALE).to_i
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
manager = ORTools::RoutingIndexManager.new(locations.size, 1, 0)
|
23
|
+
routing = ORTools::RoutingModel.new(manager)
|
24
|
+
|
25
|
+
distance_callback = lambda do |from_index, to_index|
|
26
|
+
from_node = manager.index_to_node(from_index)
|
27
|
+
to_node = manager.index_to_node(to_index)
|
28
|
+
distance_matrix[from_node][to_node]
|
29
|
+
end
|
30
|
+
|
31
|
+
transit_callback_index = routing.register_transit_callback(distance_callback)
|
32
|
+
routing.set_arc_cost_evaluator_of_all_vehicles(transit_callback_index)
|
33
|
+
assignment = routing.solve(first_solution_strategy: :path_cheapest_arc)
|
34
|
+
|
35
|
+
@route_indexes = []
|
36
|
+
@distances = []
|
37
|
+
|
38
|
+
index = routing.start(0)
|
39
|
+
while !routing.end?(index)
|
40
|
+
@route_indexes << manager.index_to_node(index)
|
41
|
+
previous_index = index
|
42
|
+
index = assignment.value(routing.next_var(index))
|
43
|
+
@distances << routing.arc_cost_for_vehicle(previous_index, index, 0) / DISTANCE_SCALE.to_f
|
44
|
+
end
|
45
|
+
@route_indexes << manager.index_to_node(index)
|
46
|
+
@route = locations.values_at(*@route_indexes)
|
47
|
+
@total_distance = @distances.sum
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def distance(from, to)
|
53
|
+
from_lat = from[:latitude] * DEGREES_TO_RADIANS
|
54
|
+
from_lng = from[:longitude] * DEGREES_TO_RADIANS
|
55
|
+
to_lat = to[:latitude] * DEGREES_TO_RADIANS
|
56
|
+
to_lng = to[:longitude] * DEGREES_TO_RADIANS
|
57
|
+
2 * 6371 * Math.asin(Math.sqrt(Math.sin((to_lat - from_lat) / 2.0)**2 + Math.cos(from_lat) * Math.cos(to_lat) * Math.sin((from_lng - to_lng) / 2.0)**2))
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/lib/or_tools/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: or-tools
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-07-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rice
|
@@ -95,19 +95,24 @@ files:
|
|
95
95
|
- ext/or-tools/extconf.rb
|
96
96
|
- ext/or-tools/vendor.rb
|
97
97
|
- lib/or-tools.rb
|
98
|
+
- lib/or_tools/basic_scheduler.rb
|
98
99
|
- lib/or_tools/bool_var.rb
|
99
100
|
- lib/or_tools/comparison.rb
|
100
101
|
- lib/or_tools/comparison_operators.rb
|
101
102
|
- lib/or_tools/cp_model.rb
|
102
103
|
- lib/or_tools/cp_solver.rb
|
103
104
|
- lib/or_tools/cp_solver_solution_callback.rb
|
105
|
+
- lib/or_tools/ext.bundle
|
104
106
|
- lib/or_tools/int_var.rb
|
105
107
|
- lib/or_tools/knapsack_solver.rb
|
106
108
|
- lib/or_tools/linear_expr.rb
|
107
109
|
- lib/or_tools/routing_model.rb
|
108
110
|
- lib/or_tools/sat_int_var.rb
|
109
111
|
- lib/or_tools/sat_linear_expr.rb
|
112
|
+
- lib/or_tools/seating.rb
|
110
113
|
- lib/or_tools/solver.rb
|
114
|
+
- lib/or_tools/sudoku.rb
|
115
|
+
- lib/or_tools/tsp.rb
|
111
116
|
- lib/or_tools/version.rb
|
112
117
|
homepage: https://github.com/ankane/or-tools
|
113
118
|
licenses:
|