Skip to content
DispatchAtlas
Search

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

TutorialYou will learnPackages touched
Model and solve a first dispatch problemPick 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 resultsConfigure 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-1

Each 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.0

Both 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, deterministic

This 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: 0

Four 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.json

One 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.1

summarize_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 core

The 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.py runs this same shape into the experiments workspace and serializes its configuration for the dispatchatlas-lab command-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.