Tutorials
Learning-oriented, end-to-end DispatchAtlas tutorials — every step a runnable command, from the first benchmark to statistical comparison and evidence export.
These tutorials are learning-oriented, end-to-end walks: every step is a
runnable script, every output shown is real output from the bundled smoke
data, and every run is deterministic — the same seeds produce the same
numbers on your machine. Complete the
install and the
quick start first, then run each script
from the repository checkout with uv run python <file>.py.
🧭 Choose A Tutorial
| Tutorial | You will learn | Packages touched |
|---|---|---|
| Model and solve a first dispatch problem | Pick a benchmark instance, run two solvers on it, compare schedules, and read solver metadata. | dispatchatlas.core, dispatchatlas.bench, dispatchatlas.solve |
| Run a small campaign and read its results | Configure a checkpointed campaign, execute it into a workspace, and load the results for analysis. | dispatchatlas.lab, dispatchatlas.analytica |
Deeper reference paths continue where the tutorials end: domain contracts for the scheduling model, benchmark model for catalog generation, solver system for the full registry, campaign engine for orchestration detail, and analysis exports for evidence bundles. Larger campaigns require the statistical and release gates described in readiness.
🛠️ Tutorial 1: Model And Solve A First Dispatch Problem
The quick start solved one problem with one solver. This tutorial goes one level deeper: you choose a specific benchmark instance, run two dispatching baselines on it, compare the schedules they build, and read the metadata that explains each solver.
1️⃣ See what the smoke catalog offers
The bundled smoke catalog materializes two small deterministic instances per
scheduling family from a single root seed. Save this as list_problems.py
and run it with uv run python list_problems.py:
from dispatchatlas.bench import smoke_benchmark_provider
provider = smoke_benchmark_provider(root_seed=20260527)
for problem_id in provider.list_problem_ids():
print(problem_id.value)Output:
smoke-cloud-edge-0
smoke-cloud-edge-1
smoke-workflow-0
smoke-workflow-1
smoke-machine-scheduling-unrelated-0
smoke-machine-scheduling-unrelated-1
smoke-job-shop-0
smoke-job-shop-1
smoke-flexible-job-shop-0
smoke-flexible-job-shop-1
smoke-permutation-flow-shop-0
smoke-permutation-flow-shop-1
smoke-setup-flow-shop-0
smoke-setup-flow-shop-1
smoke-rcpsp-renewable-0
smoke-rcpsp-renewable-1
smoke-open-shop-0
smoke-open-shop-1
smoke-hybrid-flow-shop-0
smoke-hybrid-flow-shop-1
smoke-distributed-permutation-flow-shop-0
smoke-distributed-permutation-flow-shop-1
smoke-no-wait-flow-shop-0
smoke-no-wait-flow-shop-1
smoke-blocking-flow-shop-0
smoke-blocking-flow-shop-1
smoke-distributed-assembly-flow-shop-0
smoke-distributed-assembly-flow-shop-1
smoke-multi-objective-pfsp-0
smoke-multi-objective-pfsp-1
smoke-rcpsp-max-0
smoke-rcpsp-max-1
smoke-rcpsp-multi-mode-0
smoke-rcpsp-multi-mode-1
smoke-multi-project-rcpsp-0
smoke-multi-project-rcpsp-1
smoke-unrelated-parallel-setup-0
smoke-unrelated-parallel-setup-1
smoke-reentrant-fab-0
smoke-reentrant-fab-1
smoke-distributed-flexible-job-shop-0
smoke-distributed-flexible-job-shop-1
smoke-facility-assignment-0
smoke-facility-assignment-1Each id names its scheduling family and a zero-based
instance index. The rest of the tutorial uses smoke-job-shop-0.
2️⃣ Run two solvers on one instance
provider.get_problem takes a ProblemId and returns a ValidatedProblem —
the problem spec plus its validation stamp and report, so a solver never
receives an unvalidated instance. The two solvers below are deterministic
dispatching baselines: earliest-start schedules tasks in topological input
order, while shortest-processing-time prioritizes shorter tasks. Save this
as compare_solvers.py:
from dispatchatlas.bench import smoke_benchmark_provider
from dispatchatlas.core import ProblemId, TerminationPolicy, derive_seed
from dispatchatlas.solve import default_solver_registry
provider = smoke_benchmark_provider(root_seed=20260527)
validated = provider.get_problem(ProblemId("smoke-job-shop-0"))
spec = validated.spec
print(f"problem: {spec.id.value}")
print(f"tasks: {len(spec.tasks)} resources: {len(spec.resources)}")
registry = default_solver_registry()
for solver_id in ("earliest-start", "shortest-processing-time"):
metadata = registry.get_metadata(solver_id)
solver = registry.create(solver_id)
run = solver.solve(
problem=validated,
stop=TerminationPolicy(max_iterations=metadata.default_stop.max_iterations),
seed=derive_seed(20260610, "docs.tutorial.compare", 0),
)
makespan = run.result.objective_values[0]
print(
f"{solver_id}: feasible={run.result.feasible} "
f"{makespan.objective_name}={makespan.value:.1f}"
)Run it with uv run python compare_solvers.py:
problem: smoke-job-shop-0
tasks: 9 resources: 3
earliest-start: feasible=True makespan=353.0
shortest-processing-time: feasible=True makespan=433.0Both schedules are feasible, and on this instance the plainer rule wins: earliest-start finishes at 353.0 while shortest-processing-time, which prioritizes shorter tasks, finishes at 433.0. That is worth sitting with, because shortest-processing-time is a good rule in general — on a job shop it can defer a long task that a downstream task is waiting on, and the whole schedule waits with it. A rule's reputation does not tell you what it does on your instance.
Which is the point of the shape rather than the number. One instance proves nothing in either direction — that is what campaigns and the statistical methods are for — but the comparison here (same validated problem, same stop criteria, same derived seed) is exactly how larger evidence is built, and a single run is exactly what it is not.
3️⃣ Read the solver metadata
Every registered solver carries metadata as its public contract: supported
objectives, capability tags, stochasticity, default stop criteria, and a
canonical citation. Save this as inspect_metadata.py:
from dispatchatlas.solve import default_solver_registry
registry = default_solver_registry()
metadata = registry.get_metadata("shortest-processing-time")
print(f"solver: {metadata.solver_id}")
print(f"stochasticity: {metadata.stochasticity}")
print(f"citation: {metadata.citation.reference}")
print("capabilities:", ", ".join(c.value for c in metadata.capabilities))Run it with uv run python inspect_metadata.py:
solver: shortest-processing-time
stochasticity: deterministic
citation: Smith, W. E. (1956). Various optimizers for single-stage production. Naval Research Logistics Quarterly, 3(1-2), 59-66.
capabilities: single-objective, capacity-aware, precedence-aware, constructive, dispatching, deterministicThis metadata is what registry.select filters on and what the
solver recommender explains from. The full
registry — baselines, metaheuristics, exact adapters, and the NDSO family —
is cataloged in algorithms and
solver system.
🧪 Tutorial 2: Run A Small Campaign And Read Its Results
A campaign is a configured set of runs — benchmark instances crossed with
solvers and objectives — executed with explicit seeds, budgets, checkpoints,
and an on-disk result repository. This tutorial runs the same shape as the
tracked recipe at experiments/configs/smoke-pilot.json: two smoke
instances, two deterministic baselines, one objective.
1️⃣ Configure and run the campaign
Save this as first_campaign.py. It declares the campaign, validates it
into a deterministic plan, estimates its cost with a dry run, then executes
it into a local my-campaigns/ workspace:
from pathlib import Path
from dispatchatlas.core import TerminationPolicy
from dispatchatlas.lab import (
CampaignConfig,
CampaignKind,
CampaignStage,
ExecutionMode,
OutputPolicy,
ResourceBudget,
default_campaign_runner,
)
workspace = Path("my-campaigns")
config = CampaignConfig(
campaign_id="first-campaign",
benchmark_ids=("smoke-job-shop-0", "smoke-workflow-0"),
solver_ids=("earliest-start", "shortest-processing-time"),
objectives=("makespan",),
root_seed=20260610,
seed_namespace="docs.tutorial.first-campaign",
stop=TerminationPolicy(max_iterations=5),
output=OutputPolicy(root_dir=str(workspace)),
stage=CampaignStage.SMOKE,
kind=CampaignKind.PILOT,
resources=ResourceBudget(
max_workers=2,
max_concurrent_runs=2,
estimated_seconds_per_run=0.5,
),
execution_mode=ExecutionMode.SEQUENTIAL,
)
runner = default_campaign_runner(workspace)
plan = runner.validate(config)
budget = runner.dry_run(plan)
print(f"planned runs: {len(plan.runs)}")
print(f"estimated wall time: {budget.estimated_wall_time_seconds:.1f}s")
index = runner.run(plan)
print(f"completed runs: {index.run_count}")
print(f"failed attempts: {len(index.failures)}")Run it with uv run python first_campaign.py:
planned runs: 4
estimated wall time: 2.0s
completed runs: 4
failed attempts: 0Four runs is exactly the cross product: 2 benchmark instances × 2 solvers ×
1 objective, with one run per cell because both solvers are deterministic.
Every run's seed derives from root_seed and the run's position, so
re-running the script reproduces the same records; the checkpoint lets an
interrupted campaign resume without repeating completed work.
2️⃣ Inspect what landed on disk
The runner wrote a workspace with the same layout as the tracked experiments workspace:
my-campaigns/
.checkpoints/first-campaign.json
ENVIRONMENT.md
logs/first-campaign_<timestamp>.log
results/first-campaign/
plan.json
environment.json
earliest-start/smoke-job-shop-0/run_0.json
earliest-start/smoke-workflow-0/run_0.json
shortest-processing-time/smoke-job-shop-0/run_0.json
shortest-processing-time/smoke-workflow-0/run_0.jsonOne JSON record per run, addressed by campaign, solver, benchmark, and
replicate index — the path itself is the index. Each record is
content-hashed over its deterministic payload, which is what replay mode
verifies against.
3️⃣ Load the results for analysis
dispatchatlas.analytica reads completed campaign directories without
importing the campaign runtime. Save this as read_results.py:
from pathlib import Path
from dispatchatlas.analytica import load_result_dataset, summarize_dataset
dataset = load_result_dataset(Path("my-campaigns"), "first-campaign")
print(f"campaign: {dataset.campaign_id}")
print(f"completed runs: {len(dataset.completed)}")
summary = summarize_dataset(dataset)
for solver in summary.solver_summaries:
print(
f"{solver.solver_id}: runs={solver.count} "
f"feasible={solver.feasible_count} "
f"mean {solver.objective_name}={solver.mean:.1f}"
)Run it with uv run python read_results.py:
campaign: first-campaign
completed runs: 4
earliest-start: runs=2 feasible=2 mean makespan=182.1
shortest-processing-time: runs=2 feasible=2 mean makespan=222.1summarize_dataset computes descriptive statistics per solver and
objective. With only two runs per solver the inferential methods
(significance tests, confidence intervals) route to the limitations surface
instead of producing under-powered results — the statistical-power floor of
30 independent runs per stochastic solver-instance cell is described in
analysis exports.
4️⃣ Export an evidence bundle (optional)
The same campaign directory feeds the export command, which writes a disclosure-filtered evidence bundle of tables, figures, and supplement:
uv run dispatchatlas export `
--campaign-dir .\my-campaigns\results\first-campaign `
--target-dir .\exports\first-campaign `
--authorized-output-root .\exports `
--tier coreThe bundle lands at
exports/first-campaign/evidence-bundles/first-campaign-core/. The
evidence bundles page explains the four tiers and
the bundle contents.
🎓 Where this leads
- The tracked recipe
experiments/scripts/run_smoke_pilot.pyruns this same shape into the experiments workspace and serializes its configuration for thedispatchatlas-labcommand-line interface. - The campaign engine page covers execution modes, retry policy, resume, replay verification, dual stopping protocols, and fair-comparison guarantees.
- Full-stage campaigns stay blocked until statistical design approval is recorded — see release readiness.