Alice and Bob are running a paint shop for bikes where they pimp bikes with fresh colors. Today they have to paint a green and a red bike. To get started they import pyschedule and create a new scenario. We use hours as granularity and expect a working day of at most 10 hours, so we set the planning horizon to 10. Some solvers do not need this parameter, but the default solver requires it:
In [24]:
from pyschedule import Scenario, solvers, plotters
S = Scenario('bike_paint_shop', horizon=10)
Then they create themselves as resources:
In [25]:
Alice = S.Resource('Alice')
Bob = S.Resource('Bob')
Painting a bike takes two hours. Moreover, after the bike has been painted, it needs to get post-processed (e.g. tires pumped) which takes one hour (which is the default). This translates into four tasks in total:
In [26]:
green_paint, red_paint = S.Task('green_paint', length=2), S.Task('red_paint', length=2)
green_post, red_post = S.Task('green_post'), S.Task('red_post')
Clearly, one can only do the post-processing after the painting with an arbitrary gap in between. For the red paint we are a little stricter, here we want to start the post-processing exactly one hour after the painting since this is the time the color takes to dry:
In [27]:
S += green_paint < green_post, red_paint + 1 <= red_post
Each task can be done by either Alice or Bob:
In [28]:
green_paint += Alice|Bob
green_post += Alice|Bob
red_paint += Alice|Bob
red_post += Alice|Bob
So lets have a look at the scenario:
In [29]:
S.clear_solution()
print(S)
We havent defined an objective yet. We want to finish all tasks as early as possible, and so we use the MakeSpan and check the scenario again:
In [30]:
S.use_makespan_objective()
print(S)
Hence, we want to minimize the position of the MakeSpan task subject to the constraint that it is scheduled after all other tasks. Thus, the position of the MakeSpan is the length of our schedule. Now we have the first version of our scenario, lets solve and plot it:
In [31]:
# Set some colors for the tasks
task_colors = { green_paint : '#A1D372',
green_post : '#A1D372',
red_paint : '#EB4845',
red_post : '#EB4845',
S['MakeSpan'] : '#7EA7D8'}
# A small helper method to solve and plot a scenario
def run(S) :
if solvers.mip.solve(S):
%matplotlib inline
plotters.matplotlib.plot(S,task_colors=task_colors,fig_size=(10,5))
else:
print('no solution exists')
run(S)
Note that it could happen that somebody needs to paints the red bike and then do the green post-processing. This would be annoying, switching bikes takes too much time. We use the following constraints to ensure that the green/red painting and post-processing is always done by the same persons:
In [32]:
# green_post will use the same resources as green_paint if there is an overlap in resource requirement
green_post += green_paint*[Alice,Bob]
# same for red_post and red_paint
red_post += red_paint*[Alice,Bob]
run(S)
This schedule completes after four hours and suggests to paint both bikes at the same time. However, Alice and Bob have only a single paint shop which they need to share:
In [33]:
Paint_Shop = S.Resource('Paint_Shop')
red_paint += Paint_Shop
green_paint += Paint_Shop
run(S)
Great, everybody can still go home after five hours and have a late lunch! Unfortunately, Alice receives a call that the red bike will only arrive after two hours:
In [34]:
S += red_paint > 2
run(S)
Too bad, everything takes now size hours to finish. Therefore Alica and Bob decide to schedule a lunch after the third hour and before the fifth hour:
In [35]:
Lunch = S.Task('Lunch')
Lunch += {Alice, Bob}
S += Lunch > 3, Lunch < 5
task_colors[Lunch] = '#7EA7D8'
S.clear_objective() #we need to remove the objective and readd it because of the new lunch task
S.use_makespan_objective()
task_colors[S['MakeSpan']] = '#7EA7D8'
run(S)
Alice is a morning person and wants to finish three hours of work before lunch, that is, before the third hour:
In [36]:
S += Alice['length'][0:3] >= 3
run(S)
The weather forecast is really good for the afternoon, so Alice and Bob decide to close the shop after lunch, that is, they fix the horizon to 5 hours. Unfortunately, the following happens:
In [37]:
S.horizon = 5
run(S)
This happens because there is not enough time to finish all task. Therefore, they need to priorize: Lets say the green bike gives a reward of 100 when finished while the red one only 50, which corresponds to negative schedule cost:
In [38]:
green_post.schedule_cost = -100 #negative schedule cost correspond to a reward for scheduling that task
green_paint.schedule_cost = 0 #we need to make the paint steps optional by setting the reward to 0
red_post.schedule_cost = -50
red_paint.schedule_cost = 0
# tasks are optional, but we only want to have the post-processing scheduled if there is some painting
red_paint += red_post
green_paint += green_post
run(S)
All this sounds quite trivial, but think about the same problem with many bikes and many persons!