2121- 15 min cleaning time after the last shift
2222- 2 min waiting time after each shift for passenger boarding and alighting
2323"""
24- from __future__ import print_function
2524
2625import collections
2726import math
2827
28+ from google .protobuf import text_format
29+ from absl import app
30+ from absl import flags
2931from ortools .sat .python import cp_model
3032
33+ FLAGS = flags .FLAGS
34+
35+ flags .DEFINE_string ('output_proto' , '' ,
36+ 'Output file to write the cp_model proto to.' )
37+ flags .DEFINE_string ('params' , 'num_search_workers:8,log_search_progress:true' ,
38+ 'Sat solver parameters.' )
39+ flags .DEFINE_integer ('instance' , 1 , 'Instance to select (1, 2, 3).' , 1 , 3 )
40+
3141SAMPLE_SHIFTS_SMALL = [
3242 #
3343 # column description:
16521662 [1355 , '00:57' , '01:07' , 1497 , 1507 , 10 ]
16531663] # yapf:disable
16541664
1655-
1656- SAMPLE_SHIFTS = SAMPLE_SHIFTS_MEDIUM
1665+ # pytype: disable=wrong-arg-types
16571666
16581667
16591668def bus_driver_scheduling (minimize_drivers , max_num_drivers ):
16601669 """Optimize the bus driver scheduling problem.
16611670
1662- This model has two modes.
1671+ This model has two modes.
1672+
1673+ If minimize_drivers == True, the objective will be to find the minimal
1674+ number of drivers, independently of the working times of each drivers.
16631675
1664- If minimize_drivers == True, the objective will be to find the minimal
1665- number of drivers, independently of the working times of each drivers.
1676+ Otherwise, will will create max_num_drivers non optional drivers, and
1677+ minimize the sum of working times of these drivers.
16661678
1667- Otherwise, will will create max_num_drivers non optional drivers, and
1668- minimize the sum of working times of these drivers.
1669- """
1670- num_shifts = len (SAMPLE_SHIFTS )
1679+ Args:
1680+ minimize_drivers: A Boolean parameter specifying the objective of the
1681+ problem. If True, it tries to minimize the number of used drivers. If
1682+ false, it minimizes the sum of working times per workers.
1683+ max_num_drivers: This number specifies the exact number of non optional
1684+ drivers to use. This is only used if 'minimize_drivers' is False.
1685+
1686+ Returns:
1687+ The objective value of the model.
1688+ """
1689+ shifts = None
1690+ if FLAGS .instance == 1 :
1691+ shifts = SAMPLE_SHIFTS_SMALL
1692+ elif FLAGS .instance == 2 :
1693+ shifts = SAMPLE_SHIFTS_MEDIUM
1694+ elif FLAGS .instance == 3 :
1695+ shifts = SAMPLE_SHIFTS_LARGE
1696+
1697+ num_shifts = len (shifts )
16711698
16721699 # All durations are in minutes.
16731700 max_driving_time = 540 # 8 hours.
@@ -1680,12 +1707,12 @@ def bus_driver_scheduling(minimize_drivers, max_num_drivers):
16801707 cleanup_time = 15
16811708
16821709 # Computed data.
1683- total_driving_time = sum (shift [5 ] for shift in SAMPLE_SHIFTS )
1684- min_num_drivers = int (
1685- math . ceil ( total_driving_time * 1.0 / max_driving_time ))
1710+ total_driving_time = sum (shift [5 ] for shift in shifts )
1711+ min_num_drivers = int (math . ceil ( total_driving_time * 1.0 /
1712+ max_driving_time ))
16861713 num_drivers = 2 * min_num_drivers if minimize_drivers else max_num_drivers
1687- min_start_time = min (shift [3 ] for shift in SAMPLE_SHIFTS )
1688- max_end_time = max (shift [4 ] for shift in SAMPLE_SHIFTS )
1714+ min_start_time = min (shift [3 ] for shift in shifts )
1715+ max_end_time = max (shift [4 ] for shift in shifts )
16891716
16901717 print ('Bus driver scheduling' )
16911718 print (' num shifts =' , num_shifts )
@@ -1760,7 +1787,7 @@ def bus_driver_scheduling(minimize_drivers, max_num_drivers):
17601787 performed [d , s ] = model .NewBoolVar ('performed_%i_%i' % (d , s ))
17611788
17621789 for s in range (num_shifts ):
1763- shift = SAMPLE_SHIFTS [s ]
1790+ shift = shifts [s ]
17641791 duration = shift [5 ]
17651792
17661793 # Arc from source to shift.
@@ -1772,10 +1799,9 @@ def bus_driver_scheduling(minimize_drivers, max_num_drivers):
17721799 shared_incoming_literals [s ].append (source_lit )
17731800 model .Add (start_times [d ] == shift [3 ] -
17741801 setup_time ).OnlyEnforceIf (source_lit )
1775- model .Add (
1776- total_driving [d , s ] == duration ).OnlyEnforceIf (source_lit )
1777- model .Add (
1778- no_break_driving [d , s ] == duration ).OnlyEnforceIf (source_lit )
1802+ model .Add (total_driving [d , s ] == duration ).OnlyEnforceIf (source_lit )
1803+ model .Add (no_break_driving [d ,
1804+ s ] == duration ).OnlyEnforceIf (source_lit )
17791805 starting_shifts [d , s ] = source_lit
17801806
17811807 # Arc from shift to sink
@@ -1787,14 +1813,15 @@ def bus_driver_scheduling(minimize_drivers, max_num_drivers):
17871813 incoming_sink_literals .append (sink_lit )
17881814 model .Add (end_times [d ] == shift [4 ] +
17891815 cleanup_time ).OnlyEnforceIf (sink_lit )
1790- model .Add (driving_times [ d ] == total_driving [ d , s ]). OnlyEnforceIf (
1791- sink_lit )
1816+ model .Add (
1817+ driving_times [ d ] == total_driving [ d , s ]). OnlyEnforceIf ( sink_lit )
17921818
17931819 # Node not performed
17941820 # - set both driving times to 0
17951821 # - add a looping arc on the node
1796- model .Add (total_driving [d , s ] == 0 ).OnlyEnforceIf (
1797- performed [d , s ].Not ())
1822+ model .Add (total_driving [d ,
1823+ s ] == 0 ).OnlyEnforceIf (performed [d ,
1824+ s ].Not ())
17981825 model .Add (no_break_driving [d , s ] == 0 ).OnlyEnforceIf (
17991826 performed [d , s ].Not ())
18001827 incoming_literals [s ].append (performed [d , s ].Not ())
@@ -1811,7 +1838,7 @@ def bus_driver_scheduling(minimize_drivers, max_num_drivers):
18111838 performed [d , s ])
18121839
18131840 for o in range (num_shifts ):
1814- other = SAMPLE_SHIFTS [o ]
1841+ other = shifts [o ]
18151842 delay = other [3 ] - shift [4 ]
18161843 if delay < min_delay_between_shifts :
18171844 continue
@@ -1826,9 +1853,8 @@ def bus_driver_scheduling(minimize_drivers, max_num_drivers):
18261853 model .Add (
18271854 no_break_driving [d , o ] == other [5 ]).OnlyEnforceIf (lit )
18281855 else :
1829- model .Add (
1830- no_break_driving [d , o ] == no_break_driving [d , s ] +
1831- other [5 ]).OnlyEnforceIf (lit )
1856+ model .Add (no_break_driving [d , o ] == no_break_driving [d , s ] +
1857+ other [5 ]).OnlyEnforceIf (lit )
18321858
18331859 # Add arc
18341860 outgoing_literals [s ].append (lit )
@@ -1890,26 +1916,32 @@ def bus_driver_scheduling(minimize_drivers, max_num_drivers):
18901916 working_drivers [d + 1 ].Not ())
18911917
18921918 # Redundant constraints: sum of driving times = sum of shift driving times
1893- model .Add (sum (driving_times ) == total_driving_time )
1919+ model .Add (cp_model . LinearExpr . Sum (driving_times ) == total_driving_time )
18941920 if not minimize_drivers :
18951921 model .Add (
1896- sum (working_times ) == total_driving_time +
1922+ cp_model . LinearExpr . Sum (working_times ) == total_driving_time +
18971923 num_drivers * (setup_time + cleanup_time ) +
18981924 cp_model .LinearExpr .ScalProd (delay_literals , delay_weights ))
18991925
19001926 if minimize_drivers :
19011927 # Minimize the number of working drivers
1902- model .Minimize (sum (working_drivers ))
1928+ model .Minimize (cp_model . LinearExpr . Sum (working_drivers ))
19031929 else :
19041930 # Minimize the sum of delays between tasks, which in turns minimize the
19051931 # sum of working times as the total driving time is fixed
19061932 model .Minimize (
19071933 cp_model .LinearExpr .ScalProd (delay_literals , delay_weights ))
19081934
1935+ if not minimize_drivers and FLAGS .output_proto :
1936+ print ('Writing proto to %s' % FLAGS .output_proto )
1937+ with open (FLAGS .output_proto , 'w' ) as text_file :
1938+ text_file .write (str (model ))
1939+
19091940 # Solve model.
19101941 solver = cp_model .CpSolver ()
1911- solver .parameters .log_search_progress = True # not minimize_drivers
1912- solver .parameters .num_search_workers = 8
1942+ if FLAGS .params :
1943+ text_format .Parse (FLAGS .params , solver .parameters )
1944+
19131945 status = solver .Solve (model )
19141946
19151947 if status != cp_model .OPTIMAL and status != cp_model .FEASIBLE :
@@ -1929,7 +1961,7 @@ def bus_driver_scheduling(minimize_drivers, max_num_drivers):
19291961
19301962 first = True
19311963 for s in range (num_shifts ):
1932- shift = SAMPLE_SHIFTS [s ]
1964+ shift = shifts [s ]
19331965
19341966 if not solver .BooleanValue (performed [d , s ]):
19351967 continue
@@ -1939,18 +1971,22 @@ def bus_driver_scheduling(minimize_drivers, max_num_drivers):
19391971 # no_break_driving which was reinitialized in that case.
19401972 if solver .Value (no_break_driving [d , s ]) == shift [5 ] and not first :
19411973 print (' **break**' )
1942- print (' shift ' , shift [0 ], ':' , shift [1 ], "-" , shift [2 ])
1974+ print (' shift ' , shift [0 ], ':' , shift [1 ], '-' , shift [2 ])
19431975 first = False
19441976
19451977 return int (solver .ObjectiveValue ())
19461978
19471979
1948- def optimize_bus_driver_allocation ( ):
1980+ def main ( _ ):
19491981 """Optimize the bus driver allocation in two passes."""
19501982 print ('----------- first pass: minimize the number of drivers' )
19511983 num_drivers = bus_driver_scheduling (True , - 1 )
1952- print ('----------- second pass: minimize the sum of working times' )
1953- bus_driver_scheduling (False , num_drivers )
1984+ if num_drivers == - 1 :
1985+ print ('no solution found, skipping the final step' )
1986+ else :
1987+ print ('----------- second pass: minimize the sum of working times' )
1988+ bus_driver_scheduling (False , num_drivers )
19541989
19551990
1956- optimize_bus_driver_allocation ()
1991+ if __name__ == '__main__' :
1992+ app .run (main )
0 commit comments