diff --git a/examples/cpp/jobshop_sat.cc b/examples/cpp/jobshop_sat.cc index 0f37fa1503..a9d44ea6c0 100644 --- a/examples/cpp/jobshop_sat.cc +++ b/examples/cpp/jobshop_sat.cc @@ -269,8 +269,8 @@ void AddAlternativeTaskDurationRelaxation( const Task& task = job.tasks(t); const int num_alternatives = task.machine_size(); - int64_t min_duration = kint64max; - int64_t max_duration = kint64min; + 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); diff --git a/examples/cpp/weighted_tardiness_sat.cc b/examples/cpp/weighted_tardiness_sat.cc index f5315b88e6..3e901c330e 100644 --- a/examples/cpp/weighted_tardiness_sat.cc +++ b/examples/cpp/weighted_tardiness_sat.cc @@ -30,7 +30,6 @@ #include "ortools/base/logging.h" #include "ortools/base/timer.h" #include "ortools/sat/cp_model.h" -#include "ortools/sat/cp_model_solver.h" #include "ortools/sat/model.h" ABSL_FLAG(std::string, input, "examples/data/weighted_tardiness/wt40.txt", @@ -170,14 +169,8 @@ void Solve(const std::vector& durations, // Note that we only fully instantiate the start/end and only look at the // lower bound for the objective and the tardiness variables. Model model; - SatParameters parameters; - // Parse the --params flag. - if (!absl::GetFlag(FLAGS_params).empty()) { - CHECK(google::protobuf::TextFormat::MergeFromString( - absl::GetFlag(FLAGS_params), ¶meters)) - << absl::GetFlag(FLAGS_params); - model.Add(NewSatParameters(parameters)); - } + model.Add(NewSatParameters(absl::GetFlag(FLAGS_params))); + model.GetOrCreate()->set_log_search_progress(true); model.Add(NewFeasibleSolutionObserver([&](const CpSolverResponse& r) { // Note that we compute the "real" cost here and do not use the tardiness // variables. This is because in the core based approach, the tardiness @@ -224,9 +217,6 @@ void Solve(const std::vector& durations, // Solve. const CpSolverResponse response = SolveCpModel(cp_model.Build(), &model); - if (!parameters.log_search_progress()) { - LOG(INFO) << CpSolverResponseStats(response); - } } void ParseAndSolve() { diff --git a/examples/python/rcpsp_sat.py b/examples/python/rcpsp_sat.py index f8e3acb972..57e9855a23 100644 --- a/examples/python/rcpsp_sat.py +++ b/examples/python/rcpsp_sat.py @@ -109,104 +109,90 @@ def SolveRcpsp(problem, proto_file, params): horizon += abs(d) print(f' - horizon = {horizon}') - # Containers used to build resources. - intervals_per_resource = collections.defaultdict(list) - demands_per_resource = collections.defaultdict(list) - presences_per_resource = collections.defaultdict(list) - starts_per_resource = collections.defaultdict(list) - - # Starts and ends for each task (shared between all alternatives) + # Containers. task_starts = {} task_ends = {} + task_durations = {} + task_intervals = {} + task_to_resource_demands = collections.defaultdict(list) - # Containers for per-recipe per task alternatives variables. - presences_per_task = collections.defaultdict(list) - durations_per_task = collections.defaultdict(list) + task_to_presence_literals = collections.defaultdict(list) + task_to_recipe_durations = collections.defaultdict(list) + task_resource_to_fixed_demands = collections.defaultdict(dict) - one = model.NewConstant(1) + resource_to_sum_of_demand_max = collections.defaultdict(int) - # Create tasks variables. + # Create task variables. for t in all_active_tasks: task = problem.tasks[t] + num_recipes = len(task.recipes) + all_recipes = range(num_recipes) - if len(task.recipes) == 1: - # Create main and unique interval. - recipe = task.recipes[0] - task_starts[t] = model.NewIntVar(0, horizon, f'start_of_task_{t}') - task_ends[t] = model.NewIntVar(0, horizon, f'end_of_task_{t}') - interval = model.NewIntervalVar(task_starts[t], recipe.duration, - task_ends[t], f'interval_{t}') + start_var = model.NewIntVar(0, horizon, f'start_of_task_{t}') + end_var = model.NewIntVar(0, horizon, f'end_of_task_{t}') - # Store as a single alternative for later. - presences_per_task[t].append(one) - durations_per_task[t].append(recipe.duration) + # Create one literal per recipe. + literals = [ + model.NewBoolVar(f'is_present_{t}_{r}') for r in all_recipes + ] - # Register the interval in resources specified by the demands. - for i in range(len(recipe.demands)): - demand = recipe.demands[i] - res = recipe.resources[i] - demands_per_resource[res].append(demand) - if problem.resources[res].renewable: - intervals_per_resource[res].append(interval) - else: - starts_per_resource[res].append(task_starts[t]) - presences_per_resource[res].append(1) - else: # Multiple alternative recipes. - all_recipes = range(len(task.recipes)) + # Exactly one recipe must be performed. + model.Add(cp_model.LinearExpr.Sum(literals) == 1) - start = model.NewIntVar(0, horizon, f'start_of_task_{t}') - end = model.NewIntVar(0, horizon, f'end_of_task_{t}') + # Temporary data structure to fill in 0 demands. + demand_matrix = collections.defaultdict(int) - # Store for precedences. - task_starts[t] = start - task_ends[t] = end + # Scan recipes and build the demand matrix and the vector of durations. + for recipe_index, recipe in enumerate(task.recipes): + task_to_recipe_durations[t].append(recipe.duration) + for demand, resource in zip(recipe.demands, recipe.resources): + demand_matrix[(resource, recipe_index)] = demand - # Create one optional interval per recipe. - for r in all_recipes: - recipe = task.recipes[r] - is_present = model.NewBoolVar(f'is_present_{t}_{r}') - interval = model.NewOptionalIntervalVar(start, recipe.duration, - end, is_present, - f'interval_{t}_{r}') + # Create the duration variable from the accumulated durations. + duration_var = model.NewIntVarFromDomain( + cp_model.Domain.FromValues(task_to_recipe_durations[t]), + f'duration_of_task_{t}') - # Store alternative variables. - presences_per_task[t].append(is_present) - durations_per_task[t].append(recipe.duration) + # linear encoding of the duration (link recipe literals and duration). + min_duration = min(task_to_recipe_durations[t]) + shifted = [x - min_duration for x in task_to_recipe_durations[t]] + model.Add(duration_var == min_duration + + cp_model.LinearExpr.ScalProd(literals, shifted)) - # Register the interval in resources specified by the demands. - for i in range(len(recipe.demands)): - demand = recipe.demands[i] - res = recipe.resources[i] - demands_per_resource[res].append(demand) - if problem.resources[res].renewable: - intervals_per_resource[res].append(interval) - else: - starts_per_resource[res].append(start) - presences_per_resource[res].append(is_present) + # Create the interval of the task. + task_interval = model.NewIntervalVar(start_var, duration_var, end_var, + f'task_interval_{t}') - # Exactly one alternative must be performed. - model.Add(sum(presences_per_task[t]) == 1) + # Store task variables. + task_starts[t] = start_var + task_ends[t] = end_var + task_durations[t] = duration_var + task_intervals[t] = task_interval + task_to_presence_literals[t] = literals - # linear encoding of the duration. - min_duration = min(durations_per_task[t]) - max_duration = max(durations_per_task[t]) - shifted = [x - min_duration for x in durations_per_task[t]] + # Create the demand variable of the task for each resource. + for resource in all_resources: + demands = [ + demand_matrix[(resource, recipe)] for recipe in all_recipes + ] + task_resource_to_fixed_demands[(t, resource)] = demands + demand_var = model.NewIntVarFromDomain( + cp_model.Domain.FromValues(demands), f'demand_{t}_{resource}') + task_to_resource_demands[t].append(demand_var) - duration = model.NewIntVar(min_duration, max_duration, - f'duration_of_task_{t}') - model.Add( - duration == min_duration + - cp_model.LinearExpr.ScalProd(presences_per_task[t], shifted)) - - # We do not create a 'main' interval. Instead, we link start, end, and - # duration. - model.Add(start + duration == end) + # linear encoding of the demand per resource. + min_demand = min(demands) + shifted = [x - min_demand for x in demands] + model.Add(demand_var == min_demand + + cp_model.LinearExpr.ScalProd(literals, shifted)) + resource_to_sum_of_demand_max[resource] += max(demands) # Create makespan variable makespan = model.NewIntVar(0, horizon, 'makespan') - interval_makespan = model.NewIntervalVar( - makespan, model.NewIntVar(1, horizon, 'interval_makespan_size'), - model.NewConstant(horizon + 1), 'interval_makespan') + makespan_size = model.NewIntVar(1, horizon, 'interval_makespan_size') + interval_makespan = model.NewIntervalVar(makespan, makespan_size, + model.NewConstant(horizon + 1), + 'interval_makespan') # Add precedences. if problem.is_rcpsp_max: @@ -222,7 +208,7 @@ def SolveRcpsp(problem, proto_file, params): num_next_modes = len(problem.tasks[next_id].recipes) for m1 in range(num_modes): s1 = task_starts[task_id] - p1 = presences_per_task[task_id][m1] + p1 = task_to_presence_literals[task_id][m1] if next_id == num_tasks - 1: delay = delay_matrix.recipe_delays[m1].min_delays[0] model.Add(s1 + delay <= makespan).OnlyEnforceIf(p1) @@ -231,7 +217,7 @@ def SolveRcpsp(problem, proto_file, params): delay = delay_matrix.recipe_delays[m1].min_delays[ m2] s2 = task_starts[next_id] - p2 = presences_per_task[next_id][m2] + p2 = task_to_presence_literals[next_id][m2] model.Add(s1 + delay <= s2).OnlyEnforceIf([p1, p2]) else: # Normal dependencies (task ends before the start of successors). @@ -251,35 +237,57 @@ def SolveRcpsp(problem, proto_file, params): resource = problem.resources[r] c = resource.max_capacity if c == -1: - c = sum(demands_per_resource[r]) + print(f'No capacity: {resource}') + c = resource_to_sum_of_demand_max[r] + + # RIP problems have only renewable resources, and no makespan. + if problem.is_resource_investment or resource.renewable: + intervals = [task_intervals[t] for t in all_active_tasks] + demands = [task_to_resource_demands[t][r] for t in all_active_tasks] + + if problem.is_resource_investment: + capacity = model.NewIntVar(0, c, f'capacity_of_{r}') + model.AddCumulative(intervals, demands, capacity) + capacities.append(capacity) + max_cost += c * resource.unit_cost + else: # Standard renewable resource. + energies = [] + for t in all_active_tasks: + literals = task_to_presence_literals[t] + fixed_energies = [ + task_resource_to_fixed_demands[(t, r)][index] * + task_to_recipe_durations[t][index] + for index in range(len(literals)) + ] + min_energy = min(fixed_energies) + scaled_energies = [x - min_energy for x in fixed_energies] + energies.append( + min_energy + + cp_model.LinearExpr.ScalProd(literals, scaled_energies)) - if problem.is_resource_investment: - # RIP problems have only renewable resources. - capacity = model.NewIntVar(0, c, f'capacity_of_{r}') - model.AddCumulative(intervals_per_resource[r], - demands_per_resource[r], capacity) - capacities.append(capacity) - max_cost += c * resource.unit_cost - elif resource.renewable: - if intervals_per_resource[r]: if FLAGS.use_interval_makespan: - model.AddCumulative( - intervals_per_resource[r] + [interval_makespan], - demands_per_resource[r] + [c], c) - else: - model.AddCumulative(intervals_per_resource[r], - demands_per_resource[r], c) - elif presences_per_resource[r]: # Non empty non renewable resource. + intervals.append(interval_makespan) + demands.append(c) + energies.append(c * makespan_size) + model.AddCumulativeWithEnergy(intervals, demands, energies, c) + else: # Non empty non renewable resource. (single mode only) if problem.is_consumer_producer: - model.AddReservoirConstraint(starts_per_resource[r], - demands_per_resource[r], + reservoir_starts = [] + reservoir_demands = [] + for t in all_active_tasks: + if task_resource_to_fixed_demands[(t, r)][0]: + reservoir_starts.append(task_starts[t]) + reservoir_demands.append( + task_resource_to_fixed_demands[(t, r)][0]) + model.AddReservoirConstraint(reservoir_starts, + reservoir_demands, resource.min_capacity, resource.max_capacity) - else: + else: # No producer-consumer. We just sum the demands. model.Add( - sum(presences_per_resource[r][i] * - demands_per_resource[r][i] - for i in range(len(presences_per_resource[r]))) <= c) + cp_model.LinearExpr.Sum([ + task_to_resource_demands[t][r] for t in all_active_tasks + ]) <= c) # Objective. if problem.is_resource_investment: