Saads Perfektewelt
🐞

How I dynamically generate Gantt charts for my recipes

Notation

\( i \in M \) : Set of machines
\( j \in J \) : Set of jobs
\( p_{i,j} \) : processing time of job j in machine i
\( C_{i,j} \) : Completion time of job j in machine i
\( LST_{j} \) : Latest starting time of job j

Intro

As a guy who is very passionate about combinatorial optimization, and as much as I kept editing the core of my website's layout, I thought about adding some spiciness to my articles, even if it defies my minimalistic philosophies.

Living here in Germany slowly and subconsciously turned me into a time-slave. Evil marketers did a really good job at putting emphasis on time, creating a need for goods and services that make me priorities rhythm before myself.

Scheduling was something that I was a fan of, the goal of their science is to come up with strategies and solution methods to solve scheduling problems. Their motives may sound simple but the average scheduling puzzles are \(NP-hard\), our current understanding on combinatorics encourages the development of heuristic approaches to tackle the hardships of investigating permutations by offering a solution that is good enough.

Cooking just began to slowly become an integral part of my life, my life style makes it hard for me to innovate, I just keep cycling through a limited set of recipes. Old habits die hard, at least they do. When I feel energetic, my dexterity goes top notch, enabling fast reactions and quick maneuvers across the kitchen, but sometimes I just brute force my structures without thinking, which can result in a chunk of wasted time, and sometimes effort.

In the theory of scheduling, if a given situation is properly formulated, you may gain access to tools that aid you reach your goals, without having to rely on any form of soft experience. This can be achieved by understanding the setup and trying to perceive it with intelligence, after all, the methods of logistics aim to exploit all kinds of information in order to fulfill an objective. I thought a lot about how to design a nice scheduling setup for a cooking context, not chaining tasks as quick as possible may cause your food to catch a cold.

After tinkering with lots of tools, I thought about simplifying the scheduling formulation in a way that reduces the amount of constraints that I have to account for. What I will be proposing is the work of an amateur, as of yet:

Small disclaimer:

This work is purely an implementation of the scheduling theory. My intentions were focused only on seeing if I can pull this off with some Javascript / Hugo. Although it was a success, I need to add that you should cook at your own pace. Just take your time and never rush things up!

The machines setup

This setup assumes the following characteristics:


My specs:

The formulation:

Before talking about the design let me define what a machine is. Pretty much anything that processes a task on its own without any active moderation from my end. My kitchen is equipped with a stove, an oven, and microwave- all of which can act on their own provided I initiate them - As for me, I have my own hands which will act as a medium to craft the best of the best. Specs-wise, I think the machines and I are modern enough.
Concerning the precedence graph, I will not go into deep details on the chain of tasks unless it is necessary. The quantity of my descriptions should, in theory, not influence the makespan; micro tasks like "breaking an egg and then mixing it up" will be summed up as "preparing the egg" for the sake of simplicity.

Example:

Here I will be showcasing a small example of how to model the Gantt chart of preparing an omelet sandwich.

Tasks:

  1. Prepare the egg
  2. Cut the veggies
  3. Cook the egg
  4. Prepare the bread
  5. Stuff in the bread

Machines/Tasks set:

These will be the tasks assigned to each machine:

And these can be modeled like so, because you can't stuff anything in the bread as long as your eggs aren't ready:

I will not attribute any processing times in this example, just picture any arbitrary number for each task for now.

Tasks marked with red can be shifted to the right as long as they do not introduce any extra time to the total makespan by exceeding the white space in their front. You can cut the bread anytime you want between the 5th and 7th time mark. These are called tasks with slack times, or in other literature as buffer times. Their times are computed using the following formula:
\[ LST(j) = \max_{j' \in S(j)} \left( C_{i,j'} - p_{i,j'} \right) \quad \forall j \in J \]

Small observation:

A couple of people pointed out that I was cooking eggs the wrong way, more specifically, I put the eggs on a pan and let the pan do the rest @ 6/10 heat. Apparently the popular belief recommends against this, instead one should agitate the egg as it heats up using a pressure tool. If that is the case then becomes a single machine since I would then be actively doing all the cooking, increasing the total time up to 15. Despite all of this, I will keep this mistake up there for demonstrative purposes.

About the computation

Using a solver is totally unnecessary since the sequencing is already fixed, meaning all I need is a topological sort which has a complexity of \(O(V + E)\). The following Javascript snippet will be plugged together with a module that generates Gantt charts, for now I am using Tikzjax but its slow rendering speed and its lack of CSS flexibility makes me want to seek another package. The code computes the Makespan and outputs the starting- and finishing times of each machine, all of which are calculated using the classic formula:
$$C_{i,j} = \max\left(C_{i-1,j}, C_{i,j-1}\right) + p_{i,j} \quad \forall j \in J, i \in M$$
The data is extracted from the recipes's front matter and formulated as a list that contains all the information that I would need. Thanks to Hugo's flexibility, plugging in the input in Javascript made me write less code.
            {{ $tasks := .Params.tasks }}
                var processSteps = [
  {{- range $index, $task := $tasks -}}
    {
      id: {{ $task.id }},
      name: "{{ $task.name }}",
      machine: "{{ $task.machine }}",
      processing_time: {{ $task.processing_time }},
      dependencies: [{{ range $task.dependencies }}{{ . }},{{ end }}],
    },
  {{- end -}}
];

function calculateSchedule(processSteps) {
    const tasks = processSteps.map((step, index) => ({
        id: index + 1,
        name: step.name,
        machine: step.machine,
        processingTime: step.processing_time,
        dependencies: step.dependencies,
        startTime: 0,
        finishTime: 0,
        lst: 0,
    }));

    for (const task of tasks) {
        task.startTime = Math.max(...task.dependencies.map(depId => tasks[depId - 1].finishTime), 0);
        task.finishTime = task.startTime + task.processingTime;
    }

    for (const task of tasks) {
        const successors = tasks.filter(t => t.dependencies.includes(task.id));
        const lstCandidates = successors.map(s => s.startTime - task.processingTime);
        task.lst = Math.max(...lstCandidates, 0);
    }

    const makespan = Math.max(...tasks.map(task => task.finishTime));

    return { makespan, tasks };
}

const scheduleResult = calculateSchedule(processSteps);
console.log('Makespan:', scheduleResult.makespan);
console.log('Task Schedule:');
scheduleResult.tasks.forEach(task => {
    console.log(`${task.name}: Start Time ${task.startTime}, Finish Time ${task.finishTime}, Machine ${task.machine}`);
});

Can a better formulation beat my makespan ?

I would like to see that! Everything that you have read so far was the effort of two days and is subject for potential improvements. For now I am happy with the results, though if I get any new ideas, I will surely implement provide updates. Feel free to submit an issue on the website's repo or send me an email and I will be content to have a discussion on the matter.

Change log

28/12/2024

An acquaintance of mine, who happens to teach scheduling have read this post here, indicating that what I did does not fall in the parallel machine category. The post is now edited to reflect a general implantation without diving deeper into the specifics of the problem, as I could not myself find the correct one. I looked up many variants and none was satisfactory with respect to the setup. Until I find the correct formulation and for the sake of consistency, I'll keep this post in this state.

11/02/2024

Conception added, every recipe now has a Gantt chart detailing the processing steps while aiming to minimize the makespan subject to a kitchen setup.

17/02/2024

Added Slack times indicators! Tasks marked with red can be shifted freely as long as they do not exceed their latest finishing times!