// Copyright 2010-2021 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. #include #include #include #include #include #include "absl/flags/flag.h" #include "absl/flags/parse.h" #include "absl/flags/usage.h" #include "absl/strings/match.h" #include "absl/strings/str_join.h" #include "google/protobuf/text_format.h" #include "google/protobuf/wrappers.pb.h" #include "ortools/base/logging.h" #include "ortools/base/timer.h" #include "ortools/data/jobshop_scheduling.pb.h" #include "ortools/data/jobshop_scheduling_parser.h" #include "ortools/graph/connected_components.h" #include "ortools/sat/cp_model.h" #include "ortools/sat/cp_model.pb.h" #include "ortools/sat/model.h" ABSL_FLAG(std::string, input, "", "Jobshop data file name."); ABSL_FLAG(std::string, params, "", "Sat parameters in text proto format."); ABSL_FLAG(bool, use_optional_variables, false, "Whether we use optional variables for bounds of an optional " "interval or not."); ABSL_FLAG(bool, use_interval_makespan, true, "Whether we encode the makespan using an interval or not."); ABSL_FLAG( bool, use_cumulative_relaxation, true, "Whether we regroup multiple machines to create a cumulative relaxation."); ABSL_FLAG( int, job_suffix_relaxation_length, 5, "The maximum length of the suffix of a job used in the linear relaxation."); ABSL_FLAG(bool, display_model, false, "Display jobshop proto before solving."); ABSL_FLAG(bool, display_sat_model, false, "Display sat proto before solving."); ABSL_FLAG(int, horizon, -1, "Override horizon computation."); using operations_research::data::jssp::Job; using operations_research::data::jssp::JobPrecedence; using operations_research::data::jssp::JsspInputProblem; using operations_research::data::jssp::Machine; using operations_research::data::jssp::Task; using operations_research::data::jssp::TransitionTimeMatrix; namespace operations_research { namespace sat { // Compute a valid horizon from a problem. int64_t ComputeHorizon(const JsspInputProblem& problem) { int64_t sum_of_durations = 0; int64_t max_latest_end = 0; int64_t max_earliest_start = 0; for (const Job& job : problem.jobs()) { if (job.has_latest_end()) { max_latest_end = std::max(max_latest_end, job.latest_end().value()); } else { max_latest_end = std::numeric_limits::max(); } if (job.has_earliest_start()) { max_earliest_start = std::max(max_earliest_start, job.earliest_start().value()); } for (const Task& task : job.tasks()) { int64_t max_duration = 0; for (int64_t d : task.duration()) { max_duration = std::max(max_duration, d); } sum_of_durations += max_duration; } } const int num_jobs = problem.jobs_size(); int64_t sum_of_transitions = 0; for (const Machine& machine : problem.machines()) { if (!machine.has_transition_time_matrix()) continue; const TransitionTimeMatrix& matrix = machine.transition_time_matrix(); for (int i = 0; i < num_jobs; ++i) { int64_t max_transition = 0; for (int j = 0; j < num_jobs; ++j) { max_transition = std::max(max_transition, matrix.transition_time(i * num_jobs + j)); } sum_of_transitions += max_transition; } } return std::min(max_latest_end, sum_of_durations + sum_of_transitions + max_earliest_start); } // A job is a sequence of tasks. For each task, we store the main interval, as // well as its start, size, and end variables. struct JobTaskData { IntervalVar interval; IntVar start; IntVar duration; IntVar end; }; // Each task in a job can have multiple alternative ways of being performed. // This structure stores the start, end, and presence variables attached to one // alternative for a given task. struct AlternativeTaskData { IntervalVar interval; IntVar start; BoolVar presence; }; // Create the job structure as a chain of tasks. Fills in the job_to_tasks // vector. void CreateJobs(const JsspInputProblem& problem, int64_t horizon, std::vector>& job_to_tasks, bool& has_variable_duration_tasks, CpModelBuilder& cp_model) { const int num_jobs = problem.jobs_size(); for (int j = 0; j < num_jobs; ++j) { const Job& job = problem.jobs(j); const int num_tasks_in_job = job.tasks_size(); std::vector& task_data = job_to_tasks[j]; const int64_t hard_start = job.has_earliest_start() ? job.earliest_start().value() : 0L; const int64_t hard_end = job.has_latest_end() ? job.latest_end().value() : horizon; for (int t = 0; t < num_tasks_in_job; ++t) { const Task& task = job.tasks(t); const int num_alternatives = task.machine_size(); CHECK_EQ(num_alternatives, task.duration_size()); // Add the "main" task interval. std::vector durations; int64_t min_duration = task.duration(0); int64_t max_duration = task.duration(0); durations.push_back(task.duration(0)); for (int a = 1; a < num_alternatives; ++a) { min_duration = std::min(min_duration, task.duration(a)); max_duration = std::max(max_duration, task.duration(a)); durations.push_back(task.duration(a)); } if (min_duration != max_duration) has_variable_duration_tasks = true; const IntVar start = cp_model.NewIntVar(Domain(hard_start, hard_end)); const IntVar duration = cp_model.NewIntVar(Domain::FromValues(durations)); const IntVar end = cp_model.NewIntVar(Domain(hard_start, hard_end)); const IntervalVar interval = cp_model.NewIntervalVar(start, duration, end); // Fill in job_to_tasks. task_data.push_back({interval, start, duration, end}); // Chain the task belonging to the same job. if (t > 0) { cp_model.AddLessOrEqual(task_data[t - 1].end, task_data[t].start); } } } } // For each task of each jobs, create the alternative tasks and link them to the // main task of the job. void CreateAlternativeTasks( const JsspInputProblem& problem, const std::vector>& job_to_tasks, int64_t horizon, std::vector>>& job_task_to_alternatives, CpModelBuilder& cp_model) { const int num_jobs = problem.jobs_size(); const BoolVar true_var = cp_model.TrueVar(); for (int j = 0; j < num_jobs; ++j) { const Job& job = problem.jobs(j); const int num_tasks_in_job = job.tasks_size(); job_task_to_alternatives[j].resize(num_tasks_in_job); const std::vector& tasks = job_to_tasks[j]; const int64_t hard_start = job.has_earliest_start() ? job.earliest_start().value() : 0L; const int64_t hard_end = job.has_latest_end() ? job.latest_end().value() : horizon; for (int t = 0; t < num_tasks_in_job; ++t) { const Task& task = job.tasks(t); const int num_alternatives = task.machine_size(); CHECK_EQ(num_alternatives, task.duration_size()); std::vector& alt_data = job_task_to_alternatives[j][t]; absl::flat_hash_map> duration_supports; duration_supports[task.duration(0)].push_back(0); for (int a = 1; a < num_alternatives; ++a) { duration_supports[task.duration(a)].push_back(a); } if (num_alternatives == 1) { alt_data.push_back({tasks[t].interval, tasks[t].start, true_var}); } else { for (int a = 0; a < num_alternatives; ++a) { const BoolVar local_presence = cp_model.NewBoolVar(); const IntVar local_start = absl::GetFlag(FLAGS_use_optional_variables) ? cp_model.NewIntVar(Domain(hard_start, hard_end)) : tasks[t].start; const IntervalVar local_interval = cp_model.NewOptionalFixedSizeIntervalVar( local_start, task.duration(a), local_presence); // Link local and global variables. if (absl::GetFlag(FLAGS_use_optional_variables)) { cp_model.AddEquality(tasks[t].start, local_start) .OnlyEnforceIf(local_presence); cp_model.AddEquality(tasks[t].duration, task.duration(a)) .OnlyEnforceIf(local_presence); } alt_data.push_back({local_interval, local_start, local_presence}); } // Exactly one alternative interval is present. std::vector interval_presences; for (const AlternativeTaskData& alternative : alt_data) { interval_presences.push_back(alternative.presence); } cp_model.AddEquality(LinearExpr::BooleanSum(interval_presences), 1); } } } } void AddAlternativeTaskDurationRelaxation( const JsspInputProblem& problem, const std::vector>& job_to_tasks, std::vector>>& job_task_to_alternatives, CpModelBuilder& cp_model) { const int num_jobs = problem.jobs_size(); for (int j = 0; j < num_jobs; ++j) { const Job& job = problem.jobs(j); const int num_tasks_in_job = job.tasks_size(); const std::vector& tasks = job_to_tasks[j]; for (int t = 0; t < num_tasks_in_job; ++t) { const Task& task = job.tasks(t); const int num_alternatives = task.machine_size(); int64_t min_duration = std::numeric_limits::max(); int64_t max_duration = std::numeric_limits::min(); for (const int64_t duration : task.duration()) { min_duration = std::min(min_duration, duration); max_duration = std::max(max_duration, duration); } std::vector presence_literals; std::vector shifted_durations; for (int a = 0; a < num_alternatives; ++a) { const int64_t duration = task.duration(a); if (duration != min_duration) { shifted_durations.push_back(duration - min_duration); presence_literals.push_back( job_task_to_alternatives[j][t][a].presence); } } // end == start + min_duration + // sum(shifted_duration[i] * presence_literals[i]) cp_model.AddEquality( LinearExpr::ScalProd({tasks[t].end, tasks[t].start}, {1, -1}), LinearExpr::BooleanScalProd(presence_literals, shifted_durations) .AddConstant(min_duration)); } } } // Tasks or alternative tasks are added to machines one by one. // This structure records the characteristics of each task added on a machine. // This information is indexed on each vector by the order of addition. struct MachineTaskData { IntervalVar interval; int job; IntVar start; int64_t duration; BoolVar presence; }; void CreateMachines( const JsspInputProblem& problem, const std::vector>>& job_task_to_alternatives, IntervalVar makespan_interval, CpModelBuilder& cp_model) { const int num_jobs = problem.jobs_size(); const int num_machines = problem.machines_size(); std::vector> machine_to_tasks(num_machines); // Fills in the machine data vector. for (int j = 0; j < num_jobs; ++j) { const Job& job = problem.jobs(j); const int num_tasks_in_job = job.tasks_size(); for (int t = 0; t < num_tasks_in_job; ++t) { const Task& task = job.tasks(t); const int num_alternatives = task.machine_size(); CHECK_EQ(num_alternatives, task.duration_size()); const std::vector& alt_data = job_task_to_alternatives[j][t]; for (int a = 0; a < num_alternatives; ++a) { // Record relevant variables for later use. machine_to_tasks[task.machine(a)].push_back( {alt_data[a].interval, j, alt_data[a].start, task.duration(a), alt_data[a].presence}); } } } // Add one no_overlap constraint per machine. for (int m = 0; m < num_machines; ++m) { std::vector intervals; for (const MachineTaskData& task : machine_to_tasks[m]) { intervals.push_back(task.interval); } if (absl::GetFlag(FLAGS_use_interval_makespan) && problem.makespan_cost_per_time_unit() != 0L) { intervals.push_back(makespan_interval); } cp_model.AddNoOverlap(intervals); } // Add transition times if needed. // // TODO(user): If there is just a few non-zero transition, there is probably // a better way than this quadratic blowup. int64_t num_non_zero_transitions = 0; for (int m = 0; m < num_machines; ++m) { if (problem.machines(m).has_transition_time_matrix()) { const int num_intervals = machine_to_tasks[m].size(); const TransitionTimeMatrix& transitions = problem.machines(m).transition_time_matrix(); // Create circuit constraint on a machine. Node 0 is both the source and // sink, i.e. the first and last job. CircuitConstraint circuit = cp_model.AddCircuitConstraint(); for (int i = 0; i < num_intervals; ++i) { const int job_i = machine_to_tasks[m][i].job; const IntVar p_start = machine_to_tasks[m][i].start; const int64_t p_duration = machine_to_tasks[m][i].duration; // TODO(user, lperron): simplify the code! CHECK_EQ(i, job_i); // Source to nodes. circuit.AddArc(0, i + 1, cp_model.NewBoolVar()); // Node to sink. circuit.AddArc(i + 1, 0, cp_model.NewBoolVar()); // Node to node. for (int j = 0; j < num_intervals; ++j) { if (i == j) { circuit.AddArc(i + 1, i + 1, Not(machine_to_tasks[m][i].presence)); } else { const int job_j = machine_to_tasks[m][j].job; // TODO(user, lperron): simplify the code! CHECK_EQ(j, job_j); const int64_t transition = transitions.transition_time(job_i * num_jobs + job_j); const BoolVar lit = cp_model.NewBoolVar(); const IntVar start = machine_to_tasks[m][j].start; circuit.AddArc(i + 1, j + 1, lit); // Push the new start with an extra transition. if (transition != 0) ++num_non_zero_transitions; cp_model .AddLessOrEqual(p_start.AddConstant(transition + p_duration), start) .OnlyEnforceIf(lit); } } } } } if (num_non_zero_transitions > 0) { LOG(INFO) << "Num non-zeros transition delay: " << num_non_zero_transitions; } } // Collect all objective terms and add them to the model. void CreateObjective( const JsspInputProblem& problem, const std::vector>& job_to_tasks, const std::vector>>& job_task_to_alternatives, int64_t horizon, IntVar makespan, CpModelBuilder& cp_model) { int64_t objective_offset = 0; std::vector objective_vars; std::vector objective_coeffs; const int num_jobs = problem.jobs_size(); for (int j = 0; j < num_jobs; ++j) { const Job& job = problem.jobs(j); const int num_tasks_in_job = job.tasks_size(); // Add the cost associated to each task. for (int t = 0; t < num_tasks_in_job; ++t) { const Task& task = job.tasks(t); const int num_alternatives = task.machine_size(); for (int a = 0; a < num_alternatives; ++a) { // Add cost if present. if (task.cost_size() > 0) { objective_vars.push_back(job_task_to_alternatives[j][t][a].presence); objective_coeffs.push_back(task.cost(a)); } } } // Job lateness cost. const int64_t lateness_penalty = job.lateness_cost_per_time_unit(); if (lateness_penalty != 0L) { const int64_t due_date = job.late_due_date(); const IntVar job_end = job_to_tasks[j].back().end; if (due_date == 0) { objective_vars.push_back(job_end); objective_coeffs.push_back(lateness_penalty); } else { const IntVar lateness_var = cp_model.NewIntVar(Domain(0, horizon)); cp_model.AddLinMaxEquality( lateness_var, {LinearExpr(0), job_end.AddConstant(-due_date)}); objective_vars.push_back(lateness_var); objective_coeffs.push_back(lateness_penalty); } } // Job earliness cost. const int64_t earliness_penalty = job.earliness_cost_per_time_unit(); if (earliness_penalty != 0L) { const int64_t due_date = job.early_due_date(); const IntVar job_end = job_to_tasks[j].back().end; if (due_date > 0) { const IntVar earliness_var = cp_model.NewIntVar(Domain(0, horizon)); cp_model.AddLinMaxEquality( earliness_var, {LinearExpr(0), LinearExpr::Term(job_end, -1).AddConstant(due_date)}); objective_vars.push_back(earliness_var); objective_coeffs.push_back(earliness_penalty); } } } // Makespan objective. if (problem.makespan_cost_per_time_unit() != 0L) { objective_coeffs.push_back(problem.makespan_cost_per_time_unit()); objective_vars.push_back(makespan); } // Add the objective to the model. cp_model.Minimize(LinearExpr::ScalProd(objective_vars, objective_coeffs) .AddConstant(objective_offset)); if (problem.has_scaling_factor()) { cp_model.ScaleObjectiveBy(problem.scaling_factor().value()); } } // This is a relaxation of the problem where we only consider the main tasks, // and not the alternate copies. void AddCumulativeRelaxation( const JsspInputProblem& problem, const std::vector>& job_to_tasks, IntervalVar makespan_interval, CpModelBuilder& cp_model) { const int num_jobs = problem.jobs_size(); const int num_machines = problem.machines_size(); // Build a graph where two machines are connected if they appear in the same // set of alternate machines for a given task. int num_tasks = 0; std::vector> neighbors(num_machines); for (int j = 0; j < num_jobs; ++j) { const Job& job = problem.jobs(j); const int num_tasks_in_job = job.tasks_size(); num_tasks += num_tasks_in_job; for (int t = 0; t < num_tasks_in_job; ++t) { const Task& task = job.tasks(t); for (int a = 1; a < task.machine_size(); ++a) { neighbors[task.machine(0)].insert(task.machine(a)); } } } // Search for connected components in the above graph. std::vector components = util::GetConnectedComponents(num_machines, neighbors); absl::flat_hash_map> machines_per_component; for (int c = 0; c < components.size(); ++c) { machines_per_component[components[c]].push_back(c); } LOG(INFO) << "Found " << machines_per_component.size() << " connected machine components."; for (const auto& it : machines_per_component) { // Ignore the trivial cases. if (it.second.size() < 2 || it.second.size() == num_machines) continue; absl::flat_hash_set component(it.second.begin(), it.second.end()); std::vector intervals; for (int j = 0; j < num_jobs; ++j) { const Job& job = problem.jobs(j); const int num_tasks_in_job = job.tasks_size(); for (int t = 0; t < num_tasks_in_job; ++t) { const Task& task = job.tasks(t); for (const int m : task.machine()) { if (component.contains(m)) { intervals.push_back(job_to_tasks[j][t].interval); break; } } } } LOG(INFO) << "Found machine connected component: [" << absl::StrJoin(it.second, ", ") << "] with " << intervals.size() << " intervals"; // Ignore trivial case with all intervals. if (intervals.size() == 1 || intervals.size() == num_tasks) continue; const IntVar capacity = cp_model.NewConstant(component.size()); const IntVar one = cp_model.NewConstant(1); CumulativeConstraint cumul = cp_model.AddCumulative(capacity); for (int i = 0; i < intervals.size(); ++i) { cumul.AddDemand(intervals[i], one); } if (absl::GetFlag(FLAGS_use_interval_makespan)) { cumul.AddDemand(makespan_interval, capacity); } } } // There are two linear redundant constraints. // // The first one states that the sum of durations of all tasks is a lower bound // of the makespan * number of machines. // // The second one takes a suffix of one job chain, and states that the start of // the suffix + the sum of all task durations in the suffix is a lower bound of // the makespan. void AddMakespanRedundantConstraints( const JsspInputProblem& problem, const std::vector>& job_to_tasks, IntVar makespan, bool has_variable_duration_tasks, CpModelBuilder& cp_model) { const int num_machines = problem.machines_size(); // Global energetic reasoning. std::vector all_task_durations; for (const std::vector& tasks : job_to_tasks) { for (const JobTaskData& task : tasks) { all_task_durations.push_back(task.duration); } } cp_model.AddLessOrEqual(LinearExpr::Sum(all_task_durations), LinearExpr::Term(makespan, num_machines)); } // Solve a JobShop scheduling problem using CP-SAT. void Solve(const JsspInputProblem& problem) { if (absl::GetFlag(FLAGS_display_model)) { LOG(INFO) << problem.DebugString(); } CpModelBuilder cp_model; // Compute an over estimate of the horizon. const int64_t horizon = absl::GetFlag(FLAGS_horizon) != -1 ? absl::GetFlag(FLAGS_horizon) : ComputeHorizon(problem); // Create the main job structure. const int num_jobs = problem.jobs_size(); std::vector> job_to_tasks(num_jobs); bool has_variable_duration_tasks = false; CreateJobs(problem, horizon, job_to_tasks, has_variable_duration_tasks, cp_model); // For each task of each jobs, create the alternative copies if needed and // fill in the AlternativeTaskData vector. std::vector>> job_task_to_alternatives(num_jobs); CreateAlternativeTasks(problem, job_to_tasks, horizon, job_task_to_alternatives, cp_model); // Adds a linear relaxation of the interval variables. AddAlternativeTaskDurationRelaxation(problem, job_to_tasks, job_task_to_alternatives, cp_model); // Create the makespan variable and interval. // If this flag is true, we will add to each no overlap constraint a special // "makespan interval" that must necessarily be last by construction. This // gives us a better lower bound on the makespan because this way we known // that it must be after all other intervals in each no-overlap constraint. // // Otherwise, we will just add precence constraints between the last task of // each job and the makespan variable. Alternatively, we could have added a // precedence relation between all tasks and the makespan for a similar // propagation thanks to our "precedence" propagator in the dijsunctive but // that was slower than the interval trick when I tried. const IntVar makespan = cp_model.NewIntVar(Domain(0, horizon)); IntervalVar makespan_interval; if (absl::GetFlag(FLAGS_use_interval_makespan)) { makespan_interval = cp_model.NewIntervalVar( /*start=*/makespan, /*size=*/cp_model.NewIntVar(Domain(1, horizon)), /*end=*/cp_model.NewIntVar(Domain(horizon + 1))); } else if (problem.makespan_cost_per_time_unit() != 0L) { for (int j = 0; j < num_jobs; ++j) { // The makespan will be greater than the end of each job. // This is not needed if we add the makespan "interval" to each // disjunctive. cp_model.AddLessOrEqual(job_to_tasks[j].back().end, makespan); } } // Machine constraints. CreateMachines(problem, job_task_to_alternatives, makespan_interval, cp_model); // Try to detect connected components of alternative machines. // If this is happens, we can add a cumulative constraint as a relaxation of // all no_ovelap constraints on the set of alternative machines. if (absl::GetFlag(FLAGS_use_cumulative_relaxation)) { AddCumulativeRelaxation(problem, job_to_tasks, makespan_interval, cp_model); } // Various redundant constraints. They are mostly here to improve the LP // relaxation. if (problem.makespan_cost_per_time_unit() != 0L) { AddMakespanRedundantConstraints(problem, job_to_tasks, makespan, has_variable_duration_tasks, cp_model); } // Add job precedences. for (const JobPrecedence& precedence : problem.precedences()) { const IntVar start = job_to_tasks[precedence.second_job_index()].front().start; const IntVar end = job_to_tasks[precedence.first_job_index()].back().end; cp_model.AddLessOrEqual(end.AddConstant(precedence.min_delay()), start); } // Objective. CreateObjective(problem, job_to_tasks, job_task_to_alternatives, horizon, makespan, cp_model); // Decision strategy. // CP-SAT now has a default strategy for scheduling problem that works best. // Display problem statistics. int num_tasks = 0; int num_tasks_with_variable_duration = 0; int num_tasks_with_alternatives = 0; for (const std::vector& job : job_to_tasks) { num_tasks += job.size(); for (const JobTaskData& task : job) { if (task.duration.Proto().domain_size() != 2 || task.duration.Proto().domain(0) != task.duration.Proto().domain(1)) { num_tasks_with_variable_duration++; } } } for (const std::vector>& task_to_alternatives : job_task_to_alternatives) { for (const std::vector& alternatives : task_to_alternatives) { if (alternatives.size() > 1) num_tasks_with_alternatives++; } } LOG(INFO) << "#machines:" << problem.machines_size(); LOG(INFO) << "#jobs:" << num_jobs; LOG(INFO) << "horizon:" << horizon; LOG(INFO) << "#tasks: " << num_tasks; LOG(INFO) << "#tasks with alternative: " << num_tasks_with_alternatives; LOG(INFO) << "#tasks with variable duration: " << num_tasks_with_variable_duration; if (absl::GetFlag(FLAGS_display_sat_model)) { LOG(INFO) << cp_model.Proto().DebugString(); } SatParameters parameters; parameters.set_log_search_progress(true); // Parse the --params flag. if (!absl::GetFlag(FLAGS_params).empty()) { CHECK(google::protobuf::TextFormat::MergeFromString( absl::GetFlag(FLAGS_params), ¶meters)) << absl::GetFlag(FLAGS_params); } const CpSolverResponse response = SolveWithParameters(cp_model.Build(), parameters); // Abort if we don't have any solution. if (response.status() != CpSolverStatus::OPTIMAL && response.status() != CpSolverStatus::FEASIBLE) return; // Check cost, recompute it from scratch. int64_t final_cost = 0; if (problem.makespan_cost_per_time_unit() != 0) { int64_t makespan = 0; for (const std::vector& tasks : job_to_tasks) { const IntVar job_end = tasks.back().end; makespan = std::max(makespan, SolutionIntegerValue(response, job_end)); } final_cost += makespan * problem.makespan_cost_per_time_unit(); } for (int j = 0; j < num_jobs; ++j) { const int64_t early_due_date = problem.jobs(j).early_due_date(); const int64_t late_due_date = problem.jobs(j).late_due_date(); const int64_t early_penalty = problem.jobs(j).earliness_cost_per_time_unit(); const int64_t late_penalty = problem.jobs(j).lateness_cost_per_time_unit(); const int64_t end = SolutionIntegerValue(response, job_to_tasks[j].back().end); 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; } } // Note that this the objective is a variable of the model, there is actually // no strong guarantee that in an intermediate solution, it is packed to its // minimum possible value. We do observe this from time to time. The DCHECK is // mainly to warn when this happen. // // TODO(user): Support alternative cost in check. const double tolerance = 1e-6; DCHECK_GE(response.objective_value(), final_cost - tolerance); DCHECK_LE(response.objective_value(), final_cost + tolerance); } } // namespace sat } // namespace operations_research int main(int argc, char** argv) { absl::SetFlag(&FLAGS_logtostderr, true); google::InitGoogleLogging(argv[0]); absl::ParseCommandLine(argc, argv); if (absl::GetFlag(FLAGS_input).empty()) { LOG(FATAL) << "Please supply a data file with --input="; } operations_research::data::jssp::JsspParser parser; CHECK(parser.ParseFile(absl::GetFlag(FLAGS_input))); operations_research::sat::Solve(parser.problem()); return EXIT_SUCCESS; }