Bakery Problem¶

Use the PuLP library https://pypi.org/project/PuLP/ to solve the following problems. Documentation to PuLP can be found here: https://coin-or.github.io/pulp/main/index.html

Bakery problem (20 points)¶

Consider a small bakery with a single oven. It needs to schedule baking of n pastries, each of them having three requirements:

time when the preparations are done and pastry is ready for baking time needed for baking, i.e., for how long should it remain in the oven deadline: time when the custommer comes to pick up the pastry At each moment, only one kind of pastry can be present in the oven. Use an ILP to find a shortest baking schedule. Schedule, in this context, is a set of starting times s1, ..., sn denoting when should each pastry be put into oven. Note: these times need not be integral. However, integral variables will be useful to enforce that the periods when two different kinds of pastries are in the oven do not overlap.

Let us denote $e_1$, ..., $e_n$ the ending times of baking of each of the pastries, i.e., $e_i = s_i + baking time of pastry_i$. We need to make sure that for each two pastries $i,j$, one of the following needs to be true: $e_i ≤ s_j$ or $e_j ≤ s_i$. Obviously, they cannot hold at the same time and it depends on the precedence between i and j which one is true. Since we do not know the precedence in advance, which of these constraints should we include in the LP?

Big-M method¶

This name usually refers to an alternative way how to start the simplex method without knowledge of the initial basic feasible solution. We did not cover this in the class and I do not go into details of this here either. But the other meaning of Big-M is a method for switching someof the constraints on/off depending on the value of some binary variable.

Imagine, we have variable x which should be bounded by $10$ if and only if some binary variable $z$ is set to zero. Also, assume that there is no reason to increase x beyound some large number $M$ (e.g., because we are minimizing over $x$, or we know that no feasible solution can have $x > M$ for some other reasons). Then, we can write $x ≤ 10 + Mz$: if $z$ is $0$, this switches the constraint ON. If $z=1$, this constraint evaluates to $x ≤ 10 + M$ which, by choice of $M$ is satisfied by any reasonable solution to our LP and this effectively switches the costraint OFF. Usually, due to possible numerical issues, it is recommended to use $M$ as small as possible. You can check the following blog for more discussion of big-M: https://orinanobworld.blogspot.com/2011/07/perils-of-big-m.html.

You may check that there is a suitable choice of $M$ in our problem and use this approach in your solution.

Input¶

Text file containing a single line for each kind of pastry consisting of four numbers (integers) separated by spaces:

ID PRE DLN BAK

ID denotes the numerical ID of the pastry, PRE the time since midnight since when the pastry is ready for baking, BAK is the time it needs to spend in the oven, and DLN is the deadline when the pastry needs to be surely finished. All times are in seconds.

Output¶

List of starting times of each pastry. Should look like this:

s1: 23.0
s2: 4.0
s3: 25.0
s4: 72.0
s5: 34.0

...

Bakery problem visualization (10 points)¶

Use library matplotlib to visualize your solution suitably. I leave to your creativity how to do it, but it should be clear what are the moments when oven needs to be open, what pastry goes out and what should be put in. There are many other things to visualize: expected arrivals of custommers and times when each pastry is ready, critical preparations (which pastry needs special care to be prepared on time, otherwise it would delay the whole schedule, etc). The main criterion for evaluation of this will be clarity and information it provides.

In [ ]:
# Import packages. You can import additional packages, if you want
# You can change the way they are imported, e.g import pulp as pl or whatever
# But take care to adapt the solver configuration accordingly.
from pulp import *
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import time
import matplotlib.patches as mpatches
import matplotlib.lines as mlines

# Use the following solver
solver = COIN_CMD(threads=8)
# at home, you can try it with different solvers, e.g. GLPK, or with a different
# number of threads.
# WARNING: your code when run in vocareum should finish within 10 minutes!!!

def bakery():
    # Input file is called ./bakery.txt
    input_filename = './bakery.txt'

    # Use solver defined above as a parameter of .solve() method.
    # e.g., if your LpProblem is called prob, then run
    # prob.solve(solver) to solve it.

    # Load input data
    with open(input_filename, "r") as f:
        data = [list(map(int, line.strip().split())) for line in f]

    n = len(data)

    # Create table
    df = pd.DataFrame(data, columns = ['ID', 'PRE', 'DLN', 'BAK'])

    # Define the problem
    prob = LpProblem('Scheduling', LpMinimize)

    # We estimate the big-M value in such a way that is bigger than the deadlines,
    # but not big enough to create problems to our code
    M = 27001

    # Dependent variable
    y = LpVariable('y', lowBound=0, cat='Continuous')
    # Starting time variables
    s = LpVariable.dicts('s', [i for i in range(n)], cat = 'Continuous')
    # Auxiliary binary variables
    b = LpVariable.dicts('z', [(i,j) for i in range(n) for j in range(n)], cat=('Binary'))

    # Objective function
    prob += y

    # Add the constraints
    for i in range(n):
        prob += s[i] >= df['PRE'][i]
        prob += s[i] <= (df['DLN'][i] - df['BAK'][i])
        prob += y >= (s[i] + df['BAK'][i])
        for j in range(n):
            if i>j:
                prob += s[i] >= (s[j] + df["BAK"][j] - M * b[i, j])
                prob += s[j] >= (s[i] + df["BAK"][i] - M + M * b[i, j])

    # Solve the ILP
    prob.solve(PULP_CBC_CMD(msg=False))

    # extract solution
    retval = {}
    for i in range(n):
        retval[f's_{i}'] = s[i].value()

    # Create a dictionary storing start time - end time of each pastry
    startend = {}
    for i in range(n):
        startend[i] = (retval[f's_{i}'], retval[f's_{i}'] + df.loc[i,'BAK'])

    # Define the figure and axes for the plot
    fig, ax = plt.subplots(figsize=(15, 8))

    # Set title
    ax.set_title('Baking Schedule', fontsize = 20)

    # Sort the rows of the dataframe by the 'PRE' column
    df = df.sort_values(by='PRE')

    # Create a list of tuples with starting time and row index for each pastry
    pastry_order = [(startend[i][0], i) for i in range(n)]

    # I reverse the order because I want the top bars to be those corresponding to the first pastries
    pastry_order.sort(reverse=True)

    # Create a list of categorical values for the y-axis
    y_values = [str(i) for i in range(n)]

    # Reorder the y-axis categorical values based on the sorted pastry order
    y_values = [y_values[i[1]] for i in pastry_order]

    # Initialize a flag to keep track of the current label position
    label_above = True

    # Loop over each pastry in the sorted order
    for _, i in pastry_order:
        # Get the starting and ending times for the pastry
        start_time, end_time = startend[i]
        # Get the baking time for the pastry
        baking_time = df.loc[i, 'BAK']
        prep_time = df.loc[i, 'PRE'] - retval[f's_{i}']

        # Add a horizontal bar for the pastry
        ax.barh(str(i), baking_time, left=start_time, height=0.5, color='#FFA8A8')

        # Add a horizontal bar for the preparation time
        ax.barh(str(i), prep_time, left=start_time, height=0.1, color='yellow')

        # Add a horizontal bar for the time from the end of baking to the deadline
        deadline_bar_start = end_time
        deadline_bar_width = df.loc[i, 'DLN'] - deadline_bar_start
        ax.barh(str(i), deadline_bar_width, left=deadline_bar_start, height=0.1, color='#B7F0B1')

        # Create red border around 'critical' preparations (those in which the pastry has to be baked immediately)
        if prep_time == 0:
            # Add a red border to the baking time bar
            ax.barh(str(i), baking_time, left=start_time, height=0.5, edgecolor='red', lw=2, fill=False)
        else:
            # Add a regular red baking time bar
            ax.barh(str(i), baking_time, left=start_time, height=0.5, color='#FFA8A8')

        # Creat black star after the preparations in which pastries must be sold immediately after being baked
        if deadline_bar_width == 0:
            plt.scatter(start_time + baking_time + 250, f'{i}', marker='*',  color='red')

        # Add a label for the pastry
        ax.text(start_time + baking_time / 2, str(i), f"{i}", ha='center', va='center')

        # Add a vertical line from the end of the baking time bar to the x-axis
        ax.plot([end_time, end_time], [0, n], linestyle='dashed', color='gray', alpha=0.5)

        # Add a label for the end time of the baking time bar
        end_time_hours = int(end_time // 3600)
        end_time_minutes = int((end_time % 3600) // 60)
        end_time_seconds = int(end_time % 60)
        if end_time_seconds == 0:
            end_time_str = f"{end_time_hours:02d}:{end_time_minutes:02d}"
        else:
            end_time_str = f"{end_time_hours:02d}:{end_time_minutes:02d}:{end_time_seconds:02d}"

        # Choose the label position based on the current flag value
        if label_above:
            label_ypos = -0.4
            va = 'top'
        else:
            label_ypos = n + 0.1
            va = 'bottom'

        ax.text(end_time, label_ypos, end_time_str, ha='center', va=va, fontsize=8)

        # Switch the label position flag for the next iteration
        label_above = not label_above

    # Set the y-axis label
    ax.set_ylabel('Pastry ID')

    # Set the x-axis limits and label
    ax.set_xlim(0, 28200)
    ax.set_xlabel('Time (hour:minute)')

    # Define the tick locations and labels
    x_ticks = [0, 3600, 7200, 10800, 14400, 18000, 21600, 25200]
    x_ticklabels = ['0:00', '1:00', '2:00', '3:00', '4:00', '5:00', '6:00', '7:00']

    # Set the tick locations
    ax.set_xticks(x_ticks)

    # Set the tick labels
    ax.set_xticklabels(x_ticklabels)

    # Create a list of y-axis tick locations
    y_ticks = list(range(n))

    # Set the y-axis tick locations and labels
    ax.set_yticks(y_ticks)
    ax.set_yticklabels(y_values)

    # Remove the y-axis ticks
    ax.tick_params(axis='y', which='both', length=0)

    # Create the legend handles
    prep_patch = mpatches.Patch(color='yellow', label='Ready to Bake')
    bake_patch = mpatches.Patch(color='#FFA8A8', label='Baking')
    dead_patch = mpatches.Patch(color='#B7F0B1', label='Ready to sell')
    crit_patch = mpatches.Patch(facecolor='none', edgecolor='red', label='Critical Preparations')
    star_handle = mlines.Line2D([], [], color='red', marker='*', linestyle='None', markersize=10, label='Sell immediately')

    # Add the legend
    ax.legend(handles=[prep_patch, bake_patch, dead_patch, crit_patch, star_handle], loc='lower center', bbox_to_anchor=(0.5, -0.15), ncol=5)

    # Write visualization to the correct file:
    visualization_filename = './visualization.png'

    # Save the plot
    plt.savefig(visualization_filename, dpi=300, bbox_inches='tight')

    # retval should be a dictionary such that retval['s_i'] is the starting
    # time of pastry i
    print(value(prob.objective))
    return retval