ferroplan-cli 0.6.0

Command-line interface (the `ff` binary) for the ferroplan PDDL planner
<p align="center">
  <img src="https://raw.githubusercontent.com/hhh42/ferroplan/main/assets/logo.svg" alt="ferroplan" width="360">
</p>

# ferroplan

[![CI](https://github.com/hhh42/ferroplan/actions/workflows/ci.yml/badge.svg)](https://github.com/hhh42/ferroplan/actions/workflows/ci.yml)
[![docs](https://img.shields.io/badge/docs-mdbook-blue)](https://hhh42.github.io/ferroplan)
[![live demo](https://img.shields.io/badge/live_demo-try_in_browser-6c5ce7)](https://hhh42.github.io/ferroplan/demo/index.html)
[![license](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue)](#license)

A fast, data-parallel **PDDL planner** in Rust — a deterministic planning core for
the age of AI.

**The bet:** an LLM should be the *author and supervisor* of a planner, not its
runtime. The same reason you don't ask a model to add a column of numbers — you have
it emit code that does the arithmetic deterministically, and for free — applies one
level up: don't ask an LLM to *be* the planner for a whole village of agents. Have it
author a PDDL domain that then plans deterministically, cheaply, and inspectably at
scale, and let it only *nudge* that domain at runtime. PDDL is the auditable interface
between your intent, the model's authoring, and a fast solver — and `ferroplan` is
that solver.

**Why PDDL, not prompt-spaghetti:**

- **Cost** — a solved domain plans essentially for free; an LLM call per decision per agent does not.
- **Determinism** — same problem, same plan; you can regression-test it.
- **Inspectability** — you can read a domain and an axiom; you cannot read a model's weights.
- **Scale** — a village of agents each replanning is tractable for a fast solver, not as a wall of LLM calls.

> **[▶ Try it live in your browser]https://hhh42.github.io/ferroplan/demo/index.html**> pick a built-in example or paste your own PDDL; it plans entirely client-side via
> WebAssembly, no install. There's also a
> [browser visualizer + block editor]https://hhh42.github.io/ferroplan/gui/index.html.

`ferroplan` is a from-scratch reimplementation of the FF family of planners with a
data-oriented core (bitset states, structure-of-arrays / CSR operator tables),
**enforced hill-climbing** (EHC) with a best-first fallback, parallel grounding
and parallel heuristic evaluation, plus an SGPlan-style partition-and-resolve mode,
PDDL3 preference/metric optimization, and **PDDL2.1 temporal** planning (durative
actions). It ships both a **library** (with a structured, JSON-serializable API)
and the **`ff`** command-line binary — a drop-in for Metric-FF's
`ff -o domain -f problem`.

On classical and ADL benchmarks it runs within ~1.4× of the heavily-optimized C
Metric-FF (EHC reaches goals in dozens of evaluations, not thousands); numeric
trails and IPC-5 preference quality is competitive-not-winning — see
[Benchmarks](#benchmarks).

> Status: **v0.6.0**`ferroplan` + `ferroplan-cli` are on [crates.io]https://crates.io/crates/ferroplan. APIs may shift before 1.0.

> **What's new in 0.6.0 — Selection: solve the choice, then plan to it.**
> Plan forensics ([the write-up]docs/forensics-tpp.md) proved the remaining
> quality gap on preference domains was a *selection* problem, not a search
> problem — so ferroplan now solves the preference-subset choice **exactly**
> (`selection.rs`: mutex-variable end states, branch-and-bound, an admissible
> bound that can *prove* optimality) and plans to it as a target, and keeps
> init-satisfied "trap" preferences visible to the guidance. Results on pure
> defaults vs SGPlan5, the IPC-5 winner: **storage becomes an 8/8 domain
> sweep** (totals 234 vs 547), **tpp p06 an exact tie**, rovers' totals lead
> widens to 4862 vs 5632, and the suite tally reaches **19W/16T/13L**
> ([scoreboard]benchmarks/ipc5-scoreboard.md) — three domains led under
> both quality conventions, deterministic at any thread count, every default
> change with a restore hatch, every dead end recorded
> ([0.6 roadmap]docs/roadmap-0.6.md).

> **What's new in 0.5.0 — closing on first.** On the vendored IPC-5
> simple-preferences suite, **pure defaults** (one configuration, no env vars,
> deterministic at any thread count) now **lead SGPlan5 — the IPC-5 winner —
> under BOTH quality conventions on three of the six domains**: openstacks
> (wins p04–p08), storage (wins p01–p07), and rovers (wins p04/p06/p07/p08,
> exact ties p01/p05) — with trucks ahead on the domain total and a suite-wide
> instance tally of **19W/14T/15L** ([scoreboard]benchmarks/ipc5-scoreboard.md).
> Under the hood: the ESPC penalty loop **graduated to a deterministically
> budgeted default**, both B&B loops gained **anytime in-sweep tightening + a
> diversified restart ladder** (which broke the storage/tpp plateaus), and
> folded numeric metrics **route through the exact-closure optimizer** (the
> rovers flip). Every default change keeps a restore hatch
> ([tuning reference]https://hhh42.github.io/ferroplan/tuning.html);
> negative results are recorded, not hidden (two seeding levers measured
> neutral, shipped opt-in). See the [CHANGELOG]CHANGELOG.md and the executed
> [0.5 roadmap]docs/roadmap-0.5.md.

> **What's new in 0.4.0** — the PDDL3 preference-metric release, measured
> against the official IPC-5 winner on the vendored simple-preferences suite
> ([scoreboard]benchmarks/ipc5-scoreboard.md): ferroplan now **leads SGPlan5
> on two of the six domains** — openstacks (opt-in `FF_ESPC=1` partitioned
> penalty loop: 19/23/17/**16/21/22/66/87** vs 13/16/12/26/36/33/67/123) and
> storage (plain defaults: **3/5/6/9/46**/145/200/263 vs 5/8/14/17/87/…, up
> from 2/8 coverage) — is **ahead on the trucks total**, and **ties SGPlan5 on
> every tpp and pathways p01–p04 instance**. Under the hood: an
> **exact-closure metric optimizer** (search real states, close the compiled
> preference bookkeeping with a provably-optimal phase tail), **static
> preference simplification** (storage's 62k-instance quadratic forall
> collapses ~97% at compile), barrier-free full-DNF guidance, and a
> **budget-escalating B&B** whose deterministic eval budget
> (`FF_PREF_EVAL_BUDGET`) is a real quality dial. Every change has a restore
> hatch. See the [CHANGELOG]CHANGELOG.md for the full breakdown, including
> 0.3.0's temporal solver depth (65 → 73/75 corpus, default goal-relevance
> pruning, escalation ladder, `Session` API), the animator's transport bar and
> Gantt view, and the move to **Bevy 0.19**.

## Features

- **EHC + best-first** — enforced hill-climbing with helpful actions (the FF
  speed default), falling back to weighted best-first when it stalls. Selectable
  per solve (`--search auto|ehc|best-first|…`).
- **FF heuristic** — delete-relaxation relaxed-plan heuristic over a
  data-oriented task, deferred evaluation, tunable `g`/`h` weights.
- **Data parallelism** — parallel grounding and parallel batch heuristic
  evaluation (`std::thread`); the plan found is identical for any thread count.
- **PDDL coverage** — STRIPS, typing, negative/disjunctive preconditions,
  numeric fluents (Metric-FF style), **ADL** (conditional effects,
  `forall`/`exists`, equality), and **derived predicates / axioms** (`:derived`,
  static/stratified — closed into the initial state via a datalog fixpoint).
- **PDDL3 preferences** — soft goal preferences (incl. `forall`-quantified and
  precondition preferences) compiled away, with anytime branch-and-bound metric
  optimization. *(Exact-optimal on small/medium instances; best-found, flagged,
  on the largest — see [Limitations]#limitations.)*
- **PDDL2.1 temporal**`:durative-action`s with `at start`/`over all`/`at end`
  conditions & effects, **constant or parameter-dependent durations**, and
  required concurrency, via a decision-epoch forward search; output in the IPC
  temporal plan format (`t: (action) [dur]`) with a makespan.
- **SGPlan-style partitioning** — an optional partition-and-resolve mode.
- **Robust** — a published library shouldn't crash: malformed/pathological PDDL
  (incl. deeply-nested forms) returns a typed error, never a panic.
- **Structured output** — the library returns typed, `serde`-serializable
  results; the CLI emits classic FF text **or** JSON.

## GUI

[`ferroplan-bevy`](crates/ferroplan-bevy) is a Bevy app that visualizes a
domain+problem as a typed graph, animates the plan, and edits both problems and
domains in a Blockly-style block editor (`cargo run -p ferroplan-bevy`).

![ferroplan-bevy visualizing a delivery problem as a typed graph](https://raw.githubusercontent.com/hhh42/ferroplan/main/book/src/images/graph.png)

## Install / build

```sh
# install the `ff` CLI from crates.io
cargo install ferroplan-cli    # puts `ff` on your PATH

# …or build from a clone
cargo build --release          # produces target/release/ff
cargo run --release --bin ff -- -o domain.pddl -f problem.pddl
```

As a library dependency: `cargo add ferroplan` (see [Library](#library) below).

## CLI (`ff`)

```sh
# drop-in: classic Metric-FF text output
ff -o domain.pddl -f problem.pddl

# structured JSON solution
ff -o domain.pddl -f problem.pddl --json

# pick a mode / search strategy
ff -o domain.pddl -f problem.pddl --mode partition
ff -o domain.pddl -f problem.pddl --search best-first --weight-h 3

# temporal (durative actions) — auto-detected; prints the IPC temporal plan
ff -o temporal-domain.pddl -f problem.pddl --mode temporal

# decompose a too-big temporal goal into ordered, individually-solved contracts
# (the "LLM authors, planner decomposes" bet, made inspectable — text or --json)
ff -o temporal-domain.pddl -f problem.pddl --mode temporal --decompose

# self-contained JSON job: {"domain": "...", "problem": "...", "options": {...}}
ff --json-request job.json
```

Run `ff --help` for all flags (`--search`, `--weight-g/--weight-h`,
`--max-evaluated`, `--satisfice`, `--threads`, …).

## Library

```rust
use ferroplan::{solve, Options};

let domain  = std::fs::read_to_string("domain.pddl")?;
let problem = std::fs::read_to_string("problem.pddl")?;

// Syntax-check before solving (no grounding/solving) — fast authoring feedback.
let report = ferroplan::parse(&domain);
assert!(report.ok, "{:?}", report.error);

let solution = ferroplan::solve(&domain, &problem, &Options::default())?;
if let Some(plan) = solution.plan {
    for step in &plan.steps {
        println!("{} {}", step.action, step.args.join(" "));
    }
    println!("metric: {:?}", plan.metric);
}
# Ok::<(), ferroplan::SolveError>(())
```

The public, `serde`-serializable surface: **`solve`** (plan a domain+problem),
**`decompose`** (split a too-big temporal goal into validated contracts),
**`parse`** (syntax-check + summarize PDDL without solving),
**`Session`** (ground once, replan many — for a live loop that re-solves the same
world every tick), and **`plan::validate_plan`** (independently check a plan). See
[`examples/`](crates/ferroplan/examples) for `solve`, `parse`, `json_api`, and
`replan` (`Session` vs. re-solving from scratch, with timings).

## Configuration

Every solver knob lives on one `Options` struct (library-first, `serde`-
serializable). The CLI flags and JSON job options map to the same fields; omitted
JSON fields fall back to the defaults shown.

```rust
ferroplan::solve(&domain, &problem, &ferroplan::Options {
    mode:            Mode::Auto,        // auto | ff | partition | pddl3 | temporal
    search:          Search::Auto,      // auto | ehc | best-first | ehc-then-best-first
    helpful_actions: true,              // helpful-action pruning (EHC)
    weight_g:        1.0,               // best-first path-length weight
    weight_h:        5.0,               // best-first heuristic weight  (1·g + 5·h)
    threads:         0,                 // 0 = auto
    max_evaluated:   None,              // search node cap
    optimize:        true,              // PDDL3: optimize metric vs. satisfice
    ..Default::default()                // every field is optional
})?;
```

CLI equivalents: `--mode`, `--search`, `--no-helpful`, `--weight-g/--weight-h`,
`--max-evaluated`, `--satisfice`, `--threads`. Via JSON:
`{"domain": "...", "problem": "...", "options": {"search": "best-first"}}`.

## Workspace layout

| crate | what |
|---|---|
| [`ferroplan`]crates/ferroplan | the library: engine + modes + `solve` / `decompose` / `Session` API |
| [`ferroplan-cli`]crates/ferroplan-cli | the `ff` binary (clap + JSON) |
| [`ferroplan-mcp`]crates/ferroplan-mcp | an MCP server exposing `solve` / `validate` / `decompose` over stdio — so an LLM agent can author PDDL and drive the planner |
| [`ferroplan-bevy`]crates/ferroplan-bevy | Bevy app: visualize, inspect & animate a domain+problem (`cargo run -p ferroplan-bevy [domain.pddl problem.pddl]`) |
| [`ferroplan-wasm`]crates/ferroplan-wasm | WebAssembly binding behind the client-side [browser demo]https://hhh42.github.io/ferroplan/demo/index.html`solve` a domain+problem entirely in-page |
| [`ferroplan-py`]crates/ferroplan-py | Python binding (`pip`-installable extension module) exposing `solve` for embedding in Python tools |

## Examples

[`examples/`](examples) collects worked domains that exercise the full feature set
— see the [examples index](examples/README.md) for a feature-by-feature map and a
suggested reading order. Highlights:

- [`rpg`]examples/rpg — the clean intro: durative actions with renewable
  (workers) and consumable resources, gather → craft → build.
- [`rpg-world`]examples/rpg-world — a ~120-action crafting/economy domain
  (durative actions, numeric resources, renewable capacities, a reachability
  axiom) with a corpus of validated contracts, a flavor-×-scale [`suite/`]examples/rpg-world/suite,
  an adversarial [`hard/`]examples/rpg-world/hard batch, and an
  [industrial-city]examples/rpg-world/industrial-city showcase that runs a whole
  metal/stone/wood industry as a pipeline of contracts.
- [`cabin`]examples/cabin — deep numeric build plus a durative "crew" twin
  (makespan vs. crew size, skill-gated scheduling).
- [`reachability`]examples/reachability — the worked **derived-axiom**
  (`:derived`) example: static transitive-closure reachability.
- [`village`]examples/village — a full-ADL stress test (`when`, `forall`+`when`,
  `or`, negation) over durative + numeric state.
- [`villagers`]examples/villagers — a data-driven recipe planner with numeric
  **PDDL3 metric** optimization; the "embed in a game" model.
- [`logistics`]examples/logistics — transshipment: per-location goods, trucks
  with capacity, a train line.
- [`jobshop`]examples/jobshop — scheduling with machine-exclusion (scales to 100
  concurrent jobs).
- [`BORDERS.md`]examples/BORDERS.md — a measured map of where one-shot planning
  solves vs. where a goal must be decomposed into contracts. The **`decompose` API /
  `ff --decompose`** acts on that border: it splits a too-big temporal goal into
  ordered, individually-solved contracts and stitches them into one validated plan
  (e.g. `hard/order-8` → 8 named contracts), falling back to a monolithic solve when
  a goal can't be split.

## Benchmarks

Classical and ADL coverage/speed are measured against the C **Metric-FF**; the
IPC-5 preference quality is measured against **SGPlan5** (the IPC-5 winner), over
a subset of the IPC contest suites. Headline (native Metric-FF, EHC default):

| category | ferroplan solved | speed vs Metric-FF |
|---|---:|---|
| STRIPS | 40/40 | 0.71× (~1.4× slower) |
| ADL | 23/24 | 0.77× (~1.3× slower) |
| numeric | 36/40 | 0.22× |

The IPC-5 preference scoreboard (vs SGPlan5):
[`benchmarks/ipc5-scoreboard.md`](benchmarks/ipc5-scoreboard.md); classical/numeric
detail: [`benchmarks/results.md`](benchmarks/results.md) (and the
[project site](https://hhh42.github.io/ferroplan)). The oracles are not bundled
(GPL / non-commercial licences) — reproduce per
[`benchmarks/COMPARING.md`](benchmarks/COMPARING.md).

**Profiling & perf tracking:** [`PROFILING.md`](PROFILING.md) — a deterministic
metrics harness (`benchmarks/perf.py run`/`compare` against a committed baseline,
so improvement/regression is measurable across machines) plus the samply /
flamegraph / criterion-baseline workflow for finding and tracking hotspots.

## Limitations

- **Numeric** trails Metric-FF: EHC's helpful-action lookahead stalls on some
  numeric domains and falls back to (complete, slower) best-first.
- **IPC-5 preferences**: compiled away, then optimized by an **exact-closure
  metric optimizer** with anytime sweeps, a diversified restart ladder, and the
  deterministically-budgeted ESPC penalty loop — all defaults. Coverage is
  **full (48/48)** on the vendored simple-preferences suite and ferroplan
  **leads SGPlan5 under both quality conventions on three of the six domains**
  (openstacks, storage, rovers), with trucks ahead on totals — see the
  [scoreboard]benchmarks/ipc5-scoreboard.md. The tpp/pathways p05–p08 tails
  still trail (best-found, flagged *not proven optimal*, measured
  direction-bound); the design record for the remaining work is in
  [`docs/espc-preferences-spec.md`]docs/espc-preferences-spec.md and
  [`docs/roadmap-0.5.md`]docs/roadmap-0.5.md.
- **PDDL3 trajectory constraints** (`(:constraints ...)``always`, `sometime`,
  `within`, …) are parsed but not yet enforced; rather than silently drop a hard
  constraint, ferroplan **rejects** any domain/problem that carries one. Model the
  requirement as hard goals or goal preferences instead.
- **Temporal**: durative actions with constant or parameter-dependent durations
  and required concurrency are supported, and **plans are VAL-validated** on real
  IPC temporal domains (44/45 produced plans valid — see
  [`benchmarks/temporal-results.md`]benchmarks/temporal-results.md). Coverage is
  currently search-limited (the decision-epoch search times out on large
  instances). Duration *inequalities* (`(>= ?duration L)` / `(<= ?duration U)` /
  `and` ranges) are supported — the search commits to the shortest feasible
  duration — as are **timed initial literals** (`(at <time> <literal>)` in `:init`).
  Continuous (`#t`) effects are not yet supported.
- **Derived predicates** (`:derived`): static/stratified axioms are supported
  (closed into the initial state); *dynamic* derived predicates (bodies over
  changing facts) are not yet.

## Acknowledgments

This project is built in deep respect for the planners that came before it.

**SGPlan** (SGPlan5 / SGPlan6), by Chih-Wei Hsu and Benjamin W. Wah at the
University of Illinois, has been the standard to beat in this corner of automated
planning for the better part of two decades — the IPC-winning system whose
constraint-partitioning and extended-saddle-point penalty-coordination ideas still
define the state of the art for satisficing planning with preferences and with
temporal/resource constraints. I've followed that line of research for many years,
and to build something that even comes *close* to it on a slice of the benchmarks
is, genuinely, an honor. Enormous credit to that team for the depth, rigor, and
sheer durability of the work — ferroplan is in no small part an attempt to learn
from it, in Rust.

Equal thanks to Jörg Hoffmann's **FF / Metric-FF**, whose relaxed-plan heuristic
and enforced hill-climbing are the backbone of this engine; to the IPC organizers
and domain authors whose benchmarks make progress measurable; and to Derek Long and
Maria Fox's **VAL**, used here to independently validate the temporal plans.

## License

Dual-licensed under either of [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE),
at your option.