forked from google/or-tools
-
Notifications
You must be signed in to change notification settings - Fork 9
/
shift_minimization_sat.cc
317 lines (275 loc) · 10.6 KB
/
shift_minimization_sat.cc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
// Copyright 2010-2024 Google LLC
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Reader and solver for the shift minimization personnel task
// scheduling problem (see
// https://publications.csiro.au/rpr/download?pid=csiro:EP104071&dsid=DS2)/
//
// Data files are in
// /cns/li-d/home/operations-research/shift_minization_scheduling
//
// The problem is the following:
// - There is a list of jobs. Each job has a start date and an end date. They
// must all be performed.
// - There is a set of workers. Each worker can perform one or more jobs among
// a subset of job. One worker cannot perform two jobs that overlaps.
// - The objective it to minimize the number of active workers, while
// performing all the jobs.
#include <algorithm>
#include <map>
#include <set>
#include <string>
#include <vector>
#include "absl/container/btree_set.h"
#include "absl/flags/flag.h"
#include "absl/strings/numbers.h"
#include "absl/strings/str_split.h"
#include "absl/strings/string_view.h"
#include "ortools/base/commandlineflags.h"
#include "ortools/base/init_google.h"
#include "ortools/base/logging.h"
#include "ortools/sat/cp_model.h"
#include "ortools/sat/model.h"
#include "ortools/util/filelineiter.h"
ABSL_FLAG(std::string, input, "", "Input file.");
ABSL_FLAG(std::string, params, "", "Sat parameters in text proto format.");
namespace operations_research {
namespace sat {
class ShiftMinimizationParser {
public:
struct Job {
int start;
int end;
};
struct Assignment {
int worker_id;
int job_index;
};
ShiftMinimizationParser()
: load_status_(NOT_STARTED),
declared_num_jobs_(0),
declared_num_workers_(0),
num_workers_read_(0) {}
const std::vector<Job>& jobs() const { return jobs_; }
const std::vector<std::vector<int>>& possible_jobs_per_worker() const {
return possible_jobs_per_worker_;
}
const std::vector<std::vector<Assignment>>& possible_assignments_per_job()
const {
return possible_assignments_per_job_;
}
// The file format is the following
// # comments...
// Type = 1
// Jobs = <n>
// <start> <end> // Repeated n times
// Qualifications = <k>
// c: job_1 .. job_c // repeated k times (a counter and job ids after).
bool LoadFile(const std::string& file_name) {
if (load_status_ != NOT_STARTED) {
return false;
}
load_status_ = STARTED;
for (const std::string& line :
FileLines(file_name, FileLineIterator::REMOVE_LINEFEED |
FileLineIterator::REMOVE_INLINE_CR)) {
ProcessLine(line);
}
LOG(INFO) << "Read file " << file_name << " with " << declared_num_jobs_
<< " jobs, and " << declared_num_workers_ << " workers.";
return declared_num_jobs_ != 0 && jobs_.size() == declared_num_jobs_ &&
declared_num_workers_ != 0 &&
declared_num_workers_ == num_workers_read_;
}
private:
enum LoadStatus { NOT_STARTED, STARTED, JOBS_SEEN, WORKERS_SEEN };
int strtoint32(absl::string_view word) {
int result;
CHECK(absl::SimpleAtoi(word, &result));
return result;
}
void ProcessLine(const std::string& line) {
if (line.empty() || line[0] == '#') {
return;
}
const std::vector<std::string> words =
absl::StrSplit(line, absl::ByAnyChar(" :\t"), absl::SkipEmpty());
switch (load_status_) {
case NOT_STARTED: {
LOG(FATAL) << "Wrong status: NOT_STARTED";
break;
}
case STARTED: {
if (words.size() == 3 && words[0] == "Type") {
CHECK_EQ(1, strtoint32(words[2]));
} else if (words.size() == 3 && words[0] == "Jobs") {
declared_num_jobs_ = strtoint32(words[2]);
possible_assignments_per_job_.resize(declared_num_jobs_);
load_status_ = JOBS_SEEN;
} else {
LOG(FATAL) << "Wrong state STARTED with line " << line;
}
break;
}
case JOBS_SEEN: {
if (words.size() == 2) {
jobs_.push_back({strtoint32(words[0]), strtoint32(words[1])});
} else if (words.size() == 3 && words[0] == "Qualifications") {
declared_num_workers_ = strtoint32(words[2]);
possible_jobs_per_worker_.resize(declared_num_workers_);
load_status_ = WORKERS_SEEN;
} else {
LOG(FATAL) << "Wrong state JOBS_SEEN with line " << line;
}
break;
}
case WORKERS_SEEN: {
CHECK_EQ(strtoint32(words[0]), words.size() - 1);
for (int i = 1; i < words.size(); ++i) {
const int job = strtoint32(words[i]);
const int pos = possible_jobs_per_worker_[num_workers_read_].size();
possible_jobs_per_worker_[num_workers_read_].push_back(job);
possible_assignments_per_job_[job].push_back(
{num_workers_read_, pos});
}
num_workers_read_++;
break;
}
}
}
std::vector<Job> jobs_;
std::vector<std::vector<int>> possible_jobs_per_worker_;
std::vector<std::vector<Assignment>> possible_assignments_per_job_;
LoadStatus load_status_;
int declared_num_jobs_;
int declared_num_workers_;
int num_workers_read_;
};
bool Overlaps(const ShiftMinimizationParser::Job& j1,
const ShiftMinimizationParser::Job& j2) {
// TODO(user): Are end date inclusive or exclusive? To check.
// For now, we assume that they are exclusive.
return !(j1.start > j2.end || j2.start > j1.end);
}
void LoadAndSolve(const std::string& file_name) {
ShiftMinimizationParser parser;
CHECK(parser.LoadFile(file_name));
CpModelBuilder cp_model;
const int num_workers = parser.possible_jobs_per_worker().size();
const std::vector<ShiftMinimizationParser::Job>& jobs = parser.jobs();
const int num_jobs = jobs.size();
std::vector<BoolVar> active_workers(num_workers);
std::vector<std::vector<BoolVar>> worker_job_vars(num_workers);
std::vector<std::vector<BoolVar>> possible_workers_per_job(num_jobs);
for (int w = 0; w < num_workers; ++w) {
// Status variables for workers, are they active or not?
active_workers[w] = cp_model.NewBoolVar();
// Job-Worker variable. worker_job_vars[w][i] is true iff worker w
// performs it's ith possible job.
const std::vector<int>& possible = parser.possible_jobs_per_worker()[w];
for (int p : possible) {
const BoolVar var = cp_model.NewBoolVar();
worker_job_vars[w].push_back(var);
possible_workers_per_job[p].push_back(var);
}
// Add conflicts on overlapping jobs for the same worker.
for (int i = 0; i < possible.size() - 1; ++i) {
for (int j = i + 1; j < possible.size(); ++j) {
const int job1 = possible[i];
const int job2 = possible[j];
if (Overlaps(jobs[job1], jobs[job2])) {
const BoolVar v1 = worker_job_vars[w][i];
const BoolVar v2 = worker_job_vars[w][j];
cp_model.AddBoolOr({~v1, ~v2});
}
}
}
// Maintain active_workers variable.
cp_model.AddBoolOr(worker_job_vars[w]).OnlyEnforceIf(active_workers[w]);
for (const BoolVar& var : worker_job_vars[w]) {
cp_model.AddImplication(var, active_workers[w]);
}
}
// All jobs must be performed.
for (int j = 0; j < num_jobs; ++j) {
// this does not enforce that at most one worker performs one job.
// It should not change the solution cost.
// TODO(user): Checks if sum() == 1 improves the solving speed.
cp_model.AddBoolOr(possible_workers_per_job[j]);
}
// Redundant constraint:
// For each time point, count the number of active jobs at that time,
// then the number of active workers on these jobs is equal to the number of
// active jobs.
absl::btree_set<int> time_points;
absl::btree_set<std::vector<int>> visited_job_lists;
for (int j = 0; j < num_jobs; ++j) {
time_points.insert(parser.jobs()[j].start);
time_points.insert(parser.jobs()[j].end);
}
int num_count_constraints = 0;
int max_intersection_size = 0;
// Add one counting constraint per time point.
for (int t : time_points) {
// Collect all jobs that intersects with this time point.
std::vector<int> intersecting_jobs;
for (int j = 0; j < num_jobs; ++j) {
const ShiftMinimizationParser::Job& job = parser.jobs()[j];
// Assumption: End date are inclusive.
if (t >= job.start && t <= job.end) {
intersecting_jobs.push_back(j);
}
}
// Check that we have not already visited this exact set of candidate jobs.
if (visited_job_lists.contains(intersecting_jobs)) continue;
visited_job_lists.insert(intersecting_jobs);
// Collect the relevant worker job vars.
std::vector<BoolVar> overlapping_worker_jobs;
for (int j : intersecting_jobs) {
for (const auto& p : parser.possible_assignments_per_job()[j]) {
const BoolVar var = worker_job_vars[p.worker_id][p.job_index];
overlapping_worker_jobs.push_back(var);
}
}
// Add the count constraints: We have as many active workers as jobs.
const int num_jobs = intersecting_jobs.size();
cp_model.AddEquality(LinearExpr::Sum(overlapping_worker_jobs), num_jobs);
// Book keeping.
max_intersection_size = std::max(max_intersection_size, num_jobs);
num_count_constraints++;
}
LOG(INFO) << "Added " << num_count_constraints
<< " count constraints while processing " << time_points.size()
<< " time points.";
LOG(INFO) << "Lower bound = " << max_intersection_size;
// Objective.
const IntVar objective_var =
cp_model.NewIntVar(Domain(max_intersection_size, num_workers));
cp_model.AddEquality(LinearExpr::Sum(active_workers), objective_var);
cp_model.Minimize(objective_var);
// Solve.
Model model;
model.Add(NewSatParameters(absl::GetFlag(FLAGS_params)));
const CpSolverResponse response = SolveCpModel(cp_model.Build(), &model);
LOG(INFO) << CpSolverResponseStats(response);
}
} // namespace sat
} // namespace operations_research
int main(int argc, char** argv) {
absl::SetFlag(&FLAGS_stderrthreshold, 0);
InitGoogle(argv[0], &argc, &argv, true);
if (absl::GetFlag(FLAGS_input).empty()) {
LOG(FATAL) << "Please supply a data file with --input=";
}
operations_research::sat::LoadAndSolve(absl::GetFlag(FLAGS_input));
return EXIT_SUCCESS;
}