{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Use case: formula one pitstop\n", "\n", "This example is based on the DailyMail blog entry https://www.dailymail.co.uk/sport/formulaone/article-4401632/Formula-One-pit-stop-does-crew-work.html where a nice image shows 21 people changing the 4 tires of a Formula 1 Ferrari. In this example, only 16 out 21 people are represented. This notebook can be tested online at mybinder.org\n", "[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/tpaviot/ProcessScheduler/HEAD?filepath=doc/use-case-formula-one-change-tires.ipynb)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from IPython.display import YouTubeVideo\n", "\n", "YouTubeVideo(\"aHSUp7msCIE\", width=800, height=300)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Imports" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import processscheduler as ps\n", "\n", "%config InlineBackend.figure_formats = ['svg']" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Create the scheduling problem\n", "The total horizon is not knwown, leave it empty and only set the problem name." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "change_tires_problem = ps.SchedulingProblem(\"ChangeTires\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Create the 16 available resources\n", "Each people in and around the car is represented as a worker." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "nb_lifters = 2\n", "nb_gunners = 4\n", "nb_tyre_handlers = 8\n", "nb_stabilizers = 2" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Lift tasks\n", "lifters = [ps.Worker(\"JackOperator%i\" % (i + 1)) for i in range(nb_lifters)]\n", "gunners = [ps.Worker(\"Gunner%i\" % (i + 1)) for i in range(nb_gunners)]\n", "tyre_handlers = [ps.Worker(\"Handler%i\" % (i + 1)) for i in range(nb_tyre_handlers)]\n", "stabilizers = [ps.Worker(\"Stabilizer%i\" % (i + 1)) for i in range(nb_stabilizers)]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Create tasks and assign resources\n", "One period is mapped to one second. For example, if lifting the rear take 2sec then the duration will be set to 2." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# lift tasks and lifters\n", "# both lift tasks can be processed by any one of the lifters\n", "lift_rear_up = ps.FixedDurationTask(\"LiftRearUp\", duration=2)\n", "lift_front_up = ps.FixedDurationTask(\"LiftFrontUp\", duration=2)\n", "lift_rear_up.add_required_resource(lifters[0])\n", "lift_front_up.add_required_resource(lifters[1])\n", "\n", "lift_rear_down = ps.FixedDurationTask(\"LiftRearDown\", duration=2)\n", "lift_front_down = ps.FixedDurationTask(\"LiftFrontDown\", duration=2)\n", "lift_rear_down.add_required_resource(lifters[0])\n", "lift_front_down.add_required_resource(lifters[1])\n", "\n", "# unscrew tasks\n", "unscrew_front_left_tyre = ps.FixedDurationTask(\"UnScrewFrontLeftTyre\", duration=2)\n", "unscrew_front_right_tyre = ps.FixedDurationTask(\"UnScrewFrontRightTyre\", duration=2)\n", "unscrew_rear_left_tyre = ps.FixedDurationTask(\"UnScrewRearLeftTyre\", duration=2)\n", "unscrew_rear_right_tyre = ps.FixedDurationTask(\"UnScrewRearRightTyre\", duration=2)\n", "\n", "gunner_unscrew_front_left_tyre = ps.SelectWorkers(gunners, 1)\n", "unscrew_front_left_tyre.add_required_resource(gunner_unscrew_front_left_tyre)\n", "\n", "gunner_unscrew_front_right_tyre = ps.SelectWorkers(gunners, 1)\n", "unscrew_front_right_tyre.add_required_resource(gunner_unscrew_front_right_tyre)\n", "\n", "gunner_unscrew_rear_left_tyre = ps.SelectWorkers(gunners, 1)\n", "unscrew_rear_left_tyre.add_required_resource(gunner_unscrew_rear_left_tyre)\n", "\n", "gunner_unscrew_rear_right_tyre = ps.SelectWorkers(gunners, 1)\n", "unscrew_rear_right_tyre.add_required_resource(gunner_unscrew_rear_right_tyre)\n", "\n", "# screw tasks and gunners\n", "screw_front_left_tyre = ps.FixedDurationTask(\"ScrewFrontLeftTyre\", duration=2)\n", "screw_front_right_tyre = ps.FixedDurationTask(\"ScrewFrontRightTyre\", duration=2)\n", "screw_rear_left_tyre = ps.FixedDurationTask(\"ScrewRearLeftTyre\", duration=2)\n", "screw_rear_right_tyre = ps.FixedDurationTask(\"ScrewRearRightTyre\", duration=2)\n", "\n", "gunner_screw_front_left_tyre = ps.SelectWorkers(gunners)\n", "screw_front_left_tyre.add_required_resource(gunner_screw_front_left_tyre)\n", "\n", "gunner_screw_front_right_tyre = ps.SelectWorkers(gunners)\n", "screw_front_right_tyre.add_required_resource(gunner_screw_front_right_tyre)\n", "\n", "gunner_screw_rear_left_tyre = ps.SelectWorkers(gunners)\n", "screw_rear_left_tyre.add_required_resource(gunner_screw_rear_left_tyre)\n", "\n", "gunner_screw_rear_right_tyre = ps.SelectWorkers(gunners)\n", "screw_rear_right_tyre.add_required_resource(gunner_screw_rear_right_tyre)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# tires OFF and handlers\n", "front_left_tyre_off = ps.FixedDurationTask(\"FrontLeftTyreOff\", duration=2)\n", "front_right_tyre_off = ps.FixedDurationTask(\"FrontRightTyreOff\", duration=2)\n", "rear_left_tyre_off = ps.FixedDurationTask(\"RearLeftTyreOff\", duration=2)\n", "rear_right_tyre_off = ps.FixedDurationTask(\"RearRightTyreOff\", duration=2)\n", "\n", "for tyre_off_task in [\n", " front_left_tyre_off,\n", " front_right_tyre_off,\n", " rear_left_tyre_off,\n", " rear_right_tyre_off,\n", "]:\n", " tyre_off_task.add_required_resource(ps.SelectWorkers(tyre_handlers))\n", "\n", "# tires ON and handlers, same as above\n", "front_left_tyre_on = ps.FixedDurationTask(\"FrontLeftTyreOn\", duration=2)\n", "front_right_tyre_on = ps.FixedDurationTask(\"FrontRightTyreOn\", duration=2)\n", "rear_left_tyre_on = ps.FixedDurationTask(\"RearLeftTyreOn\", duration=2)\n", "rear_right_tyre_on = ps.FixedDurationTask(\"RearRightTyreOn\", duration=2)\n", "\n", "for tyre_on_task in [\n", " front_left_tyre_on,\n", " front_right_tyre_on,\n", " rear_left_tyre_on,\n", " rear_right_tyre_on,\n", "]:\n", " tyre_on_task.add_required_resource(ps.SelectWorkers(tyre_handlers))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Stabilizers** start their job as soon as the car is stopped until the end of the whole activity." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "stabilize_left = ps.VariableDurationTask(\"StabilizeLeft\")\n", "stabilize_right = ps.VariableDurationTask(\"StabilizeRight\")\n", "\n", "stabilize_left.add_required_resource(stabilizers[0])\n", "stabilize_right.add_required_resource(stabilizers[1])\n", "\n", "ps.TaskStartAt(stabilize_left, 0)\n", "ps.TaskStartAt(stabilize_right, 0)\n", "\n", "ps.TaskEndAt(stabilize_left, change_tires_problem.horizon)\n", "ps.TaskEndAt(stabilize_right, change_tires_problem.horizon)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Task precedences" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# front left tyre operations\n", "fr_left = [\n", " unscrew_front_left_tyre,\n", " front_left_tyre_off,\n", " front_left_tyre_on,\n", " screw_front_left_tyre,\n", "]\n", "for i in range(len(fr_left) - 1):\n", " ps.TaskPrecedence(fr_left[i], fr_left[i + 1])\n", "# front right tyre operations\n", "fr_right = [\n", " unscrew_front_right_tyre,\n", " front_right_tyre_off,\n", " front_right_tyre_on,\n", " screw_front_right_tyre,\n", "]\n", "for i in range(len(fr_right) - 1):\n", " ps.TaskPrecedence(fr_right[i], fr_right[i + 1])\n", "# rear left tyre operations\n", "re_left = [\n", " unscrew_rear_left_tyre,\n", " rear_left_tyre_off,\n", " rear_left_tyre_on,\n", " screw_rear_left_tyre,\n", "]\n", "for i in range(len(re_left) - 1):\n", " ps.TaskPrecedence(re_left[i], re_left[i + 1])\n", "# front left tyre operations\n", "re_right = [\n", " unscrew_rear_right_tyre,\n", " rear_right_tyre_off,\n", " rear_right_tyre_on,\n", " screw_rear_right_tyre,\n", "]\n", "for i in range(len(re_right) - 1):\n", " ps.TaskPrecedence(re_right[i], re_right[i + 1])\n", "\n", "# all un screw operations must start after the car is lift by both front and rear jacks\n", "for unscrew_tasks in [\n", " unscrew_front_left_tyre,\n", " unscrew_front_right_tyre,\n", " unscrew_rear_left_tyre,\n", " unscrew_rear_right_tyre,\n", "]:\n", " ps.TaskPrecedence(lift_rear_up, unscrew_tasks)\n", " ps.TaskPrecedence(lift_front_up, unscrew_tasks)\n", "\n", "# lift down operations must occur after each screw task is completed\n", "for screw_task in [\n", " screw_front_left_tyre,\n", " screw_front_right_tyre,\n", " screw_rear_left_tyre,\n", " screw_rear_right_tyre,\n", "]:\n", " ps.TaskPrecedence(screw_task, lift_rear_down)\n", " ps.TaskPrecedence(screw_task, lift_front_down)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### First solution, plot the schedule" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "solver = ps.SchedulingSolver(change_tires_problem)\n", "solution_1 = solver.solve()\n", "solution_1.render_gantt_matplotlib(fig_size=(10, 5), render_mode=\"Resource\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Second solution: add a makespan objective\n", "Obviously, the former solution is not the *best* solution, not sure Ferrari will win this race ! The whole \"change tires\" activity must be as short as possible, so let's add a *makespan* objective, i.e. a constraint that minimizes the schedule horizon." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# add makespan objective\n", "change_tires_problem.add_objective_makespan()\n", "\n", "solver_2 = ps.SchedulingSolver(change_tires_problem)\n", "solution_2 = solver_2.solve()\n", "solution_2.render_gantt_matplotlib(fig_size=(9, 5), render_mode=\"Task\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Third solution: constraint workers\n", "This is not the best possible solution. Indeed, we can notice that the Gunner2 unscrews the RearRightTyre and screw the RearLeft tyre. We cannot imagine that a solution where gunners turn around the car is acceptable. There are two solutions to fix the schedule:\n", "- let the gunner be able to turn around the car, and add a \"Move\" task with a duration that represent the time necessary to move from one tyre to the other,\n", "- constraint the worker to screw the same tyre he unscrewed. Let's go this way" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "ps.SameWorkers(gunner_unscrew_front_left_tyre, gunner_screw_front_left_tyre)\n", "ps.SameWorkers(gunner_unscrew_front_right_tyre, gunner_screw_front_right_tyre)\n", "ps.SameWorkers(gunner_unscrew_rear_left_tyre, gunner_screw_rear_left_tyre)\n", "ps.SameWorkers(gunner_unscrew_rear_right_tyre, gunner_screw_rear_right_tyre)\n", "\n", "solver_3 = ps.SchedulingSolver(change_tires_problem)\n", "solution_3 = solver_3.solve()\n", "solution_3.render_gantt_matplotlib(fig_size=(9, 5), render_mode=\"Task\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This is much better !" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.6" } }, "nbformat": 4, "nbformat_minor": 4 }