Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions tutorials/first-propagator/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
book
8 changes: 8 additions & 0 deletions tutorials/first-propagator/book.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[book]
title = 'Tutorial: Implementing Propagators in Pumpkin'
authors = ['Emir Demirović', 'Maarten Flippo', 'Imko Marijnissen']
language = 'en'

[output.html]
mathjax-support = true
additional-css = ["theme/css/custom.css"]
16 changes: 16 additions & 0 deletions tutorials/first-propagator/src/01-introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Introduction

The goal of this tutorial is to help you become comfortable with the fundamental development workflow in **Pumpkin**, our constraint‑programming solver. We will guide you through the implementation, testing, and integration of a simple propagator, allowing you to gain hands‑on experience with the core ideas behind implementing propagators.

Pumpkin is implemented in [Rust](https://rust-lang.org), a modern systems programming language designed with a strong focus on **safety** and **performance**. If you are new to Rust, we recommend briefly reviewing the [Rust Book](https://doc.rust-lang.org/book/), particularly the sections on *ownership* and *borrowing*. You are not expected to master these topics for this tutorial—however, a light familiarity will make the examples easier to follow. When relevant, we include links to the corresponding Rust documentation.

This tutorial assumes basic familiarity with `git` and introductory concepts from constraint programming (specifically, Chapters 1 and 2 of the course lecture notes). By the end of this tutorial, you will know how to:

- Implement a propagator in Pumpkin
- Register and integrate it into the solving pipeline
- Write systematic tests using `TestSolver`
- Validate propagations using a checker
- Understand common pitfalls and how to avoid them
- Run Pumpkin on FlatZinc models generated via MiniZinc

We hope this tutorial helps you build intuition for how constraint propagation works under the hood and gives you the confidence to extend Pumpkin with new propagators.
48 changes: 48 additions & 0 deletions tutorials/first-propagator/src/02-setting-up.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Setting Up Your Environment

Before working with Pumpkin, make sure your environment is properly set up. This section provides steps to prepare your Rust toolchain and explore the Pumpkin codebase.

### Install Rust

Install Rust using the official installer:

<https://rust-lang.org/tools/install/>

This will install:

- the Rust compiler (`rustc`)
- `cargo`, Rust’s package manager and build tool

### Clone the repository

Clone the course repository:

```bash
git clone https://github.com/ConSol-Lab/CS4535.git
```

### Verify your Rust installation

Navigate into the repository and build the project:

```bash
cd CS4535
cargo build
```

This compiles the solver and confirms that your Rust toolchain is functioning correctly.

### Generate the documentation

Pumpkin includes extensive inline documentation. You can build and view it locally using:

```bash
cargo doc --no-deps --open --document-private-items
```

This opens a local documentation page in your browser, which is helpful when navigating the codebase.

### TODO
- Discuss Clippy.
- A few words on IDEs. Suggest that running Clippy upon save (CRTL+S) is a good setup (vscode specific?).
- How to add git hooks.
17 changes: 17 additions & 0 deletions tutorials/first-propagator/src/03-your-first-propagator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Your First Propagator: `a = b`

We will go through the complete implementation of a toy propagator for the equality constraint

$$ a = b, $$

where _a_ and _b_ are integer variables.

This example introduces all core components of Pumpkin’s propagation framework: domain events, propagator constructors, propagation logic, explanation generation, and mechanisms to ensure the implementation is correct, namely propagation and retention checkers. By the end, you will have a clear understanding of how a simple constraint is translated into a fully functioning propagator.

Our implementation of the the propagator will enforce **domain consistency**, so only values that could be part of a feasible assignment remain in the domains of the variables. To achieve this, the propagator performs two types of filtering:

1. _Bounds Filtering_: Align the lower and upper bounds of both variables.

2. _Value Filtering_: Remove values that occur in the domain of one variable but not the other.

Before we examine the Rust implementation, let us first build intuition through a small example.
40 changes: 40 additions & 0 deletions tutorials/first-propagator/src/04-illustrative-example.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Illustrative Example

To understand how the equality constraint

$$ a = b $$

behaves during propagation, let us walk through a concrete example. The propagator ensures that both variables eventually take the **same value**, meaning their domains must shrink to the **intersection** of the original domains.

Consider the following initial domains:

$$ a \in [5, 8] $$
$$ b \in \\{4, 5, 7, 9\\} $$

Propagation proceeds in two phases.

### Filtering the domain of `a`

- _Bounds filtering_: The bounds of `b` (4 and 9) do not constrain the bounds of `a`.
- _Value filtering_: The values 6 and 8 are not present in the domain of `b`, so they can be removed from `a`.

After filtering:

$$ a = \\{5, 7\\} $$

### Filtering the domain of `b`

- _Bounds filtering_: The bounds of `a` (5 and 7) constrain the bounds of `b`, removing 4 and 9.
– _Value filtering_: Both 5 and 7 appear in the domain of `a`, and 6 does not, so no additional values need to be removed from `b`.

After filtering:

$$ b = \\{5, 7\\} $$

### Final State

Both variables have the same domain:

$$ a = \\{5, 7\\}, \qquad b = \\{5, 7\\} $$

This is exactly the behaviour our propagator must reproduce.
11 changes: 11 additions & 0 deletions tutorials/first-propagator/src/05-propagator-structure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Propagator Structure

Now that we have seen how the propagators should behave, we take a step back and examine how to represent this propagator within Pumpkin.

A propagator in Pumpkin is built from three core components:

1. **A constructor** – specifies which variables the propagator operates on and registers the domain events it should listen to.
2. **The propagator implementation** – contains the propagation logic executed by the solver.
3. **A consistency checker** – verifies that every value removed from or retained in a domain is properly justified, ensuring that the propagator behaves correctly.

These components allow Pumpkin to create, schedule, and execute propagators efficiently during search, while also providing an automated mechanism for verifying their correctness.
125 changes: 125 additions & 0 deletions tutorials/first-propagator/src/06-constructor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@

# Constructor

The constructor encapsulates the information required to create an instance of the propagator. In our case, it stores:

- the variables `a` and `b` involved in the equality constraint, and
- a `ConstraintTag`, which Pumpkin uses internally to label inference steps for proof logging and explanation generation.

Here is the definition, containing only the data held by the constructor’s [`struct`](https://doc.rust-lang.org/book/ch05-00-structs.html):

```rust,no_run,noplayground
#[derive(Clone, Debug)]
pub struct BinaryEqualsPropagatorConstructor<AVar, BVar> {
pub a: AVar,
pub b: BVar,
pub constraint_tag: ConstraintTag,
}
```

<details>
<summary>Explanation of Rust language features</summary>

- The type parameters `AVar` and `BVar` are [generic](https://doc.rust-lang.org/book/ch10-01-syntax.html), meaning the propagator can work with any integer variable type Pumpkin supports. Rust generics play a role similar to Java generics and C++ templates, although they behave differently in important ways.
- The fields are declared as [`pub`](https://doc.rust-lang.org/book/ch07-03-paths-for-referring-to-an-item-in-the-module-tree.html#exposing-paths-with-the-pub-keyword), allowing other [crates](https://doc.rust-lang.org/book/ch07-01-packages-and-crates.html) to construct this propagator.
- The [derived traits](https://doc.rust-lang.org/book/appendix-03-derivable-traits.html#appendix-c-derivable-traits) `Clone` and `Debug` are added for convenience. Traits are conceptually similar to Java interfaces or C++ abstract base classes, but not identical.
</details>

Before implementing the constructor, we first define the *propagator struct* so the constructor has a concrete type to reference:

```rust,no_run,noplayground
#[derive(Clone, Debug)]
pub struct BinaryEqualsPropagator<AVar, BVar> {
a: AVar,
b: BVar,
inference_code: InferenceCode,
}
```

This struct represents the propagator. It stores the two variables involved in the constraint, as well as an `inference_code`, whose purpose will become clearer later.

To complete the constructor, we implement the `PropagatorConstructor` [trait](https://doc.rust-lang.org/book/ch10-02-traits.html). A trait behaves like an interface: it specifies which methods must be provided. Here, it requires us to implement the `create` function, which tells Pumpkin how to:

1. register the propagator with the solver, and
2. construct the propagator instance.

The function signature is:

```rust,no_run,noplayground
fn create(
self,
mut context: PropagatorConstructorContext,
) -> Self::PropagatorImpl
```

The `context` is Pumpkin’s handle used during construction. Within this method, we register each variable with the solver:

```rust,no_run,noplayground
context.register(
self.a.clone(),
DomainEvents::ANY_INT,
LocalId::from(0),
);

context.register(
self.b.clone(),
DomainEvents::ANY_INT,
LocalId::from(1),
);
```

Let us examine each part of a `register` call:

- `self.a.clone()` — provides the variable to register. The clone is used for simplicity.
- `DomainEvents::ANY_INT` — indicates the propagator should be notified about *any* change to the variable’s domain, including bounds updates and value removals. Since this propagator maintains domain consistency, it must react to all such events. Other propagators may listen to other domain events, see the documentation for `DomainEvent`.
- `LocalId::from(0)` — assigns a numeric ID for how Pumpkin identifies this variable *within this propagator*. When `a` changes, Pumpkin will report that “local variable 0” changed. This indirection simplifies internal bookkeeping.

We perform a similar registration for `b`, using ID `1`, and then construct and return the actual propagator.

Below is the full implementation of the propagator constructor:

```rust,no_run,noplayground
impl<AVar, BVar> PropagatorConstructor
for BinaryEqualsPropagatorConstructor<AVar, BVar>
where
AVar: IntegerVariable + 'static,
BVar: IntegerVariable + 'static,
{
type PropagatorImpl = BinaryEqualsPropagator<AVar, BVar>;

fn create(
self,
mut context: PropagatorConstructorContext,
) -> Self::PropagatorImpl {
context.register(
self.a.clone(),
DomainEvents::ANY_INT,
LocalId::from(0),
);

context.register(
self.b.clone(),
DomainEvents::ANY_INT,
LocalId::from(1),
);

BinaryEqualsPropagator {
a: self.a,
b: self.b,
inference_code: InferenceCode::new(self.constraint_tag, BinaryEquals),
}
}
}
```

> **Notes**
> - The constructor works with any integer variable type supported by Pumpkin that implements the `IntegerVariable` trait.
> - `PropagatorImpl` declares that this constructor produces `BinaryEqualsPropagator` instances.
> - The constructor builds and returns the propagator, along with its associated `InferenceCode`.

Once this constructor is in place, Pumpkin knows both **when** to invoke the propagator and **how** to initialise it. The next step is to implement the propagation logic itself.

### TODO
- Explain who actually creates the constructor.
- The constructor has a "inference_code", but here we did not show how this value is set.
- It might be confusing for people, since `create` sounds like it constructs the PropagatorConstructor, but instead it actually creates the propagator and not the constructor.
Loading
Loading