A practical, top-to-bottom deep dive into Constraint Programming with OR-Tools CP-SAT — in Python and Kotlin — culminating in a full end-to-end Nurse Scheduling Problem (NSP) application.
Why does this repo exist? Read the companion essay: The field that schedules your world — a plain-English tour of Constraint Programming and why the Nurse Scheduling Problem is the perfect way to learn it.
| What | Hours |
|---|---|
| Reading + coding the 18 chapters (one concept → Python → Kotlin → MiniZinc) | ~66 h |
| Working through end-of-chapter exercises | +20–30 h |
| Reading the linked papers and the OR-Tools / MiniZinc docs | +5–10 h |
| Total — cold start to "I can model and solve a real production problem" | ~100–120 h |
Most people finish the 18-chapter spine in 2–3 weeks full-time, or about 3 months at 8–10 hours a week. Time compresses significantly if you already know one of the two languages or have prior OR / SAT experience.
See docs/plan.md for per-chapter hour targets and
PROGRESS.md for a personal tracker you can fork.
- What you'll learn
- Who this is for
- The 18-chapter ladder
- What you'll build
- Quick start
- How to learn with this repo
- Track your progress
- Using Claude Code (optional but recommended)
- Repo layout
- Tech stack
- Status
- Contributing, licence, citation
- The vocabulary of Constraint Programming: variables, domains, propagation, search, global constraints, symmetry breaking, linear relaxations.
- How OR-Tools CP-SAT — Google's hybrid CP + SAT + LP solver — actually works, and how to coax it into solving your problem fast.
- How to model in Python with the first-party bindings, and in Kotlin
with our idiomatic DSL
cpsat-ktthat wraps the OR-Tools Java API without leaking its Java-isms. - How to use MiniZinc as a declarative companion — write the model solver-agnostically, then port it to CP-SAT by hand.
- How to build a full Nurse Scheduling Problem solver and ship it behind twin FastAPI / Ktor 3 backends with a Vite + React web UI.
- How to compare CP-SAT against Timefold (ex-OptaPlanner local search) and Choco (classic CP backtracking) on the same instances, so you know when each solver wins.
- Software engineers who have a real scheduling / allocation / routing problem and want to learn CP well enough to model it.
- Data scientists and ML engineers exploring combinatorial decision-making beyond gradient-based optimisation.
- CS students who've seen SAT / ILP in class and want to see CP at production scale.
- Polyglots who are comfortable in either Python or Kotlin and want to build fluency in the other.
You don't need prior CP experience. You do need comfort with one modern programming language (Python or Kotlin), basic git, and a willingness to read runnable code.
Phase 1 Intuition + vocabulary ch. 1–3 first working solve
Phase 2 Core modelling ch. 4–6 puzzles, optimization, global constraints
Phase 3 Bridges ch. 7–8 MiniZinc → CP-SAT
Phase 4 Scheduling primitives ch. 9–10 job-shop + shifts
Phase 5 Nurse Scheduling I / II / III ch. 11–13 hard → soft → benchmarks
Phase 6 Your own DSL library ch. 14 cpsat-kt internals
Phase 7 End-to-end NSP application ch. 15–17 backends, frontend, deploy
Phase 8 Ecosystem ch. 18 Timefold, Choco, retro
Full breakdown with hours and concrete deliverables per chapter:
docs/plan.md.
Working by the end of the curriculum:
cpsat-kt— an idiomatic Kotlin DSL over OR-Tools CP-SAT, published as v0.1.0 in this repo underlibs/cpsat-kt/(41+ Kotest specs passing, ready for Maven Central).nsp-core— a shared NSP solver library in both Python (apps/py-cp-sat/nsp-core/) and Kotlin (apps/kt-cp-sat/nsp-core/).py-api/kt-api— two production-grade backends for the same OpenAPI 3.1 contract: FastAPI + SQLModel + SSE, and Ktor 3 + Exposed + SSE. Same endpoints, same schemas, byte-for-byte identical/openapi.yamloutput, 30 and 17 tests respectively.web— a Vite + React 19 + React Router v7 frontend: instance upload, live solve view (SSE-streamed incumbents), schedule / coverage / Gantt views, infeasibility explorer, backend switch, dark mode.- Timefold and Choco ports — the same NSP, solved two other ways, with
a benchmark harness that runs all three solvers on every instance and
emits CSV + per-run JSON under
benchmarks/results/.
# 1. Clone
git clone https://github.com/vpetreski/cp-deep-dive.git
cd cp-deep-dive
# 2. Read the overview + the plan (30 min)
$EDITOR docs/overview.md docs/plan.md
# 3. Open Chapter 1 and follow from there
$EDITOR docs/chapters/01-what-is-cp.mdPer-language prerequisites (install only when you reach the phase that needs them — each chapter README tells you):
- Python chapters (Phase 2+): Python 3.12+, uv.
Then in the chapter directory:
uv sync --all-packages && uv run pytest. - Kotlin chapters (Phase 2+): JDK 25 (Temurin via SDKMAN).
Then:
./gradlew test(Gradle comes from the wrapper). - MiniZinc chapters (Phase 3): MiniZinc 2.8+.
- App build (Phase 7): Node 22 LTS, Docker (for containerisation demo).
To run the finished end-to-end app today:
# Python backend
cd apps/py-api && uv sync && uv run uvicorn py_api.main:app --reload
# Kotlin backend (in another shell)
cd apps/kt-api && ./gradlew run
# Web UI (in a third shell)
cd apps/web && npm ci && npm run devOpen http://localhost:5173, upload data/nsp/toy-01.json, and watch it solve.
Linear, one chapter at a time:
- Read
docs/chapters/NN-*.md— intuition → formal → worked example. - Run the Python code:
cd apps/py-cp-sat/chNN-* && uv run python -m <pkg>. - Run the Kotlin twin:
cd apps/kt-cp-sat/chNN-* && ./gradlew run. - Skim the MiniZinc model if the chapter has one in
apps/mzn/. - Do the 3–5 exercises at the end of the chapter; hidden answers live in
solutions/next to the exercise. - Tick the chapter off in your fork's
PROGRESS.md, commit, move on.
Don't skip the Kotlin twin even if you're only here for Python (or vice versa): translating a model into a second language is the single best trick for understanding what the model actually means rather than what it looks like in one dialect.
PROGRESS.md is a personal, markdown-only tracker. Fork the
repo, open that file in your copy, and tick items off as you go. It has
per-chapter checkboxes, space for estimated vs actual hours, a log for
exercise answers, and prompts for end-of-phase reflection.
Because it's ordinary markdown, you can:
- edit it in your editor or on GitHub directly;
- commit it on your own branch (
learn/<you>) to have a history of your progress; - ask Claude Code (see below) to "update
PROGRESS.mdwith what I just finished" — Claude can edit it the same way you can.
This repo was written alongside Claude Code, and is instrumented for agentic collaboration:
CLAUDE.mddeclares project-wide rules and routing (where to write docs, which files are authoritative, commit conventions) so Claude behaves consistently across sessions.- QMD indexes
docs/knowledge/so Claude can semantically retrieve chunks instead of reading whole files. The.mcp.jsonin the repo root wires QMD in as an MCP server. A post-commit hook re-indexes automatically. claude-memory/(mirrored from~/.claude/projects/<slug>/memory/per machine viatools/setup-memory-hook.sh) stores conversation-spanning facts about you and how you like to work. Lives outside.claude/because the latter is a hardcoded protected-path prefix that prompts on every edit.
You can absolutely learn the material without any of this — it's all plain markdown and code. But with Claude Code attached, you can:
- ask "why does CP-SAT prefer
AddAllowedAssignmentshere?" and get an explanation grounded in the current chapter's knowledge docs; - paste an error from your exercise attempt and get a focused hint;
- say "mark Chapter 7 done in PROGRESS.md and open Chapter 8" and have it happen.
Per-machine setup if you want the full experience:
./tools/setup-memory-link.sh # once per machine
./tools/setup-qmd-hook.sh # once per machine
qmd collection add docs --name cp-deep-dive-docs
qmd update && qmd embedcp-deep-dive/
├── README.md ← you are here
├── CLAUDE.md ← project rules + agent routing
├── PROGRESS.md ← personal learner tracker (fork & fill)
├── CONTRIBUTING.md CODE_OF_CONDUCT.md SECURITY.md CHANGELOG.md CITATION.cff
├── LICENSE (Apache-2.0) NOTICE
├── docs/
│ ├── overview.md ← the field in one page
│ ├── plan.md ← 18-chapter roadmap, hours, deliverables
│ ├── chapters/ ← per-chapter teaching notes
│ ├── knowledge/ ← reference encyclopedia (QMD-indexed)
│ │ ├── cp-theory/ cp-sat/ cpsat-kt/ minizinc/
│ │ ├── nurse-scheduling/ ecosystem/
│ └── adr/ ← architecture decision records
├── libs/cpsat-kt/ ← our Kotlin DSL over OR-Tools CP-SAT
├── specs/nsp-app/ ← locked v1.0 NSP app spec (11 files)
├── apps/
│ ├── py-cp-sat/ ← Python chapter code (ch02–ch13 + nsp-core)
│ ├── kt-cp-sat/ ← Kotlin chapter code (ch02–ch13 + nsp-core)
│ ├── mzn/ ← MiniZinc teaching models
│ ├── py-api/ ← FastAPI NSP backend (Phase 7)
│ ├── kt-api/ ← Ktor 3 NSP backend (Phase 7)
│ ├── web/ ← Vite + React 19 + RR7 NSP frontend
│ ├── shared/ ← OpenAPI 3.1 + JSON Schemas
│ └── alt-solver/ ← Timefold + Choco NSP ports
├── benchmarks/ ← cross-solver benchmark runner + baselines
├── data/nsp/ ← NSP instances (toy + NSPLib + INRC pointers)
├── tools/ ← Claude Code + QMD setup scripts
└── .github/workflows/ ← CI (Python, Kotlin, Web jobs)
| Layer | Choice |
|---|---|
| Python | 3.12+ via uv |
| JVM | JDK 25 LTS |
| Kotlin | 2.3+, Gradle 9.4+ Kotlin DSL |
| Node | 22 LTS |
| Primary solver | OR-Tools CP-SAT 9.15+ |
| Kotlin DSL | cpsat-kt (ours, published from this repo) |
| Modelling companion | MiniZinc 2.8+ |
| Python backend | FastAPI + SQLModel + sse-starlette + Pydantic v2 |
| Kotlin backend | Ktor 3.x + Exposed + kotlinx.coroutines |
| Frontend | Vite + React 19 + React Router v7 + TypeScript 5 + Tailwind + shadcn/ui + TanStack Query |
| Testing | pytest + Hypothesis (Py), Kotest 5 (Kt), Vitest + Playwright (Web) |
| Lint | ruff + mypy --strict (Py), ktlint + detekt (Kt), eslint + tsc --noEmit (Web) |
| Containers | Docker, Docker Compose |
| CI | GitHub Actions |
v0.1.0 — the curriculum is complete and runnable end-to-end.
- ✅ 18 chapters written (
docs/chapters/) - ✅
cpsat-ktv0.1.0 with tests green - ✅ Python nsp-core + Kotlin nsp-core libraries
- ✅ FastAPI backend (13 endpoints, 30 tests) and Ktor backend (13 endpoints, 17 tests)
- ✅ Web UI (9 routes, instance upload, live solve, schedule / coverage / Gantt / infeasibility views)
- ✅ Timefold + Choco ports
- ✅ Benchmark harness with baseline results for CP-SAT, Choco, Timefold on
toy-01andtoy-02 - ✅ NSP spec v1.0 locked (
specs/nsp-app/) - ✅ GitHub Actions CI green (Python, Kotlin, Web)
See CHANGELOG.md for the full release notes.
- Contributing: see
CONTRIBUTING.md. Typo fixes and clearer explanations are especially welcome; bigger changes should start as an issue. Thecpsat-ktlibrary accepts feature PRs under semver. - Code of Conduct:
CODE_OF_CONDUCT.md(Contributor Covenant 2.1). - Security: private report flow in
SECURITY.md. - Licence: Apache-2.0 — same as OR-Tools, with third-party
attributions in
NOTICE. - Citation:
CITATION.cff— the "Cite this repository" button on GitHub uses it.
If this curriculum helped you ship a real-world optimisation project, a one-line note in the issue tracker would make the author's week.