def main():
"""Entry point of the program."""
num_nodes = len(DISTANCE_MATRIX)
all_nodes = range(num_nodes)
print("Num nodes =", num_nodes)
# Model.
model: cp_model.CpModel = cp_model.CpModel()
obj_vars: list[cp_model.LiteralT] = []
obj_coeffs: list[int] = []
# Create the circuit constraint.
arcs: list[cp_model.ArcT] = []
arc_literals: dict[tuple[int, int], cp_model.LiteralT] = {}
for i in all_nodes:
for j in all_nodes:
if i == j:
continue
lit = model.new_bool_var("%i follows %i" % (j, i))
arcs.append((i, j, lit))
arc_literals[i, j] = lit
obj_vars.append(lit)
obj_coeffs.append(DISTANCE_MATRIX[i][j])
model.add_circuit(arcs)
# Minimize weighted sum of arcs. Because this s
model.minimize(sum(obj_vars[i] * obj_coeffs[i] for i in range(len(obj_vars))))arcs: list[cp_model.ArcT] = []
for i in all_tasks:
# if node i is first.
start_lit = model.new_bool_var(f"start_{i}")
arcs.append((0, i + 1, start_lit))
# if node i is last.
end_lit = model.new_bool_var(f"end_{i}")
arcs.append((i + 1, 0, end_lit))
for j in all_tasks:
if i == j:
continue
lit = model.new_bool_var(f"arc_{i}_to_{j}")
arcs.append((i + 1, j + 1, lit))arcs: list[cp_model.ArcT] = []
for i in all_tasks:
# if node i is first.
start_lit = model.new_bool_var(f"start_{i}")
arcs.append((0, i + 1, start_lit))
# if node i can be skipped, add a self loop
skip_lit = model.new_bool_var(f"skip_{i}")
arcs.append((i + 1, i + 1, skip_lit))
# if node i is last.
end_lit = model.new_bool_var(f"end_{i}")
arcs.append((i + 1, 0, end_lit))
for j in all_tasks:
if i == j:
continue
lit = model.new_bool_var(f"arc_{i}_to_{j}")
arcs.append((i + 1, j + 1, lit))for i in all_nodes:
for j in all_nodes:
if i == j:
continue
lit = model.new_bool_var("%i follows %i" % (j, i))
arcs.append((i, j, lit))
arc_literals[i, j] = lit
obj_vars.append(lit)
obj_coeffs.append(DISTANCE_MATRIX[i][j])
model.add_circuit(arcs)
# Minimize weighted sum of arcs.
model.minimize(sum(obj_vars[i] * obj_coeffs[i] for i in range(len(obj_vars))))
add the duration of the shift to the total driving time
reset the accumulated driving time if the distance between the two shifts is more than 30 minutes, add the duration of the shift to the accumulated driving time since the last break otherwise
# Per (driver, node) info (driving time, performed,
# driving time since break)
total_driving = {}
no_break_driving = {}
performed = {}
starting_shifts = {}
# Per driver info (start, end, driving times, is working)
start_times = []
end_times = []
driving_times = []
working_drivers = []
working_times = []
# Weighted objective
delay_literals = []
delay_weights = []
# Used to propagate more between drivers
shared_incoming_literals = collections.defaultdict(list)
shared_outgoing_literals = collections.defaultdict(list)
for d in range(num_drivers):
start_times.append(
model.new_int_var(min_start_time - setup_time, max_end_time, "start_%i" % d)
)
end_times.append(
model.new_int_var(min_start_time, max_end_time + cleanup_time, "end_%i" % d)
)
driving_times.append(model.new_int_var(0, max_driving_time, "driving_%i" % d))
working_times.append(
model.new_int_var(0, max_working_time, "working_times_%i" % d)
)
incoming_literals = collections.defaultdict(list)
outgoing_literals = collections.defaultdict(list)
outgoing_source_literals = []
incoming_sink_literals = []
# Create all the shift variables before iterating on the transitions
# between these shifts.
for s in range(num_shifts):
total_driving[d, s] = model.new_int_var(
0, max_driving_time, "dr_%i_%i" % (d, s)
)
no_break_driving[d, s] = model.new_int_var(
0, max_driving_time_without_pauses, "mdr_%i_%i" % (d, s)
)
performed[d, s] = model.new_bool_var("performed_%i_%i" % (d, s))
for s in range(num_shifts):
shift = shifts[s]
duration = shift[5]
# Arc from source to shift.
# - set the start time of the driver
# - increase driving time and driving time since break
source_lit = model.new_bool_var("%i from source to %i" % (d, s))
outgoing_source_literals.append(source_lit)
incoming_literals[s].append(source_lit)
shared_incoming_literals[s].append(source_lit)
model.add(start_times[d] == shift[3] - setup_time).only_enforce_if(
source_lit
)
model.add(total_driving[d, s] == duration).only_enforce_if(source_lit)
model.add(no_break_driving[d, s] == duration).only_enforce_if(source_lit)
starting_shifts[d, s] = source_lit
# Arc from shift to sink
# - set the end time of the driver
# - set the driving times of the driver
sink_lit = model.new_bool_var("%i from %i to sink" % (d, s))
outgoing_literals[s].append(sink_lit)
shared_outgoing_literals[s].append(sink_lit)
incoming_sink_literals.append(sink_lit)
model.add(end_times[d] == shift[4] + cleanup_time).only_enforce_if(sink_lit)
model.add(driving_times[d] == total_driving[d, s]).only_enforce_if(sink_lit)
# Node not performed
# - set both driving times to 0
# - add a looping arc on the node
model.add(total_driving[d, s] == 0).only_enforce_if(~performed[d, s])
model.add(no_break_driving[d, s] == 0).only_enforce_if(~performed[d, s])
incoming_literals[s].append(~performed[d, s])
outgoing_literals[s].append(~performed[d, s])
# negated adding to the shared lists, because, globally, each node will
# have one incoming literal, and one outgoing literal.
# Node performed:
# - add upper bound on start_time
# - add lower bound on end_times
model.add(start_times[d] <= shift[3] - setup_time).only_enforce_if(
performed[d, s]
)
model.add(end_times[d] >= shift[4] + cleanup_time).only_enforce_if(
performed[d, s]
)
for o in range(num_shifts):
other = shifts[o]
delay = other[3] - shift[4]
if delay < min_delay_between_shifts:
continue
lit = model.new_bool_var("%i from %i to %i" % (d, s, o))
# Increase driving time
model.add(
total_driving[d, o] == total_driving[d, s] + other[5]
).only_enforce_if(lit)
# Increase no_break_driving or reset it to 0 depending on the delay
if delay >= min_pause_after_4h:
model.add(no_break_driving[d, o] == other[5]).only_enforce_if(lit)
else:
model.add(
no_break_driving[d, o] == no_break_driving[d, s] + other[5]
).only_enforce_if(lit)
# add arc
outgoing_literals[s].append(lit)
shared_outgoing_literals[s].append(lit)
incoming_literals[o].append(lit)
shared_incoming_literals[o].append(lit)
# Cost part
delay_literals.append(lit)
delay_weights.append(delay)
model.add(working_times[d] == end_times[d] - start_times[d])
if minimize_drivers:
# Driver is not working.
working = model.new_bool_var("working_%i" % d)
model.add(start_times[d] == min_start_time).only_enforce_if(~working)
model.add(end_times[d] == min_start_time).only_enforce_if(~working)
model.add(driving_times[d] == 0).only_enforce_if(~working)
working_drivers.append(working)
outgoing_source_literals.append(~working)
incoming_sink_literals.append(~working)
# Conditional working time constraints
model.add(working_times[d] >= min_working_time).only_enforce_if(working)
model.add(working_times[d] == 0).only_enforce_if(~working)
else:
# Working time constraints
model.add(working_times[d] >= min_working_time)
# Create circuit constraint.
model.add_exactly_one(outgoing_source_literals)
for s in range(num_shifts):
model.add_exactly_one(outgoing_literals[s])
model.add_exactly_one(incoming_literals[s])
model.add_exactly_one(incoming_sink_literals)
# Each shift is covered.
for s in range(num_shifts):
model.add_exactly_one(performed[d, s] for d in range(num_drivers))
# Globally, each node has one incoming and one outgoing literal
model.add_exactly_one(shared_incoming_literals[s])
model.add_exactly_one(shared_outgoing_literals[s])add_circuit call is not used!for d in range(num_drivers):
...
# Create circuit constraint.
model.add_exactly_one(outgoing_source_literals)
for s in range(num_shifts):
model.add_exactly_one(outgoing_literals[s])
model.add_exactly_one(incoming_literals[s])
model.add_exactly_one(incoming_sink_literals)
# Each shift is covered.
for s in range(num_shifts):
model.add_exactly_one(performed[d, s] for d in range(num_drivers))
# Globally, each node has one incoming and one outgoing literal
model.add_exactly_one(shared_incoming_literals[s])
model.add_exactly_one(shared_outgoing_literals[s])