Optional tasks

Open In Colab

If you’re using this notebook in Google Colab, be sure to install PyJobShop first by executing pip install pyjobshop in a cell.

This notebook demonstrates how to solve scheduling problems with optional tasks, which means that not all tasks are required to be scheduled.

Optional tasks appear in practice when products can be built following different processing plans. Each plan may require a different number of tasks, with each task having specific dependencies or constraints. For example, a product could be built using steps A followed by B, which require two different resources, or simply through step C, requiring one resource but with higher processing time.

Scheduling with optional tasks is known in the scheduling literature under different names:

We will show how to define scheduling problems with optional tasks and how to use selection constraints to solve complex versions of this problem.

Setting optional tasks

Let’s define a simple problem instance to demonstrate how to schedule with optional tasks. Tasks are required by default, but we can make them optional by passing the optional=True argument.

[1]:
from pyjobshop import Model
[2]:
model = Model()

machine = model.add_machine()

A = model.add_task(optional=True, name="A")
B = model.add_task(optional=True, name="B")
C = model.add_task(optional=True, name="C")

model.add_mode(A, machine, duration=1)
model.add_mode(B, machine, duration=1)
model.add_mode(C, machine, duration=3);

Next, we need to specify how to select the tasks; otherwise, none of the tasks will be scheduled. PyJobShop supports a set of selection constraints (see the next section for an overview) to provide a simple way to declare which tasks need to be selected. This problem can be specified as follows:

[3]:
model.add_select_exactly_one([A, C])  # Either A or C is scheduled
model.add_select_all_or_none([A, B]);  # A and B are both present or absent

In addition, if A is scheduled, then it must be scheduled before B:

[4]:
model.add_end_before_start(A, B);

Now let’s solve this problem instance and plot the solution:

[5]:
result = model.solve(display=False)
[6]:
from pyjobshop.plot import plot_task_gantt

plot_task_gantt(result.best, model.data(), plot_labels=True)
../_images/examples_optional_tasks_12_0.png

Great, this solves the problem as expected! Tasks A and B are scheduled because this approach was shorter than scheduling C.

Of course, this was just a simple toy problem. In realistic scenarios, task C might use a resource that is always available, whereas A and B could require a bottleneck machine. This would then favor scheduling task C.

Selection constraints

PyJobShop provides three selection constraints that can be used to select optional tasks:

  1. SelectAllOrNone - Either all tasks in the group are scheduled, or none are

  2. SelectAtLeastOne - At least one task from the group must be scheduled

  3. SelectExactlyOne - Exactly one task from the group is scheduled

All selection constraints follow the same interface pattern, making them easy to use and combine:

model.add_select_all_or_none(tasks, condition_task=None)
model.add_select_at_least_one(tasks, condition_task=None)
model.add_select_exactly_one(tasks, condition_task=None)

The parameters are as follows:

  • tasks: A list of tasks to apply the constraint to

  • condition_task (optional): If specified, the constraint only applies when this specific task is scheduled

The condition_task parameter enables powerful “if-then” logic. This allows you to build complex processing plans by combining simple, well-defined selection rules.

The following constraint enforces that if task X is scheduled, then exactly one of A, B, or C must be scheduled:

model.add_select_exactly_one([A, B, C], condition_task=X);

Here’s another example, which enforces that if task Y is scheduled, then all of D, E, F, and Y must be scheduled. Notice the inclusion of Y, which enforces scheduling all tasks in the group:

model.add_select_all_or_none([D, E, F, Y], condition_task=Y)

Advanced if-then constraints

Let’s put the if-then constraints into action. We’ll use an example problem from Van der Beek et al. (2025). The diagram below illustrates two graphs: a selection graph, which dictates the selection of tasks and their subsequent tasks, and a precedence graph, which dictates the precedence between tasks if they are present.

example

A selection relationship between two tasks (\(u\), \(v\)) means that if task \(u\) is selected, then task \(v\) must also be selected. A selection group, which has a condition task \(u\) and a group of tasks \(V\), requires that if \(u\) is selected, then at least one task from \(V\) must be selected.

Let’s model this specific example, which defines how tasks C-A, C-B, C-AB, I-A, I-AB, and I-B should be selected based on the presence of others.

[7]:
model = Model()

First, we have a source task marking the beginning of the project and a target task marking the end. These are typically required tasks. We also define all other tasks, which are all optional.

[8]:
tasks = {"source": model.add_task(name="source")}

names = ["C-A", "C-B", "C-AB", "I-A", "I-AB", "I-B"]
tasks |= {name: model.add_task(optional=True, name=name) for name in names}

tasks["target"] = model.add_task(name="target")

For convenience, we assume there is a single resource, and that each optional task takes a random duration between 1 and 5 time units, whereas the source and target tasks require zero duration.

[9]:
import random

random.seed(42)

machine = model.add_machine()

model.add_mode(tasks["source"], machine, duration=0)
model.add_mode(tasks["target"], machine, duration=0)

for name in names:
    model.add_mode(tasks[name], machine, duration=random.randint(1, 5))

Let’s put all the selection constraints in a list and then add them as selection constraints:

[10]:
selection_groups = [
    ("source", ["C-A"]),
    ("source", ["C-B"]),
    ("C-A", ["I-A", "C-AB"]),
    ("C-B", ["C-AB", "I-B"]),
    ("C-AB", ["I-AB"]),
    ("I-A", ["target"]),
    ("I-B", ["target"]),
    ("I-AB", ["target"]),
]

for condition, group in selection_groups:
    model.add_select_exactly_one(
        [tasks[name] for name in group],
        condition_task=tasks[condition],
    )

We do the same for the precedence constraints. Note that precedence constraints (and other constraints) only apply if both tasks are selected.

[11]:
precedence = [
    ("source", "C-A"),
    ("source", "C-B"),
    ("C-A", "I-A"),
    ("C-A", "C-AB"),
    ("C-B", "C-AB"),
    ("C-B", "I-B"),
    ("C-AB", "I-AB"),
    ("I-A", "I-B"),
    ("I-A", "target"),
    ("I-B", "target"),
    ("I-AB", "target"),
]

for pred, succ in precedence:
    model.add_end_before_start(tasks[pred], tasks[succ])
[12]:
result = model.solve(display=False)
print(result)
Solution results
================
  objective: 6.00
lower bound: 6.00
     status: Optimal
    runtime: 0.01 seconds
[13]:
plot_task_gantt(result.best, model.data(), plot_labels=True)
../_images/examples_optional_tasks_28_0.png

Great, this result shows that a process plan from source to target is selected, while tasks C-AB and I-AB are skipped. Note how the precedence constraints are also respected: I-A and only starts after C-A is completed, and I-B can only start after both I-A and C-B are finished.

Conclusion

This notebook showed how to model scheduling problems with optional tasks in PyJobShop. Key features include:

  • Optional tasks: Set optional=True when creating tasks to let the solver decide whether to schedule them.

  • Selection constraints: Use SelectAllOrNone, SelectAtLeastOne, and SelectExactlyOne to control task selection.

  • Conditional selection logic: The condition_task parameter enables “if-then” relationships between tasks.

These capabilities enable modeling of alternative process plans, flexible project structures, and complex scheduling scenarios where not all tasks need to be executed.