So,back to the initial methodLet's get a granularity 1 time unit = 1 day, scheduling over 3 weeks, with week-end, and 1 day off (Thu of the second week)My task has a duration of 4total duration = ( I use X to indicate impossible)M T W T F S S M T W T F S S M T W T F S S4 4 6 6 6 X X 5 7 7 X 6 X X 4 4 X X X X XI have duration = 4 when start in [0..1] U [14..15]I have duration = 5 when start in [7..7]I have duration = 6 when start in [2..4] U [11..11]I have duration = 7 when start in [8..9]I have duration = X when start in [5..6] U [10..10] U [12..13] U [16..20]And the domain of start is [0..4] U [7..9] U [11..11] U [14..15] (complement of X)Now I can create my interval variables (in python for the example)# Create variablesstart = model.NewEnumeratedIntVar([0, 4, 7, 9, 11, 11, 14, 15], 'start')duration = model.NewIntVar(4, 7, 'duration')end = model.NewIntVar(0, 20, 'end') # We can compute it exactly.# Create indicatorsd4 = model.NewBoolVar('d4')d5 = model.NewBoolVar('d5')d6 = model.NewBoolVar('d6')d7 = model.NewBoolVar('d7')# Link start to indicatorsmodel.AddLinearConstraintWithBounds([(start, 1)], [0, 1, 14, 15]).OnlyEnforceIf(d4)model.Add(start == 7).OnlyEnforceIf(d5)model.AddLinearConstraintWithBounds([(start, 1)], [2, 4, 11, 11]).OnlyEnforceIf(d6)model.AddLinearConstraintWithBounds([(start, 1)], [8, 9]).OnlyEnforceIf(d7)# Exactly one indicator is truemodel.Add(d4 + d5 + d6 + d7 == 1)# Indicators constraint the durationmodel.Add(duration == 4).OnlyEnforceIf(d4)model.Add(duration == 5).OnlyEnforceIf(d5)
model.Add(duration == 6).OnlyEnforceIf(d6)
model.Add(duration == 7).OnlyEnforceIf(d7)This way, you are completely flexible.
For those curious, you can see my schedule below (grey areas marks machine downtime implemented according to examples/python/jobshop_with_maintenance_sat.py)
Minimize(sum(all_jobs[job_id].end - all_jobs[job_id].start for job_id, job in enumerate(Job_Operation))task_type = collections.namedtuple('task_type', 'start end interval')job_type = collections.namedtuple('job_type', 'start end')tard_type = collections.namedtuple('order_type', 'tard ind')all_tasks = {}all_jobs = {}all_orders = {}machine_to_intervals = collections.defaultdict(list)
#Integer/Bool varaible for tardinessfor sales_id, sales_order in enumerate(dates): suffix = '_%i' % (sales_order[0]) tard_time = model.NewIntVar(-horizon, horizon, 'tard_time_index' + suffix) tard_indicator = model.NewBoolVar('tard_indicator_index' + suffix) all_orders[sales_id] = tard_type(tard = tard_time, ind=tard_indicator)
#Integer variable for job endfor job_id, job in enumerate(Job_Operation): suffix = '_%i' % (job[0]) start_var = model.NewIntVar(0, horizon, 'start' + suffix) end_var = model.NewIntVar(0, horizon, 'end' + suffix)
all_jobs[job_id] = job_type(start=start_var, end=end_var)
#Integer variable for operation start, duration and endfor job_id, job in enumerate(Job_Operation): for task_id, task in enumerate(job[1]): machine = task[0] duration = task[1].item() suffix = '_%i_%s:%i_%i' % (job[0], task[0], task_id, task[1]) start_var = model.NewIntVar(0, horizon, 'start' + suffix) end_var = model.NewIntVar(0, horizon, 'end' + suffix) interval_var = model.NewIntervalVar(start_var, duration, end_var, 'interval' + suffix) all_tasks[job_id, task_id] = task_type(start=start_var, end=end_var, interval=interval_var) machine_to_intervals[machine].append(interval_var)
#Adding unavailable capacity slots.for machine in capacity: suffix = '%s_%s_%s' % (machine[0], machine[4], machine[5]) machine_to_intervals[machine[0]].append(model.NewIntervalVar(int(machine[1]), int(machine[2]), int(machine[3]), suffix))# Create disjunctive constraint, i.e. no overlappingfor machine in machines: model.AddNoOverlap(machine_to_intervals[machine]) #Start + end time of job is equal to start + end of last operationfor job_id, job in enumerate(Job_Operation): model.Add(all_jobs[job_id].start == all_tasks[job_id, 0].start) model.Add(all_jobs[job_id].end == max(all_tasks[job_id, task_id].end for task_id in range(len(job[1])))) # Precedences inside a jobfor job_id, job in enumerate(Job_Operation): #print(job) for task_id in range(len(job[1]) - 1): if str(all_tasks[job_id, task_id + 1][0]).split(":", 4)[0] == str(all_tasks[job_id, task_id][0]).split(":", 4)[0]: model.Add(all_tasks[job_id, task_id + 1].start >= all_tasks[job_id, task_id].end) #print(all_tasks[job_id, task_id + 1].start, ">=", all_tasks[job_id, task_id].end) else: model.Add(all_tasks[job_id, task_id + 1].start >= 24 + all_tasks[job_id, task_id].end) #print(all_tasks[job_id, task_id + 1].start, ">=", 24 + all_tasks[job_id, task_id].end) # Precedences inside a sales order (try/except because some sales orders jobs are not in the load file)for sales_order in order: prev_job = 0 for job_id, job in enumerate(sales_order[1]): index = np.where(job == jobs) try: if prev_job != 0: model.Add(all_jobs[index[0][0]].start >= (prev_job.end + 72)) #print(sales_order, all_jobs[index[0][0]].start, ">=", (prev_job.end + 72)) prev_job = all_jobs[index[0][0]] except: pass
# Calculate tardiness of jobs and a binary indicatorfor sales_id, sales_order in enumerate(dates): index = np.where(sales_order[0] == jobs) try: model.Add(all_orders[sales_id].tard >= all_jobs[index[0][0]].end - dates[sales_id][1]) #print(all_orders[sales_id].tard >= all_jobs[index[0][0]].end - dates[sales_id][1]) model.Add(all_orders[sales_id].tard - all_orders[sales_id].ind * horizon <= 0) except: passmakespan = model.NewIntVar(0, horizon, 'makespan')model.AddMaxEquality(makespan, [all_jobs[job_id].end for job_id, job in enumerate(Job_Operation)])
job_durations = []for job_id, job in enumerate(Job_Operation):
job_duration = model.NewIntVar(0, horizon, f'job_duration_{job_id}') model.Add(job_duration == all_jobs[job_id].end - all_jobs[job_id].start) job_durations.append(job_duration) tards = [] for sales_id, sales_order in enumerate(dates): tards.append(all_orders[sales_id].ind) model.Minimize((50 * makespan) + (35 * sum(tards)) + (15 * sum(job_durations)))
# Solve model.
start = time.time()solution_printer = SolutionPrinter()solver.parameters.num_search_workers = 8solver.parameters.max_time_in_seconds = 3600.0
status = solver.SolveWithSolutionCallback(model, solution_printer)
end = time.time()hours, rem = divmod(end-start, 3600)minutes, seconds = divmod(rem, 60)print("{:0>2}:{:0>2}:{:05.2f}".format(int(hours),int(minutes),seconds))