tpaviot / ProcessScheduler

A Python package for automatic and optimized resource scheduling
https://processscheduler.github.io/
GNU General Public License v3.0
59 stars 19 forks source link

ResourceUnavailable constraint specification #126

Closed rnwolf closed 1 year ago

rnwolf commented 1 year ago

I seem to have a problem specifying when a specific resource is unavailable. I am looking to crate a model in which specified resources are not working weekends for example.

When I specify that Albert is unavailable with:

ps.ResourceUnavailable(Albert, [(1, 4), (6, 9)]) # Try a specific named worker

I still have Albert scheduled to work on tasked in those time periods.

Any suggestions?

albert-unavilable

#!/usr/bin/env python
# coding: utf-8

import processscheduler as ps
from datetime import timedelta, datetime

# Create the scheduling problem
# The total horizon is not known

agile_problem = ps.SchedulingProblem('agile-priorities')
# ,delta_time=timedelta(days=1))

# nb_fullstack = 1
# nb_backend = 1
# nb_frontend = 1
# nb_testers = 2
#
# Team with skills
# fullstack = [ps.Worker('Full%i' % (i + 1)) for i in range(nb_fullstack)]
# backend = [ps.Worker('Back%i' % (i + 1)) for i in range(nb_backend)]
# frontend = [ps.Worker('Front%i' % (i + 1)) for i in range(nb_frontend)]
# tester = [ps.Worker('Tester%i' % (i + 1)) for i in range(nb_testers)]

# Alternative try with named workers

Albert = ps.Worker('Albert')  # Fullstack
Bob = ps.Worker('Bob')  # Backend
Fred = ps.Worker('Fred')  # Frontend
Ted = ps.Worker('Ted')  # Tester
Tony = ps.Worker('Tony')  # Tester

ps.ResourceUnavailable(Albert, [(1, 4), (6, 9)])  # Try a specific named worker

# Create tasks and assign resources
# One period is mapped to one day.

kick_off_epic_1 = ps.FixedDurationTask('KickOffEpic_1', duration=1)
kick_off_epic_1.add_required_resource(ps.SelectWorkers([Albert, Bob, Fred, Ted]))

work_item_1 = ps.FixedDurationTask('WorkItem_1', duration=3)
work_item_1.add_required_resource(Albert)

work_item_2 = ps.FixedDurationTask('WorkItem_2', duration=2)
work_item_2.add_required_resource(Bob)

work_item_3 = ps.FixedDurationTask('WorkItem_3', duration=2)

# the testing activity can be processed by the Tester1 OR Tester2
work_item_3.add_required_resource(ps.SelectWorkers([Ted, Tony],
                                                   nb_workers_to_select=1,
                                                   kind='exact'))

work_item_4 = ps.FixedDurationTask('WorkItem_4', duration=2)
work_item_4.add_required_resource(Albert)

release_epic_1 = ps.FixedDurationTask('ReleaseEpic_1', duration=1)
release_epic_1.add_required_resource(Tony)

ps.TaskStartAt(kick_off_epic_1, 0)
ps.TaskEndAt(release_epic_1, agile_problem.horizon)

# Task precedences

ps.TaskPrecedence(kick_off_epic_1, work_item_1)
ps.TaskPrecedence(work_item_1, work_item_2)
ps.TaskPrecedence(work_item_2, work_item_3)
ps.TaskPrecedence(work_item_3,work_item_4)
ps.TaskPrecedence(work_item_4,release_epic_1)

# First solution, plot the schedule

solver = ps.SchedulingSolver(agile_problem)
solution_1 = solver.solve()
solution_1.render_gantt_matplotlib(fig_size=(10, 5), render_mode='Resource')
jbdyn commented 1 year ago

Hey @rnwolf! I ran into the same issue and found out what was going on when I exchanged the line

ps.ResourceUnavailable(Albert, [(1, 4), (6, 9)])  # Try a specific named worker

with

ps.WorkLoad(Albert, {(1, 4) : 0, (6, 9): 0})  # Try a specific named worker

since the docs mention that ps.ResourceUnavailable is a special case of ps.WorkLoad.

Now I got the following error:

---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[128], line 29
     26 Ted = ps.Worker('Ted')  # Tester
     27 Tony = ps.Worker('Tony')  # Tester
---> 29 ps.WorkLoad(Albert, {(1, 4) : 0, (6, 9): 0})  # Try a specific named worker
     31 # Create tasks and assign resources
     32 # One period is mapped to one day.
     34 kick_off_epic_1 = ps.FixedDurationTask('KickOffEpic_1', duration=1)

File ~/.mambaforge/envs/pm/lib/python3.11/site-packages/processscheduler/resource_constraint.py:124, in WorkLoad.__init__(self, resource, dict_time_intervals_and_bound, kind, optional)
    121         durations.append(dur)
    123 if not durations:
--> 124     raise AssertionError(
    125         "The resource is not assigned to any task. WorkLoad constraint meaningless."
    126     )
    128 # workload constraint depends on the kind
    129 if kind == "exact":

AssertionError: The resource is not assigned to any task. WorkLoad constraint meaningless.

So I moved the line which adds the resource constraint after the assignment of the resource (Albert) to tasks:

 ...
 # Alternative try with named workers

 Albert = ps.Worker('Albert')  # Fullstack
 Bob = ps.Worker('Bob')  # Backend
 Fred = ps.Worker('Fred')  # Frontend
 Ted = ps.Worker('Ted')  # Tester
 Tony = ps.Worker('Tony')  # Tester

-# moved resource constraint from here...

-ps.ResourceUnavailable(Albert, [(1, 4), (6, 9)])  # Try a specific named worker

 # Create tasks and assign resources
 # One period is mapped to one day.

 kick_off_epic_1 = ps.FixedDurationTask('KickOffEpic_1', duration=1)
 kick_off_epic_1.add_required_resource(ps.SelectWorkers([Albert, Bob, Fred, Ted]))

 work_item_1 = ps.FixedDurationTask('WorkItem_1', duration=3)
 work_item_1.add_required_resource(Albert)

 work_item_2 = ps.FixedDurationTask('WorkItem_2', duration=2)
 work_item_2.add_required_resource(Bob)

 work_item_3 = ps.FixedDurationTask('WorkItem_3', duration=2)

 # the testing activity can be processed by the Tester1 OR Tester2
 work_item_3.add_required_resource(ps.SelectWorkers([Ted, Tony],
                                                    nb_workers_to_select=1,
                                                    kind='exact'))

 work_item_4 = ps.FixedDurationTask('WorkItem_4', duration=2)
 work_item_4.add_required_resource(Albert)

 release_epic_1 = ps.FixedDurationTask('ReleaseEpic_1', duration=1)
 release_epic_1.add_required_resource(Tony)

+# ... to here.
+
+# Add resource constraints after resources have been assigned to tasks
+ps.ResourceUnavailable(Albert, [(1, 4), (6, 9)])  # Try a specific named worker
+
 ps.TaskStartAt(kick_off_epic_1, 0)
 ps.TaskEndAt(release_epic_1, agile_problem.horizon)
 ...
Fully working example ``` import processscheduler as ps from datetime import timedelta, datetime # Create the scheduling problem # The total horizon is not known agile_problem = ps.SchedulingProblem('agile-priorities') # ,delta_time=timedelta(days=1)) # nb_fullstack = 1 # nb_backend = 1 # nb_frontend = 1 # nb_testers = 2 # # Team with skills # fullstack = [ps.Worker('Full%i' % (i + 1)) for i in range(nb_fullstack)] # backend = [ps.Worker('Back%i' % (i + 1)) for i in range(nb_backend)] # frontend = [ps.Worker('Front%i' % (i + 1)) for i in range(nb_frontend)] # tester = [ps.Worker('Tester%i' % (i + 1)) for i in range(nb_testers)] # Alternative try with named workers Albert = ps.Worker('Albert') # Fullstack Bob = ps.Worker('Bob') # Backend Fred = ps.Worker('Fred') # Frontend Ted = ps.Worker('Ted') # Tester Tony = ps.Worker('Tony') # Tester # Create tasks and assign resources # One period is mapped to one day. kick_off_epic_1 = ps.FixedDurationTask('KickOffEpic_1', duration=1) kick_off_epic_1.add_required_resource(ps.SelectWorkers([Albert, Bob, Fred, Ted])) work_item_1 = ps.FixedDurationTask('WorkItem_1', duration=3) work_item_1.add_required_resource(Albert) work_item_2 = ps.FixedDurationTask('WorkItem_2', duration=2) work_item_2.add_required_resource(Bob) work_item_3 = ps.FixedDurationTask('WorkItem_3', duration=2) # the testing activity can be processed by the Tester1 OR Tester2 work_item_3.add_required_resource(ps.SelectWorkers([Ted, Tony], nb_workers_to_select=1, kind='exact')) work_item_4 = ps.FixedDurationTask('WorkItem_4', duration=2) work_item_4.add_required_resource(Albert) release_epic_1 = ps.FixedDurationTask('ReleaseEpic_1', duration=1) release_epic_1.add_required_resource(Tony) # Add resource constraints after resources have been assigned to tasks ps.ResourceUnavailable(Albert, [(1, 4), (6, 9)]) # Try a specific named worker ps.TaskStartAt(kick_off_epic_1, 0) ps.TaskEndAt(release_epic_1, agile_problem.horizon) # Task precedences ps.TaskPrecedence(kick_off_epic_1, work_item_1) ps.TaskPrecedence(work_item_1, work_item_2) ps.TaskPrecedence(work_item_2, work_item_3) ps.TaskPrecedence(work_item_3,work_item_4) ps.TaskPrecedence(work_item_4,release_epic_1) # First solution, plot the schedule solver = ps.SchedulingSolver(agile_problem) solution_1 = solver.solve() solution_1.render_gantt_matplotlib(fig_size=(10, 5), render_mode='Resource') ```

Rendered Gantt chart: agile-priorities-fixed

So, the issue here was that there was no indication to the user that the order of task assignment and adding resource constraints matter in the case of ps.ResourceUnavailable.

@tpaviot Thanks for this amazing package!

I had already a very quick look at the class definition, but was not a able to directly see how to add an assertion there.

Could you please have a look?

Related issue: #110

rnwolf commented 1 year ago

Brilliant I will experiment some more.

tpaviot commented 1 year ago

Thank you guys for your feedback.