I'm trying to solve a hub-and-spoke transportation scheduling problem using Job-shop scheduling. In this specific example, the sequence of task is known and we allow tasks to overlap (don't include no-overlap constraint). There are slackness in the schedule in between the sequence of task where a truck can choose if it wants to wait at the current hub or move on to wait at the next hub. The objective function is to minimize the number of overlaps at each node by making this decision. For example, if the current hub has 5 trucks but the next hub has 2 hubs, we will be incentivized to move onto wait at the next hub as soon as possible.
In the context of a transportation problem, we want to minimize the number of trucks entering each hub/node at the same time in order to reduce the cost of building a large infrastructure. I come across the concept of "cumulative constraint" which is able to compute the number of overlaps without performing time discretization. I have made an attempt to solve this problem but encounter problems in the 'interval variable' portion of the model since 'duration' cannot take a fixed value. Additionally, I am only able to implement a 'hard cumulative constraint' . I want to be able to implement a 'soft cumulative constraint''.
My implementation on Google OR Tools (using Python and CP-SAT solver) is as follows:
machines_count = 1 + max(task[0] for job in jobs_data for task in job)
all_machines = range(machines_count)
# Computes horizon dynamically as the sum of all durations.
horizon = sum(task[1] for job in jobs_data for task in job)
model = cp_model.CpModel()
# Named tuple to store information about created variables.
task_type = collections.namedtuple('task_type', 'start end interval')
# Named tuple to manipulate solution information.
assigned_task_type = collections.namedtuple('assigned_task_type',
'start job index duration')
# Creates job intervals and add to the corresponding machine lists.
all_tasks = {}
machine_to_intervals = collections.defaultdict(list)
demand_to_intervals = collections.defaultdict(list)
for job_id, job in enumerate(jobs_data):
for task_id, task in enumerate(job):
machine = task[0]
duration = task[1]
interval_start = int(task[2])
interval_end = int(task[3])
suffix = '_%i_%i' % (job_id, task_id)
if (interval_end < interval_start):
print('error')
print((interval_end, interval_start, duration, task_id, job_id))
break
start_var = model.NewIntVar(interval_start, interval_end, 'start' + suffix)
end_var = model.NewIntVar(interval_start, interval_end, '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)
demand_to_intervals[machine].append(1)
# Precedences inside a job.
for job_id, job in enumerate(jobs_data):
for task_id in range(len(job) - 1):
ori_hub = int(jobs_data[job_id][task_id][0])
dest_hub = int(jobs_data[job_id][task_id + 1][0])
rel_time = int(relocate_df.iloc[ori_hub, dest_hub])
model.Add(all_tasks[job_id, task_id + 1].start >= all_tasks[job_id, task_id].end + rel_time)
#Cumulative Constraint
for machine in all_machines:
model.AddCumulative(machine_to_intervals[machine],demand_to_intervals[machine],10)
# Makespan objective.
obj_var = model.NewIntVar(0, horizon, 'makespan')
model.AddMaxEquality(obj_var, [
all_tasks[job_id, len(job) - 1].end
for job_id, job in enumerate(jobs_data)
])
model.Minimize(obj_var)
solver = cp_model.CpSolver()
status = solver.Solve(model)