OR-Tools CP-SAT柔性作业车间未找到最优解问题咨询
问题描述
我正尝试将柔性作业车间优化与基于机器日历的可变时长功能结合,代码多数情况下运行正常,但特定场景下无法找到最优解。
场景设置
- 两台机器,各包含两次计划停机,停机将延长作业的处理时长
- 共4个作业,每个作业包含1个任务,每个任务有两个处理时长相近的可选方案
- 所有任务的处理时长均长于0到第一次停机的时间段,预期最优解为:每台机器各安排2个从0时刻开始的作业,作业时长因停机延长2个单位
实际问题
算法仅返回1个甚至0个从0时刻开始的作业,部分机器的首个作业在计划停机后才开始,导致非最优解。有趣的是,若将第一个作业的第一个可选方案的时长从8改为9,算法就能返回预期的最优解(每台机器各两个从0时刻开始的作业)。
代码基于OR-Tools的柔性作业车间示例和日历跨间隔调度示例编写,已调整至完全匹配在线示例。
代码实现
#!/usr/bin/env python3 # Copyright 2010-2024 Google LLC # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Solves a flexible jobshop problems with the CP-SAT solver. A jobshop is a standard scheduling problem when you must sequence a series of task_types on a set of machines. Each job contains one task_type per machine. The order of execution and the length of each job on each machine is task_type dependent. The objective is to minimize the maximum completion time of all jobs. This is called the makespan. """ # overloaded sum() clashes with pytype. import collections from ortools.sat.python import cp_model class SolutionPrinter(cp_model.CpSolverSolutionCallback): """Print intermediate solutions.""" def __init__(self) -> None: cp_model.CpSolverSolutionCallback.__init__(self) self.__solution_count = 0 def on_solution_callback(self) -> None: """Called at each new solution.""" print( f"Solution {self.__solution_count}, time = {self.wall_time} s," f" objective = {self.objective_value}" ) self.__solution_count += 1 def flexible_jobshop() -> None: """solve a small flexible jobshop problem.""" # Data part. jobs = [ # task = (processing_time, machine_id) [ # Job 0 [(8, 0), (7, 1)], # task 0 with 2 alternatives -- REMARK: change the processing time of alternative 0 to 9 for the optimal solution ], [ # Job 1 [(7, 0), (9, 1)], ], [ # Job 2 [(7, 0), (8, 1)], ], [ # Job 3 [(7, 0), (8, 1)], ], ] num_jobs = len(jobs) all_jobs = range(num_jobs) num_machines = 2 all_machines = range(num_machines) # Model the flexible jobshop problem. model = cp_model.CpModel() horizon = 82 print(f"Horizon = {horizon}") # Global storage of variables. intervals_per_resources = collections.defaultdict(list) starts = {} # indexed by (job_id, task_id). durations = {} # indexed by (job_id, task_id). presences = {} # indexed by (job_id, task_id, alt_id). job_ends: list[cp_model.IntVar] = [] # Scan the jobs and create the relevant variables and intervals. for job_id in all_jobs: job = jobs[job_id] num_tasks = len(job) previous_end = None for task_id in range(num_tasks): task = job[task_id] min_duration = task[0][0] max_duration = task[0][0] num_alternatives = len(task) all_alternatives = range(num_alternatives) for alt_id in range(1, num_alternatives): alt_duration = task[alt_id][0] min_duration = min(min_duration, alt_duration) max_duration = max(max_duration, alt_duration) # Create main interval for the task. suffix_name = f"_j{job_id}_t{task_id}" start = model.new_int_var(0, horizon, "start" + suffix_name) duration = model.new_int_var( min_duration, max_duration, "duration" + suffix_name ) end = model.new_int_var(0, horizon, "end" + suffix_name) interval = model.new_interval_var( start, duration, end, "interval" + suffix_name ) # Store the start for the solution. starts[(job_id, task_id)] = start # Store the duration for the solution. durations[(job_id, task_id)] = duration # Add precedence with previous task in the same job. if previous_end is not None: model.add(start >= previous_end) previous_end = end # Create alternative intervals. if num_alternatives > 1: l_presences = [] for alt_id in all_alternatives: alt_suffix = f"_j{job_id}_t{task_id}_a{alt_id}" l_presence = model.new_bool_var("presence" + alt_suffix) l_start = model.new_int_var_from_domain( # we have a break at 6 and 7 cp_model.Domain.from_intervals([(0,5), (8,73)]), "start" + alt_suffix ) l_end = model.new_int_var(0, horizon, "end" + alt_suffix) l_duration = model.new_int_var(task[alt_id][0], task[alt_id][0]+2, "duration" + alt_suffix) # We have 2 states (spanning across a break or not) l_across = model.new_bool_var("across" + alt_suffix) model.add_linear_constraint(start, 0, 5).only_enforce_if(l_across) # each tasks' processing time is longer than 5 - so we can use a linear constraint for this example model.add_linear_constraint(start, 8, 73).only_enforce_if(~l_across) model.add(l_duration == task[alt_id][0]).only_enforce_if(~l_across) model.add(l_duration == task[alt_id][0]+2).only_enforce_if(l_across) #l_duration = task[alt_id][0] l_interval = model.new_optional_interval_var( l_start, l_duration, l_end, l_presence, "interval" + alt_suffix ) l_presences.append(l_presence) # Link the primary/global variables with the local ones. model.add(start == l_start).only_enforce_if(l_presence) model.add(duration == l_duration).only_enforce_if(l_presence) model.add(end == l_end).only_enforce_if(l_presence) # Add the local interval to the right machine. intervals_per_resources[task[alt_id][1]].append(l_interval) # Store the presences for the solution. presences[(job_id, task_id, alt_id)] = l_presence # Select exactly one presence variable. model.add_exactly_one(l_presences) else: intervals_per_resources[task[0][1]].append(interval) presences[(job_id, task_id, 0)] = model.new_constant(1) if previous_end is not None: job_ends.append(previous_end) # Create machines constraints. for machine_id in all_machines: intervals = intervals_per_resources[machine_id] if len(intervals) > 1: model.add_no_overlap(intervals) # Makespan objective makespan = model.new_int_var(0, horizon, "makespan") model.add_max_equality(makespan, job_ends) model.minimize(makespan) # Solve model. solver = cp_model.CpSolver() solution_printer = SolutionPrinter() status = solver.solve(model, solution_printer) # Print final solution. if status in (cp_model.OPTIMAL, cp_model.FEASIBLE): print(f"Optimal objective value: {solver.objective_value}") for job_id in all_jobs: print(f"Job {job_id}") for task_id, task in enumerate(jobs[job_id]): start_value = solver.value(starts[(job_id, task_id)]) machine: int = -1 task_duration = solver.value(durations[(job_id, task_id)]) selected: int = -1 for alt_id, alt in enumerate(task): if solver.boolean_value(presences[(job_id, task_id, alt_id)]): machine = alt[1] selected = alt_id print( f" task_{job_id}_{task_id} starts at {start_value} (alt" f" {selected}, machine {machine}, duration {task_duration})" ) print(solver.response_stats()) flexible_jobshop()
内容的提问来源于stack exchange,提问作者Martin Stimpfl
相关产品推荐
相关产品推荐

