Google OR-Tools v9.14
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
rcpsp_parser.cc
Go to the documentation of this file.
1// Copyright 2010-2025 Google LLC
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
15
16#include <cstdint>
17#include <string>
18#include <vector>
19
20#include "absl/log/check.h"
21#include "absl/log/log.h"
22#include "absl/strings/match.h"
23#include "absl/strings/numbers.h"
24#include "absl/strings/str_split.h"
25#include "absl/strings/string_view.h"
26#include "ortools/base/path.h"
29
30namespace operations_research {
31namespace scheduling {
32namespace rcpsp {
33
35 : seed_(-1),
36 load_status_(NOT_STARTED),
37 num_declared_tasks_(-1),
38 current_task_(-1),
39 unreads_(0) {
40 rcpsp_.set_deadline(-1);
41 rcpsp_.set_horizon(-1);
42}
43
44bool RcpspParser::ParseFile(const std::string& file_name) {
45 if (load_status_ != NOT_STARTED) {
46 return false;
47 }
48
49 const bool is_rcpsp_max =
50 absl::EndsWith(file_name, ".sch") || absl::EndsWith(file_name, ".SCH");
51 const bool is_patterson = absl::EndsWith(file_name, ".rcp");
52 load_status_ = HEADER_SECTION;
53
54 for (const std::string& line : FileLines(file_name)) {
55 if (is_rcpsp_max) {
56 ProcessRcpspMaxLine(line);
57 } else if (is_patterson) {
58 ProcessPattersonLine(line);
59 } else {
60 ProcessRcpspLine(line);
61 }
62 if (load_status_ == ERROR_FOUND) {
63 LOG(INFO) << rcpsp_;
64 return false;
65 }
66 }
67 VLOG(1) << "Read file: " << file_name << ", max = " << is_rcpsp_max
68 << ", patterson = " << is_patterson << ", with "
69 << rcpsp_.tasks_size() << " tasks, and " << rcpsp_.resources_size()
70 << " resources.";
71
72 // We use a temporary string as open source protobufs do not accept
73 // set_name(string_view).
74 std::string problem_name(file::Stem(file_name));
75 rcpsp_.set_name(problem_name);
76
77 // Count the extra start and end tasks.
78 return num_declared_tasks_ + 2 == rcpsp_.tasks_size() &&
79 load_status_ == PARSING_FINISHED;
80}
81
82void RcpspParser::ReportError(const std::string& line) {
83 LOG(ERROR) << "Error: status = " << load_status_ << ", line = " << line;
84 load_status_ = ERROR_FOUND;
85}
86
87void RcpspParser::SetNumDeclaredTasks(int t) {
88 num_declared_tasks_ = t;
89 recipe_sizes_.resize(t + 2, 0); // The data format adds 2 sentinels.
90}
91
92void RcpspParser::ProcessRcpspLine(const std::string& line) {
93 if (absl::StartsWith(line, "***")) return;
94 if (absl::StartsWith(line, "---")) return;
95
96 const std::vector<std::string> words =
97 absl::StrSplit(line, absl::ByAnyChar(" :\t\r"), absl::SkipEmpty());
98
99 if (words.empty()) return;
100
101 switch (load_status_) {
102 case NOT_STARTED: {
103 ReportError(line);
104 break;
105 }
106 case HEADER_SECTION: {
107 if (words[0] == "file") {
108 rcpsp_.set_basedata(words[3]);
109 } else if (words[0] == "initial") {
110 rcpsp_.set_seed(strtoint64(words[4]));
111 load_status_ = PROJECT_SECTION;
112 } else if (words[0] == "jobs") {
113 // Workaround for the mmlib files which has less headers.
114 SetNumDeclaredTasks(strtoint32(words[4]) - 2);
115 load_status_ = PROJECT_SECTION;
116 } else {
117 ReportError(line);
118 }
119 break;
120 }
121 case PROJECT_SECTION: {
122 if (words[0] == "projects") {
123 // Nothing to do.
124 } else if (words[0] == "jobs") {
125 // This declaration counts the 2 sentinels.
126 SetNumDeclaredTasks(strtoint32(words[4]) - 2);
127 } else if (words[0] == "horizon") {
128 rcpsp_.set_horizon(strtoint32(words[1]));
129 } else if (words[0] == "RESOURCES") {
130 // Nothing to do.
131 } else if (words.size() > 1 && words[1] == "renewable") {
132 for (int i = 0; i < strtoint32(words[2]); ++i) {
133 Resource* const res = rcpsp_.add_resources();
134 res->set_max_capacity(-1);
135 res->set_renewable(true);
136 res->set_unit_cost(0);
137 }
138 } else if (words.size() > 1 && words[1] == "nonrenewable") {
139 for (int i = 0; i < strtoint32(words[2]); ++i) {
140 Resource* const res = rcpsp_.add_resources();
141 res->set_max_capacity(-1);
142 res->set_min_capacity(-1);
143 res->set_renewable(false);
144 res->set_unit_cost(0);
145 }
146 } else if (words.size() > 1 && words[1] == "doubly") {
147 // Nothing to do.
148 } else if (words.size() == 2 && words[0] == "PROJECT") {
149 load_status_ = INFO_SECTION;
150 } else if (words.size() == 2 && words[0] == "PRECEDENCE") {
151 // mmlib files have no info section.
152 load_status_ = PRECEDENCE_SECTION;
153 } else {
154 ReportError(line);
155 }
156 break;
157 }
158 case INFO_SECTION: {
159 if (words[0] == "pronr.") {
160 // Nothing to do.
161 } else if (words.size() == 6) {
162 SetNumDeclaredTasks(strtoint32(words[1]));
163 rcpsp_.set_release_date(strtoint32(words[2]));
164 rcpsp_.set_due_date(strtoint32(words[3]));
165 rcpsp_.set_tardiness_cost(strtoint32(words[4]));
166 rcpsp_.set_mpm_time(strtoint32(words[5]));
167 } else if (words.size() == 2 && words[0] == "PRECEDENCE") {
168 load_status_ = PRECEDENCE_SECTION;
169 } else {
170 ReportError(line);
171 }
172 break;
173 }
174 case PRECEDENCE_SECTION: {
175 if (words[0] == "jobnr.") {
176 // Nothing to do.
177 } else if (words.size() >= 3) {
178 const int task_index = strtoint32(words[0]) - 1;
179 CHECK_EQ(task_index, rcpsp_.tasks_size());
180 recipe_sizes_[task_index] = strtoint32(words[1]);
181 const int num_successors = strtoint32(words[2]);
182 if (words.size() != 3 + num_successors) {
183 ReportError(line);
184 break;
185 }
186 Task* const task = rcpsp_.add_tasks();
187 for (int i = 0; i < num_successors; ++i) {
188 // The array of tasks is 0-based for us.
189 task->add_successors(strtoint32(words[3 + i]) - 1);
190 }
191 } else if (words[0] == "REQUESTS/DURATIONS") {
192 load_status_ = REQUEST_SECTION;
193 } else {
194 ReportError(line);
195 }
196 break;
197 }
198 case REQUEST_SECTION: {
199 if (words[0] == "jobnr.") {
200 // Nothing to do.
201 } else if (words.size() == 3 + rcpsp_.resources_size()) {
202 // Start of a new task (index is 0-based for us).
203 current_task_ = strtoint32(words[0]) - 1;
204 const int current_recipe = strtoint32(words[1]) - 1;
205 CHECK_EQ(current_recipe, rcpsp_.tasks(current_task_).recipes_size());
206 if (current_recipe != 0) {
207 ReportError(line);
208 break;
209 }
210 Recipe* const recipe =
211 rcpsp_.mutable_tasks(current_task_)->add_recipes();
212 recipe->set_duration(strtoint32(words[2]));
213 for (int i = 0; i < rcpsp_.resources_size(); ++i) {
214 const int demand = strtoint32(words[3 + i]);
215 if (demand != 0) {
216 recipe->add_demands(demand);
217 recipe->add_resources(i);
218 }
219 }
220 } else if (words.size() == 2 + rcpsp_.resources_size()) {
221 // New recipe for a current task.
222 const int current_recipe = strtoint32(words[0]) - 1;
223 CHECK_EQ(current_recipe, rcpsp_.tasks(current_task_).recipes_size());
224 Recipe* const recipe =
225 rcpsp_.mutable_tasks(current_task_)->add_recipes();
226 recipe->set_duration(strtoint32(words[1]));
227 for (int i = 0; i < rcpsp_.resources_size(); ++i) {
228 const int demand = strtoint32(words[2 + i]);
229 if (demand != 0) {
230 recipe->add_demands(demand);
231 recipe->add_resources(i);
232 }
233 }
234 } else if (words[0] == "RESOURCEAVAILABILITIES" ||
235 (words[0] == "RESOURCE" && words[1] == "AVAILABILITIES")) {
236 load_status_ = RESOURCE_SECTION;
237 } else {
238 ReportError(line);
239 }
240 break;
241 }
242 case RESOURCE_SECTION: {
243 if (words.size() == 2 * rcpsp_.resources_size()) {
244 // Nothing to do.
245 } else if (words.size() == rcpsp_.resources_size()) {
246 for (int i = 0; i < words.size(); ++i) {
247 rcpsp_.mutable_resources(i)->set_max_capacity(strtoint32(words[i]));
248 }
249 load_status_ = PARSING_FINISHED;
250 } else {
251 ReportError(line);
252 }
253 break;
254 }
255 case RESOURCE_MIN_SECTION: {
256 LOG(FATAL) << "Should not be here";
257 break;
258 }
259 case PARSING_FINISHED: {
260 break;
261 }
262 case ERROR_FOUND: {
263 break;
264 }
265 }
266}
267
268void RcpspParser::ProcessRcpspMaxLine(const std::string& line) {
269 const std::vector<std::string> words =
270 absl::StrSplit(line, absl::ByAnyChar(" :\t[]\r"), absl::SkipEmpty());
271
272 switch (load_status_) {
273 case NOT_STARTED: {
274 ReportError(line);
275 break;
276 }
277 case HEADER_SECTION: {
278 rcpsp_.set_is_rcpsp_max(true);
279 if (words.size() == 2) {
280 rcpsp_.set_is_consumer_producer(true);
281 } else if (words.size() < 4 || strtoint32(words[3]) != 0) {
282 ReportError(line);
283 break;
284 }
285
286 if (words.size() == 5) {
287 rcpsp_.set_deadline(strtoint32(words[4]));
288 rcpsp_.set_is_resource_investment(true);
289 }
290
291 SetNumDeclaredTasks(strtoint32(words[0]));
292 temp_delays_.resize(num_declared_tasks_ + 2);
293
294 // Creates resources.
295 if (rcpsp_.is_consumer_producer()) {
296 const int num_nonrenewable_resources = strtoint32(words[1]);
297 for (int i = 0; i < num_nonrenewable_resources; ++i) {
298 Resource* const res = rcpsp_.add_resources();
299 res->set_max_capacity(-1);
300 res->set_min_capacity(-1);
301 res->set_renewable(false);
302 res->set_unit_cost(0);
303 }
304 } else {
305 const int num_renewable_resources = strtoint32(words[1]);
306 const int num_nonrenewable_resources = strtoint32(words[2]);
307 for (int i = 0; i < num_renewable_resources; ++i) {
308 Resource* const res = rcpsp_.add_resources();
309 res->set_max_capacity(-1);
310 res->set_renewable(true);
311 res->set_unit_cost(0);
312 }
313 for (int i = 0; i < num_nonrenewable_resources; ++i) {
314 Resource* const res = rcpsp_.add_resources();
315 res->set_max_capacity(-1);
316 res->set_min_capacity(-1);
317 res->set_renewable(false);
318 res->set_unit_cost(0);
319 }
320 }
321
322 // Set up for the next section.
323 load_status_ = PRECEDENCE_SECTION;
324 current_task_ = 0;
325 break;
326 }
327 case PROJECT_SECTION: {
328 LOG(FATAL) << "Should not be here";
329 break;
330 }
331 case INFO_SECTION: {
332 LOG(FATAL) << "Should not be here";
333 break;
334 }
335 case PRECEDENCE_SECTION: {
336 if (words.size() < 3) {
337 ReportError(line);
338 break;
339 }
340
341 const int task_id = strtoint32(words[0]);
342 if (task_id != current_task_) {
343 ReportError(line);
344 break;
345 } else {
346 current_task_++;
347 }
348
349 const int num_recipes = strtoint32(words[1]);
350 recipe_sizes_[task_id] = num_recipes;
351 const int num_successors = strtoint32(words[2]);
352
353 Task* const task = rcpsp_.add_tasks();
354
355 // Read successors.
356 for (int i = 0; i < num_successors; ++i) {
357 task->add_successors(strtoint32(words[3 + i]));
358 }
359
360 // Read flattened delays into temp_delays_.
361 for (int i = 3 + num_successors; i < words.size(); ++i) {
362 temp_delays_[task_id].push_back(strtoint32(words[i]));
363 }
364
365 if (task_id == num_declared_tasks_ + 1) {
366 // Convert the flattened delays into structured delays (1 vector per
367 // successor) in the task_size.
368 for (int t = 1; t <= num_declared_tasks_; ++t) {
369 const int num_recipes = recipe_sizes_[t];
370 const int num_successors = rcpsp_.tasks(t).successors_size();
371 int count = 0;
372 for (int s = 0; s < num_successors; ++s) {
373 PerSuccessorDelays* const succ_delays =
374 rcpsp_.mutable_tasks(t)->add_successor_delays();
375 for (int r1 = 0; r1 < num_recipes; ++r1) {
376 PerRecipeDelays* const recipe_delays =
377 succ_delays->add_recipe_delays();
378 const int other = rcpsp_.tasks(t).successors(s);
379 const int num_other_recipes = recipe_sizes_[other];
380 for (int r2 = 0; r2 < num_other_recipes; ++r2) {
381 recipe_delays->add_min_delays(temp_delays_[t][count++]);
382 }
383 }
384 }
385 CHECK_EQ(count, temp_delays_[t].size());
386 }
387
388 // Setup for next section.
389 current_task_ = 0;
390 load_status_ = REQUEST_SECTION;
391 }
392 break;
393 }
394 case REQUEST_SECTION: {
395 if (words.size() == 3 + rcpsp_.resources_size()) {
396 // Start of a new task.
397 current_task_ = strtoint32(words[0]);
398
399 // 0 based indices for the recipe.
400 const int current_recipe = strtoint32(words[1]) - 1;
401 CHECK_EQ(current_recipe, rcpsp_.tasks(current_task_).recipes_size());
402 if (current_recipe != 0) {
403 ReportError(line);
404 break;
405 }
406 Recipe* const recipe =
407 rcpsp_.mutable_tasks(current_task_)->add_recipes();
408 recipe->set_duration(strtoint32(words[2]));
409 for (int i = 0; i < rcpsp_.resources_size(); ++i) {
410 const int demand = strtoint32(words[3 + i]);
411 if (demand != 0) {
412 recipe->add_demands(demand);
413 recipe->add_resources(i);
414 }
415 }
416 } else if (words.size() == 2 + rcpsp_.resources_size() &&
417 rcpsp_.is_consumer_producer()) {
418 // Start of a new task.
419 current_task_ = strtoint32(words[0]);
420
421 // 0 based indices for the recipe.
422 const int current_recipe = strtoint32(words[1]) - 1;
423 CHECK_EQ(current_recipe, rcpsp_.tasks(current_task_).recipes_size());
424 if (current_recipe != 0) {
425 ReportError(line);
426 break;
427 }
428 Recipe* const recipe =
429 rcpsp_.mutable_tasks(current_task_)->add_recipes();
430 recipe->set_duration(0);
431 for (int i = 0; i < rcpsp_.resources_size(); ++i) {
432 const int demand = strtoint32(words[2 + i]);
433 if (demand != 0) {
434 recipe->add_demands(demand);
435 recipe->add_resources(i);
436 }
437 }
438 } else if (words.size() == 2 + rcpsp_.resources_size()) {
439 // New recipe for a current task.
440 const int current_recipe = strtoint32(words[0]) - 1;
441 CHECK_EQ(current_recipe, rcpsp_.tasks(current_task_).recipes_size());
442 Recipe* const recipe =
443 rcpsp_.mutable_tasks(current_task_)->add_recipes();
444 recipe->set_duration(strtoint32(words[1]));
445 for (int i = 0; i < rcpsp_.resources_size(); ++i) {
446 const int demand = strtoint32(words[2 + i]);
447 if (demand != 0) {
448 recipe->add_demands(demand);
449 recipe->add_resources(i);
450 }
451 }
452 }
453 if (current_task_ == num_declared_tasks_ + 1) {
454 if (rcpsp_.is_consumer_producer()) {
455 load_status_ = RESOURCE_MIN_SECTION;
456 } else {
457 load_status_ = RESOURCE_SECTION;
458 }
459 }
460 break;
461 }
462 case RESOURCE_SECTION: {
463 if (words.size() == rcpsp_.resources_size()) {
464 for (int i = 0; i < words.size(); ++i) {
465 if (rcpsp_.is_resource_investment()) {
466 rcpsp_.mutable_resources(i)->set_unit_cost(strtoint32(words[i]));
467 } else {
468 rcpsp_.mutable_resources(i)->set_max_capacity(strtoint32(words[i]));
469 }
470 }
471 load_status_ = PARSING_FINISHED;
472 } else {
473 ReportError(line);
474 }
475 break;
476 }
477 case RESOURCE_MIN_SECTION: {
478 if (words.size() == rcpsp_.resources_size()) {
479 for (int i = 0; i < words.size(); ++i) {
480 rcpsp_.mutable_resources(i)->set_min_capacity(strtoint32(words[i]));
481 }
482 load_status_ = RESOURCE_SECTION;
483 } else {
484 ReportError(line);
485 }
486 break;
487 }
488 case PARSING_FINISHED: {
489 break;
490 }
491 case ERROR_FOUND: {
492 break;
493 }
494 }
495}
496
497void RcpspParser::ProcessPattersonLine(const std::string& line) {
498 const std::vector<std::string> words =
499 absl::StrSplit(line, absl::ByAnyChar(" :\t[]\r"), absl::SkipEmpty());
500
501 if (words.empty()) return;
502
503 switch (load_status_) {
504 case NOT_STARTED: {
505 ReportError(line);
506 break;
507 }
508 case HEADER_SECTION: {
509 if (words.size() != 2) {
510 ReportError(line);
511 break;
512 }
513 SetNumDeclaredTasks(strtoint32(words[0]) - 2); // Remove the 2 sentinels.
514
515 // Creates resources.
516 const int num_renewable_resources = strtoint32(words[1]);
517 for (int i = 0; i < num_renewable_resources; ++i) {
518 Resource* const res = rcpsp_.add_resources();
519 res->set_max_capacity(-1);
520 res->set_min_capacity(-1);
521 res->set_renewable(true);
522 res->set_unit_cost(0);
523 }
524
525 // Set up for the next section.
526 load_status_ = RESOURCE_SECTION;
527 break;
528 }
529 case PROJECT_SECTION: {
530 LOG(FATAL) << "Should not be here";
531 break;
532 }
533 case INFO_SECTION: {
534 LOG(FATAL) << "Should not be here";
535 break;
536 }
537 case PRECEDENCE_SECTION: {
538 if (unreads_ > 0) {
539 for (int i = 0; i < words.size(); ++i) {
540 rcpsp_.mutable_tasks(current_task_)
541 ->add_successors(strtoint32(words[i]) - 1);
542 unreads_--;
543 CHECK_GE(unreads_, 0);
544 }
545 } else {
546 if (words.size() < 2 + rcpsp_.resources_size()) {
547 ReportError(line);
548 break;
549 }
550 CHECK_EQ(current_task_, rcpsp_.tasks_size());
551 Task* const task = rcpsp_.add_tasks();
552 Recipe* const recipe = task->add_recipes();
553 recipe->set_duration(strtoint32(words[0]));
554
555 const int num_resources = rcpsp_.resources_size();
556 for (int i = 1; i <= num_resources; ++i) {
557 const int demand = strtoint32(words[i]);
558 if (demand != 0) {
559 recipe->add_demands(demand);
560 recipe->add_resources(i - 1);
561 }
562 }
563
564 unreads_ = strtoint32(words[1 + num_resources]);
565 for (int i = 2 + num_resources; i < words.size(); ++i) {
566 // Successors are 1 based in the data file.
567 task->add_successors(strtoint32(words[i]) - 1);
568 unreads_--;
569 CHECK_GE(unreads_, 0);
570 }
571 }
572
573 if (unreads_ == 0 && ++current_task_ == num_declared_tasks_ + 2) {
574 load_status_ = PARSING_FINISHED;
575 }
576 break;
577 }
578 case REQUEST_SECTION: {
579 LOG(FATAL) << "Should not be here";
580 break;
581 }
582 case RESOURCE_SECTION: {
583 if (words.size() == rcpsp_.resources_size()) {
584 for (int i = 0; i < words.size(); ++i) {
585 rcpsp_.mutable_resources(i)->set_max_capacity(strtoint32(words[i]));
586 }
587 load_status_ = PRECEDENCE_SECTION;
588 current_task_ = 0;
589 } else {
590 ReportError(line);
591 }
592 break;
593 }
594 case RESOURCE_MIN_SECTION: {
595 LOG(FATAL) << "Should not be here";
596 break;
597 }
598 case PARSING_FINISHED: {
599 break;
600 }
601 case ERROR_FOUND: {
602 break;
603 }
604 }
605}
606
607int RcpspParser::strtoint32(absl::string_view word) {
608 int result;
609 CHECK(absl::SimpleAtoi(word, &result));
610 return result;
611}
612
613int64_t RcpspParser::strtoint64(absl::string_view word) {
614 int64_t result;
615 CHECK(absl::SimpleAtoi(word, &result));
616 return result;
617}
618
619} // namespace rcpsp
620} // namespace scheduling
621} // namespace operations_research
bool ParseFile(const std::string &file_name)
Returns false if an error occurred.
void set_basedata(Arg_ &&arg, Args_... args)
absl::string_view Stem(absl::string_view path)
Definition path.cc:129
In SWIG mode, we don't want anything besides these top-level includes.