remove jobshop_earlytardy; add support for earliness in jobshop_sat; improve jobshop_scheduling_parser to support jet file (Jobshop Early Tardy)
This commit is contained in:
@@ -71,9 +71,6 @@ foreach(TEST
|
||||
#frequency_assignment_problem
|
||||
golomb
|
||||
integer_programming
|
||||
#jobshop
|
||||
#jobshop_earlytardy
|
||||
#jobshop_ls
|
||||
#jobshop_sat
|
||||
knapsack
|
||||
linear_assignment_api
|
||||
|
||||
@@ -1,412 +0,0 @@
|
||||
// Copyright 2010-2018 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.
|
||||
//
|
||||
// This model implements a simple jobshop problem with
|
||||
// earliness-tardiness costs.
|
||||
//
|
||||
// A earliness-tardiness jobshop is a standard scheduling problem where
|
||||
// you must schedule a set of jobs on a set of machines. Each job is
|
||||
// a sequence of tasks (a task can only start when the preceding task
|
||||
// finished), each of which occupies a single specific machine during
|
||||
// a specific duration. Therefore, a job is a sequence of pairs
|
||||
// (machine id, duration), along with a release data (minimum start
|
||||
// date of the first task of the job, and due data (end time of the
|
||||
// last job) with a tardiness linear penalty.
|
||||
|
||||
// The objective is to minimize the sum of early-tardy penalties for each job.
|
||||
//
|
||||
// This will be modelled by sets of intervals variables (see class
|
||||
// IntervalVar in constraint_solver/constraint_solver.h), one per
|
||||
// task, representing the [start_time, end_time] of the task. Tasks
|
||||
// in the same job will be linked by precedence constraints. Tasks on
|
||||
// the same machine will be covered by Sequence constraints.
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "examples/cpp/jobshop_earlytardy.h"
|
||||
#include "examples/cpp/jobshop_ls.h"
|
||||
#include "ortools/base/commandlineflags.h"
|
||||
#include "ortools/base/integral_types.h"
|
||||
#include "ortools/base/logging.h"
|
||||
#include "ortools/base/stringprintf.h"
|
||||
#include "ortools/constraint_solver/constraint_solver.h"
|
||||
#include "ortools/linear_solver/linear_solver.h"
|
||||
#include "ortools/util/string_array.h"
|
||||
|
||||
DEFINE_string(
|
||||
jet_file, "",
|
||||
"Required: input file description the scheduling problem to solve, "
|
||||
"in our jet format:\n"
|
||||
" - the first line is \"<number of jobs> <number of machines>\"\n"
|
||||
" - then one line per job, with a single space-separated "
|
||||
"list of \"<machine index> <duration>\", ended by due_date, early_cost,"
|
||||
"late_cost\n"
|
||||
"note: jobs with one task are not supported");
|
||||
DEFINE_int32(machine_count, 10, "Machine count");
|
||||
DEFINE_int32(job_count, 10, "Job count");
|
||||
DEFINE_int32(max_release_date, 0, "Max release date");
|
||||
DEFINE_int32(max_early_cost, 0, "Max earliness weight");
|
||||
DEFINE_int32(max_tardy_cost, 3, "Max tardiness weight");
|
||||
DEFINE_int32(max_duration, 10, "Max duration of a task");
|
||||
DEFINE_int32(scale_factor, 130, "Scale factor (in percent)");
|
||||
DEFINE_int32(seed, 1, "Random seed");
|
||||
DEFINE_int32(time_limit_in_ms, 0, "Time limit in ms, 0 means no limit.");
|
||||
DEFINE_bool(time_placement, false, "Use MIP based time placement");
|
||||
DEFINE_int32(shuffle_length, 4, "Length of sub-sequences to shuffle LS.");
|
||||
DEFINE_int32(sub_sequence_length, 4,
|
||||
"Length of sub-sequences to relax in LNS.");
|
||||
DEFINE_int32(lns_seed, 1, "Seed of the LNS random search");
|
||||
DEFINE_int32(lns_limit, 30,
|
||||
"Limit the size of the search tree in a LNS fragment");
|
||||
DEFINE_bool(use_ls, false, "Use ls");
|
||||
DECLARE_bool(log_prefix);
|
||||
|
||||
namespace operations_research {
|
||||
class TimePlacement : public DecisionBuilder {
|
||||
public:
|
||||
TimePlacement(const EtJobShopData& data,
|
||||
const std::vector<SequenceVar*>& all_sequences,
|
||||
const std::vector<std::vector<IntervalVar*> >& jobs_to_tasks)
|
||||
: data_(data),
|
||||
all_sequences_(all_sequences),
|
||||
jobs_to_tasks_(jobs_to_tasks),
|
||||
mp_solver_("TimePlacement", MPSolver::CBC_MIXED_INTEGER_PROGRAMMING) {}
|
||||
|
||||
virtual ~TimePlacement() {}
|
||||
|
||||
virtual Decision* Next(Solver* const solver) {
|
||||
mp_solver_.Clear();
|
||||
std::vector<std::vector<MPVariable*> > all_vars;
|
||||
std::unordered_map<IntervalVar*, MPVariable*> mapping;
|
||||
const double infinity = mp_solver_.infinity();
|
||||
all_vars.resize(all_sequences_.size());
|
||||
|
||||
// Creates the MP Variables.
|
||||
for (int s = 0; s < jobs_to_tasks_.size(); ++s) {
|
||||
for (int t = 0; t < jobs_to_tasks_[s].size(); ++t) {
|
||||
IntervalVar* const task = jobs_to_tasks_[s][t];
|
||||
const std::string name = StringPrintf("J%dT%d", s, t);
|
||||
MPVariable* const var =
|
||||
mp_solver_.MakeIntVar(task->StartMin(), task->StartMax(), name);
|
||||
mapping[task] = var;
|
||||
}
|
||||
}
|
||||
|
||||
// Adds the jobs precedence constraints.
|
||||
for (int j = 0; j < jobs_to_tasks_.size(); ++j) {
|
||||
for (int t = 0; t < jobs_to_tasks_[j].size() - 1; ++t) {
|
||||
IntervalVar* const first_task = jobs_to_tasks_[j][t];
|
||||
const int duration = first_task->DurationMax();
|
||||
IntervalVar* const second_task = jobs_to_tasks_[j][t + 1];
|
||||
MPVariable* const first_var = mapping[first_task];
|
||||
MPVariable* const second_var = mapping[second_task];
|
||||
MPConstraint* const ct =
|
||||
mp_solver_.MakeRowConstraint(duration, infinity);
|
||||
ct->SetCoefficient(second_var, 1.0);
|
||||
ct->SetCoefficient(first_var, -1.0);
|
||||
}
|
||||
}
|
||||
|
||||
// Adds the ranked machines constraints.
|
||||
for (int s = 0; s < all_sequences_.size(); ++s) {
|
||||
SequenceVar* const sequence = all_sequences_[s];
|
||||
std::vector<int> rank_firsts;
|
||||
std::vector<int> rank_lasts;
|
||||
std::vector<int> unperformed;
|
||||
sequence->FillSequence(&rank_firsts, &rank_lasts, &unperformed);
|
||||
CHECK_EQ(0, rank_lasts.size());
|
||||
CHECK_EQ(0, unperformed.size());
|
||||
for (int i = 0; i < rank_firsts.size() - 1; ++i) {
|
||||
IntervalVar* const first_task = sequence->Interval(rank_firsts[i]);
|
||||
const int duration = first_task->DurationMax();
|
||||
IntervalVar* const second_task = sequence->Interval(rank_firsts[i + 1]);
|
||||
MPVariable* const first_var = mapping[first_task];
|
||||
MPVariable* const second_var = mapping[second_task];
|
||||
MPConstraint* const ct =
|
||||
mp_solver_.MakeRowConstraint(duration, infinity);
|
||||
ct->SetCoefficient(second_var, 1.0);
|
||||
ct->SetCoefficient(first_var, -1.0);
|
||||
}
|
||||
}
|
||||
|
||||
// Creates penalty terms and objective.
|
||||
std::vector<MPVariable*> terms;
|
||||
mp_solver_.MakeIntVarArray(jobs_to_tasks_.size(), 0, infinity, "terms",
|
||||
&terms);
|
||||
for (int j = 0; j < jobs_to_tasks_.size(); ++j) {
|
||||
mp_solver_.MutableObjective()->SetCoefficient(terms[j], 1.0);
|
||||
}
|
||||
mp_solver_.MutableObjective()->SetMinimization();
|
||||
|
||||
// Forces penalty terms to be above late and early costs.
|
||||
for (int j = 0; j < jobs_to_tasks_.size(); ++j) {
|
||||
IntervalVar* const last_task = jobs_to_tasks_[j].back();
|
||||
const int duration = last_task->DurationMin();
|
||||
MPVariable* const mp_start = mapping[last_task];
|
||||
const Job& job = data_.GetJob(j);
|
||||
const int ideal_start = job.due_date - duration;
|
||||
const int early_offset = job.early_cost * ideal_start;
|
||||
MPConstraint* const early_ct =
|
||||
mp_solver_.MakeRowConstraint(early_offset, infinity);
|
||||
early_ct->SetCoefficient(terms[j], 1);
|
||||
early_ct->SetCoefficient(mp_start, job.early_cost);
|
||||
|
||||
const int tardy_offset = job.tardy_cost * ideal_start;
|
||||
MPConstraint* const tardy_ct =
|
||||
mp_solver_.MakeRowConstraint(-tardy_offset, infinity);
|
||||
tardy_ct->SetCoefficient(terms[j], 1);
|
||||
tardy_ct->SetCoefficient(mp_start, -job.tardy_cost);
|
||||
}
|
||||
|
||||
// Solve.
|
||||
CHECK_EQ(MPSolver::OPTIMAL, mp_solver_.Solve());
|
||||
|
||||
// Inject MIP solution into the CP part.
|
||||
VLOG(1) << "MP cost = " << mp_solver_.Objective().Value();
|
||||
for (int j = 0; j < jobs_to_tasks_.size(); ++j) {
|
||||
for (int t = 0; t < jobs_to_tasks_[j].size(); ++t) {
|
||||
IntervalVar* const first_task = jobs_to_tasks_[j][t];
|
||||
MPVariable* const first_var = mapping[first_task];
|
||||
const int date = first_var->solution_value();
|
||||
first_task->SetStartRange(date, date);
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
virtual std::string DebugString() const { return "TimePlacement"; }
|
||||
|
||||
private:
|
||||
const EtJobShopData& data_;
|
||||
const std::vector<SequenceVar*>& all_sequences_;
|
||||
const std::vector<std::vector<IntervalVar*> >& jobs_to_tasks_;
|
||||
MPSolver mp_solver_;
|
||||
};
|
||||
|
||||
void EtJobShop(const EtJobShopData& data) {
|
||||
Solver solver("et_jobshop");
|
||||
const int machine_count = data.machine_count();
|
||||
const int job_count = data.job_count();
|
||||
const int horizon = data.horizon();
|
||||
|
||||
// ----- Creates all Intervals and vars -----
|
||||
|
||||
// Stores all tasks attached interval variables per job.
|
||||
std::vector<std::vector<IntervalVar*> > jobs_to_tasks(job_count);
|
||||
// machines_to_tasks stores the same interval variables as above, but
|
||||
// grouped my machines instead of grouped by jobs.
|
||||
std::vector<std::vector<IntervalVar*> > machines_to_tasks(machine_count);
|
||||
|
||||
// Creates all individual interval variables.
|
||||
for (int job_id = 0; job_id < job_count; ++job_id) {
|
||||
const Job& job = data.GetJob(job_id);
|
||||
const std::vector<Task>& tasks = job.all_tasks;
|
||||
for (int task_index = 0; task_index < tasks.size(); ++task_index) {
|
||||
const Task& task = tasks[task_index];
|
||||
CHECK_EQ(job_id, task.job_id);
|
||||
const std::string name =
|
||||
StringPrintf("J%dM%dI%dD%d", task.job_id, task.machine_id, task_index,
|
||||
task.duration);
|
||||
IntervalVar* const one_task = solver.MakeFixedDurationIntervalVar(
|
||||
0, horizon, task.duration, false, name);
|
||||
jobs_to_tasks[task.job_id].push_back(one_task);
|
||||
machines_to_tasks[task.machine_id].push_back(one_task);
|
||||
}
|
||||
}
|
||||
|
||||
// ----- Creates model -----
|
||||
|
||||
// Creates precedences inside jobs.
|
||||
for (int job_id = 0; job_id < job_count; ++job_id) {
|
||||
const int task_count = jobs_to_tasks[job_id].size();
|
||||
for (int task_index = 0; task_index < task_count - 1; ++task_index) {
|
||||
IntervalVar* const t1 = jobs_to_tasks[job_id][task_index];
|
||||
IntervalVar* const t2 = jobs_to_tasks[job_id][task_index + 1];
|
||||
Constraint* const prec =
|
||||
solver.MakeIntervalVarRelation(t2, Solver::STARTS_AFTER_END, t1);
|
||||
solver.AddConstraint(prec);
|
||||
}
|
||||
}
|
||||
|
||||
// Add release date.
|
||||
for (int job_id = 0; job_id < job_count; ++job_id) {
|
||||
const Job& job = data.GetJob(job_id);
|
||||
IntervalVar* const t = jobs_to_tasks[job_id][0];
|
||||
Constraint* const prec = solver.MakeIntervalVarRelation(
|
||||
t, Solver::STARTS_AFTER, job.release_date);
|
||||
solver.AddConstraint(prec);
|
||||
}
|
||||
|
||||
std::vector<IntVar*> penalties;
|
||||
for (int job_id = 0; job_id < job_count; ++job_id) {
|
||||
const Job& job = data.GetJob(job_id);
|
||||
IntervalVar* const t = jobs_to_tasks[job_id][machine_count - 1];
|
||||
IntVar* const penalty =
|
||||
solver
|
||||
.MakeConvexPiecewiseExpr(t->EndExpr(), job.early_cost, job.due_date,
|
||||
job.due_date, job.tardy_cost)
|
||||
->Var();
|
||||
penalties.push_back(penalty);
|
||||
}
|
||||
|
||||
// Adds disjunctive constraints on unary resources, and creates
|
||||
// sequence variables. A sequence variable is a dedicated variable
|
||||
// whose job is to sequence interval variables.
|
||||
std::vector<SequenceVar*> all_sequences;
|
||||
for (int machine_id = 0; machine_id < machine_count; ++machine_id) {
|
||||
const std::string name = StringPrintf("Machine_%d", machine_id);
|
||||
DisjunctiveConstraint* const ct =
|
||||
solver.MakeDisjunctiveConstraint(machines_to_tasks[machine_id], name);
|
||||
solver.AddConstraint(ct);
|
||||
all_sequences.push_back(ct->MakeSequenceVar());
|
||||
}
|
||||
|
||||
// Objective: minimize the weighted penalties.
|
||||
IntVar* const objective_var = solver.MakeSum(penalties)->Var();
|
||||
OptimizeVar* const objective_monitor = solver.MakeMinimize(objective_var, 1);
|
||||
|
||||
// ----- Search monitors and decision builder -----
|
||||
|
||||
// This decision builder will rank all tasks on all machines.
|
||||
DecisionBuilder* const sequence_phase =
|
||||
solver.MakePhase(all_sequences, Solver::CHOOSE_MIN_SLACK_RANK_FORWARD);
|
||||
|
||||
// After the ranking of tasks, the schedule is still loose and any
|
||||
// task can be postponed at will. But, because the problem is now a PERT
|
||||
// (http://en.wikipedia.org/wiki/Program_Evaluation_and_Review_Technique),
|
||||
// we can schedule each task at its earliest start time. This is
|
||||
// conveniently done by fixing the objective variable to its
|
||||
// minimum value.
|
||||
DecisionBuilder* const obj_phase =
|
||||
FLAGS_time_placement
|
||||
? solver.RevAlloc(
|
||||
new TimePlacement(data, all_sequences, jobs_to_tasks))
|
||||
: solver.MakePhase(objective_var, Solver::CHOOSE_FIRST_UNBOUND,
|
||||
Solver::ASSIGN_MIN_VALUE);
|
||||
|
||||
if (FLAGS_use_ls) {
|
||||
Assignment* const first_solution = solver.MakeAssignment();
|
||||
first_solution->Add(all_sequences);
|
||||
first_solution->AddObjective(objective_var);
|
||||
// Store the first solution in the 'solution' object.
|
||||
DecisionBuilder* const store_db =
|
||||
solver.MakeStoreAssignment(first_solution);
|
||||
|
||||
// The main decision builder (ranks all tasks, then fixes the
|
||||
// objective_variable).
|
||||
DecisionBuilder* const first_solution_phase =
|
||||
solver.Compose(sequence_phase, obj_phase, store_db);
|
||||
|
||||
LOG(INFO) << "Looking for the first solution";
|
||||
const bool first_solution_found = solver.Solve(first_solution_phase);
|
||||
if (first_solution_found) {
|
||||
LOG(INFO) << "Solution found with penalty cost of = "
|
||||
<< first_solution->ObjectiveValue();
|
||||
} else {
|
||||
LOG(INFO) << "No initial solution found!";
|
||||
return;
|
||||
}
|
||||
|
||||
LOG(INFO) << "Switching to local search";
|
||||
std::vector<LocalSearchOperator*> operators;
|
||||
LOG(INFO) << " - use swap operator";
|
||||
LocalSearchOperator* const swap_operator =
|
||||
solver.RevAlloc(new SwapIntervals(all_sequences));
|
||||
operators.push_back(swap_operator);
|
||||
LOG(INFO) << " - use shuffle operator with a max length of "
|
||||
<< FLAGS_shuffle_length;
|
||||
LocalSearchOperator* const shuffle_operator = solver.RevAlloc(
|
||||
new ShuffleIntervals(all_sequences, FLAGS_shuffle_length));
|
||||
operators.push_back(shuffle_operator);
|
||||
LOG(INFO) << " - use free sub sequences of length "
|
||||
<< FLAGS_sub_sequence_length << " lns operator";
|
||||
LocalSearchOperator* const lns_operator = solver.RevAlloc(new SequenceLns(
|
||||
all_sequences, FLAGS_lns_seed, FLAGS_sub_sequence_length));
|
||||
operators.push_back(lns_operator);
|
||||
|
||||
// Creates the local search decision builder.
|
||||
LocalSearchOperator* const concat =
|
||||
solver.ConcatenateOperators(operators, true);
|
||||
|
||||
SearchLimit* const ls_limit =
|
||||
solver.MakeLimit(kint64max, FLAGS_lns_limit, kint64max, kint64max);
|
||||
DecisionBuilder* const random_sequence_phase =
|
||||
solver.MakePhase(all_sequences, Solver::CHOOSE_RANDOM_RANK_FORWARD);
|
||||
DecisionBuilder* const ls_db = solver.MakeSolveOnce(
|
||||
solver.Compose(random_sequence_phase, obj_phase), ls_limit);
|
||||
|
||||
LocalSearchPhaseParameters* const parameters =
|
||||
solver.MakeLocalSearchPhaseParameters(concat, ls_db);
|
||||
DecisionBuilder* const final_db =
|
||||
solver.MakeLocalSearchPhase(first_solution, parameters);
|
||||
|
||||
OptimizeVar* const objective_monitor =
|
||||
solver.MakeMinimize(objective_var, 1);
|
||||
|
||||
// Search log.
|
||||
const int kLogFrequency = 1000000;
|
||||
SearchMonitor* const search_log =
|
||||
solver.MakeSearchLog(kLogFrequency, objective_monitor);
|
||||
|
||||
SearchLimit* const limit =
|
||||
FLAGS_time_limit_in_ms > 0
|
||||
? solver.MakeTimeLimit(FLAGS_time_limit_in_ms)
|
||||
: NULL;
|
||||
|
||||
// Search.
|
||||
solver.Solve(final_db, search_log, objective_monitor, limit);
|
||||
} else {
|
||||
// The main decision builder (ranks all tasks, then fixes the
|
||||
// objective_variable).
|
||||
DecisionBuilder* const main_phase =
|
||||
solver.Compose(sequence_phase, obj_phase);
|
||||
|
||||
// Search log.
|
||||
const int kLogFrequency = 1000000;
|
||||
SearchMonitor* const search_log =
|
||||
solver.MakeSearchLog(kLogFrequency, objective_monitor);
|
||||
|
||||
SearchLimit* limit = NULL;
|
||||
if (FLAGS_time_limit_in_ms > 0) {
|
||||
limit = solver.MakeTimeLimit(FLAGS_time_limit_in_ms);
|
||||
}
|
||||
|
||||
// Search.
|
||||
solver.Solve(main_phase, search_log, objective_monitor, limit);
|
||||
}
|
||||
}
|
||||
} // namespace operations_research
|
||||
|
||||
static const char kUsage[] =
|
||||
"Usage: see flags.\nThis program runs a simple job shop optimization "
|
||||
"output besides the debug LOGs of the solver.";
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
FLAGS_log_prefix = false;
|
||||
gflags::SetUsageMessage(kUsage);
|
||||
gflags::ParseCommandLineFlags(&argc, &argv, true);
|
||||
operations_research::EtJobShopData data;
|
||||
if (!FLAGS_jet_file.empty()) {
|
||||
data.LoadJetFile(FLAGS_jet_file);
|
||||
} else {
|
||||
data.GenerateRandomData(FLAGS_machine_count, FLAGS_job_count,
|
||||
FLAGS_max_release_date, FLAGS_max_early_cost,
|
||||
FLAGS_max_tardy_cost, FLAGS_max_duration,
|
||||
FLAGS_scale_factor, FLAGS_seed);
|
||||
}
|
||||
operations_research::EtJobShop(data);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
// Copyright 2010-2018 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.
|
||||
//
|
||||
// This model implements a simple jobshop problem with
|
||||
// earlyness-tardiness costs.
|
||||
//
|
||||
// A earlyness-tardinessjobshop is a standard scheduling problem where
|
||||
// you must schedule a set of jobs on a set of machines. Each job is
|
||||
// a sequence of tasks (a task can only start when the preceding task
|
||||
// finished), each of which occupies a single specific machine during
|
||||
// a specific duration. Therefore, a job is a sequence of pairs
|
||||
// (machine id, duration), along with a release data (minimum start
|
||||
// date of the first task of the job, and due data (end time of the
|
||||
// last job) with a tardiness linear penalty.
|
||||
|
||||
// The objective is to minimize the sum of early-tardy penalties for each job.
|
||||
//
|
||||
// This will be modelled by sets of intervals variables (see class
|
||||
// IntervalVar in constraint_solver/constraint_solver.h), one per
|
||||
// task, representing the [start_time, end_time] of the task. Tasks
|
||||
// in the same job will be linked by precedence constraints. Tasks on
|
||||
// the same machine will be covered by Sequence constraints.
|
||||
|
||||
#ifndef OR_TOOLS_EXAMPLES_JOBSHOP_EARLYTARDY_H_
|
||||
#define OR_TOOLS_EXAMPLES_JOBSHOP_EARLYTARDY_H_
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "ortools/base/filelineiter.h"
|
||||
#include "ortools/base/integral_types.h"
|
||||
#include "ortools/base/logging.h"
|
||||
#include "ortools/base/random.h"
|
||||
#include "ortools/base/split.h"
|
||||
#include "ortools/base/stringprintf.h"
|
||||
#include "ortools/base/strtoint.h"
|
||||
|
||||
namespace operations_research {
|
||||
struct Task {
|
||||
Task(int j, int m, int d) : job_id(j), machine_id(m), duration(d) {}
|
||||
int job_id;
|
||||
int machine_id;
|
||||
int duration;
|
||||
};
|
||||
|
||||
struct Job {
|
||||
Job(int r, int d, int ew, int tw)
|
||||
: release_date(r), due_date(d), early_cost(ew), tardy_cost(tw) {}
|
||||
int release_date;
|
||||
int due_date;
|
||||
int early_cost;
|
||||
int tardy_cost;
|
||||
std::vector<Task> all_tasks;
|
||||
};
|
||||
|
||||
class EtJobShopData {
|
||||
public:
|
||||
EtJobShopData() : machine_count_(0), job_count_(0), horizon_(0) {}
|
||||
|
||||
~EtJobShopData() {}
|
||||
|
||||
void LoadJetFile(const std::string& filename) {
|
||||
LOG(INFO) << "Reading jet file " << filename;
|
||||
name_ = StringPrintf("JetData(%s)", filename.c_str());
|
||||
for (const std::string& line : FileLines(filename)) {
|
||||
if (line.empty()) {
|
||||
continue;
|
||||
}
|
||||
ProcessNewJetLine(line);
|
||||
}
|
||||
}
|
||||
|
||||
void GenerateRandomData(int machine_count, int job_count,
|
||||
int max_release_date, int max_early_cost,
|
||||
int max_tardy_cost, int max_duration,
|
||||
int scale_factor, int seed) {
|
||||
name_ =
|
||||
StringPrintf("EtJobshop(m%d-j%d-mrd%d-mew%d-mtw%d-md%d-sf%d-s%d)",
|
||||
machine_count, job_count, max_release_date, max_early_cost,
|
||||
max_tardy_cost, max_duration, scale_factor, seed);
|
||||
LOG(INFO) << "Generating random problem " << name_;
|
||||
ACMRandom random(seed);
|
||||
machine_count_ = machine_count;
|
||||
job_count_ = job_count;
|
||||
for (int j = 0; j < job_count_; ++j) {
|
||||
const int release_date = random.Uniform(max_release_date);
|
||||
int sum_of_durations = max_release_date;
|
||||
all_jobs_.push_back(Job(release_date,
|
||||
0, // due date, to be filled later.
|
||||
random.Uniform(max_early_cost),
|
||||
random.Uniform(max_tardy_cost)));
|
||||
for (int m = 0; m < machine_count_; ++m) {
|
||||
const int duration = random.Uniform(max_duration);
|
||||
all_jobs_.back().all_tasks.push_back(Task(j, m, duration));
|
||||
sum_of_durations += duration;
|
||||
}
|
||||
all_jobs_.back().due_date = sum_of_durations * scale_factor / 100;
|
||||
horizon_ += all_jobs_.back().due_date;
|
||||
// Scramble jobs.
|
||||
for (int m = 0; m < machine_count_; ++m) {
|
||||
Task t = all_jobs_.back().all_tasks[m];
|
||||
const int target = random.Uniform(machine_count_);
|
||||
all_jobs_.back().all_tasks[m] = all_jobs_.back().all_tasks[target];
|
||||
all_jobs_.back().all_tasks[target] = t;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The number of machines in the jobshop.
|
||||
int machine_count() const { return machine_count_; }
|
||||
|
||||
// The number of jobs in the jobshop.
|
||||
int job_count() const { return job_count_; }
|
||||
|
||||
// The name of the jobshop instance.
|
||||
const std::string& name() const { return name_; }
|
||||
|
||||
// The horizon of the workshop (the sum of all durations), which is
|
||||
// a trivial upper bound of the optimal make_span.
|
||||
int horizon() const { return horizon_; }
|
||||
|
||||
// Returns the tasks of a job, ordered by precedence.
|
||||
const Job& GetJob(int job_id) const { return all_jobs_[job_id]; }
|
||||
|
||||
private:
|
||||
void ProcessNewJetLine(const std::string& line) {
|
||||
// TODO(user): more robust logic to support single-task jobs.
|
||||
static const char kWordDelimiters[] = " ";
|
||||
std::vector<std::string> words =
|
||||
absl::StrSplit(line, " ", absl::SkipEmpty());
|
||||
|
||||
if (words.size() == 2) {
|
||||
job_count_ = atoi32(words[0]);
|
||||
machine_count_ = atoi32(words[1]);
|
||||
CHECK_GT(machine_count_, 0);
|
||||
CHECK_GT(job_count_, 0);
|
||||
LOG(INFO) << machine_count_ << " - machines and " << job_count_
|
||||
<< " jobs";
|
||||
} else if (words.size() > 2 && machine_count_ != 0) {
|
||||
const int job_id = all_jobs_.size();
|
||||
CHECK_EQ(words.size(), machine_count_ * 2 + 3);
|
||||
const int due_date = atoi32(words[2 * machine_count_]);
|
||||
const int early_cost = atoi32(words[2 * machine_count_ + 1]);
|
||||
const int late_cost = atoi32(words[2 * machine_count_ + 2]);
|
||||
LOG(INFO) << "Add job with due date = " << due_date
|
||||
<< ", early cost = " << early_cost
|
||||
<< ", and late cost = " << late_cost;
|
||||
all_jobs_.push_back(Job(0, due_date, early_cost, late_cost));
|
||||
for (int i = 0; i < machine_count_; ++i) {
|
||||
const int machine_id = atoi32(words[2 * i]);
|
||||
const int duration = atoi32(words[2 * i + 1]);
|
||||
all_jobs_.back().all_tasks.push_back(
|
||||
Task(job_id, machine_id, duration));
|
||||
horizon_ += duration;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string name_;
|
||||
int machine_count_;
|
||||
int job_count_;
|
||||
int horizon_;
|
||||
std::vector<Job> all_jobs_;
|
||||
};
|
||||
} // namespace operations_research
|
||||
#endif // OR_TOOLS_EXAMPLES_JOBSHOP_EARLYTARDY_H_
|
||||
@@ -228,8 +228,6 @@ void Solve(const JsspInputProblem& problem) {
|
||||
cp_model.AddLessOrEqual(previous_end, makespan);
|
||||
}
|
||||
|
||||
// Earliness costs are not supported.
|
||||
CHECK_EQ(0L, job.earliness_cost_per_time_unit());
|
||||
const int64 lateness_penalty = job.lateness_cost_per_time_unit();
|
||||
// Lateness cost.
|
||||
if (lateness_penalty != 0L) {
|
||||
@@ -249,6 +247,22 @@ void Solve(const JsspInputProblem& problem) {
|
||||
objective_coeffs.push_back(lateness_penalty);
|
||||
}
|
||||
}
|
||||
const int64 earliness_penalty = job.earliness_cost_per_time_unit();
|
||||
// Earliness cost.
|
||||
if (earliness_penalty != 0L) {
|
||||
const int64 due_date = job.early_due_date();
|
||||
if (due_date > 0) {
|
||||
const IntVar shifted_var =
|
||||
cp_model.NewIntVar(Domain(due_date - horizon, due_date));
|
||||
cp_model.AddEquality(LinearExpr::Sum({shifted_var, previous_end}),
|
||||
due_date);
|
||||
const IntVar earliness_var = cp_model.NewIntVar(all_horizon);
|
||||
cp_model.AddMaxEquality(earliness_var,
|
||||
{cp_model.NewConstant(0), shifted_var});
|
||||
objective_vars.push_back(earliness_var);
|
||||
objective_coeffs.push_back(earliness_penalty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add one no_overlap constraint per machine.
|
||||
@@ -330,6 +344,32 @@ void Solve(const JsspInputProblem& problem) {
|
||||
|
||||
const CpSolverResponse response = SolveWithModel(cp_model, &model);
|
||||
LOG(INFO) << CpSolverResponseStats(response);
|
||||
|
||||
// Check cost, recompute it from scratch.
|
||||
int64 final_cost = 0;
|
||||
if (problem.makespan_cost_per_time_unit() != 0) {
|
||||
int64 makespan = 0;
|
||||
for (IntVar v : job_ends) {
|
||||
makespan = std::max(makespan, SolutionIntegerValue(response, v));
|
||||
}
|
||||
final_cost += makespan * problem.makespan_cost_per_time_unit();
|
||||
}
|
||||
|
||||
for (int i = 0; i < job_ends.size(); ++i) {
|
||||
const int64 early_due_date = problem.jobs(i).early_due_date();
|
||||
const int64 late_due_date = problem.jobs(i).late_due_date();
|
||||
const int64 early_penalty = problem.jobs(i).earliness_cost_per_time_unit();
|
||||
const int64 late_penalty = problem.jobs(i).lateness_cost_per_time_unit();
|
||||
const int64 end = SolutionIntegerValue(response, job_ends[i]);
|
||||
if (end < early_due_date && early_penalty != 0) {
|
||||
final_cost += (early_due_date - end) * early_penalty;
|
||||
}
|
||||
if (end > late_due_date && late_penalty != 0) {
|
||||
final_cost += (end - late_due_date) * late_penalty;
|
||||
}
|
||||
}
|
||||
// TODO(user): Support alternative cost in check.
|
||||
CHECK_EQ(response.objective_value(), final_cost);
|
||||
}
|
||||
|
||||
} // namespace sat
|
||||
|
||||
@@ -427,9 +427,6 @@ test_cc_cpp: \
|
||||
SOURCE=examples/cpp/golomb.cc \
|
||||
ARGS="--size=5"
|
||||
$(MAKE) run \
|
||||
SOURCE=examples/cpp/jobshop_earlytardy.cc \
|
||||
ARGS="--machine_count=6 --job_count=6"
|
||||
$(MAKE) run \
|
||||
SOURCE=examples/cpp/jobshop_sat.cc \
|
||||
ARGS="--input=examples/data/jobshop/ft06"
|
||||
$(MAKE) run \
|
||||
|
||||
@@ -87,13 +87,21 @@ bool JsspParser::ParseFile(const std::string& filename) {
|
||||
ProcessTardinessLine(line);
|
||||
break;
|
||||
}
|
||||
case PSS: {
|
||||
ProcessPssLine(line);
|
||||
break;
|
||||
}
|
||||
case EARLY_TARDY: {
|
||||
ProcessEarlyTardyLine(line);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
LOG(FATAL) << "Should not be here.";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return parser_state_ != ERROR;
|
||||
return parser_state_ != PARSING_ERROR;
|
||||
}
|
||||
|
||||
void JsspParser::ProcessJsspLine(const std::string& line) {
|
||||
@@ -105,6 +113,13 @@ void JsspParser::ProcessJsspLine(const std::string& line) {
|
||||
problem_.set_name(words[1]);
|
||||
parser_state_ = NAME_READ;
|
||||
current_job_index_ = 0;
|
||||
} else if (words.size() == 1 && words[0] == "1") {
|
||||
problem_type_ = PSS;
|
||||
} else if (words.size() == 2) {
|
||||
SetJobs(atoi32(words[0]));
|
||||
SetMachines(atoi32(words[1]));
|
||||
problem_type_ = EARLY_TARDY;
|
||||
parser_state_ = JOB_COUNT_READ;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -127,6 +142,16 @@ void JsspParser::ProcessJsspLine(const std::string& line) {
|
||||
task->add_machine(machine_id);
|
||||
task->add_duration(duration);
|
||||
}
|
||||
if (words.size() == declared_machine_count_ * 2 + 3) {
|
||||
// Early Tardy problem in JET format.
|
||||
const int due_date = atoi32(words[declared_machine_count_ * 2]);
|
||||
const int early_cost = atoi32(words[declared_machine_count_ * 2 + 1]);
|
||||
const int late_cost = atoi32(words[declared_machine_count_ * 2 + 2]);
|
||||
job->set_early_due_date(due_date);
|
||||
job->set_late_due_date(due_date);
|
||||
job->set_earliness_cost_per_time_unit(early_cost);
|
||||
job->set_lateness_cost_per_time_unit(late_cost);
|
||||
}
|
||||
current_job_index_++;
|
||||
if (current_job_index_ == declared_job_count_) {
|
||||
parser_state_ = DONE;
|
||||
@@ -373,6 +398,133 @@ void JsspParser::ProcessTardinessLine(const std::string& line) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void JsspParser::ProcessPssLine(const std::string& line) {
|
||||
const std::vector<std::string> words =
|
||||
absl::StrSplit(line, ' ', absl::SkipEmpty());
|
||||
switch (parser_state_) {
|
||||
case START: {
|
||||
problem_.set_makespan_cost_per_time_unit(1L);
|
||||
CHECK_EQ(1, words.size());
|
||||
SetJobs(atoi32(words[0]));
|
||||
parser_state_ = JOB_COUNT_READ;
|
||||
break;
|
||||
}
|
||||
case JOB_COUNT_READ: {
|
||||
CHECK_EQ(1, words.size());
|
||||
SetMachines(atoi32(words[0]));
|
||||
parser_state_ = MACHINE_COUNT_READ;
|
||||
current_job_index_ = 0;
|
||||
break;
|
||||
}
|
||||
case MACHINE_COUNT_READ: {
|
||||
CHECK_EQ(1, words.size());
|
||||
CHECK_EQ(declared_machine_count_, atoi32(words[0]));
|
||||
if (++current_job_index_ == declared_job_count_) {
|
||||
parser_state_ = JOB_LENGTH_READ;
|
||||
current_job_index_ = 0;
|
||||
current_machine_index_ = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case JOB_LENGTH_READ: {
|
||||
CHECK_EQ(4, words.size());
|
||||
CHECK_EQ(0, atoi32(words[2]));
|
||||
CHECK_EQ(0, atoi32(words[3]));
|
||||
const int machine_id = atoi32(words[0]) - 1;
|
||||
const int duration = atoi32(words[1]);
|
||||
Job* const job = problem_.mutable_jobs(current_job_index_);
|
||||
Task* const task = job->add_tasks();
|
||||
task->add_machine(machine_id);
|
||||
task->add_duration(duration);
|
||||
if (++current_machine_index_ == declared_machine_count_) {
|
||||
current_machine_index_ = 0;
|
||||
if (++current_job_index_ == declared_job_count_) {
|
||||
current_job_index_ = -1;
|
||||
current_machine_index_ = 0;
|
||||
parser_state_ = JOBS_READ;
|
||||
transition_index_ = 0;
|
||||
for (int m = 0; m < declared_machine_count_; ++m) {
|
||||
Machine* const machine = problem_.mutable_machines(m);
|
||||
for (int i = 0; i < declared_job_count_ * declared_job_count_;
|
||||
++i) {
|
||||
machine->mutable_transition_time_matrix()->add_transition_time(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case JOBS_READ: {
|
||||
CHECK_EQ(1, words.size());
|
||||
const int index = transition_index_++;
|
||||
const int size = declared_job_count_ * declared_machine_count_ + 1;
|
||||
const int t1 = index / size;
|
||||
const int t2 = index % size;
|
||||
if (t1 == 0 || t2 == 0) { // Dummy task.
|
||||
break;
|
||||
}
|
||||
const int item1 = t1 - 1;
|
||||
const int item2 = t2 - 1;
|
||||
const int job1 = item1 / declared_machine_count_;
|
||||
const int task1 = item1 % declared_machine_count_;
|
||||
const int m1 = problem_.jobs(job1).tasks(task1).machine(0);
|
||||
const int job2 = item2 / declared_machine_count_;
|
||||
const int task2 = item2 % declared_machine_count_;
|
||||
const int m2 = problem_.jobs(job2).tasks(task2).machine(0);
|
||||
if (m1 != m2) { // We are only interested in same machine transitions.
|
||||
break;
|
||||
}
|
||||
const int transition = atoi32(words[0]);
|
||||
Machine* const machine = problem_.mutable_machines(m1);
|
||||
machine->mutable_transition_time_matrix()->set_transition_time(
|
||||
job1 * declared_job_count_ + job2, transition);
|
||||
if (transition_index_ == size * size) {
|
||||
parser_state_ = DONE;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
LOG(FATAL) << "Should not be here with state " << parser_state_
|
||||
<< "with line " << line;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void JsspParser::ProcessEarlyTardyLine(const std::string& line) {
|
||||
const std::vector<std::string> words =
|
||||
absl::StrSplit(line, ' ', absl::SkipEmpty());
|
||||
switch (parser_state_) {
|
||||
case JOB_COUNT_READ: {
|
||||
CHECK_EQ(words.size(), declared_machine_count_ * 2 + 3);
|
||||
Job* const job = problem_.mutable_jobs(current_job_index_);
|
||||
for (int i = 0; i < declared_machine_count_; ++i) {
|
||||
const int machine_id = atoi32(words[2 * i]);
|
||||
const int64 duration = atoi64(words[2 * i + 1]);
|
||||
Task* const task = job->add_tasks();
|
||||
task->add_machine(machine_id);
|
||||
task->add_duration(duration);
|
||||
}
|
||||
// Early Tardy problem in JET format.
|
||||
const int due_date = atoi32(words[declared_machine_count_ * 2]);
|
||||
const int early_cost = atoi32(words[declared_machine_count_ * 2 + 1]);
|
||||
const int late_cost = atoi32(words[declared_machine_count_ * 2 + 2]);
|
||||
job->set_early_due_date(due_date);
|
||||
job->set_late_due_date(due_date);
|
||||
job->set_earliness_cost_per_time_unit(early_cost);
|
||||
job->set_lateness_cost_per_time_unit(late_cost);
|
||||
current_job_index_++;
|
||||
if (current_job_index_ == declared_job_count_) {
|
||||
parser_state_ = DONE;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
LOG(FATAL) << "Should not be here with state " << parser_state_;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace jssp
|
||||
} // namespace data
|
||||
} // namespace operations_research
|
||||
|
||||
@@ -30,6 +30,8 @@ class JsspParser {
|
||||
FLEXIBLE,
|
||||
SDST,
|
||||
TARDINESS,
|
||||
PSS,
|
||||
EARLY_TARDY,
|
||||
};
|
||||
|
||||
enum ParserState {
|
||||
@@ -44,18 +46,10 @@ class JsspParser {
|
||||
JOBS_READ,
|
||||
SSD_READ,
|
||||
MACHINE_READ,
|
||||
ERROR,
|
||||
PARSING_ERROR,
|
||||
DONE
|
||||
};
|
||||
|
||||
JsspParser()
|
||||
: declared_machine_count_(-1),
|
||||
declared_job_count_(-1),
|
||||
current_job_index_(0),
|
||||
current_machine_index_(0),
|
||||
problem_type_(UNDEFINED),
|
||||
parser_state_(START) {}
|
||||
|
||||
~JsspParser() {}
|
||||
|
||||
// Parses a file to load a jobshop problem.
|
||||
@@ -71,17 +65,20 @@ class JsspParser {
|
||||
void ProcessFlexibleLine(const std::string& line);
|
||||
void ProcessSdstLine(const std::string& line);
|
||||
void ProcessTardinessLine(const std::string& line);
|
||||
void ProcessPssLine(const std::string& line);
|
||||
void ProcessEarlyTardyLine(const std::string& line);
|
||||
|
||||
void SetJobs(int job_count);
|
||||
void SetMachines(int machine_count);
|
||||
|
||||
JsspInputProblem problem_;
|
||||
int declared_machine_count_;
|
||||
int declared_job_count_;
|
||||
int current_job_index_;
|
||||
int current_machine_index_;
|
||||
ProblemType problem_type_;
|
||||
ParserState parser_state_;
|
||||
int declared_machine_count_ = -1;
|
||||
int declared_job_count_ = -1;
|
||||
int current_job_index_ = 0;
|
||||
int current_machine_index_ = 0;
|
||||
int transition_index_ = 0;
|
||||
ProblemType problem_type_ = UNDEFINED;
|
||||
ParserState parser_state_ = START;
|
||||
};
|
||||
|
||||
} // namespace jssp
|
||||
|
||||
Reference in New Issue
Block a user